use std::io::Read;
use std::process::{Child, Command, Stdio};
use std::thread::JoinHandle;
pub fn ssh_command_val() -> &'static str {
if std::env::var("GITWIG_SSH_STRICT").map(|v| v == "1").unwrap_or(false) {
"ssh -o BatchMode=yes -o StrictHostKeyChecking=yes"
} else {
"ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new"
}
}
pub fn git_command() -> Command {
let mut cmd = tool_command("git");
cmd.env("GIT_TERMINAL_PROMPT", "0");
cmd.env("GIT_SSH_COMMAND", ssh_command_val());
cmd.env("GIT_ALLOW_PROTOCOL", "https:ssh:git:file");
cmd.env("GIT_PROTOCOL_FROM_USER", "0");
cmd.env("GIT_ASKPASS", "");
cmd.env("SSH_ASKPASS", "");
cmd.env("GCM_INTERACTIVE", "Never");
cmd
}
#[allow(clippy::disallowed_methods)]
pub fn tool_command(program: &str) -> Command {
let mut cmd = Command::new(program);
cmd.stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped());
cmd
}
#[allow(clippy::disallowed_methods)]
pub fn detached_command(program: &str) -> Command {
let mut cmd = Command::new(program);
cmd.stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null());
cmd
}
pub fn spawn_pipe_reader(child: &mut Child) -> JoinHandle<Vec<u8>> {
let mut pipe = child.stderr.take();
std::thread::spawn(move || {
let mut buf = Vec::new();
if let Some(p) = pipe.as_mut() {
let _ = p.read_to_end(&mut buf);
}
buf
})
}
pub fn summarize_stderr(stderr: &[u8]) -> Option<String> {
let text = String::from_utf8_lossy(stderr);
let mut seen: Vec<&str> = Vec::new();
for line in text.lines() {
let line = line.trim();
let line =
line.strip_prefix("error: ").or_else(|| line.strip_prefix("fatal: ")).unwrap_or(line);
if !line.is_empty() && !seen.contains(&line) {
seen.push(line);
}
}
match seen.as_slice() {
[] => None,
[only] => Some((*only).to_string()),
[first, rest @ ..] => Some(format!("{} (+{} more)", first, rest.len())),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn git_command_detaches_every_stdio_stream() {
let mut child = git_command().arg("--version").spawn().expect("git must be installed");
assert!(child.stdin.is_none(), "stdin must be /dev/null, not inherited");
assert!(child.stdout.is_some(), "stdout must be a pipe, not the terminal");
assert!(child.stderr.is_some(), "stderr must be a pipe, not the terminal");
let _ = child.wait();
}
#[test]
fn tool_command_detaches_every_stdio_stream() {
let mut child =
tool_command("git").arg("--version").spawn().expect("git must be installed");
assert!(child.stdin.is_none());
assert!(child.stdout.is_some());
assert!(child.stderr.is_some());
let _ = child.wait();
}
#[test]
fn detached_command_owns_no_pipes_and_can_use_status() {
let status =
detached_command("git").arg("--version").status().expect("git must be installed");
assert!(status.success());
}
#[test]
fn summarize_stderr_dedupes_and_strips_git_prefixes() {
let raw = b"error: cannot run gpg: No such file or directory\n\
error: cannot run gpg: No such file or directory\n\
fatal: something else\n";
assert_eq!(
summarize_stderr(raw).as_deref(),
Some("cannot run gpg: No such file or directory (+1 more)")
);
assert_eq!(summarize_stderr(b" \n"), None);
assert_eq!(summarize_stderr(b"error: just one\n").as_deref(), Some("just one"));
}
}