Skip to main content

git_perf/
status.rs

1use crate::git::git_interop::{
2    create_consolidated_pending_read_branch, get_commits_with_notes_content,
3};
4use crate::serialization::deserialize;
5use anyhow::Result;
6use std::collections::HashSet;
7
8/// Information about pending measurements
9#[derive(Debug)]
10pub struct PendingStatus {
11    /// Total number of commits with pending measurements
12    pub commit_count: usize,
13
14    /// Total number of measurements across all commits
15    pub measurement_count: usize,
16
17    /// Unique measurement names found in pending writes
18    pub measurement_names: HashSet<String>,
19
20    /// Per-commit breakdown (if detailed)
21    pub per_commit: Option<Vec<CommitMeasurements>>,
22}
23
24/// Measurements for a specific commit
25#[derive(Debug)]
26pub struct CommitMeasurements {
27    /// Commit SHA
28    pub commit: String,
29
30    /// Commit title
31    pub title: String,
32
33    /// Measurement names in this commit
34    pub measurement_names: Vec<String>,
35
36    /// Number of measurements in this commit
37    pub count: usize,
38}
39
40/// Display pending measurement status
41pub fn show_status(detailed: bool) -> Result<()> {
42    // 1. Check if there are any pending measurements
43    let status = gather_pending_status(detailed)?;
44
45    // 2. Display results
46    display_status(&status, detailed)?;
47
48    Ok(())
49}
50
51/// Gather information about pending measurements
52pub fn gather_pending_status(detailed: bool) -> Result<PendingStatus> {
53    // Create a consolidated read branch that includes ONLY pending writes
54    // (not the remote branch). After a successful push, the write refs are deleted,
55    // so this branch only contains measurements that haven't been pushed yet.
56    let pending_guard = create_consolidated_pending_read_branch()?;
57
58    // Get the temporary ref name from the guard
59    let pending_ref = pending_guard.ref_name();
60
61    // Batch-fetch all commits with notes + their content and metadata in 2 git
62    // calls total, regardless of how many commits have pending measurements.
63    let commits_with_notes = get_commits_with_notes_content(pending_ref)?;
64
65    let mut commit_count = 0;
66    let mut measurement_count = 0;
67    let mut all_measurement_names = HashSet::new();
68    let mut per_commit = if detailed { Some(Vec::new()) } else { None };
69
70    for commit in &commits_with_notes {
71        // Deserialize measurements from note
72        let note_text = commit.note_lines.join("\n");
73        let measurements = deserialize(&note_text);
74
75        if measurements.is_empty() {
76            continue;
77        }
78
79        commit_count += 1;
80        measurement_count += measurements.len();
81
82        // Collect measurement names
83        let measurement_names: Vec<String> = measurements.iter().map(|m| m.name.clone()).collect();
84
85        for name in &measurement_names {
86            all_measurement_names.insert(name.clone());
87        }
88
89        // Store per-commit details if requested (title already fetched in batch)
90        if let Some(ref mut per_commit_vec) = per_commit {
91            per_commit_vec.push(CommitMeasurements {
92                commit: commit.sha.clone(),
93                title: commit.title.clone(),
94                measurement_names,
95                count: measurements.len(),
96            });
97        }
98    }
99
100    Ok(PendingStatus {
101        commit_count,
102        measurement_count,
103        measurement_names: all_measurement_names,
104        per_commit,
105    })
106}
107
108/// Display status information to stdout
109fn display_status(status: &PendingStatus, detailed: bool) -> Result<()> {
110    if status.commit_count == 0 {
111        println!("No pending measurements.");
112        println!("(use \"git perf add\" or \"git perf measure\" to add measurements)");
113        return Ok(());
114    }
115
116    println!("Pending measurements:");
117    let commit_word = if status.commit_count == 1 {
118        "commit"
119    } else {
120        "commits"
121    };
122    println!(
123        "  {} {} with measurements",
124        status.commit_count, commit_word
125    );
126    let measurement_word = if status.measurement_names.len() == 1 {
127        "measurement"
128    } else {
129        "measurements"
130    };
131    println!(
132        "  {} unique {}",
133        status.measurement_names.len(),
134        measurement_word
135    );
136    println!();
137
138    if !status.measurement_names.is_empty() {
139        println!("Measurement names:");
140        let mut sorted_names: Vec<_> = status.measurement_names.iter().collect();
141        sorted_names.sort();
142        for name in sorted_names {
143            println!("  - {}", name);
144        }
145        println!();
146    }
147
148    if detailed {
149        if let Some(ref per_commit) = status.per_commit {
150            println!("Per-commit breakdown:");
151            for commit_info in per_commit {
152                let short_sha = if commit_info.commit.len() >= 12 {
153                    &commit_info.commit[..12]
154                } else {
155                    &commit_info.commit
156                };
157                let meas_word = if commit_info.count == 1 {
158                    "measurement"
159                } else {
160                    "measurements"
161                };
162                println!(
163                    "  {} ({} {}) - {}",
164                    short_sha, commit_info.count, meas_word, commit_info.title
165                );
166                for name in &commit_info.measurement_names {
167                    println!("    - {}", name);
168                }
169            }
170            println!();
171        }
172    }
173
174    println!("(use \"git perf reset\" to discard pending measurements)");
175    println!("(use \"git perf push\" to publish measurements)");
176
177    Ok(())
178}