• 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

71.64
/src/diff/commit_diff_loader.rs
1
use std::{
2
        fmt::{Debug, Formatter},
3
        path::PathBuf,
4
        sync::{Arc, LazyLock},
5
        time::{Duration, Instant},
6
};
7

8
use git2::{Diff, DiffFindOptions, DiffOptions, Repository};
9
use parking_lot::{Mutex, RwLock};
10

11
use crate::{
12
        diff::{
13
                Commit,
14
                CommitDiff,
15
                CommitDiffLoaderOptions,
16
                Delta,
17
                DiffLine,
18
                FileMode,
19
                FileStatus,
20
                FileStatusBuilder,
21
                LoadStatus,
22
                Status,
23
        },
24
        git::GitError,
25
};
26

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

29
pub(crate) trait DiffUpdateHandlerFn: Fn(LoadStatus) -> bool + Sync + Send {}
30

31
impl<FN: Fn(LoadStatus) -> bool + Sync + Send> DiffUpdateHandlerFn for FN {}
32

33
fn create_status_update(quick: bool, processed_files: usize, total_files: usize) -> LoadStatus {
3✔
34
        if quick {
2✔
NEW
35
                LoadStatus::QuickDiff(processed_files, total_files)
×
36
        }
37
        else {
38
                LoadStatus::Diff(processed_files, total_files)
3✔
39
        }
40
}
41

42
pub(crate) struct CommitDiffLoader {
43
        config: CommitDiffLoaderOptions,
44
        repository: Repository,
45
        commit_diff: Arc<RwLock<CommitDiff>>,
46
}
47

48
impl CommitDiffLoader {
49
        pub(crate) fn new(repository: Repository, config: CommitDiffLoaderOptions) -> Self {
1✔
50
                Self {
51
                        repository,
52
                        config,
53
                        commit_diff: Arc::new(RwLock::new(CommitDiff::new())),
2✔
54
                }
55
        }
56

NEW
57
        pub(crate) fn reset(&mut self) {
×
NEW
58
                self.commit_diff.write().clear();
×
59
        }
60

61
        pub(crate) fn commit_diff(&self) -> Arc<RwLock<CommitDiff>> {
1✔
62
                Arc::clone(&self.commit_diff)
1✔
63
        }
64

65
        /// Find a commit by a hash
66
        ///
67
        /// # Errors
68
        /// Will result in an error if the commit cannot be loaded.
69
        fn find_commit(&self, hash: &str) -> Result<Commit, GitError> {
2✔
70
                let oid = self
2✔
71
                        .repository
72
                        .revparse_single(hash)
NEW
73
                        .map_err(|e| GitError::CommitLoad { cause: e })?
×
74
                        .id();
75

76
                let commit = self
2✔
77
                        .repository
78
                        .find_commit(oid)
NEW
79
                        .map_err(|e| GitError::CommitLoad { cause: e })?;
×
80

81
                Ok(Commit::from(&commit))
4✔
82
        }
83

84
        /// Find a parent of a commit referenced by hash
85
        ///
86
        /// # Errors
87
        /// Will result in an error if the parent cannot be loaded.
88
        fn find_first_parent(&self, hash: &str) -> Result<Option<Commit>, GitError> {
2✔
89
                let oid = self
2✔
90
                        .repository
91
                        .revparse_single(hash)
NEW
92
                        .map_err(|e| GitError::CommitLoad { cause: e })?
×
93
                        .id();
94

95
                let commit = self
3✔
96
                        .repository
97
                        .find_commit(oid)
NEW
98
                        .map_err(|e| GitError::CommitLoad { cause: e })?;
×
99

100
                // only the first parent matter for things like diffs, the second parent, if it exists,is
101
                // only used for conflict resolution, and has no use
102
                Ok(commit.parents().next().map(|c| Commit::from(&c)))
4✔
103
        }
104

105
        fn diff<'r>(
2✔
106
                repository: &'r Repository,
107
                config: &CommitDiffLoaderOptions,
108
                commit: &git2::Commit<'_>,
109
                diff_options: &mut DiffOptions,
110
        ) -> Result<Diff<'r>, GitError> {
111
                _ = diff_options
9✔
112
                        .context_lines(config.context_lines)
2✔
NEW
113
                        .ignore_filemode(false)
×
114
                        .ignore_whitespace(config.ignore_whitespace)
2✔
115
                        .ignore_whitespace_change(config.ignore_whitespace_change)
2✔
116
                        .ignore_blank_lines(config.ignore_blank_lines)
3✔
NEW
117
                        .include_typechange(true)
×
NEW
118
                        .include_typechange_trees(true)
×
NEW
119
                        .indent_heuristic(true)
×
120
                        .interhunk_lines(config.interhunk_context)
3✔
NEW
121
                        .minimal(true);
×
122

123
                let commit_tree = commit.tree().map_err(|e| GitError::DiffLoad { cause: e })?;
3✔
124

125
                if let Some(p) = commit.parents().next() {
7✔
126
                        let parent_tree = p.tree().map_err(|e| GitError::DiffLoad { cause: e })?;
3✔
127
                        repository.diff_tree_to_tree(Some(&parent_tree), Some(&commit_tree), Some(diff_options))
1✔
128
                }
NEW
129
                else {
×
130
                        repository.diff_tree_to_tree(None, Some(&commit_tree), Some(diff_options))
1✔
131
                }
NEW
132
                .map_err(|e| GitError::DiffLoad { cause: e })
×
133
        }
134

135
        pub(crate) fn load_diff(&mut self, hash: &str, update_notifier: impl DiffUpdateHandlerFn) -> Result<(), GitError> {
1✔
136
                {
137
                        let mut commit_diff = self.commit_diff.write();
3✔
138
                        commit_diff.reset(self.find_commit(hash)?, self.find_first_parent(hash)?);
4✔
139
                        if update_notifier(LoadStatus::New) {
1✔
NEW
140
                                return Ok(());
×
141
                        }
142
                }
143

144
                // std::thread::sleep(std::time::Duration::from_millis(250));
145
                // TODO, duplicated with find_commit function
146
                let oid = self
1✔
NEW
147
                        .repository
×
NEW
148
                        .revparse_single(hash)
×
NEW
149
                        .map_err(|e| GitError::DiffLoad { cause: e })?
×
150
                        .id();
151
                let commit = self
1✔
NEW
152
                        .repository
×
NEW
153
                        .find_commit(oid)
×
NEW
154
                        .map_err(|e| GitError::DiffLoad { cause: e })?;
×
155

156
                // std::thread::sleep(std::time::Duration::from_millis(250));
157
                // when a diff contains a lot of untracked files, collecting the diff information can take
158
                // upwards of a minute. This performs a quicker diff, that does not detect copies and
159
                // renames against unmodified files.
160
                if self.config.copies {
2✔
161
                        // self.config.quick_diff_threshold {
NEW
162
                        let should_continue = self.collect(
×
NEW
163
                                &Self::diff(&self.repository, &self.config, &commit, &mut DiffOptions::new())?,
×
NEW
164
                                &update_notifier,
×
165
                                true,
166
                        )?;
167

NEW
168
                        if !should_continue || update_notifier(LoadStatus::CompleteQuickDiff) {
×
NEW
169
                                return Ok(());
×
170
                        }
171
                }
172

173
                let mut diff_options = DiffOptions::new();
2✔
174
                // include_unmodified added to find copies from unmodified files
175
                _ = diff_options.include_unmodified(self.config.copies);
2✔
176
                let mut diff = Self::diff(&self.repository, &self.config, &commit, &mut diff_options)?;
2✔
177

178
                let mut diff_find_options = DiffFindOptions::new();
1✔
179
                _ = diff_find_options
18✔
180
                        .rename_limit(self.config.rename_limit as usize)
1✔
181
                        .renames(self.config.renames)
1✔
182
                        .renames_from_rewrites(self.config.renames)
1✔
183
                        .rewrites(self.config.renames)
1✔
184
                        .copies(self.config.copies)
1✔
185
                        .copies_from_unmodified(self.config.copies);
1✔
186

187
                diff.find_similar(Some(&mut diff_find_options))
1✔
NEW
188
                        .map_err(|e| GitError::DiffLoad { cause: e })?;
×
189
                let should_continue = self.collect(&diff, &update_notifier, false)?;
3✔
190

191
                if should_continue {
1✔
192
                        _ = update_notifier(LoadStatus::DiffComplete);
1✔
193
                        return Ok(());
1✔
194
                }
NEW
195
                Ok(())
×
196
        }
197

198
        #[expect(clippy::as_conversions, reason = "Realistically safe conversion.")]
199
        #[expect(clippy::unwrap_in_result, reason = "Unwrap usage failure considered a bug.")]
200
        pub(crate) fn collect(
1✔
201
                &self,
202
                diff: &Diff<'_>,
203
                update_handler: &impl DiffUpdateHandlerFn,
204
                quick: bool,
205
        ) -> Result<bool, GitError> {
206
                let file_stats_builder = Mutex::new(FileStatusBuilder::new());
1✔
207
                let mut unmodified_file_count: usize = 0;
3✔
208
                let mut change_count: usize = 0;
1✔
209

210
                let stats = diff.stats().map_err(|e| GitError::DiffLoad { cause: e })?;
4✔
211
                let total_files_changed = stats.files_changed();
4✔
212

213
                if update_handler(create_status_update(quick, 0, total_files_changed)) {
1✔
NEW
214
                        return Ok(false);
×
215
                }
216
                let mut time = Instant::now();
4✔
217

218
                //                 std::thread::sleep(std::time::Duration::from_millis(250));
219
                let collect_result = diff.foreach(
3✔
220
                        &mut |diff_delta, _| {
6✔
221
                                change_count += 1;
1✔
222

223
                                // std::thread::sleep(std::time::Duration::from_millis(25));
224
                                if time.elapsed() > Duration::from_millis(25) {
2✔
225
                                        // std::thread::sleep(std::time::Duration::from_millis(100));
NEW
226
                                        if update_handler(create_status_update(quick, change_count, total_files_changed)) {
×
NEW
227
                                                return false;
×
228
                                        }
NEW
229
                                        time = Instant::now();
×
230
                                }
231

232
                                // unmodified files are included for copy detection, so ignore
233
                                if diff_delta.status() == git2::Delta::Unmodified {
1✔
NEW
234
                                        unmodified_file_count += 1;
×
NEW
235
                                        return true;
×
236
                                }
237

238
                                let source_file = diff_delta.old_file();
2✔
239
                                let source_file_mode = FileMode::from(source_file.mode());
1✔
240
                                let source_file_path = source_file.path().unwrap_or(UNKNOWN_PATH.as_path());
2✔
241

242
                                let destination_file = diff_delta.new_file();
3✔
243
                                let destination_file_mode = FileMode::from(destination_file.mode());
3✔
244
                                let destination_file_path = destination_file.path().unwrap_or(UNKNOWN_PATH.as_path());
3✔
245

246
                                let mut fsb = file_stats_builder.lock();
3✔
247
                                fsb.add_file_stat(FileStatus::new(
6✔
248
                                        source_file_path,
249
                                        source_file_mode,
250
                                        source_file.is_binary(),
3✔
251
                                        destination_file_path,
252
                                        destination_file_mode,
253
                                        destination_file.is_binary(),
3✔
254
                                        Status::from(diff_delta.status()),
3✔
255
                                ));
256

257
                                true
3✔
258
                        },
259
                        None,
1✔
260
                        Some(&mut |_, diff_hunk| {
6✔
261
                                let mut fsb = file_stats_builder.lock();
3✔
262
                                fsb.add_delta(Delta::from(&diff_hunk));
6✔
263
                                true
264
                        }),
265
                        Some(&mut |_, _, diff_line| {
4✔
266
                                let mut fsb = file_stats_builder.lock();
3✔
267
                                fsb.add_diff_line(DiffLine::from(&diff_line));
6✔
268
                                true
269
                        }),
270
                );
271

272
                // error caused by early return
273
                if collect_result.is_err() {
2✔
NEW
274
                        return Ok(false);
×
275
                }
276

277
                let mut commit_diff = self.commit_diff.write();
2✔
278

279
                let number_files_changed = total_files_changed - unmodified_file_count;
1✔
280
                let number_insertions = stats.insertions();
2✔
281
                let number_deletions = stats.deletions();
1✔
282

283
                let fsb = file_stats_builder.into_inner();
1✔
284
                commit_diff.update(fsb.build(), number_files_changed, number_insertions, number_deletions);
2✔
285
                Ok(true)
1✔
286
        }
287
}
288

289
impl Debug for CommitDiffLoader {
NEW
290
        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
×
NEW
291
                f.debug_struct("CommitDiffLoader")
×
292
                        .field(
293
                                "repository",
NEW
294
                                &format!("Repository({})", &self.repository.path().display()),
×
295
                        )
296
                        .finish_non_exhaustive()
297
        }
298
}
299

300
#[cfg(all(unix, test))]
301
mod tests {
302
        use std::{
303
                fs::{File, remove_file},
304
                io::Write as _,
305
                os::unix::fs::symlink,
306
        };
307

308
        use git2::Index;
309

310
        use super::*;
311
        use crate::{diff::Origin, test_helpers::with_temp_repository};
312

313
        impl CommitDiffLoader {
314
                fn take_diff(mut self) -> CommitDiff {
1✔
315
                        let diff = std::mem::replace(&mut self.commit_diff, Arc::new(RwLock::new(CommitDiff::new())));
2✔
316
                        Arc::try_unwrap(diff).unwrap().into_inner()
1✔
317
                }
318
        }
319

320
        #[cfg(not(tarpaulin_include))]
321
        fn _format_status(status: &FileStatus) -> String {
322
                let s = match status.status() {
323
                        Status::Added => "Added",
324
                        Status::Deleted => "Deleted",
325
                        Status::Modified => "Modified",
326
                        Status::Renamed => "Renamed",
327
                        Status::Copied => "Copied",
328
                        Status::Typechange => "Typechange",
329
                        Status::Other => "Other",
330
                };
331

332
                format!("Status {s}")
333
        }
334

335
        #[cfg(not(tarpaulin_include))]
336
        fn _format_file_mode(mode: FileMode) -> String {
337
                String::from(match mode {
338
                        FileMode::Normal => "n",
339
                        FileMode::Executable => "x",
340
                        FileMode::Link => "l",
341
                        FileMode::Other => "o",
342
                })
343
        }
344

345
        #[cfg(not(tarpaulin_include))]
346
        fn _format_paths(status: &FileStatus) -> String {
347
                let source_mode = _format_file_mode(status.source_mode());
348
                let source_binary = if status.source_is_binary() { ",b" } else { "" };
349

350
                if status.source_path() == status.destination_path()
351
                        && status.source_mode() == status.destination_mode()
352
                        && status.source_is_binary() == status.destination_is_binary()
353
                {
354
                        format!("{} ({source_mode}{source_binary})", status.source_path().display())
355
                }
356
                else {
357
                        let destination_binary = if status.destination_is_binary() { ",b" } else { "" };
358
                        format!(
359
                                "{} ({source_mode}{source_binary}) > {} ({}{destination_binary})",
360
                                status.source_path().display(),
361
                                status.destination_path().display(),
362
                                _format_file_mode(status.destination_mode()),
363
                        )
364
                }
365
        }
366

367
        #[cfg(not(tarpaulin_include))]
368
        #[expect(clippy::string_slice, reason = "Slice on safe range.")]
369
        fn _format_diff_line(line: &DiffLine) -> String {
370
                let origin = match line.origin() {
371
                        Origin::Addition => "+",
372
                        Origin::Binary => "B",
373
                        Origin::Context => " ",
374
                        Origin::Deletion => "-",
375
                        Origin::Header => "H",
376
                };
377
                if line.end_of_file() && line.line() != "\n" {
378
                        String::from("\\ No newline at end of file")
379
                }
380
                else {
381
                        format!(
382
                                "{origin}{} {}| {}",
383
                                line.old_line_number()
384
                                        .map_or_else(|| String::from(" "), |v| v.to_string()),
385
                                line.new_line_number()
386
                                        .map_or_else(|| String::from(" "), |v| v.to_string()),
387
                                if line.line().ends_with('\n') {
388
                                        &line.line()[..line.line().len() - 1]
389
                                }
390
                                else {
391
                                        line.line()
392
                                },
393
                        )
394
                }
395
        }
396

397
        #[cfg(not(tarpaulin_include))]
398
        fn _assert_commit_diff(diff: &CommitDiff, expected: &[String]) {
399
                let mut actual = vec![];
400
                for status in diff.file_statuses() {
401
                        actual.push(_format_paths(status));
402
                        actual.push(_format_status(status));
403
                        for delta in status.deltas() {
404
                                actual.push(format!(
405
                                        "@@ -{},{} +{},{} @@{}",
406
                                        delta.old_lines_start(),
407
                                        delta.old_number_lines(),
408
                                        delta.new_lines_start(),
409
                                        delta.new_number_lines(),
410
                                        if delta.context().is_empty() {
411
                                                String::new()
412
                                        }
413
                                        else {
414
                                                format!(" {}", delta.context())
415
                                        },
416
                                ));
417
                                for line in delta.lines() {
418
                                        actual.push(_format_diff_line(line));
419
                                }
420
                        }
421
                }
422
                pretty_assertions::assert_eq!(actual, expected);
423
        }
424

425
        macro_rules! assert_commit_diff {
426
                ($diff:expr, $($arg:expr),*) => {
427
                        let expected = vec![$( String::from($arg), )*];
3✔
428
                        _assert_commit_diff($diff, &expected);
6✔
429
                };
430
        }
431

432
        #[cfg(not(tarpaulin_include))]
433
        fn index(repository: &Repository) -> Index {
434
                repository.index().unwrap()
435
        }
436

437
        #[cfg(not(tarpaulin_include))]
438
        fn root_path(repository: &Repository) -> PathBuf {
439
                repository.path().to_path_buf().parent().unwrap().to_path_buf()
440
        }
441

442
        #[cfg(not(tarpaulin_include))]
443
        fn commit_from_ref<'repo>(repository: &'repo Repository, reference: &str) -> git2::Commit<'repo> {
444
                repository.find_reference(reference).unwrap().peel_to_commit().unwrap()
445
        }
446

447
        #[cfg(not(tarpaulin_include))]
448
        fn write_normal_file(repository: &Repository, name: &str, contents: &[&str]) {
449
                let file_path = root_path(repository).join(name);
450
                let mut file = File::create(file_path.as_path()).unwrap();
451
                if !contents.is_empty() {
452
                        writeln!(file, "{}", contents.join("\n")).unwrap();
453
                }
454

455
                index(repository).add_path(PathBuf::from(name).as_path()).unwrap();
456
        }
457

458
        #[cfg(not(tarpaulin_include))]
459
        fn remove_path(repository: &Repository, name: &str) {
460
                let file_path = root_path(repository).join(name);
461
                _ = remove_file(file_path.as_path());
462

463
                index(repository).remove_path(PathBuf::from(name).as_path()).unwrap();
464
        }
465

466
        #[cfg(not(tarpaulin_include))]
467
        fn create_commit(repository: &Repository) {
468
                let sig = git2::Signature::new("name", "name@example.com", &git2::Time::new(1_609_459_200, 0)).unwrap();
469
                let tree = repository.find_tree(index(repository).write_tree().unwrap()).unwrap();
470
                let head = commit_from_ref(repository, "refs/heads/main");
471
                _ = repository
472
                        .commit(Some("HEAD"), &sig, &sig, "title", &tree, &[&head])
473
                        .unwrap();
474
        }
475

476
        #[cfg(not(tarpaulin_include))]
477
        fn diff_from_head(repository: Repository, options: CommitDiffLoaderOptions) -> Result<CommitDiffLoader, GitError> {
478
                let commit = commit_from_ref(&repository, "refs/heads/main");
479
                let hash = commit.id().to_string();
480
                drop(commit);
481
                let mut loader = CommitDiffLoader::new(repository, options);
482
                loader.load_diff(hash.as_str(), |_| false)?;
483
                Ok(loader)
484
        }
485

486
        #[test]
487
        fn load_from_hash_commit_no_parents() {
488
                with_temp_repository(|repository| {
489
                        let loader = diff_from_head(repository, CommitDiffLoaderOptions::new()).unwrap();
490
                        let diff = loader.take_diff();
491

492
                        assert_eq!(diff.number_files_changed(), 0);
493
                        assert_eq!(diff.number_insertions(), 0);
494
                        assert_eq!(diff.number_deletions(), 0);
495
                });
496
        }
497

498
        #[test]
499
        fn load_from_hash_added_file() {
500
                with_temp_repository(|repository| {
501
                        write_normal_file(&repository, "a", &["line1"]);
502
                        create_commit(&repository);
503
                        let loader = diff_from_head(repository, CommitDiffLoaderOptions::new()).unwrap();
504
                        let diff = loader.take_diff();
505

506
                        assert_eq!(diff.number_files_changed(), 1);
507
                        assert_eq!(diff.number_insertions(), 1);
508
                        assert_eq!(diff.number_deletions(), 0);
509
                        assert_commit_diff!(&diff, "a (o) > a (n)", "Status Added", "@@ -0,0 +1,1 @@", "+  1| line1");
510
                });
511
        }
512

513
        #[test]
514
        fn load_from_hash_removed_file() {
515
                with_temp_repository(|repository| {
516
                        write_normal_file(&repository, "a", &["line1"]);
517
                        create_commit(&repository);
518
                        remove_path(&repository, "a");
519
                        create_commit(&repository);
520

521
                        let loader = diff_from_head(repository, CommitDiffLoaderOptions::new()).unwrap();
522
                        let diff = loader.take_diff();
523

524
                        assert_eq!(diff.number_files_changed(), 1);
525
                        assert_eq!(diff.number_insertions(), 0);
526
                        assert_eq!(diff.number_deletions(), 1);
527
                        assert_commit_diff!(
528
                                &diff,
529
                                "a (n) > a (o)",
530
                                "Status Deleted",
531
                                "@@ -1,1 +0,0 @@",
532
                                "-1  | line1"
533
                        );
534
                });
535
        }
536

537
        #[test]
538
        fn load_from_hash_modified_file() {
539
                with_temp_repository(|repository| {
540
                        write_normal_file(&repository, "a", &["line1"]);
541
                        create_commit(&repository);
542
                        write_normal_file(&repository, "a", &["line2"]);
543
                        create_commit(&repository);
544

545
                        let loader = diff_from_head(repository, CommitDiffLoaderOptions::new()).unwrap();
546
                        let diff = loader.take_diff();
547

548
                        assert_eq!(diff.number_files_changed(), 1);
549
                        assert_eq!(diff.number_insertions(), 1);
550
                        assert_eq!(diff.number_deletions(), 1);
551
                        assert_commit_diff!(
552
                                &diff,
553
                                "a (n)",
554
                                "Status Modified",
555
                                "@@ -1,1 +1,1 @@",
556
                                "-1  | line1",
557
                                "+  1| line2"
558
                        );
559
                });
560
        }
561

562
        //         #[test]
563
        //         fn load_from_hash_with_context() {
564
        //                 with_temp_repository(|repository| {
565
        //                         let repo = crate::git::Repository::from(repository);
566
        //                         write_normal_file(&repo, "a", &["line0", "line1", "line2", "line3", "line4", "line5"]);
567
        //                         create_commit(&repo);
568
        //                         write_normal_file(&repo, "a", &["line0", "line1", "line2", "line3-m", "line4", "line5"]);
569
        //                         create_commit(&repo);
570
        //                         let diff = diff_from_head(&repo, &CommitDiffLoaderOptions::new().context_lines(2));
571
        //                         assert_commit_diff!(
572
        //                                 &diff,
573
        //                                 "a (n)",
574
        //                                 "Status Modified",
575
        //                                 "@@ -2,5 +2,5 @@ line0",
576
        //                                 " 2 2| line1",
577
        //                                 " 3 3| line2",
578
        //                                 "-4  | line3",
579
        //                                 "+  4| line3-m",
580
        //                                 " 5 5| line4",
581
        //                                 " 6 6| line5"
582
        //                         );
583
        //                 });
584
        //         }
585
        //
586
        //         #[test]
587
        //         fn load_from_hash_ignore_white_space_change() {
588
        //                 with_temp_repository(|repository| {
589
        //                         let repo = crate::git::Repository::from(repository);
590
        //                         write_normal_file(&repo, "a", &[" line0", "line1"]);
591
        //                         create_commit(&repo);
592
        //                         write_normal_file(&repo, "a", &["  line0", " line1-m"]);
593
        //                         create_commit(&repo);
594
        //                         let diff = diff_from_head(&repo, &CommitDiffLoaderOptions::new().ignore_whitespace_change(true));
595
        //                         assert_commit_diff!(
596
        //                                 &diff,
597
        //                                 "a (n)",
598
        //                                 "Status Modified",
599
        //                                 "@@ -2,1 +2,1 @@",
600
        //                                 "-2  | line1",
601
        //                                 "+  2|  line1-m"
602
        //                         );
603
        //                 });
604
        //         }
605
        //
606
        //         #[test]
607
        //         fn load_from_hash_ignore_white_space() {
608
        //                 with_temp_repository(|repository| {
609
        //                         let repo = crate::git::Repository::from(repository);
610
        //                         write_normal_file(&repo, "a", &["line0", "line1"]);
611
        //                         create_commit(&repo);
612
        //                         write_normal_file(&repo, "a", &["  line0", " line1-m"]);
613
        //                         create_commit(&repo);
614
        //                         let diff = diff_from_head(&repo, &CommitDiffLoaderOptions::new().ignore_whitespace(true));
615
        //                         assert_commit_diff!(
616
        //                                 &diff,
617
        //                                 "a (n)",
618
        //                                 "Status Modified",
619
        //                                 "@@ -2,1 +2,1 @@ line0",
620
        //                                 "-2  | line1",
621
        //                                 "+  2|  line1-m"
622
        //                         );
623
        //                 });
624
        //         }
625
        //
626
        //         #[test]
627
        //         fn load_from_hash_copies() {
628
        //                 with_temp_repository(|repository| {
629
        //                         let repo = crate::git::Repository::from(repository);
630
        //                         write_normal_file(&repo, "a", &["line0"]);
631
        //                         create_commit(&repo);
632
        //                         write_normal_file(&repo, "b", &["line0"]);
633
        //                         create_commit(&repo);
634
        //                         let diff = diff_from_head(&repo, &CommitDiffLoaderOptions::new().copies(true));
635
        //                         assert_eq!(diff.number_files_changed(), 1);
636
        //                         assert_eq!(diff.number_insertions(), 0);
637
        //                         assert_eq!(diff.number_deletions(), 0);
638
        //                         assert_commit_diff!(&diff, "a (n) > b (n)", "Status Copied");
639
        //                 });
640
        //         }
641
        //
642
        //         #[test]
643
        //         fn load_from_hash_copies_modified_source() {
644
        //                 with_temp_repository(|repository| {
645
        //                         let repo = crate::git::Repository::from(repository);
646
        //                         write_normal_file(&repo, "a", &["line0"]);
647
        //                         create_commit(&repo);
648
        //                         write_normal_file(&repo, "a", &["line0", "a"]);
649
        //                         write_normal_file(&repo, "b", &["line0"]);
650
        //                         create_commit(&repo);
651
        //                         let diff = diff_from_head(&repo, &CommitDiffLoaderOptions::new().copies(true));
652
        //                         assert_eq!(diff.number_files_changed(), 2);
653
        //                         assert_eq!(diff.number_insertions(), 1);
654
        //                         assert_eq!(diff.number_deletions(), 0);
655
        //                         assert_commit_diff!(
656
        //                                 &diff,
657
        //                                 "a (n)",
658
        //                                 "Status Modified",
659
        //                                 "@@ -1,0 +2,1 @@ line0",
660
        //                                 "+  2| a",
661
        //                                 "a (n) > b (n)",
662
        //                                 "Status Copied"
663
        //                         );
664
        //                 });
665
        //         }
666
        //
667
        //         #[test]
668
        //         fn load_from_hash_interhunk_context() {
669
        //                 with_temp_repository(|repository| {
670
        //                         let repo = crate::git::Repository::from(repository);
671
        //                         write_normal_file(&repo, "a", &["line0", "line1", "line2", "line3", "line4", "line5"]);
672
        //                         create_commit(&repo);
673
        //                         write_normal_file(&repo, "a", &["line0", "line1-m", "line2", "line3", "line4-m", "line5"]);
674
        //                         create_commit(&repo);
675
        //                         let diff = diff_from_head(&repo, &CommitDiffLoaderOptions::new().interhunk_context(2));
676
        //                         assert_commit_diff!(
677
        //                                 &diff,
678
        //                                 "a (n)",
679
        //                                 "Status Modified",
680
        //                                 "@@ -2,4 +2,4 @@ line0",
681
        //                                 "-2  | line1",
682
        //                                 "+  2| line1-m",
683
        //                                 " 3 3| line2",
684
        //                                 " 4 4| line3",
685
        //                                 "-5  | line4",
686
        //                                 "+  5| line4-m"
687
        //                         );
688
        //                 });
689
        //         }
690
        //
691
        //         #[test]
692
        //         fn load_from_hash_rename_source_not_modified() {
693
        //                 with_temp_repository(|repository| {
694
        //                         let repo = crate::git::Repository::from(repository);
695
        //                         write_normal_file(&repo, "a", &["line0"]);
696
        //                         create_commit(&repo);
697
        //                         remove_path(&repo, "a");
698
        //                         write_normal_file(&repo, "b", &["line0"]);
699
        //                         create_commit(&repo);
700
        //                         let diff = diff_from_head(&repo, &CommitDiffLoaderOptions::new().renames(true, 100));
701
        //                         assert_eq!(diff.number_files_changed(), 1);
702
        //                         assert_eq!(diff.number_insertions(), 0);
703
        //                         assert_eq!(diff.number_deletions(), 0);
704
        //                         assert_commit_diff!(&diff, "a (n) > b (n)", "Status Renamed");
705
        //                 });
706
        //         }
707
        //
708
        //         #[test]
709
        //         fn load_from_hash_rename_source_modified() {
710
        //                 // this test can be confusing to follow, here is how it is created:
711
        //                 // - starting with an existing tracked file "a"
712
        //                 // - move "a" and call it "b"
713
        //                 // - create a new file "a" with different contents
714
        //                 // this creates a situation where git detects the rename from the original unmodified
715
        //                 // version of "a" before a new file called "a" was created
716
        //                 with_temp_repository(|repository| {
717
        //                         let repo = crate::git::Repository::from(repository);
718
        //                         write_normal_file(&repo, "a", &["line0"]);
719
        //                         create_commit(&repo);
720
        //                         write_normal_file(&repo, "a", &["other0"]);
721
        //                         write_normal_file(&repo, "b", &["line0"]);
722
        //                         create_commit(&repo);
723
        //                         let diff = diff_from_head(&repo, &CommitDiffLoaderOptions::new().renames(true, 100));
724
        //                         assert_eq!(diff.number_files_changed(), 2);
725
        //                         assert_eq!(diff.number_insertions(), 1);
726
        //                         assert_eq!(diff.number_deletions(), 0);
727
        //                         assert_commit_diff!(
728
        //                                 &diff,
729
        //                                 "a (o) > a (n)",
730
        //                                 "Status Added",
731
        //                                 "@@ -0,0 +1,1 @@",
732
        //                                 "+  1| other0",
733
        //                                 "a (n) > b (n)",
734
        //                                 "Status Renamed"
735
        //                         );
736
        //                 });
737
        //         }
738
        //
739
        //         #[cfg(unix)]
740
        //         #[test]
741
        //         fn load_from_hash_file_mode_executable() {
742
        //                 with_temp_repository(|repository| {
743
        //                         use std::os::unix::fs::PermissionsExt as _;
744
        //
745
        //                         let repo = crate::git::Repository::from(repository);
746
        //
747
        //                         let root = repo.repo_path().parent().unwrap().to_path_buf();
748
        //
749
        //                         write_normal_file(&repo, "a", &["line0"]);
750
        //                         create_commit(&repo);
751
        //                         let file = File::open(root.join("a")).unwrap();
752
        //                         let mut permissions = file.metadata().unwrap().permissions();
753
        //                         permissions.set_mode(0o755);
754
        //                         file.set_permissions(permissions).unwrap();
755
        //                         repo.add_path_to_index(PathBuf::from("a").as_path()).unwrap();
756
        //                         create_commit(&repo);
757
        //                         let diff = diff_from_head(&repo, &CommitDiffLoaderOptions::new().renames(true, 100));
758
        //                         assert_eq!(diff.number_files_changed(), 1);
759
        //                         assert_eq!(diff.number_insertions(), 0);
760
        //                         assert_eq!(diff.number_deletions(), 0);
761
        //                         assert_commit_diff!(&diff, "a (n) > a (x)", "Status Modified");
762
        //                 });
763
        //         }
764
        //
765
        //         #[cfg(unix)]
766
        //         #[test]
767
        //         fn load_from_hash_type_changed() {
768
        //                 with_temp_repository(|repository| {
769
        //                         let repo = crate::git::Repository::from(repository);
770
        //                         let root = repo.repo_path().parent().unwrap().to_path_buf();
771
        //
772
        //                         write_normal_file(&repo, "a", &["line0"]);
773
        //                         write_normal_file(&repo, "b", &["line0"]);
774
        //                         create_commit(&repo);
775
        //                         remove_path(&repo, "a");
776
        //                         symlink(root.join("b"), root.join("a")).unwrap();
777
        //                         repo.add_path_to_index(PathBuf::from("a").as_path()).unwrap();
778
        //                         repo.add_path_to_index(PathBuf::from("b").as_path()).unwrap();
779
        //                         create_commit(&repo);
780
        //                         let diff = diff_from_head(&repo, &CommitDiffLoaderOptions::new());
781
        //                         assert_eq!(diff.number_files_changed(), 1);
782
        //                         assert_eq!(diff.number_insertions(), 0);
783
        //                         assert_eq!(diff.number_deletions(), 0);
784
        //                         assert_commit_diff!(&diff, "a (n) > a (l)", "Status Typechange");
785
        //                 });
786
        //         }
787
        //
788
        //         #[test]
789
        //         fn load_from_hash_binary_added_file() {
790
        //                 with_temp_repository(|repository| {
791
        //                         let repo = crate::git::Repository::from(repository);
792
        //                         // treat all files as binary
793
        //                         write_normal_file(&repo, ".gitattributes", &["a binary"]);
794
        //                         create_commit(&repo);
795
        //                         write_normal_file(&repo, "a", &["line1"]);
796
        //                         create_commit(&repo);
797
        //                         let diff = diff_from_head(&repo, &CommitDiffLoaderOptions::new());
798
        //                         assert_eq!(diff.number_files_changed(), 1);
799
        //                         assert_eq!(diff.number_insertions(), 0);
800
        //                         assert_eq!(diff.number_deletions(), 0);
801
        //                         assert_commit_diff!(&diff, "a (o,b) > a (n,b)", "Status Added");
802
        //                 });
803
        //         }
804
        //
805
        //         #[test]
806
        //         fn load_from_hash_binary_modified_file() {
807
        //                 with_temp_repository(|repository| {
808
        //                         let repo = crate::git::Repository::from(repository);
809
        //                         // treat all files as binary
810
        //                         write_normal_file(&repo, ".gitattributes", &["a binary"]);
811
        //                         write_normal_file(&repo, "a", &["line1"]);
812
        //                         create_commit(&repo);
813
        //                         write_normal_file(&repo, "a", &["line2"]);
814
        //                         create_commit(&repo);
815
        //                         let diff = diff_from_head(&repo, &CommitDiffLoaderOptions::new());
816
        //                         assert_eq!(diff.number_files_changed(), 1);
817
        //                         assert_eq!(diff.number_insertions(), 0);
818
        //                         assert_eq!(diff.number_deletions(), 0);
819
        //                         assert_commit_diff!(&diff, "a (n,b)", "Status Modified");
820
        //                 });
821
        //         }
822
        // }
823
        //
824
        // #[cfg(test)]
825
        // mod tests {
826
        //         use std::path::{Path, PathBuf};
827
        //
828
        //         use git2::{Oid, Signature};
829
        //
830
        //         use crate::git::{Commit, GitError, Reference, Repository};
831
        //
832
        //         impl Repository {
833
        //                 /// Find a reference by the reference name.
834
        //                 ///
835
        //                 /// # Errors
836
        //                 /// Will result in an error if the reference cannot be found.
837
        //                 pub(crate) fn find_reference(&self, reference: &str) -> Result<Reference, GitError> {
838
        //                         let git2_reference = self
839
        //                                 .repository
840
        //                                 .find_reference(reference)
841
        //                                 .map_err(|e| GitError::ReferenceNotFound { cause: e })?;
842
        //                         Ok(Reference::from(&git2_reference))
843
        //                 }
844
        //
845
        //                 /// Find a commit by a reference name.
846
        //                 ///
847
        //                 /// # Errors
848
        //                 /// Will result in an error if the reference cannot be found or is not a commit.
849
        //                 pub(crate) fn find_commit(&self, reference: &str) -> Result<Commit, GitError> {
850
        //                         let reference = self
851
        //                                 .repository
852
        //                                 .find_reference(reference)
853
        //                                 .map_err(|e| GitError::ReferenceNotFound { cause: e })?;
854
        //                         Commit::try_from(&reference)
855
        //                 }
856
        //
857
        //                 pub(crate) fn repo_path(&self) -> PathBuf {
858
        //                         self.repository.path().to_path_buf()
859
        //                 }
860
        //
861
        //                 pub(crate) fn head_id(&self, head_name: &str) -> Result<Oid, git2::Error> {
862
        //                         let ref_name = format!("refs/heads/{head_name}");
863
        //                         let revision = self.repository.revparse_single(ref_name.as_str())?;
864
        //                         Ok(revision.id())
865
        //                 }
866
        //
867
        //                 pub(crate) fn commit_id_from_ref(&self, reference: &str) -> Result<Oid, git2::Error> {
868
        //                         let commit = self.repository.find_reference(reference)?.peel_to_commit()?;
869
        //                         Ok(commit.id())
870
        //                 }
871
        //
872
        //                 pub(crate) fn add_path_to_index(&self, path: &Path) -> Result<(), git2::Error> {
873
        //                         let mut index = self.repository.index()?;
874
        //                         index.add_path(path)
875
        //                 }
876
        //
877
        //                 pub(crate) fn remove_path_from_index(&self, path: &Path) -> Result<(), git2::Error> {
878
        //                         let mut index = self.repository.index()?;
879
        //                         index.remove_path(path)
880
        //                 }
881
        //
882
        //                 pub(crate) fn create_commit_on_index(
883
        //                         &self,
884
        //                         reference: &str,
885
        //                         author: &Signature<'_>,
886
        //                         committer: &Signature<'_>,
887
        //                         message: &str,
888
        //                 ) -> Result<(), git2::Error> {
889
        //                         let tree = self.repository.find_tree(self.repository.index()?.write_tree()?)?;
890
        //                         let head = self.repository.find_reference(reference)?.peel_to_commit()?;
891
        //                         _ = self
892
        //                                 .repository
893
        //                                 .commit(Some("HEAD"), author, committer, message, &tree, &[&head])?;
894
        //                         Ok(())
895
        //                 }
896
        //
897
        //                 pub(crate) fn repository(&self) -> &git2::Repository {
898
        //                         &self.repository
899
        //                 }
900
        //         }
901
        // }
902
        //
903
        // // Paths in Windows make these tests difficult, so disable
904
        // #[cfg(all(unix, test))]
905
        // mod unix_tests {
906
        //         use claims::{assert_err_eq, assert_ok};
907
        //         use git2::{ErrorClass, ErrorCode};
908
        //
909
        //         use super::*;
910
        //         use crate::test_helpers::{create_commit, with_temp_repository};
911
        //
912
        //         #[test]
913
        //         fn load_commit_diff() {
914
        //                 with_temp_repository(|repo| {
915
        //                         let repository = Repository::from(repo);
916
        //                         create_commit(&repository, None);
917
        //                         let id = repository.commit_id_from_ref("refs/heads/main").unwrap();
918
        //                         assert_ok!(repository.load_commit_diff(id.to_string().as_str(), &CommitDiffLoaderOptions::new()));
919
        //                 });
920
        //         }
921
        //
922
        //         #[test]
923
        //         fn load_commit_diff_with_non_commit() {
924
        //                 with_temp_repository(|repo| {
925
        //                         let blob_ref = {
926
        //                                 let blob = repo.blob(b"foo").unwrap();
927
        //                                 _ = repo.reference("refs/blob", blob, false, "blob").unwrap();
928
        //                                 blob.to_string()
929
        //                         };
930
        //                         let repository = Repository::from(repo);
931
        //
932
        //                         assert_err_eq!(
933
        //                                 repository.load_commit_diff(blob_ref.as_str(), &CommitDiffLoaderOptions::new()),
934
        //                                 GitError::CommitLoad {
935
        //                                         cause: git2::Error::new(
936
        //                                                 ErrorCode::NotFound,
937
        //                                                 ErrorClass::Invalid,
938
        //                                                 "the requested type does not match the type in the ODB",
939
        //                                         ),
940
        //                                 }
941
        //                         );
942
        //                 });
943
        //         }
944
        //
945
        //         #[test]
946
        //         fn fmt() {
947
        //                 with_temp_repository(|repo| {
948
        //                         let repository = Repository::from(repo);
949
        //                         let formatted = format!("{repository:?}");
950
        //                         let path = repository.repo_path().canonicalize().unwrap();
951
        //                         assert_eq!(
952
        //                                 formatted,
953
        //                                 format!("Repository {{ [path]: \"{}/\" }}", path.to_str().unwrap())
954
        //                         );
955
        //                 });
956
        //         }
957
}
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

© 2026 Coveralls, Inc