heddle_cli_render/cli/render/
fsck.rs1use anyhow::Result;
3use verbs::FsckReport;
4
5use crate::cli::{render::write_stdout, style};
6
7pub fn fsck_json(report: &FsckReport) -> Result<()> {
8 let mut text = serde_json::to_string(report)?;
9 text.push('\n');
10 write_stdout(&text)
11}
12
13pub fn fsck_text(report: &FsckReport) -> Result<()> {
14 write_stdout(&format_fsck_text(report))
15}
16
17fn format_fsck_text(report: &FsckReport) -> String {
18 let mut text = String::new();
19 if report.valid {
20 let counted = style::count(report.objects_checked, "object");
21 text.push_str(&format!(
22 "{} repository is valid ({counted} checked)\n",
23 style::ok_marker(),
24 ));
25 if report.git_projection_checked {
26 text.push_str(&format!(
27 " {}\n",
28 style::field("Git projection", "mapping, notes, and checkout checked")
29 ));
30 }
31 } else {
32 text.push_str(&format!(
33 "{} repository has {}\n",
34 style::error_marker(),
35 style::count(report.errors.len(), "integrity error")
36 ));
37 for error in &report.errors {
38 if let Some(obj) = &error.object {
39 text.push_str(&format!(
40 " {} {} {}\n",
41 style::error(&format!("[{}]", style::human_text(&error.kind))),
42 style::human_text(&error.message),
43 style::dim(&format!("({})", style::human_text(obj)))
44 ));
45 } else {
46 text.push_str(&format!(
47 " {} {}\n",
48 style::error(&format!("[{}]", style::human_text(&error.kind))),
49 style::human_text(&error.message)
50 ));
51 }
52 }
53 }
54 if let Some(target) = &report.repair_target {
55 let status = if report.repaired {
56 "repaired"
57 } else {
58 "no changes"
59 };
60 text.push_str(&format!(
61 " {}\n",
62 style::field(
63 "Repair",
64 &format!("{}: {status}", style::human_text(target))
65 )
66 ));
67 for repair in &report.repairs {
68 if repair.count > 0 || repair.repaired {
69 text.push_str(&format!(
70 " {} {} ({})\n",
71 style::human_text(&repair.name),
72 style::human_text(&repair.detail),
73 repair.count
74 ));
75 }
76 }
77 }
78 if let Some(provenance) = &report.provenance {
79 text.push_str(&format!(
80 " {}\n",
81 style::field(
82 "Provenance registry",
83 &style::human_text(&provenance.registry_status)
84 )
85 ));
86 for state in &provenance.states {
87 let short = state
88 .state_id
89 .strip_prefix("hs-")
90 .unwrap_or(&state.state_id);
91 let short = &short[..short.len().min(12)];
92 text.push_str(&format!(
93 " {short} {:<24} {}\n",
94 state.display_status(),
95 style::human_text(&state.detail)
96 ));
97 }
98 }
99 for warning in &report.warnings {
100 text.push_str(&format!(
101 "{} {}\n",
102 style::warn_marker(),
103 style::human_text(warning)
104 ));
105 }
106 text
107}
108
109#[cfg(test)]
110mod tests {
111 use verbs::FsckError;
112
113 use super::*;
114
115 #[test]
116 fn text_renderer_consumes_the_typed_fsck_report() {
117 let report = FsckReport {
118 valid: true,
119 errors: Vec::new(),
120 warnings: vec!["legacy object retained".to_string()],
121 objects_checked: 2,
122 git_projection_checked: false,
123 provenance: None,
124 repair_target: None,
125 repaired: false,
126 repairs: Vec::new(),
127 };
128
129 let text = format_fsck_text(&report);
130 assert!(text.contains("repository is valid (2 objects checked)"));
131 assert!(text.contains("legacy object retained"));
132 }
133
134 #[test]
135 fn text_renderer_shortens_corrupt_object_id() {
136 let id = "a".repeat(64);
137 let report = FsckReport {
138 valid: false,
139 errors: vec![FsckError {
140 kind: "missing_tree".to_string(),
141 message: format!("missing tree object {id}"),
142 object: Some(id.clone()),
143 }],
144 warnings: Vec::new(),
145 objects_checked: 1,
146 git_projection_checked: false,
147 provenance: None,
148 repair_target: None,
149 repaired: false,
150 repairs: Vec::new(),
151 };
152 let text = format_fsck_text(&report);
153 assert!(text.contains("hs-aaaaaaaa"), "{text}");
154 assert!(!text.contains(&id), "{text}");
155 assert!(serde_json::to_string(&report).unwrap().contains(&id));
156 }
157}