• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

MitMaro / git-interactive-rebase-tool / 14537971056

18 Apr 2025 04:04PM UTC coverage: 97.236% (-0.1%) from 97.339%
14537971056

Pull #959

github

web-flow
Merge c6e297710 into d407c14ab
Pull Request #959: WIP: Move Diff to Thread

459 of 489 new or added lines in 33 files covered. (93.87%)

4 existing lines in 2 files now uncovered.

4819 of 4956 relevant lines covered (97.24%)

2.74 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.42
/src/modules/show_commit/view_builder.rs
1
use git2::ErrorCode;
2

3
use crate::{
4
        components::spin_indicator::SpinIndicator,
5
        diff::{Commit, CommitDiff, DiffLine, Origin, thread::LoadStatus},
6
        display::DisplayColor,
7
        modules::show_commit::util::{
8
                get_files_changed_summary,
9
                get_partition_index_on_whitespace_for_line,
10
                get_stat_item_segments,
11
        },
12
        view::{LineSegment, LineSegmentOptions, ViewDataUpdater, ViewLine},
13
};
14

15
const PADDING_CHARACTER: char = '\u{2015}'; // '―'
16

17
pub(super) struct ViewBuilderOptions {
18
        space_character: String,
19
        tab_character: String,
20
        tab_width: usize,
21
        show_leading_whitespace: bool,
22
        show_trailing_whitespace: bool,
23
}
24

25
impl ViewBuilderOptions {
26
        pub(crate) fn new(
1✔
27
                tab_width: usize,
28
                tab_character: &str,
29
                space_character: &str,
30
                show_leading_whitespace: bool,
31
                show_trailing_whitespace: bool,
32
        ) -> Self {
33
                Self {
34
                        space_character: String::from(space_character),
1✔
35
                        tab_character: String::from(tab_character),
1✔
36
                        tab_width,
37
                        show_leading_whitespace,
38
                        show_trailing_whitespace,
39
                }
40
        }
41
}
42

43
pub(super) struct ViewBuilder {
44
        invisible_tab_string: String,
45
        visible_tab_string: String,
46
        visible_space_string: String,
47
        show_leading_whitespace: bool,
48
        show_trailing_whitespace: bool,
49
        spin_indicator: SpinIndicator,
50
}
51

52
impl ViewBuilder {
53
        pub(crate) fn new(options: ViewBuilderOptions) -> Self {
1✔
54
                Self {
55
                        invisible_tab_string: " ".repeat(options.tab_width),
1✔
56
                        visible_tab_string: format!("{0:width$}", options.tab_character, width = options.tab_width),
2✔
57
                        visible_space_string: options.space_character,
1✔
58
                        show_leading_whitespace: options.show_leading_whitespace,
1✔
59
                        show_trailing_whitespace: options.show_trailing_whitespace,
1✔
60
                        spin_indicator: SpinIndicator::new(),
1✔
61
                }
62
        }
63

64
        fn replace_whitespace(&self, value: &str, visible: bool) -> String {
1✔
65
                if visible {
3✔
66
                        value
2✔
67
                                .replace(' ', self.visible_space_string.as_str())
1✔
68
                                .replace('\t', self.visible_tab_string.as_str())
1✔
69
                }
70
                else {
71
                        value.replace('\t', self.invisible_tab_string.as_str())
1✔
72
                }
73
                .replace('\n', "")
74
        }
75

76
        #[expect(
77
                clippy::string_slice,
78
                reason = "Safe slice, as it is only on the hash, which is hexadecimal"
79
        )]
80
        fn build_leading_summary(commit: &Commit, is_full_width: bool) -> ViewLine {
2✔
81
                let mut segments = vec![];
1✔
82
                if is_full_width {
2✔
83
                        segments.push(LineSegment::new_with_color("Commit: ", DisplayColor::IndicatorColor));
3✔
84
                }
85
                let hash = String::from(commit.hash());
2✔
86
                segments.push(LineSegment::new(
2✔
87
                        if is_full_width {
5✔
88
                                hash
1✔
89
                        }
90
                        else {
91
                                let max_index = hash.len().min(8);
2✔
92
                                format!("{:8}", hash[0..max_index].to_owned())
1✔
93
                        }
94
                        .as_str(),
95
                ));
96
                ViewLine::from(segments)
1✔
97
        }
98

99
        fn build_progress(spin: &mut SpinIndicator, msg: &str, progress: Option<(usize, usize)>) -> ViewLine {
1✔
100
                if let Some((c, t)) = progress {
1✔
101
                        spin.refresh();
1✔
102
                        ViewLine::from(LineSegment::new_with_color(
1✔
103
                                format!("{msg} {} [{}/{}]", spin.indicator(), c, t).as_str(),
1✔
104
                                DisplayColor::IndicatorColor,
1✔
105
                        ))
106
                }
107
                else {
108
                        ViewLine::from(LineSegment::new_with_color(msg, DisplayColor::IndicatorColor))
1✔
109
                }
110
        }
111

112
        fn build_loading_status(&mut self, updater: &mut ViewDataUpdater<'_>, load_status: &LoadStatus) -> bool {
2✔
113
                if load_status == &LoadStatus::DiffComplete {
2✔
114
                        return true;
2✔
115
                }
116

117
                if let LoadStatus::Error { code, msg, .. } = &load_status {
2✔
118
                        updater.push_line(ViewLine::from(LineSegment::new_with_color(
1✔
119
                                "Error loading diff. Press any key to return.",
120
                                DisplayColor::IndicatorColor,
1✔
121
                        )));
122
                        updater.push_line(ViewLine::from(""));
1✔
123
                        updater.push_line(ViewLine::from("Reason:"));
1✔
124
                        updater.push_line(ViewLine::from(match code {
2✔
125
                                ErrorCode::NotFound => "Commit not found",
1✔
126
                                _ => msg.as_str(),
1✔
127
                        }));
128
                        return false;
1✔
129
                }
130

131
                updater.push_trailing_line(ViewBuilder::build_progress(
1✔
132
                        &mut self.spin_indicator,
1✔
133
                        match load_status {
1✔
134
                                LoadStatus::New | LoadStatus::QuickDiff(..) | LoadStatus::DiffComplete => "Loading Diff",
1✔
135
                                LoadStatus::CompleteQuickDiff | LoadStatus::Diff(..) => "Detecting renames and copies",
1✔
NEW
136
                                LoadStatus::Error { .. } => "",
×
137
                        },
138
                        match load_status {
2✔
139
                                LoadStatus::New
140
                                | LoadStatus::CompleteQuickDiff
141
                                | LoadStatus::DiffComplete
142
                                | LoadStatus::Error { .. } => None,
1✔
143
                                LoadStatus::QuickDiff(c, t) | LoadStatus::Diff(c, t) => Some((*c, *t)),
3✔
144
                        },
145
                ));
146

147
                load_status != &LoadStatus::New
1✔
148
        }
149

150
        pub(super) fn build_view_data_for_overview(
1✔
151
                &mut self,
152
                updater: &mut ViewDataUpdater<'_>,
153
                diff: &CommitDiff,
154
                load_status: &LoadStatus,
155
                is_full_width: bool,
156
        ) {
157
                updater.clear();
1✔
158
                if !self.build_loading_status(updater, load_status) {
1✔
159
                        return;
160
                }
161

162
                let commit = diff.commit();
1✔
163
                updater.push_leading_line(Self::build_leading_summary(commit, is_full_width));
1✔
164
                // TODO handle authored date
165
                updater.push_line(ViewLine::from(vec![
5✔
166
                        LineSegment::new_with_color(
3✔
167
                                if is_full_width { "Date: " } else { "D: " },
2✔
168
                                DisplayColor::IndicatorColor,
1✔
169
                        ),
170
                        LineSegment::new(commit.committed_date().format("%c %z").to_string().as_str()),
8✔
171
                ]));
172

173
                if commit.author().is_some() {
1✔
174
                        updater.push_line(ViewLine::from(vec![
5✔
175
                                LineSegment::new_with_color(
2✔
176
                                        if is_full_width { "Author: " } else { "A: " },
2✔
177
                                        DisplayColor::IndicatorColor,
2✔
178
                                ),
179
                                LineSegment::new(commit.author().to_string().as_str()),
6✔
180
                        ]));
181
                }
182

183
                if let Some(committer) = commit.committer() {
5✔
184
                        updater.push_line(ViewLine::from(vec![
6✔
185
                                LineSegment::new_with_color(
2✔
186
                                        if is_full_width { "Committer: " } else { "C: " },
3✔
187
                                        DisplayColor::IndicatorColor,
1✔
188
                                ),
189
                                LineSegment::new(committer.to_string().as_str()),
6✔
190
                        ]));
191
                }
192

193
                if let Some(summary) = commit.summary() {
2✔
194
                        updater.push_lines(summary);
1✔
195
                        updater.push_line(ViewLine::from(""));
1✔
196
                }
197

198
                if let Some(message) = commit.message() {
3✔
199
                        updater.push_lines(message);
1✔
200
                        updater.push_line(ViewLine::from(""));
1✔
201
                }
202

203
                if commit.summary().is_none() && commit.message().is_none() {
4✔
204
                        updater.push_line(ViewLine::from(""));
1✔
205
                }
206

207
                updater.push_line(get_files_changed_summary(diff, is_full_width));
3✔
208
                for status in diff.file_statuses() {
4✔
209
                        updater.push_line(ViewLine::from(get_stat_item_segments(
2✔
210
                                status.status(),
2✔
211
                                status.destination_path(),
2✔
212
                                status.source_path(),
2✔
213
                                is_full_width,
214
                        )));
215
                }
216
        }
217

218
        fn build_diff_line_line_segment(content: &str, origin: Origin) -> LineSegment {
1✔
219
                LineSegment::new_with_color(content, match origin {
2✔
220
                        Origin::Addition => DisplayColor::DiffAddColor,
1✔
221
                        Origin::Deletion => DisplayColor::DiffRemoveColor,
2✔
222
                        Origin::Context | Origin::Binary | Origin::Header => DisplayColor::DiffContextColor,
1✔
223
                })
224
        }
225

226
        #[expect(clippy::string_slice, reason = "Safe slice, only slices across graphemes whitespace")]
227
        fn get_diff_line_segments(
1✔
228
                &self,
229
                diff_line: &DiffLine,
230
                old_largest_line_number_length: usize,
231
                new_largest_line_number_length: usize,
232
        ) -> Vec<LineSegment> {
233
                let mut line_segments = vec![
2✔
234
                        match diff_line.old_line_number() {
2✔
235
                                Some(line_number) => {
1✔
236
                                        LineSegment::new(format!("{line_number:<old_largest_line_number_length$}").as_str())
3✔
237
                                },
238
                                None => LineSegment::new(" ".repeat(old_largest_line_number_length).as_str()),
1✔
239
                        },
240
                        LineSegment::new(" "),
1✔
241
                        match diff_line.new_line_number() {
2✔
242
                                Some(line_number) => {
1✔
243
                                        LineSegment::new(format!("{line_number:<new_largest_line_number_length$}").as_str())
3✔
244
                                },
245
                                None => LineSegment::new(" ".repeat(new_largest_line_number_length).as_str()),
2✔
246
                        },
247
                        LineSegment::new("| "),
1✔
248
                ];
249

250
                if self.show_leading_whitespace || self.show_trailing_whitespace {
2✔
251
                        let line = diff_line.line();
2✔
252
                        let (leading, content, trailing) = if line.trim().is_empty() {
3✔
253
                                (
254
                                        self.replace_whitespace(line, self.show_leading_whitespace || self.show_trailing_whitespace),
1✔
255
                                        String::new(),
1✔
256
                                        String::new(),
1✔
257
                                )
258
                        }
259
                        else {
260
                                let (start, end) = get_partition_index_on_whitespace_for_line(line);
2✔
261
                                (
262
                                        self.replace_whitespace(&line[0..start], self.show_leading_whitespace),
1✔
263
                                        self.replace_whitespace(&line[start..end], false),
2✔
264
                                        self.replace_whitespace(&line[end..], self.show_trailing_whitespace),
2✔
265
                                )
266
                        };
267

268
                        if !leading.is_empty() {
2✔
269
                                line_segments.push(LineSegment::new_with_color(
1✔
270
                                        leading.as_str(),
1✔
271
                                        DisplayColor::DiffWhitespaceColor,
1✔
272
                                ));
273
                        }
274
                        if !content.is_empty() {
2✔
275
                                line_segments.push(Self::build_diff_line_line_segment(content.as_str(), diff_line.origin()));
2✔
276
                        }
277
                        if !trailing.is_empty() {
3✔
278
                                line_segments.push(LineSegment::new_with_color(
1✔
279
                                        trailing.as_str(),
1✔
280
                                        DisplayColor::DiffWhitespaceColor,
1✔
281
                                ));
282
                        }
283
                }
284
                else {
285
                        line_segments.push(Self::build_diff_line_line_segment(
1✔
286
                                self.replace_whitespace(diff_line.line(), false).as_str(),
2✔
287
                                diff_line.origin(),
1✔
288
                        ));
289
                }
290

291
                line_segments
1✔
292
        }
293

294
        pub(super) fn build_view_data_diff(
1✔
295
                &mut self,
296
                updater: &mut ViewDataUpdater<'_>,
297
                diff: &CommitDiff,
298
                load_status: &LoadStatus,
299
                is_full_width: bool,
300
        ) {
301
                updater.clear();
2✔
302

303
                if !self.build_loading_status(updater, load_status) {
2✔
304
                        return;
305
                }
306

307
                updater.push_leading_line(Self::build_leading_summary(diff.commit(), is_full_width));
2✔
308
                updater.push_leading_line(get_files_changed_summary(diff, is_full_width));
2✔
309
                updater.push_line(ViewLine::new_empty_line().set_padding(PADDING_CHARACTER));
1✔
310

311
                let file_statuses = diff.file_statuses();
2✔
312
                for (s_i, status) in file_statuses.iter().enumerate() {
2✔
313
                        updater.push_line(ViewLine::from(get_stat_item_segments(
1✔
314
                                status.status(),
2✔
315
                                status.destination_path(),
1✔
316
                                status.source_path(),
2✔
317
                                true,
318
                        )));
319

320
                        let old_largest_line_number_length = status.last_old_line_number().to_string().len();
1✔
321
                        let new_largest_line_number_length = status.last_new_line_number().to_string().len();
1✔
322
                        for delta in status.deltas() {
2✔
323
                                updater.push_line(ViewLine::new_empty_line());
1✔
324
                                updater.push_line(ViewLine::from(vec![
3✔
325
                                        LineSegment::new_with_color_and_style("@@", DisplayColor::Normal, LineSegmentOptions::DIMMED),
1✔
326
                                        LineSegment::new_with_color(
1✔
327
                                                format!(
4✔
328
                                                        " -{},{} +{},{} ",
329
                                                        delta.old_lines_start(),
2✔
330
                                                        delta.old_number_lines(),
1✔
331
                                                        delta.new_lines_start(),
1✔
332
                                                        delta.new_number_lines(),
1✔
333
                                                )
334
                                                .as_str(),
335
                                                DisplayColor::DiffContextColor,
1✔
336
                                        ),
337
                                        LineSegment::new_with_color_and_style("@@", DisplayColor::Normal, LineSegmentOptions::DIMMED),
1✔
338
                                        LineSegment::new_with_color(
1✔
339
                                                format!(" {}", delta.context()).as_str(),
2✔
340
                                                DisplayColor::DiffContextColor,
1✔
341
                                        ),
342
                                ]));
343
                                updater.push_line(ViewLine::new_pinned(vec![]).set_padding_with_color_and_style(
2✔
344
                                        PADDING_CHARACTER,
345
                                        DisplayColor::Normal,
1✔
346
                                        LineSegmentOptions::DIMMED,
347
                                ));
348

349
                                for line in delta.lines() {
2✔
350
                                        if line.end_of_file() && line.line() != "\n" {
2✔
351
                                                updater.push_line(ViewLine::from(vec![
3✔
352
                                                        LineSegment::new(
1✔
353
                                                                " ".repeat(old_largest_line_number_length + new_largest_line_number_length + 3)
2✔
354
                                                                        .as_str(),
355
                                                        ),
356
                                                        LineSegment::new_with_color("\\ No newline at end of file", DisplayColor::DiffContextColor),
1✔
357
                                                ]));
358
                                                continue;
359
                                        }
360

361
                                        updater.push_line(ViewLine::from(self.get_diff_line_segments(
1✔
362
                                                line,
363
                                                old_largest_line_number_length,
364
                                                new_largest_line_number_length,
365
                                        )));
366
                                }
367
                        }
368
                        if s_i + 1 != file_statuses.len() {
1✔
369
                                updater.push_line(ViewLine::new_empty_line().set_padding(PADDING_CHARACTER));
1✔
370
                        }
371
                }
372
        }
373
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc