1pub mod info;
2pub mod worktree;
3
4use std::path::Path;
5use std::process::Command;
6
7pub fn git_cmd(repo: &Path) -> Command {
12 let mut cmd = Command::new("git");
13 cmd.arg("-C")
14 .arg(repo)
15 .stdin(std::process::Stdio::null())
16 .env("GIT_TERMINAL_PROMPT", "0")
17 .env(
18 "GIT_SSH_COMMAND",
19 "ssh -o BatchMode=yes -o ConnectTimeout=5",
20 );
21 cmd
22}
23
24pub fn output_with_timeout(
28 cmd: &mut Command,
29 timeout: std::time::Duration,
30) -> std::io::Result<std::process::Output> {
31 output_with_timeout_inner(cmd, timeout, None)
32}
33
34pub fn output_with_timeout_limit(
36 cmd: &mut Command,
37 timeout: std::time::Duration,
38 max_stream_bytes: usize,
39) -> std::io::Result<std::process::Output> {
40 output_with_timeout_inner(cmd, timeout, Some(max_stream_bytes))
41}
42
43fn output_with_timeout_inner(
44 cmd: &mut Command,
45 timeout: std::time::Duration,
46 max_stream_bytes: Option<usize>,
47) -> std::io::Result<std::process::Output> {
48 use std::os::unix::process::CommandExt;
49 use std::process::Stdio;
50
51 cmd.process_group(0);
53 let mut child = cmd.stdout(Stdio::piped()).stderr(Stdio::piped()).spawn()?;
54 let child_pgid = child.id() as libc::pid_t;
55
56 let stdout = child.stdout.take();
57 let stderr = child.stderr.take();
58
59 let stdout_thread = std::thread::spawn(move || read_bounded(stdout, max_stream_bytes));
60 let stderr_thread = std::thread::spawn(move || read_bounded(stderr, max_stream_bytes));
61
62 let start = std::time::Instant::now();
63 loop {
64 match child.try_wait() {
65 Ok(Some(status)) => {
66 let stdout = join_reader(stdout_thread);
67 let stderr = join_reader(stderr_thread);
68 return Ok(std::process::Output {
69 status,
70 stdout: stdout?,
71 stderr: stderr?,
72 });
73 }
74 Ok(None) => {
75 if start.elapsed() >= timeout {
76 if let Ok(Some(status)) = child.try_wait() {
77 let stdout = join_reader(stdout_thread);
78 let stderr = join_reader(stderr_thread);
79 return Ok(std::process::Output {
80 status,
81 stdout: stdout?,
82 stderr: stderr?,
83 });
84 }
85 unsafe { libc::killpg(child_pgid, libc::SIGKILL) };
87 let _ = child.wait();
88 let _ = stdout_thread.join();
90 let _ = stderr_thread.join();
91 return Err(std::io::Error::new(
92 std::io::ErrorKind::TimedOut,
93 "git command timed out",
94 ));
95 }
96 std::thread::sleep(std::time::Duration::from_millis(5));
97 }
98 Err(e) => return Err(e),
99 }
100 }
101}
102
103fn read_bounded<R: std::io::Read>(
104 reader: Option<R>,
105 max_bytes: Option<usize>,
106) -> std::io::Result<Vec<u8>> {
107 use std::io::Read;
108
109 let Some(reader) = reader else {
110 return Ok(Vec::new());
111 };
112 let mut bytes = Vec::new();
113 match max_bytes {
114 Some(limit) => {
115 reader
116 .take(limit.saturating_add(1) as u64)
117 .read_to_end(&mut bytes)?;
118 if bytes.len() > limit {
119 return Err(std::io::Error::new(
120 std::io::ErrorKind::InvalidData,
121 "subprocess output exceeded byte limit",
122 ));
123 }
124 }
125 None => {
126 let mut reader = reader;
127 reader.read_to_end(&mut bytes)?;
128 }
129 }
130 Ok(bytes)
131}
132
133fn join_reader(
134 thread: std::thread::JoinHandle<std::io::Result<Vec<u8>>>,
135) -> std::io::Result<Vec<u8>> {
136 thread
137 .join()
138 .map_err(|_| std::io::Error::other("subprocess output reader panicked"))?
139}
140
141#[cfg(test)]
142mod tests {
143 use super::read_bounded;
144 use std::io::Cursor;
145
146 #[test]
147 fn bounded_reader_rejects_oversized_output() {
148 let error = read_bounded(Some(Cursor::new(b"four")), Some(3)).unwrap_err();
149 assert_eq!(error.kind(), std::io::ErrorKind::InvalidData);
150 }
151}