Skip to main content

wsx_core/git/
mod.rs

1pub mod info;
2pub mod ops;
3pub mod worktree;
4
5use std::path::Path;
6use std::process::Command;
7
8/// Base git command scoped to `repo` via `-C`.
9/// Stdin null + env vars prevent any interactive prompt from opening /dev/tty:
10///   GIT_TERMINAL_PROMPT=0  — disables git's own credential prompts
11///   GIT_SSH_COMMAND        — BatchMode=yes + ConnectTimeout=5 so SSH fails fast
12pub fn git_cmd(repo: &Path) -> Command {
13    let mut cmd = Command::new("git");
14    cmd.arg("-C")
15        .arg(repo)
16        .stdin(std::process::Stdio::null())
17        .env("GIT_TERMINAL_PROMPT", "0")
18        .env(
19            "GIT_SSH_COMMAND",
20            "ssh -o BatchMode=yes -o ConnectTimeout=5",
21        );
22    cmd
23}
24
25/// Spawn `cmd` in its own process group with piped stdout/stderr.
26/// Kills the entire group on timeout so ssh + credential helpers are also reaped.
27/// Joins reader threads after kill to prevent thread leaks.
28pub fn output_with_timeout(
29    cmd: &mut Command,
30    timeout: std::time::Duration,
31) -> std::io::Result<std::process::Output> {
32    use std::io::Read;
33    use std::os::unix::process::CommandExt;
34    use std::process::Stdio;
35
36    // ! own process group so killpg doesn't hit the parent
37    cmd.process_group(0);
38    let mut child = cmd.stdout(Stdio::piped()).stderr(Stdio::piped()).spawn()?;
39    let child_pgid = child.id() as libc::pid_t;
40
41    let stdout = child.stdout.take();
42    let stderr = child.stderr.take();
43
44    let stdout_thread = std::thread::spawn(move || {
45        let mut buf = Vec::new();
46        if let Some(mut r) = stdout {
47            let _ = r.read_to_end(&mut buf);
48        }
49        buf
50    });
51    let stderr_thread = std::thread::spawn(move || {
52        let mut buf = Vec::new();
53        if let Some(mut r) = stderr {
54            let _ = r.read_to_end(&mut buf);
55        }
56        buf
57    });
58
59    let start = std::time::Instant::now();
60    loop {
61        match child.try_wait() {
62            Ok(Some(status)) => {
63                let stdout = stdout_thread.join().unwrap_or_default();
64                let stderr = stderr_thread.join().unwrap_or_default();
65                return Ok(std::process::Output {
66                    status,
67                    stdout,
68                    stderr,
69                });
70            }
71            Ok(None) => {
72                if start.elapsed() >= timeout {
73                    if let Ok(Some(status)) = child.try_wait() {
74                        let stdout = stdout_thread.join().unwrap_or_default();
75                        let stderr = stderr_thread.join().unwrap_or_default();
76                        return Ok(std::process::Output {
77                            status,
78                            stdout,
79                            stderr,
80                        });
81                    }
82                    // Kill the entire process group — git + ssh + credential helpers
83                    unsafe { libc::killpg(child_pgid, libc::SIGKILL) };
84                    let _ = child.wait();
85                    // Pipes are now closed; readers unblock and finish
86                    let _ = stdout_thread.join();
87                    let _ = stderr_thread.join();
88                    return Err(std::io::Error::new(
89                        std::io::ErrorKind::TimedOut,
90                        "git command timed out",
91                    ));
92                }
93                std::thread::sleep(std::time::Duration::from_millis(5));
94            }
95            Err(e) => return Err(e),
96        }
97    }
98}