Skip to main content

git_sprout/
stats.rs

1// ABOUTME: Machine-readable statistics describing what a run cloned, skipped and
2// ABOUTME: left to git, emitted when SPROUT_STATS=1 so tests can assert on them.
3
4use std::fs::OpenOptions;
5use std::io::Write;
6use std::path::PathBuf;
7
8/// Which block-cloning primitive a run used.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum CloneBackend {
11    Apfs,
12    Ficlone,
13    Refs,
14    None,
15}
16
17impl CloneBackend {
18    fn as_str(self) -> &'static str {
19        match self {
20            CloneBackend::Apfs => "apfs",
21            CloneBackend::Ficlone => "ficlone",
22            CloneBackend::Refs => "refs",
23            CloneBackend::None => "none",
24        }
25    }
26}
27
28/// What a single `git sprout add` did.
29#[derive(Debug, Clone)]
30pub struct Stats {
31    /// Paths materialised by a block clone.
32    pub cloned: usize,
33    /// Subtrees cloned in a single call rather than file by file.
34    pub cloned_directories: usize,
35    /// Paths the plan considered and rejected, so git had to write them.
36    pub skipped: usize,
37    /// Paths in the target tree that git checked out itself. Zero when no plan ran.
38    pub checked_out_by_git: usize,
39    /// The checkout the clones came from.
40    pub source: Option<PathBuf>,
41    pub clone_backend: CloneBackend,
42    /// True when the run produced its result through plain `git worktree add`.
43    pub fell_back: bool,
44    pub fallback_reason: Option<String>,
45}
46
47impl Default for Stats {
48    fn default() -> Self {
49        Stats {
50            cloned: 0,
51            cloned_directories: 0,
52            skipped: 0,
53            checked_out_by_git: 0,
54            source: None,
55            clone_backend: CloneBackend::None,
56            fell_back: false,
57            fallback_reason: None,
58        }
59    }
60}
61
62impl Stats {
63    /// Records that the run produced its result through plain `git worktree add`.
64    pub fn fall_back(&mut self, reason: impl Into<String>) {
65        self.fell_back = true;
66        self.fallback_reason = Some(reason.into());
67    }
68
69    fn to_json(&self) -> String {
70        let value = serde_json::json!({
71            "cloned": self.cloned,
72            "cloned_directories": self.cloned_directories,
73            "skipped": self.skipped,
74            "checked_out_by_git": self.checked_out_by_git,
75            "source": self.source.as_ref().map(|path| path.to_string_lossy()),
76            "clone_backend": self.clone_backend.as_str(),
77            "fell_back": self.fell_back,
78            "fallback_reason": self.fallback_reason,
79        });
80        value.to_string()
81    }
82
83    /// Writes the statistics where SPROUT_STATS_FILE points, or to stderr.
84    ///
85    /// Failing to report statistics must never fail the operation, so every error
86    /// here is dropped.
87    pub fn emit(&self) {
88        if std::env::var_os("SPROUT_STATS").is_none_or(|value| value != "1") {
89            return;
90        }
91        let json = self.to_json();
92        match std::env::var_os("SPROUT_STATS_FILE") {
93            Some(path) => {
94                if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) {
95                    let _ = writeln!(file, "{json}");
96                }
97            }
98            None => {
99                let _ = writeln!(std::io::stderr(), "sprout-stats: {json}");
100            }
101        }
102    }
103}