Skip to main content

rivet_logger/processors/
git.rs

1use std::collections::BTreeMap;
2use std::process::Command;
3use std::sync::OnceLock;
4
5use crate::logger::{BoxError, Context, Level, LogRecord, LogValue, Processor};
6
7static GIT_INFO_CACHE: OnceLock<Context> = OnceLock::new();
8
9pub struct Git {
10    min_level: Level,
11}
12
13impl Git {
14    pub fn new(min_level: Level) -> Self {
15        Self { min_level }
16    }
17}
18
19impl Default for Git {
20    fn default() -> Self {
21        Self::new(Level::Debug)
22    }
23}
24
25impl Processor for Git {
26    fn process(&self, mut record: LogRecord) -> Result<LogRecord, BoxError> {
27        if record.level < self.min_level {
28            return Ok(record);
29        }
30
31        let info = GIT_INFO_CACHE.get_or_init(git_info).clone();
32        record.extra.insert("git".to_string(), LogValue::Map(info));
33
34        Ok(record)
35    }
36}
37
38fn git_info() -> Context {
39    let output = Command::new("git")
40        .args(["branch", "-v", "--no-abbrev"])
41        .output();
42
43    let Ok(output) = output else {
44        return BTreeMap::new();
45    };
46    if !output.status.success() {
47        return BTreeMap::new();
48    }
49
50    let stdout = String::from_utf8_lossy(&output.stdout);
51    parse_git_branch_output(&stdout).unwrap_or_default()
52}
53
54fn parse_git_branch_output(output: &str) -> Option<Context> {
55    for line in output.lines() {
56        let trimmed = line.trim_start();
57        if !trimmed.starts_with("* ") {
58            continue;
59        }
60
61        let mut parts = trimmed.split_whitespace();
62        let _star = parts.next()?;
63        let branch = parts.next()?;
64        let commit = parts.next()?;
65
66        if commit.len() == 40 && commit.chars().all(|ch| ch.is_ascii_hexdigit()) {
67            let mut info = BTreeMap::new();
68            info.insert("branch".to_string(), LogValue::String(branch.to_string()));
69            info.insert("commit".to_string(), LogValue::String(commit.to_string()));
70            return Some(info);
71        }
72    }
73
74    None
75}
76
77#[cfg(test)]
78mod tests {
79    use crate::logger::LogValue;
80
81    use super::*;
82
83    #[test]
84    fn parses_current_branch_and_commit() {
85        let input = "* main 0123456789abcdef0123456789abcdef01234567 commit message";
86        let parsed = parse_git_branch_output(input).expect("line should parse");
87
88        assert!(matches!(
89            parsed.get("branch"),
90            Some(LogValue::String(value)) if value == "main"
91        ));
92        assert!(matches!(
93            parsed.get("commit"),
94            Some(LogValue::String(value)) if value.len() == 40
95        ));
96    }
97
98    #[test]
99    fn returns_none_for_invalid_commit_hash() {
100        let input = "* main 1234 short";
101        assert!(parse_git_branch_output(input).is_none());
102    }
103}