diff --git a/docs/superpowers/specs/2026-10-15-remote-repositories-over-ssh-design.md b/docs/superpowers/specs/2026-10-15-remote-repositories-over-ssh-design.md new file mode 100644 index 0000000..c6f5428 --- /dev/null +++ b/docs/superpowers/specs/2026-10-15-remote-repositories-over-ssh-design.md @@ -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 `, 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= -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/` 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 `:`, 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.