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

MitMaro / git-interactive-rebase-tool / 14340401715

08 Apr 2025 06:00PM UTC coverage: 95.488% (-1.9%) from 97.339%
14340401715

Pull #959

github

web-flow
Merge 755a26246 into aa2157af9
Pull Request #959: WIP: Move Diff to Thread

372 of 483 new or added lines in 31 files covered. (77.02%)

4 existing lines in 2 files now uncovered.

4741 of 4965 relevant lines covered (95.49%)

2.74 hits per line

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

85.29
/src/modules/show_commit/view_builder.rs
1
use crate::{
2
        components::spin_indicator::SpinIndicator,
3
        diff::{Commit, CommitDiff, DiffLine, LoadStatus, Origin},
4
        display::DisplayColor,
5
        modules::show_commit::util::{
6
                get_files_changed_summary,
7
                get_partition_index_on_whitespace_for_line,
8
                get_stat_item_segments,
9
        },
10
        view::{LineSegment, LineSegmentOptions, ViewDataUpdater, ViewLine},
11
};
12

13
const PADDING_CHARACTER: char = '\u{2015}'; // '―'
14

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

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

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

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

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

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

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

110
        fn build_loading_status(&mut self, updater: &mut ViewDataUpdater<'_>, load_status: LoadStatus) -> bool {
3✔
111
                match load_status {
3✔
112
                        LoadStatus::New => {
NEW
113
                                updater.push_trailing_line(ViewBuilder::build_progress(
×
NEW
114
                                        &mut self.spin_indicator,
×
115
                                        "Loading Diff",
NEW
116
                                        None,
×
117
                                ));
NEW
118
                                false
×
119
                        },
NEW
120
                        LoadStatus::QuickDiff(c, t) => {
×
NEW
121
                                updater.push_trailing_line(ViewBuilder::build_progress(
×
NEW
122
                                        &mut self.spin_indicator,
×
123
                                        "Loading Diff",
NEW
124
                                        Some((c, t)),
×
125
                                ));
NEW
126
                                true
×
127
                        },
128
                        LoadStatus::CompleteQuickDiff => {
NEW
129
                                updater.push_trailing_line(ViewBuilder::build_progress(
×
NEW
130
                                        &mut self.spin_indicator,
×
131
                                        "Detecting renames and copies",
NEW
132
                                        None,
×
133
                                ));
NEW
134
                                true
×
135
                        },
NEW
136
                        LoadStatus::Diff(c, t) => {
×
NEW
137
                                updater.push_trailing_line(ViewBuilder::build_progress(
×
NEW
138
                                        &mut self.spin_indicator,
×
139
                                        "Detecting renames and copies",
NEW
140
                                        Some((c, t)),
×
141
                                ));
NEW
142
                                true
×
143
                        },
144
                        LoadStatus::DiffComplete => true,
3✔
145
                }
146
        }
147

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

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

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

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

190
                if let Some(summary) = commit.summary() {
3✔
191
                        updater.push_lines(summary);
1✔
192
                        updater.push_line(ViewLine::from(""));
1✔
193
                }
194

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

200
                if commit.summary().is_none() && commit.message().is_none() {
4✔
201
                        updater.push_line(ViewLine::from(""));
2✔
202
                }
203

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

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

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

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

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

288
                line_segments
1✔
289
        }
290

291
        pub(super) fn build_view_data_diff(
2✔
292
                &mut self,
293
                updater: &mut ViewDataUpdater<'_>,
294
                diff: &CommitDiff,
295
                load_status: LoadStatus,
296
                is_full_width: bool,
297
        ) {
298
                if !self.build_loading_status(updater, load_status) {
3✔
299
                        return;
300
                }
301

302
                updater.push_leading_line(Self::build_leading_summary(diff.commit(), is_full_width));
3✔
303
                updater.push_leading_line(get_files_changed_summary(diff, is_full_width));
1✔
304
                updater.push_line(ViewLine::new_empty_line().set_padding(PADDING_CHARACTER));
2✔
305

306
                let file_statuses = diff.file_statuses();
1✔
307
                for (s_i, status) in file_statuses.iter().enumerate() {
2✔
308
                        updater.push_line(ViewLine::from(get_stat_item_segments(
2✔
309
                                status.status(),
1✔
310
                                status.destination_path(),
2✔
311
                                status.source_path(),
1✔
312
                                true,
313
                        )));
314

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

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

356
                                        updater.push_line(ViewLine::from(self.get_diff_line_segments(
1✔
357
                                                line,
358
                                                old_largest_line_number_length,
359
                                                new_largest_line_number_length,
360
                                        )));
361
                                }
362
                        }
363
                        if s_i + 1 != file_statuses.len() {
1✔
364
                                updater.push_line(ViewLine::new_empty_line().set_padding(PADDING_CHARACTER));
1✔
365
                        }
366
                }
367
        }
368
}
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