# Repositories on another machine, over ssh ## Goal Open a git repository that lives on another machine and see its history exactly as gites shows a local one: branches, tags, remote branches, commits, the details panel and diffs. gites shows the state of that folder on that machine, including its local branches and which branch is checked out there, not the state of its origin. Only history is shown, as for local repositories today. Uncommitted changes are not part of this work. Linux and macOS are the platforms that matter. Windows must work, but may be slow. ## How the user opens one - Command line: `gites myserver:/srv/app` or `gites user@myserver:/srv/app`. `myserver` can be any host name from `~/.ssh/config`, so ports, user names and keys set there apply. - In the window: the repository menu (`o`) and the welcome screen get an entry "Open on another machine…" with one text field for `[user@]host:path`. - Remote repositories go into the recent list like local ones, written as `myserver:/srv/app`. ## Where a repository is The Rust side stores the open repository as one of two kinds: - a local folder, as today; - an ssh target (`myserver` or `user@myserver`) and a path on that machine. One function turns text into a location. The text means a remote repository when it has the form `[user@]host:path`, where `host` contains no `/` or `\` and is longer than one character, and no local folder with exactly that name exists. Every other text is a local folder. So `C:\code\x` and `C:/code/x` stay local, `./a:b` stays local, and a folder in the current folder named `notes:2024` stays local when it exists. A path on the server that is `~` or starts with `~/` is handed to the server's shell with the `~` outside the quotes, so the shell expands it to the home folder there. ## Running git `git()` in `src-tauri/src/git.rs` takes the repository's location instead of a folder. - Local: `git -C `, as today, with the Windows flag that keeps console windows from opening. - Remote: the program `ssh`, with these options, then the target, then one command string for the server's shell: ``` ssh -o BatchMode=yes -o ConnectTimeout=10 -o ServerAliveInterval=5 -o ServerAliveCountMax=2 -o ControlMaster=auto -o ControlPath= -o ControlPersist=10m user@myserver "git -C '/srv/app' 'log' '--format=…' …" ``` - `BatchMode=yes`: ssh never asks for a password or a yes/no answer, because gites has no terminal to ask in. Keys and the ssh agent work. - `ConnectTimeout=10`: a server that does not answer gives an error after 10 seconds instead of a window that waits for ever. - `ServerAliveInterval=5`, `ServerAliveCountMax=2`: when a server stops answering on a connection that is already open (also a shared one), ssh notices it in about 10 seconds and gives an error instead of waiting for ever. - `ControlMaster`, `ControlPath`, `ControlPersist`: the first command opens one connection and leaves it open; later commands go through it and cost about one network round trip each instead of a new login. The connection closes 10 minutes after the last command. The socket is `gites-%C` in the user's runtime folder (`/run/user/` on Linux). Where there is none (always on macOS), it is in a `gites` folder in the user's cache folder, made so that only the user can open it; never in `/tmp`. If neither folder can be found or made, these three options are left out and every command opens a new connection. `%C` is a hash ssh makes from the host, port and user, 40 characters long. macOS accepts a socket path of at most 103 characters (Linux 107), and while ssh sets up a shared connection it adds a temporary end of up to 18 characters to the file name. So gites counts the folder's length + 1 + 6 (`gites-`) + 40 + 18; if that is more than 103 (one limit on every system), these three options are left out too. The folder is found, made and checked once when gites first runs ssh, not for every command. - On Windows, ssh cannot share connections, so these three options are left out and every command opens a new connection. - Quoting: every word of the git command is put in single quotes, and a `'` inside a word is written as `'\''`. This works in sh, bash, zsh and fish, so a file name with spaces, quotes, `$`, `;` or a line break reaches git unchanged and cannot run anything else on the server. Nothing above `git()` changes: loading the graph, the details panel, diffs and the reload when the window gets focus all run through it. ## Names and saved data A remote repository's name is `:`, where the top folder is what `git rev-parse --show-toplevel` prints on the server. That name is used: - in the window title and the header; - in the recent list in `settings.json`; - as the key of the repository's filters in `filters.json`, so filters are remembered for remote repositories just like for local ones. ## Start-up A remote location given on the command line becomes the open repository at once. gites does not test the connection before the window appears, so a slow server never delays the window. If the server cannot be reached, the first load fails and the window shows the error screen, as it does today for a local path that is not a repository. ## Errors ssh exits with code 255 when the connection fails, and with git's own exit code otherwise. That tells a connection problem from a git error: | What happened | What gites shows | |---|---| | Connection failed (exit code 255) | "could not connect to myserver:" and ssh's message | | …and ssh says "Host key verification failed" | also: "Run `ssh myserver` once in a terminal to accept the server's key." | | …and ssh says "Permission denied" | also: "gites logs in with keys or your ssh agent only; check that `ssh myserver` works without a password." | | The server does not answer within 10 seconds | the connection error above, with ssh's timeout message | | git fails on the server, for example "not a git repository" | git's message, as today | If the connection drops while a repository is open, the next command shows its error as a message at the bottom of the window and the graph that is already on screen stays. The next reload connects again by itself. A message that reports an error stays for 8 seconds, or until it is clicked, and each hint line above stays on its own line. Other messages go after 1.8 seconds, as before. The commands that run git run away from the thread that draws the window, so the window does not freeze while it waits for a slow server. ## Tests - Reading locations: `host:/path`, `user@host:/path`, `host:~/x`, `C:\x`, `C:/x`, a plain relative path, and an existing local folder with a `:` in its name. - Quoting: words with spaces, `'`, `"`, `$`, `;`, `*` and a line break are quoted, run through a real `sh -c`, and come out unchanged. - End to end without a server: for tests, the ssh program can be replaced by a small script that drops the ssh options and runs the command string with `sh -c` on this machine, the way a server's shell would. With it, the existing tests for loading a repository, commit details and diffs also run against a "remote" location, including files with awkward names. CI runs these. - By hand, before release: open a repository on a real server, check the graph, a commit's details and a diff, then wait for the shared connection to close and reload. ## Not part of this work - Uncommitted changes on the remote machine. - Passwords and other questions ssh might ask. - `ssh://` addresses; ports and users go in `~/.ssh/config` or `user@host`. - Closing the shared connection when gites quits; it closes itself after 10 minutes.