review_queue/source/
diffstat.rs1pub struct FileChange {
7 pub path: String,
8 pub additions: u64,
9 pub deletions: u64,
10}
11
12const MAX_BAR_WIDTH: usize = 24;
16
17const MAX_FILES_SHOWN: usize = 10;
21
22pub fn format_diffstat(changes: &[FileChange]) -> String {
28 if changes.is_empty() {
29 return String::new();
30 }
31
32 let shown = &changes[..changes.len().min(MAX_FILES_SHOWN)];
33 let name_w = shown
34 .iter()
35 .map(|c| c.path.chars().count())
36 .max()
37 .unwrap_or(0);
38 let max_total = shown
39 .iter()
40 .map(|c| c.additions + c.deletions)
41 .max()
42 .unwrap_or(0)
43 .max(1);
44
45 let mut total_add = 0u64;
46 let mut total_del = 0u64;
47 let mut lines = Vec::with_capacity(shown.len() + 2);
48 for c in changes {
49 total_add += c.additions;
50 total_del += c.deletions;
51 }
52 for c in shown {
53 let total = c.additions + c.deletions;
54
55 let bar_len = ((total as f64 / max_total as f64) * MAX_BAR_WIDTH as f64).round() as usize;
56 let bar_len = if total > 0 { bar_len.max(1) } else { 0 };
57 let plus_len = (bar_len as u64 * c.additions)
58 .checked_div(total)
59 .unwrap_or(0) as usize;
60 let minus_len = bar_len.saturating_sub(plus_len);
61
62 lines.push(format!(
63 "{:<name_w$} | {total:>4} {}{}",
64 c.path,
65 "+".repeat(plus_len),
66 "-".repeat(minus_len),
67 ));
68 }
69 let hidden = changes.len() - shown.len();
70 if hidden > 0 {
71 lines.push(format!(
72 "{hidden} more file{} not shown",
73 if hidden == 1 { "" } else { "s" }
74 ));
75 }
76 lines.push(summary_line(changes.len() as u64, total_add, total_del));
77
78 lines.join("\n")
79}
80
81pub fn format_summary(file_count: u64, additions: u64, deletions: u64) -> String {
84 summary_line(file_count, additions, deletions)
85}
86
87fn summary_line(file_count: u64, total_add: u64, total_del: u64) -> String {
88 let mut summary = format!(
89 "{file_count} file{} changed",
90 if file_count == 1 { "" } else { "s" }
91 );
92 if total_add > 0 {
93 summary.push_str(&format!(
94 ", {total_add} insertion{}(+)",
95 if total_add == 1 { "" } else { "s" }
96 ));
97 }
98 if total_del > 0 {
99 summary.push_str(&format!(
100 ", {total_del} deletion{}(-)",
101 if total_del == 1 { "" } else { "s" }
102 ));
103 }
104 summary
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 #[test]
112 fn empty_changes_render_nothing() {
113 assert_eq!(format_diffstat(&[]), "");
114 }
115
116 #[test]
117 fn single_file_matches_git_style_summary() {
118 let changes = [FileChange {
119 path: "src/main.rs".into(),
120 additions: 3,
121 deletions: 1,
122 }];
123 let out = format_diffstat(&changes);
124 let lines: Vec<&str> = out.lines().collect();
125 assert_eq!(lines.len(), 2);
126 assert!(lines[0].starts_with("src/main.rs | 4 "));
127 assert_eq!(lines[1], "1 file changed, 3 insertions(+), 1 deletion(-)");
128 }
129
130 #[test]
131 fn omits_the_clause_for_a_side_with_no_changes() {
132 let added_only = [FileChange {
133 path: "a.rs".into(),
134 additions: 2,
135 deletions: 0,
136 }];
137 assert_eq!(
138 format_diffstat(&added_only).lines().last().unwrap(),
139 "1 file changed, 2 insertions(+)"
140 );
141
142 let removed_only = [FileChange {
143 path: "a.rs".into(),
144 additions: 0,
145 deletions: 5,
146 }];
147 assert_eq!(
148 format_diffstat(&removed_only).lines().last().unwrap(),
149 "1 file changed, 5 deletions(-)"
150 );
151 }
152
153 #[test]
154 fn bars_scale_relative_to_the_largest_change_and_never_exceed_the_cap() {
155 let changes = [
156 FileChange {
157 path: "big.rs".into(),
158 additions: 100,
159 deletions: 0,
160 },
161 FileChange {
162 path: "small.rs".into(),
163 additions: 1,
164 deletions: 0,
165 },
166 ];
167 let out = format_diffstat(&changes);
168 let lines: Vec<&str> = out.lines().collect();
169 let big_bar = lines[0].split_whitespace().last().unwrap();
170 let small_bar = lines[1].split_whitespace().last().unwrap();
171 assert_eq!(big_bar.len(), MAX_BAR_WIDTH);
172 assert!(small_bar.len() < big_bar.len());
173 }
174
175 #[test]
176 fn caps_file_lines_and_reports_the_rest_as_hidden() {
177 let changes: Vec<FileChange> = (0..12)
178 .map(|i| FileChange {
179 path: format!("f{i}.rs"),
180 additions: 1,
181 deletions: 0,
182 })
183 .collect();
184 let out = format_diffstat(&changes);
185 let lines: Vec<&str> = out.lines().collect();
186
187 assert_eq!(lines.len(), 12);
189 assert_eq!(lines[10], "2 more files not shown");
190 assert_eq!(lines[11], "12 files changed, 12 insertions(+)");
191 }
192
193 #[test]
194 fn does_not_add_a_hidden_line_when_under_the_cap() {
195 let changes = [FileChange {
196 path: "a.rs".into(),
197 additions: 1,
198 deletions: 0,
199 }];
200 let out = format_diffstat(&changes);
201 assert!(!out.contains("not shown"));
202 }
203
204 #[test]
205 fn summary_pluralizes_a_single_file_and_change() {
206 let changes = [FileChange {
207 path: "a.rs".into(),
208 additions: 1,
209 deletions: 0,
210 }];
211 assert_eq!(
212 format_diffstat(&changes).lines().last().unwrap(),
213 "1 file changed, 1 insertion(+)"
214 );
215 }
216
217 #[test]
218 fn format_summary_matches_format_diffstats_own_summary_line() {
219 assert_eq!(
220 format_summary(2, 3, 1),
221 "2 files changed, 3 insertions(+), 1 deletion(-)"
222 );
223 assert_eq!(format_summary(1, 1, 0), "1 file changed, 1 insertion(+)");
224 assert_eq!(format_summary(0, 0, 0), "0 files changed");
225 }
226}