Skip to main content

rivet_logger/processors/
mercurial.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 HG_INFO_CACHE: OnceLock<Context> = OnceLock::new();
8
9pub struct Mercurial {
10    min_level: Level,
11}
12
13impl Mercurial {
14    pub fn new(min_level: Level) -> Self {
15        Self { min_level }
16    }
17}
18
19impl Default for Mercurial {
20    fn default() -> Self {
21        Self::new(Level::Debug)
22    }
23}
24
25impl Processor for Mercurial {
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 = HG_INFO_CACHE.get_or_init(mercurial_info).clone();
32        record.extra.insert("hg".to_string(), LogValue::Map(info));
33
34        Ok(record)
35    }
36}
37
38fn mercurial_info() -> Context {
39    let output = Command::new("hg").args(["id", "-nb"]).output();
40    let Ok(output) = output else {
41        return BTreeMap::new();
42    };
43    if !output.status.success() {
44        return BTreeMap::new();
45    }
46
47    let stdout = String::from_utf8_lossy(&output.stdout);
48    parse_hg_output(stdout.trim()).unwrap_or_default()
49}
50
51fn parse_hg_output(output: &str) -> Option<Context> {
52    let parts: Vec<&str> = output.split_whitespace().collect();
53    match parts.as_slice() {
54        [revision, branch] => {
55            let mut info = BTreeMap::new();
56            info.insert(
57                "branch".to_string(),
58                LogValue::String((*branch).to_string()),
59            );
60            info.insert(
61                "revision".to_string(),
62                LogValue::String((*revision).to_string()),
63            );
64            Some(info)
65        }
66        [_, branch, revision, ..] => {
67            let mut info = BTreeMap::new();
68            info.insert(
69                "branch".to_string(),
70                LogValue::String((*branch).to_string()),
71            );
72            info.insert(
73                "revision".to_string(),
74                LogValue::String((*revision).to_string()),
75            );
76            Some(info)
77        }
78        _ => None,
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use crate::logger::LogValue;
85
86    use super::*;
87
88    #[test]
89    fn parses_two_token_hg_output() {
90        let parsed = parse_hg_output("abc123 default").expect("output should parse");
91        assert!(matches!(
92            parsed.get("branch"),
93            Some(LogValue::String(value)) if value == "default"
94        ));
95        assert!(matches!(
96            parsed.get("revision"),
97            Some(LogValue::String(value)) if value == "abc123"
98        ));
99    }
100
101    #[test]
102    fn parses_three_token_hg_output() {
103        let parsed = parse_hg_output("123 default 456").expect("output should parse");
104        assert!(matches!(
105            parsed.get("revision"),
106            Some(LogValue::String(value)) if value == "456"
107        ));
108    }
109}