Design: open repositories on another machine over ssh
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user