Design: open repositories on another machine over ssh

This commit is contained in:
2026-10-15 14:35:27 +02:00
parent e4e597dc5c
commit 761e528e15
@@ -0,0 +1,129 @@
# 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 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 <folder> <args>`, 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 ControlMaster=auto -o ControlPath=<socket> -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.
- `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/<id>` on Linux), or in `/tmp` where there is none; `%C` is a hash ssh
makes from the host, port and user, so the path stays short.
- 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 `<target>:<top folder>`, 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.
## 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.