Skip to main content

verbs/
maintenance_plan.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Pure maintenance inspect/refresh **fact** types + one human renderer each.
3//!
4//! Not twenty public `format!` sentence factories. Callers pass structured
5//! fields; CLI prints `lines()`. Repo I/O stays outside this module.
6
7/// Re-export compact yes/no token (same as log/timeline fields).
8pub use crate::log_plan::yes_no;
9
10/// present/absent token for index and cache presence fields.
11pub fn presence(value: bool) -> &'static str {
12    if value { "present" } else { "absent" }
13}
14
15/// Structured facts for `heddle maintenance inspect` human output.
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct MaintenanceInspectView {
18    pub commit_graph_present: bool,
19    pub commit_graph_nodes: usize,
20    pub commit_graph_bloom: usize,
21    pub worktree_index_present: bool,
22    pub worktree_index_files: usize,
23    pub worktree_index_directories: usize,
24    pub worktree_index_untracked_directories: usize,
25    pub change_monitor_backend: String,
26    pub change_monitor_status: String,
27    pub refs_threads: usize,
28    pub refs_markers: usize,
29    pub refs_remotes: usize,
30    pub refs_remote_threads: usize,
31    pub ref_summary_present: bool,
32    pub ref_summary_valid: bool,
33    pub ref_summary_threads: usize,
34    pub ref_summary_markers: usize,
35    pub ref_summary_remotes: usize,
36    pub ref_summary_remote_threads: usize,
37    pub pack_count: usize,
38    pub index_count: usize,
39    pub unpaired_packs: usize,
40    pub pending_install_intents: usize,
41    pub missing_blob_count: usize,
42    pub pull_planner_status: String,
43    pub pull_planner_manifests: usize,
44    pub pull_planner_entries: usize,
45}
46
47impl MaintenanceInspectView {
48    /// Human lines for inspect (stable order).
49    pub fn lines(&self) -> Vec<String> {
50        vec![
51            format!(
52                "Commit graph: {} (nodes: {}, bloom-covered: {})",
53                presence(self.commit_graph_present),
54                self.commit_graph_nodes,
55                self.commit_graph_bloom
56            ),
57            format!(
58                "Worktree index: {} (files: {}, directories: {}, untracked directories: {})",
59                presence(self.worktree_index_present),
60                self.worktree_index_files,
61                self.worktree_index_directories,
62                self.worktree_index_untracked_directories
63            ),
64            format!(
65                "Change monitor: {} / {}",
66                self.change_monitor_backend, self.change_monitor_status
67            ),
68            format!(
69                "Refs: {} threads, {} markers, {} remotes, {} remote threads",
70                self.refs_threads, self.refs_markers, self.refs_remotes, self.refs_remote_threads
71            ),
72            format!(
73                "Ref summary index: {} (valid: {}, threads: {}, markers: {}, remotes: {}, remote threads: {})",
74                presence(self.ref_summary_present),
75                yes_no(self.ref_summary_valid),
76                self.ref_summary_threads,
77                self.ref_summary_markers,
78                self.ref_summary_remotes,
79                self.ref_summary_remote_threads
80            ),
81            format!(
82                "Packs: {} pack files, {} indexes",
83                self.pack_count, self.index_count
84            ),
85            format!(
86                "Pack install: {} unpaired packs, {} pending install intents",
87                self.unpaired_packs, self.pending_install_intents
88            ),
89            format!("Partial fetch: {} missing blobs", self.missing_blob_count),
90            format!(
91                "Pull planner cache: {} (manifests: {}, planner entries: {})",
92                self.pull_planner_status, self.pull_planner_manifests, self.pull_planner_entries
93            ),
94        ]
95    }
96}
97
98/// Structured facts for `heddle maintenance refresh` human output.
99#[derive(Debug, Clone, PartialEq, Eq)]
100pub struct MaintenanceRefreshView {
101    pub rebuilt_commit_graph: bool,
102    pub rebuilt_ref_summary_index: bool,
103    pub rebuilt_worktree_index: bool,
104    pub refreshed_change_monitor: bool,
105    pub rebuilt_pull_planner_cache: bool,
106    pub pruned_pull_planner_entries: usize,
107    pub pack_install_completed: u64,
108    pub pack_install_aborted: u64,
109    pub pack_install_skipped: u64,
110    pub pack_install_quarantined: u64,
111    pub unpaired_packs_pruned: u64,
112    pub unpaired_pack_bytes_freed: u64,
113    pub commit_graph_nodes_now: usize,
114    pub commit_graph_bloom_now: usize,
115    pub ref_summary_threads_now: usize,
116    pub ref_summary_markers_now: usize,
117    pub ref_summary_remotes_now: usize,
118    pub ref_summary_remote_threads_now: usize,
119    pub worktree_index_files_now: usize,
120    pub pull_planner_manifests_now: usize,
121    pub pull_planner_entries_now: usize,
122}
123
124impl MaintenanceRefreshView {
125    /// Human lines for refresh (stable order).
126    pub fn lines(&self) -> Vec<String> {
127        let mut lines = vec![
128            format!(
129                "Rebuilt commit graph: {}",
130                yes_no(self.rebuilt_commit_graph)
131            ),
132            format!(
133                "Rebuilt ref summary index: {}",
134                yes_no(self.rebuilt_ref_summary_index)
135            ),
136            format!(
137                "Rebuilt worktree index: {}",
138                yes_no(self.rebuilt_worktree_index)
139            ),
140            format!(
141                "Refreshed change monitor: {}",
142                yes_no(self.refreshed_change_monitor)
143            ),
144            format!(
145                "Rebuilt pull planner cache: {}",
146                yes_no(self.rebuilt_pull_planner_cache)
147            ),
148            format!(
149                "Pruned pull planner entries: {}",
150                self.pruned_pull_planner_entries
151            ),
152        ];
153        let mut recover = format!(
154            "Pack install intents recovered: {} completed, {} aborted",
155            self.pack_install_completed, self.pack_install_aborted
156        );
157        if self.pack_install_skipped > 0 || self.pack_install_quarantined > 0 {
158            recover.push_str(&format!(
159                " (skipped in-progress: {}, quarantined: {})",
160                self.pack_install_skipped, self.pack_install_quarantined
161            ));
162        }
163        lines.push(recover);
164        lines.push(if self.unpaired_packs_pruned > 0 {
165            format!(
166                "Pruned {} unpaired packs (freed {} bytes)",
167                self.unpaired_packs_pruned, self.unpaired_pack_bytes_freed
168            )
169        } else {
170            "No unpaired packs to prune".to_string()
171        });
172        lines.push(format!(
173            "Commit graph now has {} nodes and {} Bloom-covered nodes",
174            self.commit_graph_nodes_now, self.commit_graph_bloom_now
175        ));
176        lines.push(format!(
177            "Ref summary index now covers {} threads, {} markers, {} remotes, and {} remote threads",
178            self.ref_summary_threads_now,
179            self.ref_summary_markers_now,
180            self.ref_summary_remotes_now,
181            self.ref_summary_remote_threads_now
182        ));
183        lines.push(format!(
184            "Worktree index now has {} files cached",
185            self.worktree_index_files_now
186        ));
187        lines.push(format!(
188            "Pull planner cache now has {} manifests and {} planner entries",
189            self.pull_planner_manifests_now, self.pull_planner_entries_now
190        ));
191        lines
192    }
193}
194
195/// Compact GC recover summary line.
196pub fn pack_install_recover_line(completed: u64, aborted: u64) -> String {
197    format!("Pack install intents recovered: {completed} completed, {aborted} aborted")
198}
199
200/// Compact unpaired-pack prune summary line (GC path).
201pub fn unpaired_packs_pruned_line(removed: u64, bytes_freed: u64) -> String {
202    if removed > 0 {
203        format!("Pruned {removed} unpaired packs (freed {bytes_freed} bytes)")
204    } else {
205        "No unpaired packs to prune".to_string()
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    #[test]
214    fn inspect_view_lines_include_packs_and_install() {
215        let v = MaintenanceInspectView {
216            commit_graph_present: true,
217            commit_graph_nodes: 10,
218            commit_graph_bloom: 4,
219            worktree_index_present: false,
220            worktree_index_files: 1,
221            worktree_index_directories: 2,
222            worktree_index_untracked_directories: 3,
223            change_monitor_backend: "fs_events".into(),
224            change_monitor_status: "ready".into(),
225            refs_threads: 1,
226            refs_markers: 2,
227            refs_remotes: 3,
228            refs_remote_threads: 4,
229            ref_summary_present: true,
230            ref_summary_valid: false,
231            ref_summary_threads: 1,
232            ref_summary_markers: 2,
233            ref_summary_remotes: 3,
234            ref_summary_remote_threads: 4,
235            pack_count: 2,
236            index_count: 2,
237            unpaired_packs: 1,
238            pending_install_intents: 2,
239            missing_blob_count: 5,
240            pull_planner_status: "ready".into(),
241            pull_planner_manifests: 1,
242            pull_planner_entries: 2,
243        };
244        let lines = v.lines();
245        assert!(lines[0].contains("Commit graph: present"));
246        assert!(lines.iter().any(|l| l.contains("Pack install: 1 unpaired")));
247        assert!(lines.iter().any(|l| l.contains("Partial fetch: 5")));
248    }
249
250    #[test]
251    fn refresh_view_lines_include_recover_detail() {
252        let v = MaintenanceRefreshView {
253            rebuilt_commit_graph: true,
254            rebuilt_ref_summary_index: false,
255            rebuilt_worktree_index: true,
256            refreshed_change_monitor: false,
257            rebuilt_pull_planner_cache: true,
258            pruned_pull_planner_entries: 3,
259            pack_install_completed: 0,
260            pack_install_aborted: 1,
261            pack_install_skipped: 3,
262            pack_install_quarantined: 2,
263            unpaired_packs_pruned: 0,
264            unpaired_pack_bytes_freed: 0,
265            commit_graph_nodes_now: 9,
266            commit_graph_bloom_now: 2,
267            ref_summary_threads_now: 1,
268            ref_summary_markers_now: 2,
269            ref_summary_remotes_now: 3,
270            ref_summary_remote_threads_now: 4,
271            worktree_index_files_now: 42,
272            pull_planner_manifests_now: 1,
273            pull_planner_entries_now: 2,
274        };
275        let lines = v.lines();
276        assert!(lines.iter().any(|l| l.contains("skipped in-progress: 3")));
277        assert!(lines.iter().any(|l| l.contains("No unpaired packs")));
278        assert_eq!(
279            pack_install_recover_line(2, 1),
280            "Pack install intents recovered: 2 completed, 1 aborted"
281        );
282        assert_eq!(
283            unpaired_packs_pruned_line(3, 99),
284            "Pruned 3 unpaired packs (freed 99 bytes)"
285        );
286    }
287}