Skip to main content

mnemo/
gitctx.rs

1//! Détection du contexte Git d'un répertoire, pour enrichir les commandes.
2//!
3//! Git est **optionnel** : si l'exécutable est absent, si le répertoire n'est
4//! pas un dépôt, ou si une commande échoue/dépasse le délai imparti, les champs
5//! concernés valent simplement `None`. mnemo ne dépend jamais de Git pour
6//! fonctionner.
7
8use std::io::Read;
9use std::path::Path;
10use std::process::{Command, Stdio};
11use std::time::{Duration, Instant};
12
13/// Délai maximal accordé à une commande `git` avant abandon (fallback propre).
14const GIT_TIMEOUT: Duration = Duration::from_secs(2);
15
16/// Contexte Git associé à un répertoire de travail.
17#[derive(Debug, Clone, Default, PartialEq, Eq)]
18pub struct GitContext {
19    /// Racine du dépôt (`git rev-parse --show-toplevel`).
20    pub root: Option<String>,
21    /// Branche courante (`git branch --show-current`).
22    pub branch: Option<String>,
23    /// URL du remote `origin` (`git config --get remote.origin.url`).
24    pub remote: Option<String>,
25}
26
27impl GitContext {
28    /// Vrai si aucune information Git n'a pu être collectée.
29    #[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
35/// Détecte le contexte Git pour `cwd`. Ne renvoie jamais d'erreur : en cas de
36/// problème (pas de Git, hors dépôt, timeout…), les champs sont à `None`.
37pub fn detect(cwd: &Path) -> GitContext {
38    // On commence par la racine : si ce n'est pas un dépôt, inutile d'aller plus
39    // loin (les autres appels seraient de toute façon vides).
40    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
51/// Exécute `git -C <cwd> <args...>` avec un délai maximal. Renvoie la sortie
52/// standard nettoyée si la commande réussit et produit un résultat non vide,
53/// sinon `None`.
54fn 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        // Évite tout blocage interactif (prompt d'identifiants, pager, verrous).
60        .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        // /tmp n'est (normalement) pas un dépôt Git.
105        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}