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
2 changed files with 73 additions and 5 deletions
Showing only changes of commit bd7c129116 - Show all commits
@@ -69,7 +69,12 @@ A path on the server that is `~` or starts with `~/` is handed to the server's s
`gites` folder in the user's cache folder, made so that only the user can open it; `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 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 left out and every command opens a new connection. `%C` is a hash ssh makes from
the host, port and user, so the path stays short. 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 - On Windows, ssh cannot share connections, so these three options are left out and
every command opens a new connection. every command opens a new connection.
- Quoting: every word of the git command is put in single quotes, and a `'` inside a - Quoting: every word of the git command is put in single quotes, and a `'` inside a
+67 -4
View File
@@ -75,6 +75,33 @@ fn socket_dir(runtime: Option<PathBuf>, cache: Option<PathBuf>) -> Option<PathBu
Some(dir) Some(dir)
} }
/// The start of every socket's file name; ssh puts `%C`, a hash of the connection, after it.
#[cfg(not(windows))]
const SOCKET_PREFIX: &str = "gites-";
/// The longest socket path macOS accepts (Linux accepts 107). One limit for every system
/// keeps one rule.
#[cfg(not(windows))]
const MAX_SOCKET_PATH: usize = 103;
/// True when sockets in `dir` stay short enough for the system. ssh puts in place of `%C` a
/// hash of 40 characters, and while it sets up a shared connection it adds a temporary end of
/// up to 18 characters to the file name.
#[cfg(not(windows))]
fn socket_path_fits(dir: &std::path::Path) -> bool {
const HASH: usize = 40;
const TEMPORARY_END: usize = 18;
// The 1 is the `/` between the folder and the file name.
dir.as_os_str().len() + 1 + SOCKET_PREFIX.len() + HASH + TEMPORARY_END <= MAX_SOCKET_PATH
}
/// The path ssh is given for the socket of shared connections, or `None` to share none: when
/// there is no folder for sockets, or the path would be too long.
#[cfg(not(windows))]
fn control_path(dir: Option<PathBuf>) -> Option<PathBuf> {
dir.filter(|d| socket_path_fits(d)).map(|d| d.join(format!("{SOCKET_PREFIX}%C")))
}
/// Runs git in the folder `loc` names: here, or on another machine through ssh. /// Runs git in the folder `loc` names: here, or on another machine through ssh.
pub fn git(loc: &Location, args: &[&str]) -> Result<String, String> { pub fn git(loc: &Location, args: &[&str]) -> Result<String, String> {
// Tests swap in a script that acts like ssh. // Tests swap in a script that acts like ssh.
@@ -104,11 +131,17 @@ pub fn git_via(ssh: &[OsString], loc: &Location, args: &[&str]) -> Result<String
cmd.args(["-o", "ServerAliveInterval=5", "-o", "ServerAliveCountMax=2"]); cmd.args(["-o", "ServerAliveInterval=5", "-o", "ServerAliveCountMax=2"]);
// Later commands to the same server reuse one connection for a while. Windows ssh // Later commands to the same server reuse one connection for a while. Windows ssh
// does not support this. // does not support this.
// The folder is found, made and checked once per run, not for every command.
#[cfg(not(windows))] #[cfg(not(windows))]
if let Some(dir) = socket_dir(dirs::runtime_dir(), dirs::cache_dir()) { {
cmd.args(["-o", "ControlMaster=auto", "-o"]); static CONTROL_PATH: std::sync::OnceLock<Option<PathBuf>> = std::sync::OnceLock::new();
cmd.arg(format!("ControlPath={}", dir.join("gites-%C").display())); let path = CONTROL_PATH
cmd.args(["-o", "ControlPersist=10m"]); .get_or_init(|| control_path(socket_dir(dirs::runtime_dir(), dirs::cache_dir())));
if let Some(path) = path {
cmd.args(["-o", "ControlMaster=auto", "-o"]);
cmd.arg(format!("ControlPath={}", path.display()));
cmd.args(["-o", "ControlPersist=10m"]);
}
} }
// `--` keeps a target from being read as an option. // `--` keeps a target from being read as an option.
cmd.arg("--").arg(target).arg(remote_command(path, args)); cmd.arg("--").arg(target).arg(remote_command(path, args));
@@ -399,6 +432,36 @@ mod tests {
let _ = std::fs::remove_dir_all(&base); let _ = std::fs::remove_dir_all(&base);
} }
#[cfg(not(windows))]
#[test]
fn shares_connections_through_a_socket_in_a_short_folder() {
assert!(socket_path_fits(&PathBuf::from("/run/user/1000")));
assert_eq!(
control_path(Some(PathBuf::from("/run/user/1000"))),
Some(PathBuf::from("/run/user/1000/gites-%C"))
);
}
#[cfg(not(windows))]
#[test]
fn shares_no_connection_when_the_socket_path_would_be_too_long() {
// The longest folder that fits: the rest of the path takes 1 + 6 + 40 + 18 characters.
let longest = 103 - 1 - SOCKET_PREFIX.len() - 40 - 18;
let too_long = PathBuf::from(format!("/{}", "a".repeat(longest)));
assert!(!socket_path_fits(&too_long));
assert_eq!(control_path(Some(too_long)), None);
assert_eq!(control_path(None), None);
}
#[cfg(not(windows))]
#[test]
fn shares_connections_when_the_socket_path_is_exactly_at_the_limit() {
let longest = 103 - 1 - SOCKET_PREFIX.len() - 40 - 18;
let at_limit = PathBuf::from(format!("/{}", "a".repeat(longest - 1)));
assert_eq!(at_limit.as_os_str().len(), longest);
assert!(socket_path_fits(&at_limit));
}
#[test] #[test]
fn reports_a_missing_ssh_program() { fn reports_a_missing_ssh_program() {
let loc = Location::Ssh { target: "myserver".into(), path: "/x".into() }; let loc = Location::Ssh { target: "myserver".into(), path: "/x".into() };