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

MitMaro / git-interactive-rebase-tool / 13798245227

11 Mar 2025 09:09PM UTC coverage: 97.311% (-0.01%) from 97.325%
13798245227

push

github

MitMaro
Fix ordering in build.rs

4596 of 4723 relevant lines covered (97.31%)

2.74 hits per line

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

81.61
/src/git/commit_diff_loader.rs
1
use std::{
2
        path::PathBuf,
3
        sync::{Arc, LazyLock},
4
};
5

6
use git2::{DiffFindOptions, DiffOptions, Oid, Repository};
7
use parking_lot::{Mutex, MutexGuard};
8

9
use crate::git::{
10
        Commit,
11
        CommitDiff,
12
        CommitDiffLoaderOptions,
13
        Delta,
14
        DiffLine,
15
        FileMode,
16
        FileStatus,
17
        FileStatusBuilder,
18
        Status,
19
};
20

21
static UNKNOWN_PATH: LazyLock<PathBuf> = LazyLock::new(|| PathBuf::from("unknown"));
2✔
22

23
pub(crate) struct CommitDiffLoader<'options> {
24
        config: &'options CommitDiffLoaderOptions,
25
        repo: Arc<Mutex<Repository>>,
26
}
27

28
impl<'options> CommitDiffLoader<'options> {
29
        pub(crate) const fn new(repo: Arc<Mutex<Repository>>, config: &'options CommitDiffLoaderOptions) -> Self {
1✔
30
                Self { config, repo }
31
        }
32

33
        pub(crate) fn load_from_hash(&self, oid: Oid) -> Result<Vec<CommitDiff>, git2::Error> {
2✔
34
                let repo = self.repo.lock();
1✔
35
                let commit = repo.find_commit(oid)?;
6✔
36
                let no_parents = commit.parent_ids().count() == 0;
6✔
37

38
                // some commits do not have parents, and can't have file stats
39
                let diffs = if no_parents {
4✔
40
                        vec![self.load_diff(&repo, None, &commit)?]
2✔
41
                }
42
                else {
×
43
                        //
44
                        let mut diffs = vec![];
3✔
45
                        for parent in commit.parents() {
6✔
46
                                diffs.push(self.load_diff(&repo, Some(&parent), &commit)?);
5✔
47
                        }
48
                        diffs
2✔
49
                };
50
                Ok(diffs)
1✔
51
        }
52

53
        #[expect(clippy::as_conversions, reason = "Mostly safe difference between APIs.")]
54
        #[expect(clippy::unwrap_in_result, reason = "Unwrap usage failure considered a bug.")]
55
        fn load_diff(
3✔
56
                &self,
57
                repo: &MutexGuard<'_, Repository>,
58
                parent: Option<&git2::Commit<'_>>,
59
                commit: &git2::Commit<'_>,
60
        ) -> Result<CommitDiff, git2::Error> {
61
                let mut diff_options = DiffOptions::new();
3✔
62
                // include_unmodified added to find copies from unmodified files
63
                _ = diff_options
21✔
64
                        .context_lines(self.config.context_lines)
3✔
65
                        .ignore_filemode(false)
×
66
                        .ignore_whitespace(self.config.ignore_whitespace)
3✔
67
                        .ignore_whitespace_change(self.config.ignore_whitespace_change)
3✔
68
                        .ignore_blank_lines(self.config.ignore_blank_lines)
3✔
69
                        .include_typechange(true)
×
70
                        .include_typechange_trees(true)
×
71
                        .include_unmodified(self.config.copies)
3✔
72
                        .indent_heuristic(true)
×
73
                        .interhunk_lines(self.config.interhunk_context)
3✔
74
                        .minimal(true);
×
75

76
                let mut diff_find_options = DiffFindOptions::new();
3✔
77
                _ = diff_find_options
18✔
78
                        .rename_limit(self.config.rename_limit as usize)
3✔
79
                        .renames(self.config.renames)
3✔
80
                        .renames_from_rewrites(self.config.renames)
3✔
81
                        .rewrites(self.config.renames)
3✔
82
                        .copies(self.config.copies)
3✔
83
                        .copies_from_unmodified(self.config.copies);
3✔
84

85
                let mut diff = if let Some(p) = parent {
2✔
86
                        repo.diff_tree_to_tree(Some(&p.tree()?), Some(&commit.tree()?), Some(&mut diff_options))?
6✔
87
                }
88
                else {
×
89
                        repo.diff_tree_to_tree(None, Some(&commit.tree()?), Some(&mut diff_options))?
3✔
90
                };
91

92
                diff.find_similar(Some(&mut diff_find_options))?;
3✔
93

94
                let mut unmodified_file_count: usize = 0;
1✔
95

96
                let file_stats_builder = Mutex::new(FileStatusBuilder::new());
2✔
97

98
                diff.foreach(
2✔
99
                        &mut |diff_delta, _| {
3✔
100
                                // unmodified files are included for copy detection, so ignore
101
                                if diff_delta.status() == git2::Delta::Unmodified {
1✔
102
                                        unmodified_file_count += 1;
2✔
103
                                        return true;
1✔
104
                                }
105

106
                                let mut fsb = file_stats_builder.lock();
2✔
107

108
                                let source_file = diff_delta.old_file();
3✔
109
                                let source_file_mode = FileMode::from(source_file.mode());
1✔
110
                                let source_file_path = source_file.path().unwrap_or(UNKNOWN_PATH.as_path());
1✔
111

112
                                let destination_file = diff_delta.new_file();
3✔
113
                                let destination_file_mode = FileMode::from(destination_file.mode());
3✔
114
                                let destination_file_path = destination_file.path().unwrap_or(UNKNOWN_PATH.as_path());
3✔
115

116
                                fsb.add_file_stat(FileStatus::new(
6✔
117
                                        source_file_path,
×
118
                                        source_file_mode,
×
119
                                        source_file.is_binary(),
3✔
120
                                        destination_file_path,
×
121
                                        destination_file_mode,
×
122
                                        destination_file.is_binary(),
3✔
123
                                        Status::from(diff_delta.status()),
3✔
124
                                ));
125

126
                                true
3✔
127
                        },
128
                        None,
2✔
129
                        Some(&mut |_, diff_hunk| {
2✔
130
                                let mut fsb = file_stats_builder.lock();
1✔
131
                                fsb.add_delta(Delta::from(&diff_hunk));
2✔
132
                                true
×
133
                        }),
134
                        Some(&mut |_, _, diff_line| {
3✔
135
                                let mut fsb = file_stats_builder.lock();
1✔
136
                                fsb.add_diff_line(DiffLine::from(&diff_line));
2✔
137
                                true
×
138
                        }),
139
                )
140
                .expect("diff.foreach failed. Please report this as a bug.");
141

142
                let stats = diff.stats()?;
1✔
143
                let number_files_changed = stats.files_changed() - unmodified_file_count;
2✔
144
                let number_insertions = stats.insertions();
2✔
145
                let number_deletions = stats.deletions();
1✔
146

147
                let fsb = file_stats_builder.into_inner();
1✔
148

149
                Ok(CommitDiff::new(
3✔
150
                        Commit::from(commit),
2✔
151
                        parent.map(Commit::from),
3✔
152
                        fsb.build(),
1✔
153
                        number_files_changed,
×
154
                        number_insertions,
×
155
                        number_deletions,
×
156
                ))
157
        }
158
}
159

160
#[cfg(all(unix, test))]
161
mod tests {
162
        use std::{
163
                fs::{File, remove_file},
164
                io::Write as _,
165
                os::unix::fs::symlink,
166
        };
167

168
        use super::*;
169
        use crate::{git::Origin, test_helpers::with_temp_repository};
170

171
        #[cfg(not(tarpaulin_include))]
172
        fn _format_status(status: &FileStatus) -> String {
173
                let s = match status.status() {
174
                        Status::Added => "Added",
175
                        Status::Deleted => "Deleted",
176
                        Status::Modified => "Modified",
177
                        Status::Renamed => "Renamed",
178
                        Status::Copied => "Copied",
179
                        Status::Typechange => "Typechange",
180
                        Status::Other => "Other",
181
                };
182

183
                format!("Status {s}")
184
        }
185

186
        #[cfg(not(tarpaulin_include))]
187
        fn _format_file_mode(mode: FileMode) -> String {
188
                String::from(match mode {
189
                        FileMode::Normal => "n",
190
                        FileMode::Executable => "x",
191
                        FileMode::Link => "l",
192
                        FileMode::Other => "o",
193
                })
194
        }
195

196
        #[cfg(not(tarpaulin_include))]
197
        fn _format_paths(status: &FileStatus) -> String {
198
                let source_mode = _format_file_mode(status.source_mode());
199
                let source_binary = if status.source_is_binary() { ",b" } else { "" };
200

201
                if status.source_path() == status.destination_path()
202
                        && status.source_mode() == status.destination_mode()
203
                        && status.source_is_binary() == status.destination_is_binary()
204
                {
205
                        format!("{} ({source_mode}{source_binary})", status.source_path().display())
206
                }
207
                else {
208
                        let destination_binary = if status.destination_is_binary() { ",b" } else { "" };
209
                        format!(
210
                                "{} ({source_mode}{source_binary}) > {} ({}{destination_binary})",
211
                                status.source_path().display(),
212
                                status.destination_path().display(),
213
                                _format_file_mode(status.destination_mode()),
214
                        )
215
                }
216
        }
217

218
        #[cfg(not(tarpaulin_include))]
219
        #[expect(clippy::string_slice, reason = "Slice on safe range.")]
220
        fn _format_diff_line(line: &DiffLine) -> String {
221
                let origin = match line.origin() {
222
                        Origin::Addition => "+",
223
                        Origin::Binary => "B",
224
                        Origin::Context => " ",
225
                        Origin::Deletion => "-",
226
                        Origin::Header => "H",
227
                };
228
                if line.end_of_file() && line.line() != "\n" {
229
                        String::from("\\ No newline at end of file")
230
                }
231
                else {
232
                        format!(
233
                                "{origin}{} {}| {}",
234
                                line.old_line_number()
235
                                        .map_or_else(|| String::from(" "), |v| v.to_string()),
236
                                line.new_line_number()
237
                                        .map_or_else(|| String::from(" "), |v| v.to_string()),
238
                                if line.line().ends_with('\n') {
239
                                        &line.line()[..line.line().len() - 1]
240
                                }
241
                                else {
242
                                        line.line()
243
                                },
244
                        )
245
                }
246
        }
247

248
        #[cfg(not(tarpaulin_include))]
249
        fn _assert_commit_diff(diff: &CommitDiff, expected: &[String]) {
250
                let mut actual = vec![];
251
                for status in diff.file_statuses() {
252
                        actual.push(_format_paths(status));
253
                        actual.push(_format_status(status));
254
                        for delta in status.deltas() {
255
                                actual.push(format!(
256
                                        "@@ -{},{} +{},{} @@{}",
257
                                        delta.old_lines_start(),
258
                                        delta.old_number_lines(),
259
                                        delta.new_lines_start(),
260
                                        delta.new_number_lines(),
261
                                        if delta.context().is_empty() {
262
                                                String::new()
263
                                        }
264
                                        else {
265
                                                format!(" {}", delta.context())
266
                                        },
267
                                ));
268
                                for line in delta.lines() {
269
                                        actual.push(_format_diff_line(line));
270
                                }
271
                        }
272
                }
273
                pretty_assertions::assert_eq!(actual, expected);
274
        }
275

276
        macro_rules! assert_commit_diff {
277
                ($diff:expr, $($arg:expr),*) => {
278
                        let expected = vec![$( String::from($arg), )*];
19✔
279
                        _assert_commit_diff($diff, &expected);
30✔
280
                };
281
        }
282

283
        #[cfg(not(tarpaulin_include))]
284
        fn write_normal_file(repository: &crate::git::Repository, name: &str, contents: &[&str]) {
285
                let root = repository.repo_path().parent().unwrap().to_path_buf();
286

287
                let file_path = root.join(name);
288
                let mut file = File::create(file_path.as_path()).unwrap();
289
                if !contents.is_empty() {
290
                        writeln!(file, "{}", contents.join("\n")).unwrap();
291
                }
292
                repository.add_path_to_index(PathBuf::from(name).as_path()).unwrap();
293
        }
294

295
        #[cfg(not(tarpaulin_include))]
296
        fn remove_path(repository: &crate::git::Repository, name: &str) {
297
                let root = repository.repo_path().parent().unwrap().to_path_buf();
298

299
                let file_path = root.join(name);
300
                _ = remove_file(file_path);
301

302
                repository
303
                        .remove_path_from_index(PathBuf::from(name).as_path())
304
                        .unwrap();
305
        }
306

307
        #[cfg(not(tarpaulin_include))]
308
        fn create_commit(repository: &crate::git::Repository) {
309
                let sig = git2::Signature::new("name", "name@example.com", &git2::Time::new(1_609_459_200, 0)).unwrap();
310
                repository
311
                        .create_commit_on_index("refs/heads/main", &sig, &sig, "title")
312
                        .unwrap();
313
        }
314

315
        #[cfg(not(tarpaulin_include))]
316
        fn diff_from_head(repository: &crate::git::Repository, options: &CommitDiffLoaderOptions) -> CommitDiff {
317
                let id = repository.commit_id_from_ref("refs/heads/main").unwrap();
318
                let loader = CommitDiffLoader::new(repository.repository(), options);
319
                loader.load_from_hash(id).unwrap().remove(0)
320
        }
321

322
        #[test]
323
        fn load_from_hash_commit_no_parents() {
324
                with_temp_repository(|repo| {
325
                        let diff = diff_from_head(&repo, &CommitDiffLoaderOptions::new());
326
                        assert_eq!(diff.number_files_changed(), 0);
327
                        assert_eq!(diff.number_insertions(), 0);
328
                        assert_eq!(diff.number_deletions(), 0);
329
                });
330
        }
331

332
        #[test]
333
        fn load_from_hash_added_file() {
334
                with_temp_repository(|repo| {
335
                        write_normal_file(&repo, "a", &["line1"]);
336
                        create_commit(&repo);
337
                        let diff = diff_from_head(&repo, &CommitDiffLoaderOptions::new());
338
                        assert_eq!(diff.number_files_changed(), 1);
339
                        assert_eq!(diff.number_insertions(), 1);
340
                        assert_eq!(diff.number_deletions(), 0);
341
                        assert_commit_diff!(&diff, "a (o) > a (n)", "Status Added", "@@ -0,0 +1,1 @@", "+  1| line1");
342
                });
343
        }
344

345
        #[test]
346
        fn load_from_hash_removed_file() {
347
                with_temp_repository(|repo| {
348
                        write_normal_file(&repo, "a", &["line1"]);
349
                        create_commit(&repo);
350
                        remove_path(&repo, "a");
351
                        create_commit(&repo);
352
                        let diff = diff_from_head(&repo, &CommitDiffLoaderOptions::new());
353
                        assert_eq!(diff.number_files_changed(), 1);
354
                        assert_eq!(diff.number_insertions(), 0);
355
                        assert_eq!(diff.number_deletions(), 1);
356
                        assert_commit_diff!(
357
                                &diff,
358
                                "a (n) > a (o)",
359
                                "Status Deleted",
360
                                "@@ -1,1 +0,0 @@",
361
                                "-1  | line1"
362
                        );
363
                });
364
        }
365

366
        #[test]
367
        fn load_from_hash_modified_file() {
368
                with_temp_repository(|repo| {
369
                        write_normal_file(&repo, "a", &["line1"]);
370
                        create_commit(&repo);
371
                        write_normal_file(&repo, "a", &["line2"]);
372
                        create_commit(&repo);
373
                        let diff = diff_from_head(&repo, &CommitDiffLoaderOptions::new());
374
                        assert_eq!(diff.number_files_changed(), 1);
375
                        assert_eq!(diff.number_insertions(), 1);
376
                        assert_eq!(diff.number_deletions(), 1);
377
                        assert_commit_diff!(
378
                                &diff,
379
                                "a (n)",
380
                                "Status Modified",
381
                                "@@ -1,1 +1,1 @@",
382
                                "-1  | line1",
383
                                "+  1| line2"
384
                        );
385
                });
386
        }
387

388
        #[test]
389
        fn load_from_hash_with_context() {
390
                with_temp_repository(|repo| {
391
                        write_normal_file(&repo, "a", &["line0", "line1", "line2", "line3", "line4", "line5"]);
392
                        create_commit(&repo);
393
                        write_normal_file(&repo, "a", &["line0", "line1", "line2", "line3-m", "line4", "line5"]);
394
                        create_commit(&repo);
395
                        let diff = diff_from_head(&repo, &CommitDiffLoaderOptions::new().context_lines(2));
396
                        assert_commit_diff!(
397
                                &diff,
398
                                "a (n)",
399
                                "Status Modified",
400
                                "@@ -2,5 +2,5 @@ line0",
401
                                " 2 2| line1",
402
                                " 3 3| line2",
403
                                "-4  | line3",
404
                                "+  4| line3-m",
405
                                " 5 5| line4",
406
                                " 6 6| line5"
407
                        );
408
                });
409
        }
410

411
        #[test]
412
        fn load_from_hash_ignore_white_space_change() {
413
                with_temp_repository(|repo| {
414
                        write_normal_file(&repo, "a", &[" line0", "line1"]);
415
                        create_commit(&repo);
416
                        write_normal_file(&repo, "a", &["  line0", " line1-m"]);
417
                        create_commit(&repo);
418
                        let diff = diff_from_head(&repo, &CommitDiffLoaderOptions::new().ignore_whitespace_change(true));
419
                        assert_commit_diff!(
420
                                &diff,
421
                                "a (n)",
422
                                "Status Modified",
423
                                "@@ -2,1 +2,1 @@",
424
                                "-2  | line1",
425
                                "+  2|  line1-m"
426
                        );
427
                });
428
        }
429

430
        #[test]
431
        fn load_from_hash_ignore_white_space() {
432
                with_temp_repository(|repo| {
433
                        write_normal_file(&repo, "a", &["line0", "line1"]);
434
                        create_commit(&repo);
435
                        write_normal_file(&repo, "a", &["  line0", " line1-m"]);
436
                        create_commit(&repo);
437
                        let diff = diff_from_head(&repo, &CommitDiffLoaderOptions::new().ignore_whitespace(true));
438
                        assert_commit_diff!(
439
                                &diff,
440
                                "a (n)",
441
                                "Status Modified",
442
                                "@@ -2,1 +2,1 @@ line0",
443
                                "-2  | line1",
444
                                "+  2|  line1-m"
445
                        );
446
                });
447
        }
448

449
        #[test]
450
        fn load_from_hash_copies() {
451
                with_temp_repository(|repo| {
452
                        write_normal_file(&repo, "a", &["line0"]);
453
                        create_commit(&repo);
454
                        write_normal_file(&repo, "b", &["line0"]);
455
                        create_commit(&repo);
456
                        let diff = diff_from_head(&repo, &CommitDiffLoaderOptions::new().copies(true));
457
                        assert_eq!(diff.number_files_changed(), 1);
458
                        assert_eq!(diff.number_insertions(), 0);
459
                        assert_eq!(diff.number_deletions(), 0);
460
                        assert_commit_diff!(&diff, "a (n) > b (n)", "Status Copied");
461
                });
462
        }
463

464
        #[test]
465
        fn load_from_hash_copies_modified_source() {
466
                with_temp_repository(|repo| {
467
                        write_normal_file(&repo, "a", &["line0"]);
468
                        create_commit(&repo);
469
                        write_normal_file(&repo, "a", &["line0", "a"]);
470
                        write_normal_file(&repo, "b", &["line0"]);
471
                        create_commit(&repo);
472
                        let diff = diff_from_head(&repo, &CommitDiffLoaderOptions::new().copies(true));
473
                        assert_eq!(diff.number_files_changed(), 2);
474
                        assert_eq!(diff.number_insertions(), 1);
475
                        assert_eq!(diff.number_deletions(), 0);
476
                        assert_commit_diff!(
477
                                &diff,
478
                                "a (n)",
479
                                "Status Modified",
480
                                "@@ -1,0 +2,1 @@ line0",
481
                                "+  2| a",
482
                                "a (n) > b (n)",
483
                                "Status Copied"
484
                        );
485
                });
486
        }
487

488
        #[test]
489
        fn load_from_hash_interhunk_context() {
490
                with_temp_repository(|repo| {
491
                        write_normal_file(&repo, "a", &["line0", "line1", "line2", "line3", "line4", "line5"]);
492
                        create_commit(&repo);
493
                        write_normal_file(&repo, "a", &["line0", "line1-m", "line2", "line3", "line4-m", "line5"]);
494
                        create_commit(&repo);
495
                        let diff = diff_from_head(&repo, &CommitDiffLoaderOptions::new().interhunk_context(2));
496
                        assert_commit_diff!(
497
                                &diff,
498
                                "a (n)",
499
                                "Status Modified",
500
                                "@@ -2,4 +2,4 @@ line0",
501
                                "-2  | line1",
502
                                "+  2| line1-m",
503
                                " 3 3| line2",
504
                                " 4 4| line3",
505
                                "-5  | line4",
506
                                "+  5| line4-m"
507
                        );
508
                });
509
        }
510

511
        #[test]
512
        fn load_from_hash_rename_source_not_modified() {
513
                with_temp_repository(|repo| {
514
                        write_normal_file(&repo, "a", &["line0"]);
515
                        create_commit(&repo);
516
                        remove_path(&repo, "a");
517
                        write_normal_file(&repo, "b", &["line0"]);
518
                        create_commit(&repo);
519
                        let diff = diff_from_head(&repo, &CommitDiffLoaderOptions::new().renames(true, 100));
520
                        assert_eq!(diff.number_files_changed(), 1);
521
                        assert_eq!(diff.number_insertions(), 0);
522
                        assert_eq!(diff.number_deletions(), 0);
523
                        assert_commit_diff!(&diff, "a (n) > b (n)", "Status Renamed");
524
                });
525
        }
526

527
        #[test]
528
        fn load_from_hash_rename_source_modified() {
529
                // this test can be confusing to follow, here is how it is created:
530
                // - starting with am existing tracked file "a"
531
                // - move "a" and call it "b"
532
                // - create a new file "a" with different contents
533
                // this creates a situation where git detects the rename from the original unmodified
534
                // version of "a" before a new file called "a" was created
535
                with_temp_repository(|repo| {
536
                        write_normal_file(&repo, "a", &["line0"]);
537
                        create_commit(&repo);
538
                        write_normal_file(&repo, "a", &["other0"]);
539
                        write_normal_file(&repo, "b", &["line0"]);
540
                        create_commit(&repo);
541
                        let diff = diff_from_head(&repo, &CommitDiffLoaderOptions::new().renames(true, 100));
542
                        assert_eq!(diff.number_files_changed(), 2);
543
                        assert_eq!(diff.number_insertions(), 1);
544
                        assert_eq!(diff.number_deletions(), 0);
545
                        assert_commit_diff!(
546
                                &diff,
547
                                "a (o) > a (n)",
548
                                "Status Added",
549
                                "@@ -0,0 +1,1 @@",
550
                                "+  1| other0",
551
                                "a (n) > b (n)",
552
                                "Status Renamed"
553
                        );
554
                });
555
        }
556

557
        #[cfg(unix)]
558
        #[test]
559
        fn load_from_hash_file_mode_executable() {
560
                with_temp_repository(|repo| {
561
                        use std::os::unix::fs::PermissionsExt as _;
562

563
                        let root = repo.repo_path().parent().unwrap().to_path_buf();
564

565
                        write_normal_file(&repo, "a", &["line0"]);
566
                        create_commit(&repo);
567
                        let file = File::open(root.join("a")).unwrap();
568
                        let mut permissions = file.metadata().unwrap().permissions();
569
                        permissions.set_mode(0o755);
570
                        file.set_permissions(permissions).unwrap();
571
                        repo.add_path_to_index(PathBuf::from("a").as_path()).unwrap();
572
                        create_commit(&repo);
573
                        let diff = diff_from_head(&repo, &CommitDiffLoaderOptions::new().renames(true, 100));
574
                        assert_eq!(diff.number_files_changed(), 1);
575
                        assert_eq!(diff.number_insertions(), 0);
576
                        assert_eq!(diff.number_deletions(), 0);
577
                        assert_commit_diff!(&diff, "a (n) > a (x)", "Status Modified");
578
                });
579
        }
580

581
        #[cfg(unix)]
582
        #[test]
583
        fn load_from_hash_type_changed() {
584
                with_temp_repository(|repo| {
585
                        let root = repo.repo_path().parent().unwrap().to_path_buf();
586

587
                        write_normal_file(&repo, "a", &["line0"]);
588
                        write_normal_file(&repo, "b", &["line0"]);
589
                        create_commit(&repo);
590
                        remove_path(&repo, "a");
591
                        symlink(root.join("b"), root.join("a")).unwrap();
592
                        repo.add_path_to_index(PathBuf::from("a").as_path()).unwrap();
593
                        repo.add_path_to_index(PathBuf::from("b").as_path()).unwrap();
594
                        create_commit(&repo);
595
                        let diff = diff_from_head(&repo, &CommitDiffLoaderOptions::new());
596
                        assert_eq!(diff.number_files_changed(), 1);
597
                        assert_eq!(diff.number_insertions(), 0);
598
                        assert_eq!(diff.number_deletions(), 0);
599
                        assert_commit_diff!(&diff, "a (n) > a (l)", "Status Typechange");
600
                });
601
        }
602

603
        #[test]
604
        fn load_from_hash_binary_added_file() {
605
                with_temp_repository(|repo| {
606
                        // treat all files as binary
607
                        write_normal_file(&repo, ".gitattributes", &["a binary"]);
608
                        create_commit(&repo);
609
                        write_normal_file(&repo, "a", &["line1"]);
610
                        create_commit(&repo);
611
                        let diff = diff_from_head(&repo, &CommitDiffLoaderOptions::new());
612
                        assert_eq!(diff.number_files_changed(), 1);
613
                        assert_eq!(diff.number_insertions(), 0);
614
                        assert_eq!(diff.number_deletions(), 0);
615
                        assert_commit_diff!(&diff, "a (o,b) > a (n,b)", "Status Added");
616
                });
617
        }
618

619
        #[test]
620
        fn load_from_hash_binary_modified_file() {
621
                with_temp_repository(|repo| {
622
                        // treat all files as binary
623
                        write_normal_file(&repo, ".gitattributes", &["a binary"]);
624
                        write_normal_file(&repo, "a", &["line1"]);
625
                        create_commit(&repo);
626
                        write_normal_file(&repo, "a", &["line2"]);
627
                        create_commit(&repo);
628
                        let diff = diff_from_head(&repo, &CommitDiffLoaderOptions::new());
629
                        assert_eq!(diff.number_files_changed(), 1);
630
                        assert_eq!(diff.number_insertions(), 0);
631
                        assert_eq!(diff.number_deletions(), 0);
632
                        assert_commit_diff!(&diff, "a (n,b)", "Status Modified");
633
                });
634
        }
635
}
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