use std::path::Path;
use std::process::Command;
use std::sync::OnceLock;
use anyhow::{Result, bail};
use crate::process;
const SSH_COMMAND: &str = "ssh -o BatchMode=yes -o ServerAliveInterval=15 -o ServerAliveCountMax=4";
pub fn run(dir: &Path, args: &[&str]) -> Result<String> {
let mut cmd = Command::new("git");
cmd.arg("-C").arg(dir);
if matches!(args.first(), Some(&("clone" | "fetch" | "pull" | "remote"))) {
let mut user = global_settings().clone();
user.extend(repo_settings(&dir.join(".git").join("config")));
let env_ssh = ["GIT_SSH_COMMAND", "GIT_SSH"]
.iter()
.any(|k| std::env::var_os(k).is_some());
let (config, ssh) = network_guards(&user, env_ssh);
for c in config {
cmd.arg("-c").arg(c);
}
if let Some(ssh) = ssh {
cmd.env("GIT_SSH_COMMAND", ssh);
}
}
cmd.args(args)
.env("GIT_TERMINAL_PROMPT", "0")
.env("GCM_INTERACTIVE", "never");
let out = process::output(&mut cmd, process::timeout("GIT", 3600))?;
if !out.status.success() {
bail!("{}", String::from_utf8_lossy(&out.stderr).trim());
}
Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
}
const GUARDED: [&str; 3] = ["core.sshcommand", "http.lowspeedlimit", "http.lowspeedtime"];
fn global_settings() -> &'static Vec<String> {
static GLOBAL: OnceLock<Vec<String>> = OnceLock::new();
GLOBAL.get_or_init(|| {
let out = Command::new("git")
.arg("-C")
.arg(std::env::temp_dir())
.args(["config", "--get-regexp", r"^(core|http)\."])
.output();
let Ok(out) = out else { return Vec::new() };
String::from_utf8_lossy(&out.stdout)
.lines()
.filter_map(|l| l.split_whitespace().next())
.map(str::to_lowercase)
.filter(|k| GUARDED.contains(&k.as_str()))
.collect()
})
}
fn repo_settings(config: &Path) -> Vec<String> {
let Ok(text) = std::fs::read_to_string(config) else {
return Vec::new();
};
let mut section = String::new();
let mut found = Vec::new();
for line in text.lines().map(str::trim) {
if let Some(head) = line.strip_prefix('[') {
section = head
.split(|c: char| c == ']' || c.is_whitespace() || c == '"')
.next()
.unwrap_or_default()
.to_lowercase();
} else if let Some(key) = line.split('=').next() {
let key = format!("{section}.{}", key.trim().to_lowercase());
if GUARDED.contains(&key.as_str()) {
found.push(key);
}
}
}
found
}
fn network_guards(user: &[String], env_ssh: bool) -> (Vec<String>, Option<String>) {
let set = |k: &str| user.iter().any(|u| u == k);
let mut config = Vec::new();
if !set("http.lowspeedlimit") && !set("http.lowspeedtime") {
config.push("http.lowSpeedLimit=1000".to_string());
config.push("http.lowSpeedTime=60".to_string());
}
let ssh = (!env_ssh && !set("core.sshcommand")).then(|| SSH_COMMAND.to_string());
(config, ssh)
}
pub fn is_clone(dir: &Path) -> bool {
dir.join(".git").is_dir()
}
pub fn is_dirty(dir: &Path) -> bool {
run(dir, &["status", "--porcelain"]).is_ok_and(|s| !s.is_empty())
}
pub fn available() -> bool {
which::which("git").is_ok()
}
pub fn invalid_on_windows(path: &str) -> bool {
path.split('/').any(|c| {
let stem = c
.split('.')
.next()
.unwrap_or(c)
.trim_end()
.to_ascii_uppercase();
let device = matches!(stem.as_str(), "CON" | "PRN" | "AUX" | "NUL")
|| (stem.len() == 4
&& (stem.starts_with("COM") || stem.starts_with("LPT"))
&& matches!(stem.as_bytes()[3], b'1'..=b'9'));
device
|| c.ends_with(['.', ' '])
|| c.chars().any(|ch| ch < ' ' || r#"<>:"|?*\"#.contains(ch))
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn network_guards_never_override_the_user() {
let (config, ssh) = network_guards(&[], false);
assert_eq!(config, ["http.lowSpeedLimit=1000", "http.lowSpeedTime=60"]);
assert_eq!(ssh.as_deref(), Some(SSH_COMMAND));
let user = [
"core.sshcommand".to_string(),
"http.lowspeedtime".to_string(),
];
assert_eq!(network_guards(&user, false), (vec![], None));
assert_eq!(network_guards(&[], true).1, None);
let (config, ssh) = network_guards(&["core.sshcommand".to_string()], false);
assert_eq!(config, ["http.lowSpeedLimit=1000", "http.lowSpeedTime=60"]);
assert_eq!(ssh, None);
let (config, ssh) = network_guards(&["http.lowspeedlimit".to_string()], false);
assert!(config.is_empty());
assert_eq!(ssh.as_deref(), Some(SSH_COMMAND));
}
#[test]
fn repo_settings_match_what_git_reads() {
let tmp = tempfile::tempdir().unwrap();
run(tmp.path(), &["init", "--quiet"]).unwrap();
let config = tmp.path().join(".git").join("config");
assert!(repo_settings(&config).is_empty());
run(tmp.path(), &["config", "core.sshCommand", "ssh -i key"]).unwrap();
run(
tmp.path(),
&["config", "http.https://h.example.lowSpeedTime", "5"],
)
.unwrap();
run(tmp.path(), &["config", "user.name", "x"]).unwrap();
assert_eq!(
repo_settings(&config),
["core.sshcommand", "http.lowspeedtime"]
);
}
#[test]
fn detects_paths_windows_cannot_hold() {
for bad in [
r#"web/"Geneva": Renew.md"#,
"a|b",
"dir./file",
"trailing ",
"CON",
"x/nul.txt",
"com1.log",
"LPT9",
"back\\slash",
"tab\there",
] {
assert!(invalid_on_windows(bad), "{bad}");
}
for ok in [
"README.md",
".github/ci.yml",
"console.txt",
"COM10",
"a b/c.d",
] {
assert!(!invalid_on_windows(ok), "{ok}");
}
}
}