1use std::io::Read;
9use std::path::Path;
10use std::process::{Command, Stdio};
11use std::time::{Duration, Instant};
12
13const GIT_TIMEOUT: Duration = Duration::from_secs(2);
15
16#[derive(Debug, Clone, Default, PartialEq, Eq)]
18pub struct GitContext {
19 pub root: Option<String>,
21 pub branch: Option<String>,
23 pub remote: Option<String>,
25}
26
27impl GitContext {
28 #[allow(dead_code)]
30 pub fn is_empty(&self) -> bool {
31 self.root.is_none() && self.branch.is_none() && self.remote.is_none()
32 }
33}
34
35pub fn detect(cwd: &Path) -> GitContext {
38 let root = run_git(cwd, &["rev-parse", "--show-toplevel"]);
41 if root.is_none() {
42 return GitContext::default();
43 }
44 GitContext {
45 root,
46 branch: run_git(cwd, &["branch", "--show-current"]),
47 remote: run_git(cwd, &["config", "--get", "remote.origin.url"]),
48 }
49}
50
51fn run_git(cwd: &Path, args: &[&str]) -> Option<String> {
55 let mut child = Command::new("git")
56 .arg("-C")
57 .arg(cwd)
58 .args(args)
59 .env("GIT_TERMINAL_PROMPT", "0")
61 .env("GIT_OPTIONAL_LOCKS", "0")
62 .stdin(Stdio::null())
63 .stdout(Stdio::piped())
64 .stderr(Stdio::null())
65 .spawn()
66 .ok()?;
67
68 let deadline = Instant::now() + GIT_TIMEOUT;
69 loop {
70 match child.try_wait() {
71 Ok(Some(status)) => {
72 if !status.success() {
73 return None;
74 }
75 let mut out = String::new();
76 child.stdout.take()?.read_to_string(&mut out).ok()?;
77 let trimmed = out.trim();
78 return if trimmed.is_empty() {
79 None
80 } else {
81 Some(trimmed.to_string())
82 };
83 }
84 Ok(None) => {
85 if Instant::now() >= deadline {
86 let _ = child.kill();
87 let _ = child.wait();
88 return None;
89 }
90 std::thread::sleep(Duration::from_millis(10));
91 }
92 Err(_) => return None,
93 }
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use std::path::PathBuf;
101
102 #[test]
103 fn hors_depot_renvoie_un_contexte_vide() {
104 let ctx = detect(&PathBuf::from("/"));
106 assert!(ctx.is_empty(), "racine systeme ne doit pas etre un depot");
107 }
108
109 #[test]
110 fn chemin_inexistant_ne_panique_pas() {
111 let ctx = detect(&PathBuf::from("/chemin/qui/n/existe/pas/12345"));
112 assert!(ctx.is_empty());
113 }
114}