diff --git a/src-tauri/src/git.rs b/src-tauri/src/git.rs index 3c9f4ea..47dc064 100644 --- a/src-tauri/src/git.rs +++ b/src-tauri/src/git.rs @@ -1,6 +1,6 @@ use serde::Serialize; use crate::location::{Location, remote_command}; -use std::ffi::OsStr; +use std::ffi::OsString; use std::path::PathBuf; use std::process::Command; @@ -53,13 +53,14 @@ pub fn git(loc: &Location, args: &[&str]) -> Result { // Tests swap in a script that acts like ssh. #[cfg(test)] if let Some(fake) = crate::test_ssh::SSH_OVERRIDE.get() { - return git_via(fake.as_os_str(), loc, args); + return git_via(fake, loc, args); } - git_via(OsStr::new("ssh"), loc, args) + git_via(&[OsString::from("ssh")], loc, args) } -/// Does the work of `git`, starting the program `ssh` for a remote location. -pub fn git_via(ssh: &OsStr, loc: &Location, args: &[&str]) -> Result { +/// Does the work of `git`. For a remote location it starts `ssh`: the first word of `ssh` is +/// the program and the other words come before the ssh options (tests use them). +pub fn git_via(ssh: &[OsString], loc: &Location, args: &[&str]) -> Result { let mut cmd = match loc { Location::Local(dir) => { let mut cmd = Command::new("git"); @@ -67,7 +68,8 @@ pub fn git_via(ssh: &OsStr, loc: &Location, args: &[&str]) -> Result { - let mut cmd = Command::new(ssh); + let mut cmd = Command::new(&ssh[0]); + cmd.args(&ssh[1..]); // Never ask for a password: gites has no terminal to ask on. cmd.args(["-o", "BatchMode=yes", "-o", "ConnectTimeout=10"]); // Later commands to the same server reuse one connection for a while. Windows ssh @@ -294,7 +296,7 @@ mod tests { #[test] fn reports_a_missing_ssh_program() { let loc = Location::Ssh { target: "myserver".into(), path: "/x".into() }; - let err = git_via(OsStr::new("/no/such/ssh"), &loc, &["status"]).unwrap_err(); + let err = git_via(&[OsString::from("/no/such/ssh")], &loc, &["status"]).unwrap_err(); assert!(err.starts_with("could not run ssh:"), "{err}"); } } diff --git a/src-tauri/src/test_ssh.rs b/src-tauri/src/test_ssh.rs index 5b433a7..900619a 100644 --- a/src-tauri/src/test_ssh.rs +++ b/src-tauri/src/test_ssh.rs @@ -1,25 +1,28 @@ //! A stand-in for ssh, so tests can run the remote code path without a server. -use std::path::PathBuf; +use std::ffi::OsString; use std::sync::OnceLock; -/// The program `git::git` starts instead of `ssh` while tests run. -pub static SSH_OVERRIDE: OnceLock = OnceLock::new(); +/// The words `git::git` starts instead of `ssh` while tests run: a program, then its arguments. +pub static SSH_OVERRIDE: OnceLock> = OnceLock::new(); -/// Writes (once per test run) a script that acts like ssh: it skips the `-o` options, -/// the `--` and the target, then runs the command on this machine. Returns its path. -pub fn fake_ssh() -> PathBuf { - SSH_OVERRIDE - .get_or_init(|| { - let path = std::env::temp_dir().join(format!("gites-fake-ssh-{}", std::process::id())); - let script = "#!/bin/sh\nwhile [ \"$1\" = \"-o\" ]; do shift 2; done\nshift 2\nexec sh -c \"$1\"\n"; - std::fs::write(&path, script).unwrap(); - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).unwrap(); - } - path - }) - .clone() +/// The shell script that acts like ssh. It checks the arguments the way a real ssh would +/// need them (so a wrong argument list fails a test), skips the options and the target, +/// then runs the command on this machine. +const SCRIPT: &str = r#" +ok= +while [ "$1" = "-o" ]; do + [ "$2" = "BatchMode=yes" ] && ok=1 + shift 2 +done +[ -n "$ok" ] || { echo "fake ssh: BatchMode=yes is missing" >&2; exit 98; } +[ "$1" = "--" ] || { echo "fake ssh: expected -- before the target" >&2; exit 99; } +shift 2 +exec sh -c "$1" +"#; + +/// Makes `git::git` use the stand-in for the rest of the test run. No file is written: +/// the script is given to `sh` on its command line. +pub fn fake_ssh() { + SSH_OVERRIDE.get_or_init(|| ["sh", "-c", SCRIPT, "fake-ssh"].map(OsString::from).to_vec()); }