rivet_logger/processors/
git.rs1use 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}