diff --git a/apps/staged/src-tauri/src/git/cli.rs b/apps/staged/src-tauri/src/git/cli.rs index 0cedf33e..0c129b15 100644 --- a/apps/staged/src-tauri/src/git/cli.rs +++ b/apps/staged/src-tauri/src/git/cli.rs @@ -92,6 +92,8 @@ fn run_with_env(repo: &Path, args: &[&str], source: EnvSource) -> Result bool { + command + .get_envs() + .any(|(k, v)| v.is_some() && k == std::ffi::OsStr::new(key)) +} + +/// Detach the spawned git from the parent's controlling TTY. Without this, +/// ssh can `open("/dev/tty", O_RDWR)` directly and bypass the piped stdio +/// to prompt on the user's real terminal — and worse, `tcsetpgrp` the user's +/// TTY onto a soon-to-die PGID, which later wedges the outer zsh with EIO. +fn detach_from_ctty(command: &mut Command) { + #[cfg(unix)] + unsafe { + use std::os::unix::process::CommandExt; + // SAFETY: `setsid()` is async-signal-safe. + command.pre_exec(|| { + libc::setsid(); + Ok(()) + }); + } + #[cfg(not(unix))] + let _ = command; +} + /// Pin git's locale so stderr stays in English regardless of the captured /// shell snapshot or the host `LC_*`. The substring checks in /// [`is_missing_ref_error`] and the `NotARepo` parse depend on git's @@ -296,4 +345,35 @@ mod tests { "expected NotARepo, got {err:?}" ); } + + /// Without a user-provided `GIT_SSH_COMMAND` (the common case), + /// `force_non_interactive` injects the BatchMode/ConnectTimeout default so + /// ssh fails fast instead of prompting. + #[test] + fn force_non_interactive_injects_default_ssh_command() { + let mut cmd = Command::new("git"); + force_non_interactive(&mut cmd); + let ssh = cmd + .get_envs() + .find(|(k, _)| *k == std::ffi::OsStr::new("GIT_SSH_COMMAND")) + .and_then(|(_, v)| v) + .expect("GIT_SSH_COMMAND should be set"); + assert_eq!(ssh, "ssh -o BatchMode=yes -o ConnectTimeout=10"); + } + + /// A `GIT_SSH_COMMAND` carried in from the captured shell env (e.g. a + /// corporate ssh wrapper) must not be clobbered. Users who set this + /// have accepted responsibility for non-interactive behavior themselves. + #[test] + fn force_non_interactive_respects_user_ssh_command() { + let mut cmd = Command::new("git"); + cmd.env("GIT_SSH_COMMAND", "/usr/local/bin/my-ssh-wrapper"); + force_non_interactive(&mut cmd); + let ssh = cmd + .get_envs() + .find(|(k, _)| *k == std::ffi::OsStr::new("GIT_SSH_COMMAND")) + .and_then(|(_, v)| v) + .expect("GIT_SSH_COMMAND should still be set"); + assert_eq!(ssh, "/usr/local/bin/my-ssh-wrapper"); + } }