Open repositories on another machine over ssh; keep the window responsive while git runs #5

Open
szaman wants to merge 14 commits from feature/remote-ssh into master
Owner

Opens a git repository on another machine over ssh and shows its history like a local one. Fixes #4.

Design: docs/superpowers/specs/2026-10-15-remote-repositories-over-ssh-design.md. Plan: docs/superpowers/plans/2026-10-15-remote-repositories-over-ssh.md.

How to use it

  • gites myserver:/srv/app or gites user@myserver:/srv/app. myserver can be any host from ~/.ssh/config, where a port other than 22 also goes.
  • In the window: "Open on another machine…" in the repository menu (o) and on the welcome screen.
  • ssh must log in with keys or the ssh agent, without a password. git must be installed on the server.

What changed

  • Where a repository is: src-tauri/src/location.rs reads [user@]host:path. Anything else, including C:\x, a text starting with -, and an existing local folder with a : in its name, stays local.
  • Running git: git() runs git locally or as ssh <options> -- <target> "printf '<every byte as an octal escape>' | sh". The login shell on the server (sh, bash, zsh or fish) sees only digits and backslashes, so no file name can break out and run anything else. One shared ssh connection is reused. Its socket goes in the runtime folder, or else in the cache folder with permissions 0700, never in /tmp. Sharing is left off when the path would be too long.
  • Issue #4: the commands that run git now run off the window's thread. The window no longer freezes while git runs, for local repositories too.
  • Errors: connection errors carry a hint (unknown host key, login refused). Error messages stay until clicked or for 8 seconds. A failed reload keeps the graph on screen.
  • Replies out of order: opening a repository wins over a reload that is still running. A late reply for a previous repository is never shown.

Checked

  • All unit tests pass, for the window code and for Rust, and the type checks pass. The Windows build compiles (cargo check --target x86_64-pc-windows-msvc).
  • Tests without a server: a stand-in for ssh runs the command through sh -c on this machine. Through it, loading a repository, commit details and diffs give the same result as locally, including a file named it's $a; b.txt, a folder with a space, and a ~/ path.
  • Real ssh, against a private OpenSSH 10.5 server on this machine with throwaway keys: gites' exact ssh arguments open the shared connection in 0.18 s with no hang. Later commands take about 0.04 s, and git's errors come through.
  • The window parts were tried in Firefox with a stand-in backend.

Not checked

  • gites itself against one of your servers. Please try this before merging:
    1. Open a repository and check the graph, a commit's details and a diff.
    2. Press r.
    3. Wait more than 10 minutes and reload.
    4. Try a wrong path.
    5. Try an unknown host.
    6. Put the laptop to sleep, or switch networks, then reload.
  • macOS was not tried at all.

Decisions made during the work

Each decision is followed by what it costs if it was wrong.

  1. Remote commands are sent as printf '<octal>' | sh instead of plain single quotes. The plan's quoting rule broke on fish: a file name with \' could run commands on the server. Cost: commands are about four times longer.
  2. A text starting with - is a local path, and ssh gets -- before the target. Cost: none; ssh itself does not allow such host names.
  3. No GITES_SSH setting. Tests swap the ssh program in code only. Cost: the ssh program cannot be changed while gites runs.
  4. A command-line path that is not valid UTF-8 stays an untouched local path. Cost: none.
  5. The test stand-in for ssh runs inline and writes no script file. Executing a freshly written script sometimes failed with "Text file busy". Cost: none; this is test-only.
  6. Commands run off the main thread, with ssh keep-alive (ServerAliveInterval=5, ServerAliveCountMax=2). Cost: a connection that stays silent for about 10 seconds is treated as dead. No gites command does that.
  7. The socket goes in the runtime or cache folder with permissions 0700, never /tmp, and sharing is off when the path would be over 103 characters. Cost: on Linux, a very long cache path loses sharing slightly early.
  8. Error messages stay until clicked or for 8 seconds, with line breaks kept. Cost: none.
  9. A remote path that is exactly ~ means the home folder. The welcome screen shows the remote example. Cost: none.
  10. Opening wins over reloads. A reload's reply is dropped while an open runs, or after one started, or if the reload began during an open. An open's reply is dropped only if a newer open started. Cost: a reload that ran during an open is wasted; the next focus reloads again.
  11. The check against a real server through the app is left to you, and listed above as not done.

Known small problems left as they are

  • Two quick opens where the newer one fails. If you open A and then B quickly, and B fails, the screen keeps the previous repository while the Rust side points at A. The next reload puts them back in step.
  • Repeated error messages. A message appears on every window focus while a server stays unreachable.
  • No pinned ssh argument list. No test pins the exact list of ssh arguments. The stand-in only checks for BatchMode=yes and --.
Opens a git repository on another machine over ssh and shows its history like a local one. Fixes #4. Design: `docs/superpowers/specs/2026-10-15-remote-repositories-over-ssh-design.md`. Plan: `docs/superpowers/plans/2026-10-15-remote-repositories-over-ssh.md`. ## How to use it - `gites myserver:/srv/app` or `gites user@myserver:/srv/app`. `myserver` can be any host from `~/.ssh/config`, where a port other than 22 also goes. - In the window: "Open on another machine…" in the repository menu (`o`) and on the welcome screen. - ssh must log in with keys or the ssh agent, without a password. git must be installed on the server. ## What changed - **Where a repository is:** `src-tauri/src/location.rs` reads `[user@]host:path`. Anything else, including `C:\x`, a text starting with `-`, and an existing local folder with a `:` in its name, stays local. - **Running git:** `git()` runs git locally or as `ssh <options> -- <target> "printf '<every byte as an octal escape>' | sh"`. The login shell on the server (sh, bash, zsh or fish) sees only digits and backslashes, so no file name can break out and run anything else. One shared ssh connection is reused. Its socket goes in the runtime folder, or else in the cache folder with permissions 0700, never in `/tmp`. Sharing is left off when the path would be too long. - **Issue #4:** the commands that run git now run off the window's thread. The window no longer freezes while git runs, for local repositories too. - **Errors:** connection errors carry a hint (unknown host key, login refused). Error messages stay until clicked or for 8 seconds. A failed reload keeps the graph on screen. - **Replies out of order:** opening a repository wins over a reload that is still running. A late reply for a previous repository is never shown. ## Checked - All unit tests pass, for the window code and for Rust, and the type checks pass. The Windows build compiles (`cargo check --target x86_64-pc-windows-msvc`). - **Tests without a server:** a stand-in for ssh runs the command through `sh -c` on this machine. Through it, loading a repository, commit details and diffs give the same result as locally, including a file named `it's $a; b.txt`, a folder with a space, and a `~/` path. - **Real ssh, against a private OpenSSH 10.5 server on this machine with throwaway keys:** gites' exact ssh arguments open the shared connection in 0.18 s with no hang. Later commands take about 0.04 s, and git's errors come through. - The window parts were tried in Firefox with a stand-in backend. ## Not checked - **gites itself against one of your servers.** Please try this before merging: 1. Open a repository and check the graph, a commit's details and a diff. 2. Press `r`. 3. Wait more than 10 minutes and reload. 4. Try a wrong path. 5. Try an unknown host. 6. Put the laptop to sleep, or switch networks, then reload. - macOS was not tried at all. ## Decisions made during the work Each decision is followed by what it costs if it was wrong. 1. **Remote commands are sent as `printf '<octal>' | sh` instead of plain single quotes.** The plan's quoting rule broke on fish: a file name with `\'` could run commands on the server. *Cost:* commands are about four times longer. 2. **A text starting with `-` is a local path, and ssh gets `--` before the target.** *Cost:* none; ssh itself does not allow such host names. 3. **No `GITES_SSH` setting. Tests swap the ssh program in code only.** *Cost:* the ssh program cannot be changed while gites runs. 4. **A command-line path that is not valid UTF-8 stays an untouched local path.** *Cost:* none. 5. **The test stand-in for ssh runs inline and writes no script file.** Executing a freshly written script sometimes failed with "Text file busy". *Cost:* none; this is test-only. 6. **Commands run off the main thread, with ssh keep-alive (`ServerAliveInterval=5`, `ServerAliveCountMax=2`).** *Cost:* a connection that stays silent for about 10 seconds is treated as dead. No gites command does that. 7. **The socket goes in the runtime or cache folder with permissions 0700, never `/tmp`, and sharing is off when the path would be over 103 characters.** *Cost:* on Linux, a very long cache path loses sharing slightly early. 8. **Error messages stay until clicked or for 8 seconds, with line breaks kept.** *Cost:* none. 9. **A remote path that is exactly `~` means the home folder. The welcome screen shows the remote example.** *Cost:* none. 10. **Opening wins over reloads.** A reload's reply is dropped while an open runs, or after one started, or if the reload began during an open. An open's reply is dropped only if a newer open started. *Cost:* a reload that ran during an open is wasted; the next focus reloads again. 11. **The check against a real server through the app is left to you, and listed above as not done.** ## Known small problems left as they are - **Two quick opens where the newer one fails.** If you open A and then B quickly, and B fails, the screen keeps the previous repository while the Rust side points at A. The next reload puts them back in step. - **Repeated error messages.** A message appears on every window focus while a server stays unreachable. - **No pinned ssh argument list.** No test pins the exact list of ssh arguments. The stand-in only checks for `BatchMode=yes` and `--`.
szaman added 14 commits 2026-10-15 13:34:49 +00:00
Commands that run git now run away from the thread that draws the
window, so a slow server no longer freezes it. ssh now notices a server
that stopped answering in about 10 seconds. The sockets for shared
connections go in the runtime folder, or else in a private gites folder
in the cache folder, never in /tmp; without either, connections are not
shared. A path that is exactly ~ now means the home folder on the
server.

Tests cover commit details through ssh, the message for a failed
connection, and a ~/ path end to end. The stand-in ssh is now set per
test, so a test can give it its own home folder.
Error messages now stay for 8 seconds or until clicked, and keep each
hint on its own line. The welcome screen shows how to start gites with
a repository on another machine. The spec follows these changes and the
new ssh options and socket folder.
Every reload and every open takes a number before asking git; a reply is
used only if no newer request was sent after it, and an out-of-date error
is not shown. Commit details and diffs that come back after another
repository was opened are neither kept nor shown.
macOS accepts socket paths of at most 103 characters. Count the folder,
the file name, the 40-character hash and the temporary end ssh adds; when
the total is over 103, leave the connection-sharing options out. Find,
make and check the socket folder once per run instead of for every
command.
Let opening a repository win over a reload
ci / check (push) Successful in 2m58s
898135121c
A reload's reply or error is dropped when an open was running as it
started, an open started after it, or an open is still running when it
answers. An open's reply or error is dropped only when a newer open
started after it.
All checks were successful
ci / check (push) Successful in 2m58s
You are not authorized to merge this pull request.
This pull request can be merged automatically.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feature/remote-ssh:feature/remote-ssh
git checkout feature/remote-ssh
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: szaman/gites#5