Open repositories on another machine over ssh; keep the window responsive while git runs #5
@@ -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
@@ -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() };
|
||||||
|
|||||||
Reference in New Issue
Block a user