1use sley_core::{
4 object_id_for_bytes, BString, GitError, ObjectFormat, ObjectId, RepoPath, Result,
5};
6use sley_index::{BorrowedIndex, Index, IndexStatCache};
7use sley_object::{Commit, EncodedObject, ObjectType, Tree, TreeEntries, TreeEntry};
8use sley_odb::{FileObjectDatabase, ObjectReader, ObjectWriter};
9use sley_refs::{FileRefStore, RefTarget};
10use std::collections::{BTreeMap, BTreeSet, HashMap};
11use std::fs;
12use std::path::{Path, PathBuf};
13use std::sync::Arc;
14
15use crate::line_diff::DiffAlgorithm;
16
17pub fn gitlink_git_dir(sub_root: &Path) -> Option<PathBuf> {
35 let dot_git = sub_root.join(".git");
36 let metadata = fs::symlink_metadata(&dot_git).ok()?;
37 if metadata.is_dir() {
38 return Some(dot_git);
39 }
40 if !metadata.is_file() {
41 return None;
42 }
43 let contents = fs::read_to_string(&dot_git).ok()?;
44 let target = contents.strip_prefix("gitdir:")?.trim();
45 if target.is_empty() {
46 return None;
47 }
48 let target = PathBuf::from(target);
49 let git_dir = if target.is_absolute() {
50 target
51 } else {
52 sub_root.join(target)
53 };
54 if git_dir.is_dir() {
55 Some(git_dir)
56 } else {
57 None
58 }
59}
60
61pub fn gitlink_broken_gitdir(sub_root: &Path) -> Option<PathBuf> {
69 let dot_git = sub_root.join(".git");
70 let metadata = fs::symlink_metadata(&dot_git).ok()?;
71 if !metadata.is_file() {
72 return None;
74 }
75 let contents = fs::read_to_string(&dot_git).ok()?;
76 let target = contents.strip_prefix("gitdir:")?.trim();
77 if target.is_empty() {
78 return None;
79 }
80 let target_path = if Path::new(target).is_absolute() {
81 PathBuf::from(target)
82 } else {
83 sub_root.join(target)
84 };
85 if target_path.is_dir() {
86 None
87 } else {
88 Some(target_path)
89 }
90}
91
92pub fn gitlink_head_oid(sub_root: &Path, format: ObjectFormat) -> Option<ObjectId> {
98 let git_dir = gitlink_git_dir(sub_root)?;
99 let store = FileRefStore::new(&git_dir, format);
100 let mut target = store.read_ref("HEAD").ok()??;
101 for _ in 0..10 {
103 match target {
104 RefTarget::Direct(oid) => return Some(oid),
105 RefTarget::Symbolic(name) => target = store.read_ref(&name).ok()??,
106 }
107 }
108 None
109}
110#[derive(Debug, Clone, PartialEq, Eq)]
111pub enum FileChange {
112 Add { path: RepoPath },
113 Delete { path: RepoPath },
114 Modify { path: RepoPath },
115 Rename { old: RepoPath, new: RepoPath },
116 Copy { source: RepoPath, dest: RepoPath },
117}
118
119#[derive(Debug, Clone, PartialEq, Eq)]
120pub struct Conflict {
121 pub path: RepoPath,
122 pub ours: Vec<u8>,
123 pub theirs: Vec<u8>,
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub enum NameStatus {
128 Added,
129 Deleted,
130 Modified,
131 TypeChanged,
137 Renamed(u8),
138 Copied(u8),
139 Unmerged,
143}
144
145impl NameStatus {
146 pub const fn code(self) -> char {
147 match self {
148 Self::Added => 'A',
149 Self::Deleted => 'D',
150 Self::Modified => 'M',
151 Self::TypeChanged => 'T',
152 Self::Renamed(_) => 'R',
153 Self::Copied(_) => 'C',
154 Self::Unmerged => 'U',
155 }
156 }
157
158 pub fn label(self) -> String {
159 match self {
160 Self::Renamed(score) => format!("R{score:03}"),
161 Self::Copied(score) => format!("C{score:03}"),
162 _ => self.code().to_string(),
163 }
164 }
165}
166
167pub const S_IFMT: u32 = 0o170000;
171
172#[must_use]
177pub const fn is_type_change(old_mode: u32, new_mode: u32) -> bool {
178 (old_mode & S_IFMT) != (new_mode & S_IFMT)
179}
180
181#[must_use]
186pub const fn modify_or_type_change(old_mode: u32, new_mode: u32) -> NameStatus {
187 if is_type_change(old_mode, new_mode) {
188 NameStatus::TypeChanged
189 } else {
190 NameStatus::Modified
191 }
192}
193
194#[derive(Debug, Clone, PartialEq, Eq)]
195pub struct NameStatusEntry {
196 pub status: NameStatus,
197 pub path: BString,
198 pub old_path: Option<BString>,
199 pub old_mode: Option<u32>,
200 pub new_mode: Option<u32>,
201 pub old_oid: Option<ObjectId>,
202 pub new_oid: Option<ObjectId>,
203}
204
205impl NameStatusEntry {
206 pub fn line(&self) -> String {
207 if let Some(old_path) = &self.old_path {
208 format!(
209 "{}\t{}\t{}",
210 self.status.label(),
211 String::from_utf8_lossy(old_path.as_bytes()),
212 String::from_utf8_lossy(self.path.as_bytes())
213 )
214 } else {
215 format!(
216 "{}\t{}",
217 self.status.label(),
218 String::from_utf8_lossy(self.path.as_bytes())
219 )
220 }
221 }
222}
223
224#[derive(Debug, Clone, PartialEq, Eq)]
225pub struct IndexGitlinkEntry {
226 pub path: BString,
227 pub oid: ObjectId,
228}
229
230#[derive(Debug, Clone, PartialEq, Eq)]
231pub struct IndexWorktreeDiff {
232 pub entries: Vec<NameStatusEntry>,
233 pub staged_gitlinks: Vec<IndexGitlinkEntry>,
234}
235
236#[derive(Clone, Copy)]
237pub struct IndexWorktreeValidationEntry<'a> {
238 pub path: &'a [u8],
239 pub mode: u32,
240 pub oid: ObjectId,
241 pub size: u32,
242}
243
244pub struct IndexWorktreeValidatedEntry {
245 pub mode: u32,
246 pub oid: ObjectId,
247}
248
249pub type IndexWorktreeStatCleanValidator<'a> = dyn FnMut(
250 IndexWorktreeValidationEntry<'_>,
251 &Path,
252 &fs::Metadata,
253 ) -> Result<Option<IndexWorktreeValidatedEntry>>
254 + 'a;
255
256#[derive(Debug, Clone, Copy, PartialEq, Eq)]
257pub struct DiffNameStatusOptions {
258 pub detect_renames: bool,
259 pub detect_copies: bool,
260 pub find_copies_harder: bool,
261 pub rename_empty: bool,
262 pub detect_inexact: bool,
263 pub rename_threshold: u8,
264 pub copy_threshold: u8,
265 pub rename_limit: usize,
266}
267
268impl Default for DiffNameStatusOptions {
269 fn default() -> Self {
270 Self {
271 detect_renames: true,
272 detect_copies: false,
273 find_copies_harder: false,
274 rename_empty: true,
275 detect_inexact: false,
276 rename_threshold: DEFAULT_RENAME_THRESHOLD,
277 copy_threshold: DEFAULT_RENAME_THRESHOLD,
278 rename_limit: 0,
279 }
280 }
281}
282
283impl DiffNameStatusOptions {
284 pub fn inexact(mut self) -> Self {
285 self.detect_inexact = true;
286 self
287 }
288}
289
290pub const DEFAULT_RENAME_THRESHOLD: u8 = 50;
294
295#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
296pub struct RenameLimitDiagnostics {
297 pub inexact_renames_skipped: bool,
298 pub inexact_copies_skipped: bool,
299 pub inexact_copies_degraded: bool,
300}
301
302impl RenameLimitDiagnostics {
303 pub fn any_skipped(self) -> bool {
304 self.inexact_renames_skipped || self.inexact_copies_skipped
305 }
306
307 pub fn any_warning(self) -> bool {
308 self.any_skipped() || self.inexact_copies_degraded
309 }
310}
311
312#[derive(Debug, Clone, PartialEq, Eq)]
313pub struct NameStatusWithRenameDiagnostics {
314 pub entries: Vec<NameStatusEntry>,
315 pub rename_limit: RenameLimitDiagnostics,
316}
317
318pub fn diff_name_status_head_worktree(
319 worktree_root: impl AsRef<Path>,
320 git_dir: impl AsRef<Path>,
321 format: ObjectFormat,
322) -> Result<Vec<NameStatusEntry>> {
323 diff_name_status_head_worktree_with_options(
324 worktree_root,
325 git_dir,
326 format,
327 DiffNameStatusOptions::default(),
328 )
329}
330
331pub fn diff_name_status_head_worktree_with_options(
332 worktree_root: impl AsRef<Path>,
333 git_dir: impl AsRef<Path>,
334 format: ObjectFormat,
335 options: DiffNameStatusOptions,
336) -> Result<Vec<NameStatusEntry>> {
337 let worktree_root = worktree_root.as_ref();
338 let git_dir = git_dir.as_ref();
339 let db = FileObjectDatabase::from_git_dir(git_dir, format);
340 let head = head_tree_entries(git_dir, format, &db)?;
341 let IndexSnapshot {
342 entries: index,
343 stat_cache,
344 missing_skip_worktree_entries,
345 present_skip_worktree_entries,
346 } = read_index_snapshot(git_dir, format)?;
347 let index_gitlinks = index_gitlinks(&index);
348 let candidate_paths = candidate_path_set(head.keys().chain(index.keys()));
349 let worktree = worktree_entries_for_path_set(
350 worktree_root,
351 format,
352 &candidate_paths,
353 &index_gitlinks,
354 Some(&stat_cache),
355 Some(&missing_skip_worktree_entries),
356 Some(&present_skip_worktree_entries),
357 )?;
358 let changes = if options.detect_inexact {
359 let cache = worktree_blob_cache_for_path_set(
360 worktree_root,
361 &head,
362 &worktree,
363 &candidate_paths,
364 Some(&present_skip_worktree_entries),
365 options,
366 )?;
367 diff_name_status_maps_with_renames_for_path_set(
368 &head,
369 &worktree,
370 &candidate_paths,
371 options,
372 |oid| cache_or_odb_blob(&cache, &db, oid),
373 )?
374 } else {
375 diff_name_status_maps_for_path_set(&head, &worktree, &candidate_paths, options)?
376 };
377 Ok(mark_unstaged_worktree_oids_unresolved(
378 changes, &index, &worktree,
379 ))
380}
381
382pub fn diff_name_status_head_index(
383 git_dir: impl AsRef<Path>,
384 format: ObjectFormat,
385) -> Result<Vec<NameStatusEntry>> {
386 diff_name_status_head_index_with_options(git_dir, format, DiffNameStatusOptions::default())
387}
388
389pub fn diff_name_status_head_index_with_options(
390 git_dir: impl AsRef<Path>,
391 format: ObjectFormat,
392 options: DiffNameStatusOptions,
393) -> Result<Vec<NameStatusEntry>> {
394 if options.detect_inexact {
395 return Ok(
396 diff_name_status_head_index_with_options_and_diagnostics(git_dir, format, options)?
397 .entries,
398 );
399 }
400 let git_dir = git_dir.as_ref();
401 let db = FileObjectDatabase::from_git_dir(git_dir, format);
402 let head = head_tree_entries(git_dir, format, &db)?;
403 let index = read_index_entries(git_dir, format)?;
404 diff_name_status_maps(&head, &index, head.keys().chain(index.keys()), options)
405}
406
407pub fn diff_name_status_head_index_with_options_and_diagnostics(
408 git_dir: impl AsRef<Path>,
409 format: ObjectFormat,
410 options: DiffNameStatusOptions,
411) -> Result<NameStatusWithRenameDiagnostics> {
412 let git_dir = git_dir.as_ref();
413 let db = FileObjectDatabase::from_git_dir(git_dir, format);
414 let head = head_tree_entries(git_dir, format, &db)?;
415 let index = read_index_entries(git_dir, format)?;
416 diff_name_status_maps_with_renames_and_diagnostics(
417 &head,
418 &index,
419 head.keys().chain(index.keys()),
420 options,
421 |oid| read_blob_bytes(&db, oid),
422 )
423}
424
425fn tree_entries(
435 tree_oid: &ObjectId,
436 format: ObjectFormat,
437 db: &FileObjectDatabase,
438) -> Result<BTreeMap<Vec<u8>, TrackedEntry>> {
439 let mut entries = BTreeMap::new();
440 if *tree_oid == empty_tree_oid(format)? {
441 return Ok(entries);
442 }
443 collect_tree_entries(db, format, tree_oid, Vec::new(), &mut entries)?;
444 Ok(entries)
445}
446
447pub(crate) fn empty_tree_oid(format: ObjectFormat) -> Result<ObjectId> {
450 object_id_for_bytes(format, "tree", b"")
451}
452
453pub fn diff_name_status_tree_index_with_options(
457 git_dir: impl AsRef<Path>,
458 format: ObjectFormat,
459 tree_oid: &ObjectId,
460 options: DiffNameStatusOptions,
461) -> Result<Vec<NameStatusEntry>> {
462 if options.detect_inexact {
463 return Ok(
464 diff_name_status_tree_index_with_options_and_diagnostics(
465 git_dir, format, tree_oid, options,
466 )?
467 .entries,
468 );
469 }
470 let git_dir = git_dir.as_ref();
471 let db = FileObjectDatabase::from_git_dir(git_dir, format);
472 let tree = tree_entries(tree_oid, format, &db)?;
473 let index = read_index_entries(git_dir, format)?;
474 diff_name_status_maps(&tree, &index, tree.keys().chain(index.keys()), options)
475}
476
477pub fn diff_name_status_tree_index_with_options_and_diagnostics(
478 git_dir: impl AsRef<Path>,
479 format: ObjectFormat,
480 tree_oid: &ObjectId,
481 options: DiffNameStatusOptions,
482) -> Result<NameStatusWithRenameDiagnostics> {
483 let git_dir = git_dir.as_ref();
484 let db = FileObjectDatabase::from_git_dir(git_dir, format);
485 let tree = tree_entries(tree_oid, format, &db)?;
486 let index = read_index_entries(git_dir, format)?;
487 diff_name_status_maps_with_renames_and_diagnostics(
488 &tree,
489 &index,
490 tree.keys().chain(index.keys()),
491 options,
492 |oid| read_blob_bytes(&db, oid),
493 )
494}
495
496pub fn diff_name_status_tree_worktree_with_options(
502 worktree_root: impl AsRef<Path>,
503 git_dir: impl AsRef<Path>,
504 format: ObjectFormat,
505 tree_oid: &ObjectId,
506 options: DiffNameStatusOptions,
507) -> Result<Vec<NameStatusEntry>> {
508 let worktree_root = worktree_root.as_ref();
509 let git_dir = git_dir.as_ref();
510 let db = FileObjectDatabase::from_git_dir(git_dir, format);
511 let tree = tree_entries(tree_oid, format, &db)?;
512 let IndexSnapshot {
513 entries: index,
514 stat_cache,
515 missing_skip_worktree_entries,
516 present_skip_worktree_entries,
517 } = read_index_snapshot(git_dir, format)?;
518 let index_gitlinks = index_gitlinks(&index);
519 let candidate_paths = candidate_path_set(tree.keys().chain(index.keys()));
520 let worktree = worktree_entries_for_path_set(
521 worktree_root,
522 format,
523 &candidate_paths,
524 &index_gitlinks,
525 Some(&stat_cache),
526 Some(&missing_skip_worktree_entries),
527 Some(&present_skip_worktree_entries),
528 )?;
529 let changes = if options.detect_inexact {
530 let cache = worktree_blob_cache_for_path_set(
531 worktree_root,
532 &tree,
533 &worktree,
534 &candidate_paths,
535 Some(&present_skip_worktree_entries),
536 options,
537 )?;
538 diff_name_status_maps_with_renames_for_path_set(
539 &tree,
540 &worktree,
541 &candidate_paths,
542 options,
543 |oid| cache_or_odb_blob(&cache, &db, oid),
544 )?
545 } else {
546 diff_name_status_maps_for_path_set(&tree, &worktree, &candidate_paths, options)?
547 };
548 Ok(mark_unstaged_worktree_oids_unresolved(
549 changes, &index, &worktree,
550 ))
551}
552
553pub fn diff_name_status_index_worktree(
554 worktree_root: impl AsRef<Path>,
555 git_dir: impl AsRef<Path>,
556 format: ObjectFormat,
557) -> Result<Vec<NameStatusEntry>> {
558 diff_name_status_index_worktree_with_options(
559 worktree_root,
560 git_dir,
561 format,
562 DiffNameStatusOptions::default(),
563 )
564}
565
566pub fn diff_name_status_index_worktree_with_options(
567 worktree_root: impl AsRef<Path>,
568 git_dir: impl AsRef<Path>,
569 format: ObjectFormat,
570 options: DiffNameStatusOptions,
571) -> Result<Vec<NameStatusEntry>> {
572 Ok(diff_name_status_index_worktree_with_options_and_gitlinks(
573 worktree_root,
574 git_dir,
575 format,
576 options,
577 )?
578 .entries)
579}
580
581pub fn diff_name_status_index_worktree_with_options_and_gitlinks(
582 worktree_root: impl AsRef<Path>,
583 git_dir: impl AsRef<Path>,
584 format: ObjectFormat,
585 options: DiffNameStatusOptions,
586) -> Result<IndexWorktreeDiff> {
587 let IndexWorktreeDiff {
588 entries,
589 staged_gitlinks,
590 } = diff_name_status_index_worktree_changes(
591 worktree_root.as_ref(),
592 git_dir.as_ref(),
593 format,
594 None,
595 )?;
596 let entries = apply_name_status_options_to_index_worktree_changes(entries, options)?;
597 Ok(IndexWorktreeDiff {
598 entries,
599 staged_gitlinks,
600 })
601}
602
603pub fn diff_name_status_index_worktree_with_options_and_gitlinks_validated(
604 worktree_root: impl AsRef<Path>,
605 git_dir: impl AsRef<Path>,
606 format: ObjectFormat,
607 options: DiffNameStatusOptions,
608 stat_clean_validator: &mut IndexWorktreeStatCleanValidator<'_>,
609) -> Result<IndexWorktreeDiff> {
610 let IndexWorktreeDiff {
611 entries,
612 staged_gitlinks,
613 } = diff_name_status_index_worktree_changes(
614 worktree_root.as_ref(),
615 git_dir.as_ref(),
616 format,
617 Some(stat_clean_validator),
618 )?;
619 let entries = apply_name_status_options_to_index_worktree_changes(entries, options)?;
620 Ok(IndexWorktreeDiff {
621 entries,
622 staged_gitlinks,
623 })
624}
625
626fn diff_name_status_index_worktree_changes(
627 worktree_root: &Path,
628 git_dir: &Path,
629 format: ObjectFormat,
630 mut stat_clean_validator: Option<&mut IndexWorktreeStatCleanValidator<'_>>,
631) -> Result<IndexWorktreeDiff> {
632 let index_path = sley_index::repository_index_path(git_dir);
633 let index_metadata = match fs::metadata(&index_path) {
634 Ok(metadata) => metadata,
635 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
636 return Ok(IndexWorktreeDiff {
637 entries: Vec::new(),
638 staged_gitlinks: Vec::new(),
639 });
640 }
641 Err(err) => return Err(err.into()),
642 };
643 let index_bytes = fs::read(&index_path)?;
644 if let Ok(index) = BorrowedIndex::parse(&index_bytes, format)
645 && index.extension(&sley_index::INDEX_EXT_LINK)?.is_none()
646 && !index.entries.iter().any(borrowed_entry_is_sparse_dir)
647 {
648 let (has_non_normal_stage, staged_gitlinks) =
649 index_worktree_metadata_for_entries(&index.entries);
650 if has_non_normal_stage {
651 return diff_name_status_index_worktree_changes_from_snapshot(
652 worktree_root,
653 git_dir,
654 format,
655 );
656 }
657 let stat_cache =
658 IndexStatCache::from_index_mtime_only(sley_index::file_mtime_parts(&index_metadata));
659 let entries = diff_name_status_index_worktree_changes_for_borrowed_entries(
660 worktree_root,
661 format,
662 &index.entries,
663 &stat_cache,
664 stat_clean_validator.as_deref_mut(),
665 )?;
666 return Ok(IndexWorktreeDiff {
667 entries,
668 staged_gitlinks,
669 });
670 }
671 let index = expand_sparse_index_for_worktree_diff(
672 sley_index::read_repository_index(git_dir, format)?,
673 git_dir,
674 format,
675 )?;
676 let (has_non_normal_stage, staged_gitlinks) =
677 index_worktree_metadata_for_entries(&index.entries);
678 if has_non_normal_stage {
679 return diff_name_status_index_worktree_changes_from_snapshot(
680 worktree_root,
681 git_dir,
682 format,
683 );
684 }
685 let stat_cache =
686 IndexStatCache::from_index_mtime_only(sley_index::file_mtime_parts(&index_metadata));
687 let entries = diff_name_status_index_worktree_changes_for_entries(
688 worktree_root,
689 format,
690 &index.entries,
691 &stat_cache,
692 stat_clean_validator.as_deref_mut(),
693 )?;
694 Ok(IndexWorktreeDiff {
695 entries,
696 staged_gitlinks,
697 })
698}
699
700fn borrowed_entry_is_sparse_dir(entry: &sley_index::IndexEntryRef<'_>) -> bool {
701 entry.mode == sley_index::SPARSE_DIR_MODE && entry.is_skip_worktree()
702}
703
704fn expand_sparse_index_for_worktree_diff(
705 mut index: Index,
706 git_dir: &Path,
707 format: ObjectFormat,
708) -> Result<Index> {
709 if !index
710 .entries
711 .iter()
712 .any(sley_index::IndexEntry::is_sparse_dir)
713 {
714 return Ok(index);
715 }
716
717 let db = FileObjectDatabase::from_git_dir(git_dir, format);
718 let mut expanded = Vec::with_capacity(index.entries.len());
719 for entry in std::mem::take(&mut index.entries) {
720 if !entry.is_sparse_dir() {
721 expanded.push(entry);
722 continue;
723 }
724
725 let dir_prefix = entry.path.as_bytes();
726 for (rel_path, (mode, oid)) in flatten_tree(&db, format, &entry.oid)? {
727 let mut path = dir_prefix.to_vec();
728 path.extend_from_slice(&rel_path);
729 let mut expanded_entry = sley_index::IndexEntry {
730 ctime_seconds: 0,
731 ctime_nanoseconds: 0,
732 mtime_seconds: 0,
733 mtime_nanoseconds: 0,
734 dev: 0,
735 ino: 0,
736 mode,
737 uid: 0,
738 gid: 0,
739 size: 0,
740 oid,
741 flags: 0,
742 flags_extended: 0,
743 path: BString::from(path),
744 };
745 expanded_entry.set_skip_worktree(true);
746 expanded_entry.refresh_name_length();
747 expanded.push(expanded_entry);
748 }
749 }
750
751 expanded.sort_by(|left, right| left.path.as_bytes().cmp(right.path.as_bytes()));
752 index.entries = expanded;
753 index.clear_sparse_extension()?;
754 Ok(index)
755}
756
757fn diff_name_status_index_worktree_changes_for_borrowed_entries(
758 worktree_root: &Path,
759 format: ObjectFormat,
760 entries: &[sley_index::IndexEntryRef<'_>],
761 stat_cache: &IndexStatCache,
762 stat_clean_validator: Option<&mut IndexWorktreeStatCleanValidator<'_>>,
763) -> Result<Vec<NameStatusEntry>> {
764 const PARALLEL_SCAN_MIN_ENTRIES: usize = 2048;
765 let workers = std::thread::available_parallelism()
766 .map(|count| count.get())
767 .unwrap_or(1)
768 .min(8);
769 if stat_clean_validator.is_some() || workers <= 1 || entries.len() < PARALLEL_SCAN_MIN_ENTRIES {
770 return diff_name_status_index_worktree_changes_for_borrowed_entry_chunk(
771 worktree_root,
772 format,
773 entries,
774 stat_cache,
775 stat_clean_validator,
776 );
777 }
778 let chunk_size = entries.len().div_ceil(workers);
779 std::thread::scope(|scope| {
780 let mut handles = Vec::new();
781 for chunk in entries.chunks(chunk_size) {
782 handles.push(scope.spawn(move || {
783 diff_name_status_index_worktree_changes_for_borrowed_entry_chunk(
784 worktree_root,
785 format,
786 chunk,
787 stat_cache,
788 None,
789 )
790 }));
791 }
792 let mut changes = Vec::new();
793 for handle in handles {
794 let chunk_changes = handle
795 .join()
796 .map_err(|_| GitError::Command("diff worker panicked".into()))??;
797 changes.extend(chunk_changes);
798 }
799 Ok(changes)
800 })
801}
802
803fn diff_name_status_index_worktree_changes_for_entries(
804 worktree_root: &Path,
805 format: ObjectFormat,
806 entries: &[sley_index::IndexEntry],
807 stat_cache: &IndexStatCache,
808 stat_clean_validator: Option<&mut IndexWorktreeStatCleanValidator<'_>>,
809) -> Result<Vec<NameStatusEntry>> {
810 const PARALLEL_SCAN_MIN_ENTRIES: usize = 2048;
811 let workers = std::thread::available_parallelism()
812 .map(|count| count.get())
813 .unwrap_or(1)
814 .min(8);
815 if stat_clean_validator.is_some() || workers <= 1 || entries.len() < PARALLEL_SCAN_MIN_ENTRIES {
816 return diff_name_status_index_worktree_changes_for_entry_chunk(
817 worktree_root,
818 format,
819 entries,
820 stat_cache,
821 stat_clean_validator,
822 );
823 }
824 let chunk_size = entries.len().div_ceil(workers);
825 std::thread::scope(|scope| {
826 let mut handles = Vec::new();
827 for chunk in entries.chunks(chunk_size) {
828 handles.push(scope.spawn(move || {
829 diff_name_status_index_worktree_changes_for_entry_chunk(
830 worktree_root,
831 format,
832 chunk,
833 stat_cache,
834 None,
835 )
836 }));
837 }
838 let mut changes = Vec::new();
839 for handle in handles {
840 let chunk_changes = handle
841 .join()
842 .map_err(|_| GitError::Command("diff worker panicked".into()))??;
843 changes.extend(chunk_changes);
844 }
845 Ok(changes)
846 })
847}
848
849fn diff_name_status_index_worktree_changes_for_entry_chunk(
850 worktree_root: &Path,
851 format: ObjectFormat,
852 entries: &[sley_index::IndexEntry],
853 stat_cache: &IndexStatCache,
854 mut stat_clean_validator: Option<&mut IndexWorktreeStatCleanValidator<'_>>,
855) -> Result<Vec<NameStatusEntry>> {
856 let mut changes = Vec::new();
857 let mut path = PathBuf::from(worktree_root);
858 for entry in entries {
859 worktree_path_for_repo_path_into(&mut path, worktree_root, entry.path.as_bytes());
860 if let Some(change) = index_worktree_change_for_entry(
861 &path,
862 format,
863 entry,
864 stat_cache,
865 &mut stat_clean_validator,
866 )? {
867 changes.push(change);
868 }
869 }
870 Ok(changes)
871}
872
873fn diff_name_status_index_worktree_changes_for_borrowed_entry_chunk(
874 worktree_root: &Path,
875 format: ObjectFormat,
876 entries: &[sley_index::IndexEntryRef<'_>],
877 stat_cache: &IndexStatCache,
878 mut stat_clean_validator: Option<&mut IndexWorktreeStatCleanValidator<'_>>,
879) -> Result<Vec<NameStatusEntry>> {
880 let mut changes = Vec::new();
881 let mut path = PathBuf::from(worktree_root);
882 for entry in entries {
883 worktree_path_for_repo_path_into(&mut path, worktree_root, entry.path);
884 if let Some(change) = index_worktree_change_for_entry(
885 &path,
886 format,
887 entry,
888 stat_cache,
889 &mut stat_clean_validator,
890 )? {
891 changes.push(change);
892 }
893 }
894 Ok(changes)
895}
896
897fn index_worktree_metadata_for_entries(
898 entries: &[impl WorktreeIndexEntry],
899) -> (bool, Vec<IndexGitlinkEntry>) {
900 let mut needs_snapshot = false;
901 let mut staged_gitlinks = Vec::new();
902 for entry in entries {
903 if entry.stage() != sley_index::Stage::Normal {
904 needs_snapshot = true;
905 }
906 if entry.is_intent_to_add() {
909 needs_snapshot = true;
910 }
911 if sley_index::is_gitlink(entry.mode()) {
912 staged_gitlinks.push(IndexGitlinkEntry {
913 path: BString::from_bytes(entry.git_path()),
914 oid: entry.oid(),
915 });
916 }
917 }
918 (needs_snapshot, staged_gitlinks)
919}
920
921fn diff_name_status_index_worktree_changes_from_snapshot(
922 worktree_root: &Path,
923 git_dir: &Path,
924 format: ObjectFormat,
925) -> Result<IndexWorktreeDiff> {
926 let IndexSnapshot {
927 entries: index,
928 stat_cache,
929 ..
930 } = read_index_snapshot(git_dir, format)?;
931 let intent_to_add_paths = read_intent_to_add_paths(git_dir, format)?;
936 let unmerged = read_unmerged_stages(git_dir, format)?;
943 let index_gitlinks = index_gitlinks(&index);
944 let staged_gitlinks = index_gitlinks
945 .iter()
946 .map(|(path, oid)| IndexGitlinkEntry {
947 path: BString::from_bytes(path),
948 oid: *oid,
949 })
950 .collect();
951 let mut changes = Vec::new();
952 for (git_path, left) in &index {
953 let conflict_stages = unmerged.get(git_path);
957 let right = worktree_entry_for_path(
958 worktree_root,
959 format,
960 git_path,
961 &index_gitlinks,
962 Some(&stat_cache),
963 None,
964 None,
965 )?;
966 if conflict_stages.is_some() {
967 changes.push(NameStatusEntry {
971 status: NameStatus::Unmerged,
972 path: git_path.clone().into(),
973 old_path: None,
974 old_mode: None,
975 new_mode: right.as_ref().map(|entry| entry.mode),
976 old_oid: None,
977 new_oid: None,
978 });
979 }
980 let left = match conflict_stages {
985 Some(stages) => match stages.ours.as_ref() {
986 Some(ours) => ours,
987 None => continue,
988 },
989 None => left,
990 };
991 if intent_to_add_paths.contains(git_path.as_slice()) {
996 if let Some(right) = right {
997 changes.push(NameStatusEntry {
998 status: NameStatus::Added,
999 path: git_path.clone().into(),
1000 old_path: None,
1001 old_mode: None,
1002 new_mode: Some(right.mode),
1003 old_oid: None,
1004 new_oid: Some(right.oid),
1005 });
1006 }
1007 continue;
1008 }
1009 let Some(right) = right else {
1010 changes.push(NameStatusEntry {
1011 status: NameStatus::Deleted,
1012 path: git_path.clone().into(),
1013 old_path: None,
1014 old_mode: Some(left.mode),
1015 new_mode: None,
1016 old_oid: Some(left.oid),
1017 new_oid: None,
1018 });
1019 continue;
1020 };
1021 if right != *left {
1022 changes.push(NameStatusEntry {
1023 status: modify_or_type_change(left.mode, right.mode),
1024 path: git_path.clone().into(),
1025 old_path: None,
1026 old_mode: Some(left.mode),
1027 new_mode: Some(right.mode),
1028 old_oid: Some(left.oid),
1029 new_oid: Some(right.oid),
1030 });
1031 }
1032 }
1033 Ok(IndexWorktreeDiff {
1034 entries: changes,
1035 staged_gitlinks,
1036 })
1037}
1038
1039struct ConflictStages {
1041 ours: Option<TrackedEntry>,
1042}
1043
1044fn read_unmerged_stages(
1048 git_dir: &Path,
1049 format: ObjectFormat,
1050) -> Result<BTreeMap<Vec<u8>, ConflictStages>> {
1051 let index_path = sley_index::repository_index_path(git_dir);
1052 if !index_path.exists() {
1053 return Ok(BTreeMap::new());
1054 }
1055 let index = sley_index::read_repository_index(git_dir, format)?;
1056 let mut out: BTreeMap<Vec<u8>, ConflictStages> = BTreeMap::new();
1057 for entry in &index.entries {
1058 let stage = entry.stage();
1059 if stage == sley_index::Stage::Normal {
1060 continue;
1061 }
1062 let path = entry.path.clone().into_bytes();
1063 let slot = out.entry(path).or_insert(ConflictStages { ours: None });
1064 if stage == sley_index::Stage::Ours {
1065 slot.ours = Some(TrackedEntry {
1066 mode: entry.mode,
1067 oid: entry.oid,
1068 });
1069 }
1070 }
1071 Ok(out)
1072}
1073
1074fn apply_name_status_options_to_index_worktree_changes(
1075 mut changes: Vec<NameStatusEntry>,
1076 options: DiffNameStatusOptions,
1077) -> Result<Vec<NameStatusEntry>> {
1078 if options.detect_renames {
1079 changes = detect_exact_renames_from_changes(changes, options.rename_empty);
1080 } else if options.detect_copies {
1081 changes.sort_by(|left, right| diff_entry_sort_path(left).cmp(diff_entry_sort_path(right)));
1082 }
1083 Ok(changes)
1084}
1085
1086fn detect_exact_renames_from_changes(
1087 changes: Vec<NameStatusEntry>,
1088 rename_empty: bool,
1089) -> Vec<NameStatusEntry> {
1090 let added = changes
1091 .iter()
1092 .enumerate()
1093 .filter(|(_, entry)| entry.status == NameStatus::Added)
1094 .map(|(idx, entry)| (idx, entry.path.clone()))
1095 .collect::<Vec<_>>();
1096 let mut sources = changes
1097 .iter()
1098 .enumerate()
1099 .filter(|(_, entry)| entry.status == NameStatus::Deleted)
1100 .filter_map(|(idx, entry)| {
1101 entry
1102 .old_oid
1103 .map(|oid| (idx, entry.path.clone(), oid, entry.old_mode))
1104 })
1105 .collect::<Vec<_>>();
1106 sources.sort_by(|left, right| left.1.cmp(&right.1).then_with(|| left.0.cmp(&right.0)));
1107
1108 let mut src_used = vec![false; sources.len()];
1109 let mut consumed_added = BTreeSet::new();
1110 let mut consumed_deleted = BTreeSet::new();
1111 let mut result = Vec::new();
1112
1113 for (idx, new_path) in &added {
1114 let Some(right_oid) = changes[*idx].new_oid else {
1115 continue;
1116 };
1117 if !rename_empty && is_empty_blob_oid(&right_oid) {
1118 continue;
1119 };
1120 let mut best: Option<usize> = None;
1121 let mut best_score = -1i32;
1122 for (source_idx, (_, src_path, src_oid, _)) in sources.iter().enumerate() {
1123 if src_used[source_idx] || *src_oid != right_oid {
1124 continue;
1125 }
1126 let score = 1 + i32::from(path_basename(src_path) == path_basename(new_path));
1127 if score > best_score {
1128 best = Some(source_idx);
1129 best_score = score;
1130 if score == 2 {
1131 break;
1132 }
1133 }
1134 }
1135 if let Some(source_idx) = best {
1136 src_used[source_idx] = true;
1137 consumed_added.insert(*idx);
1138 let (old_idx, old_path, old_oid, old_mode) = &sources[source_idx];
1139 consumed_deleted.insert(*old_idx);
1140 if old_path.as_bytes() == new_path.as_bytes()
1141 && *old_mode == changes[*idx].new_mode
1142 && *old_oid == right_oid
1143 {
1144 continue;
1145 }
1146 result.push(NameStatusEntry {
1147 status: NameStatus::Renamed(100),
1148 path: new_path.clone(),
1149 old_path: Some(old_path.clone()),
1150 old_mode: *old_mode,
1151 new_mode: changes[*idx].new_mode,
1152 old_oid: Some(*old_oid),
1153 new_oid: Some(right_oid),
1154 });
1155 }
1156 }
1157
1158 for (index, entry) in changes.into_iter().enumerate() {
1159 if consumed_added.contains(&index) || consumed_deleted.contains(&index) {
1160 continue;
1161 }
1162 result.push(entry);
1163 }
1164 result.sort_by(|left, right| diff_entry_sort_path(left).cmp(diff_entry_sort_path(right)));
1165 result
1166}
1167
1168pub fn diff_name_status_index_worktree_for_diff_files_with_options(
1192 worktree_root: impl AsRef<Path>,
1193 git_dir: impl AsRef<Path>,
1194 format: ObjectFormat,
1195 options: DiffNameStatusOptions,
1196) -> Result<Vec<NameStatusEntry>> {
1197 let worktree_root = worktree_root.as_ref();
1198 let git_dir = git_dir.as_ref();
1199 let changes =
1200 diff_name_status_index_worktree_with_options(worktree_root, git_dir, format, options)?;
1201 augment_with_stat_dirty_entries(worktree_root, git_dir, format, changes)
1202}
1203
1204pub fn augment_with_stat_dirty_entries(
1211 worktree_root: &Path,
1212 git_dir: &Path,
1213 format: ObjectFormat,
1214 mut content_changes: Vec<NameStatusEntry>,
1215) -> Result<Vec<NameStatusEntry>> {
1216 let IndexSnapshot {
1217 entries: index,
1218 stat_cache,
1219 ..
1220 } = read_index_snapshot(git_dir, format)?;
1221 let already_reported: BTreeSet<&[u8]> = content_changes
1224 .iter()
1225 .map(|entry| entry.path.as_bytes())
1226 .collect();
1227 let mut extras = Vec::new();
1228 for (git_path, tracked) in &index {
1229 if already_reported.contains(git_path.as_slice()) {
1230 continue;
1231 }
1232 let Some(cached) = stat_cache.entry_for_git_path(git_path) else {
1233 continue;
1234 };
1235 if sley_index::is_gitlink(tracked.mode) {
1238 continue;
1239 }
1240 let path = worktree_path_for_repo_path(worktree_root, git_path);
1241 let Ok(metadata) = fs::symlink_metadata(&path) else {
1242 continue;
1245 };
1246 if !(metadata.is_file() || metadata.file_type().is_symlink()) {
1247 continue;
1248 }
1249 match stat_cache.index_entry_worktree_stat_verdict(cached, &metadata) {
1250 sley_index::StatVerdict::Clean => continue,
1251 sley_index::StatVerdict::Dirty => {}
1252 sley_index::StatVerdict::RacyNeedsContentCheck => {
1257 if worktree_oid_matches_index(worktree_root, git_path, &metadata, tracked, format)?
1258 {
1259 continue;
1260 }
1261 }
1262 }
1263 extras.push(NameStatusEntry {
1264 status: NameStatus::Modified,
1265 path: git_path.clone().into(),
1266 old_path: None,
1267 old_mode: Some(tracked.mode),
1268 new_mode: Some(tracked.mode),
1269 old_oid: Some(tracked.oid),
1270 new_oid: None,
1271 });
1272 }
1273 if !extras.is_empty() {
1274 content_changes.extend(extras);
1275 content_changes
1276 .sort_by(|left, right| diff_entry_sort_path(left).cmp(diff_entry_sort_path(right)));
1277 }
1278 Ok(content_changes)
1279}
1280
1281fn classify_worktree_entry(
1291 path: &Path,
1292 metadata: &fs::Metadata,
1293 format: ObjectFormat,
1294) -> Result<TrackedEntry> {
1295 let is_symlink = metadata.file_type().is_symlink();
1296 let body = if is_symlink {
1297 symlink_target_bytes(path)?
1298 } else {
1299 fs::read(path)?
1300 };
1301 let oid = EncodedObject::new(ObjectType::Blob, body).object_id(format)?;
1302 let mode = if is_symlink {
1303 sley_index::SYMLINK_MODE
1304 } else {
1305 file_mode(metadata)
1306 };
1307 Ok(TrackedEntry { mode, oid })
1308}
1309
1310fn worktree_oid_matches_index(
1316 worktree_root: &Path,
1317 git_path: &[u8],
1318 metadata: &fs::Metadata,
1319 index_entry: &TrackedEntry,
1320 format: ObjectFormat,
1321) -> Result<bool> {
1322 let path = worktree_path_for_repo_path(worktree_root, git_path);
1323 let entry = classify_worktree_entry(&path, metadata, format)?;
1324 Ok(entry.oid == index_entry.oid && entry.mode == index_entry.mode)
1325}
1326
1327pub fn diff_name_status_trees_with_options(
1328 db: &FileObjectDatabase,
1329 format: ObjectFormat,
1330 left_tree: &ObjectId,
1331 right_tree: &ObjectId,
1332 options: DiffNameStatusOptions,
1333) -> Result<Vec<NameStatusEntry>> {
1334 if options.detect_inexact {
1335 return Ok(
1336 diff_name_status_trees_with_options_and_diagnostics(
1337 db, format, left_tree, right_tree, options,
1338 )?
1339 .entries,
1340 );
1341 }
1342 if !options.detect_copies {
1343 let mut changes = changed_tree_name_status_entries(db, format, left_tree, right_tree)?;
1344 if options.detect_renames {
1345 changes = detect_exact_renames_from_changes(changes, options.rename_empty);
1346 }
1347 return Ok(changes);
1348 }
1349
1350 let needs_full_maps = options.detect_copies && options.find_copies_harder;
1355 let (left_entries, right_entries) = if needs_full_maps {
1356 collect_full_tree_pair(db, format, left_tree, right_tree)?
1357 } else {
1358 changed_tree_entries(db, format, left_tree, right_tree)?
1359 };
1360 diff_name_status_maps(
1361 &left_entries,
1362 &right_entries,
1363 left_entries.keys().chain(right_entries.keys()),
1364 options,
1365 )
1366}
1367
1368pub fn diff_name_status_empty_tree_with_options(
1369 db: &FileObjectDatabase,
1370 format: ObjectFormat,
1371 right_tree: &ObjectId,
1372 options: DiffNameStatusOptions,
1373) -> Result<Vec<NameStatusEntry>> {
1374 let left_entries = BTreeMap::new();
1375 let mut right_entries = BTreeMap::new();
1376 collect_tree_entries(db, format, right_tree, Vec::new(), &mut right_entries)?;
1377 if options.detect_inexact {
1378 return diff_name_status_maps_with_renames(
1379 &left_entries,
1380 &right_entries,
1381 right_entries.keys(),
1382 options,
1383 |oid| read_blob_bytes(db, oid),
1384 );
1385 }
1386 diff_name_status_maps(&left_entries, &right_entries, right_entries.keys(), options)
1387}
1388
1389pub fn diff_name_status_trees_with_options_and_diagnostics(
1390 db: &FileObjectDatabase,
1391 format: ObjectFormat,
1392 left_tree: &ObjectId,
1393 right_tree: &ObjectId,
1394 options: DiffNameStatusOptions,
1395) -> Result<NameStatusWithRenameDiagnostics> {
1396 if !options.detect_copies {
1397 let mut diagnostics = RenameLimitDiagnostics::default();
1398 let mut changes = changed_tree_name_status_entries(db, format, left_tree, right_tree)?;
1399 if options.detect_renames {
1400 changes = detect_exact_renames_from_changes(changes, options.rename_empty);
1401 }
1402 if options.detect_renames && options.detect_inexact {
1403 diagnostics.inexact_renames_skipped = inexact_rename_limit_exceeded(&changes, &options);
1404 changes = detect_inexact_renames(changes, &options, &|oid| read_blob_bytes(db, oid));
1405 }
1406 return Ok(NameStatusWithRenameDiagnostics {
1407 entries: changes,
1408 rename_limit: diagnostics,
1409 });
1410 }
1411
1412 let needs_full_maps = options.detect_copies && options.find_copies_harder;
1416 let (left_entries, right_entries) = if needs_full_maps {
1417 collect_full_tree_pair(db, format, left_tree, right_tree)?
1418 } else {
1419 changed_tree_entries(db, format, left_tree, right_tree)?
1420 };
1421 diff_name_status_maps_with_renames_and_diagnostics(
1422 &left_entries,
1423 &right_entries,
1424 left_entries.keys().chain(right_entries.keys()),
1425 options,
1426 |oid| read_blob_bytes(db, oid),
1427 )
1428}
1429
1430pub(crate) fn read_blob_bytes(db: &FileObjectDatabase, oid: &ObjectId) -> Option<Arc<[u8]>> {
1435 match db.read_object(oid) {
1436 Ok(object) if object.object_type == ObjectType::Blob => {
1437 Some(Arc::from(object.body.clone().into_boxed_slice()))
1438 }
1439 _ => None,
1440 }
1441}
1442
1443fn raw_name_status_changes_for_unique_paths<'a>(
1446 left_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
1447 right_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
1448 paths: impl Iterator<Item = &'a Vec<u8>>,
1449) -> Vec<NameStatusEntry> {
1450 let mut changes = Vec::new();
1451 for path in paths {
1452 let left = left_entries.get(path);
1453 let right = right_entries.get(path);
1454 let status = match (left, right) {
1455 (None, Some(_)) => Some(NameStatus::Added),
1456 (Some(_), None) => Some(NameStatus::Deleted),
1457 (Some(left), Some(right)) if left != right => {
1458 Some(modify_or_type_change(left.mode, right.mode))
1459 }
1460 _ => None,
1461 };
1462 if let Some(status) = status {
1463 changes.push(NameStatusEntry {
1464 status,
1465 path: path.clone().into(),
1466 old_path: None,
1467 old_mode: left.map(|entry| entry.mode),
1468 new_mode: right.map(|entry| entry.mode),
1469 old_oid: left.map(|entry| entry.oid),
1470 new_oid: right.map(|entry| entry.oid),
1471 });
1472 }
1473 }
1474 changes
1475}
1476
1477pub(crate) fn diff_name_status_maps<'a>(
1478 left_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
1479 right_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
1480 candidate_paths: impl Iterator<Item = &'a Vec<u8>>,
1481 options: DiffNameStatusOptions,
1482) -> Result<Vec<NameStatusEntry>> {
1483 let paths = candidate_path_set(candidate_paths);
1484 diff_name_status_maps_for_path_set(left_entries, right_entries, &paths, options)
1485}
1486
1487fn diff_name_status_maps_for_path_set(
1488 left_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
1489 right_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
1490 candidate_paths: &BTreeSet<Vec<u8>>,
1491 options: DiffNameStatusOptions,
1492) -> Result<Vec<NameStatusEntry>> {
1493 diff_name_status_maps_for_unique_paths(
1494 left_entries,
1495 right_entries,
1496 candidate_paths.iter(),
1497 options,
1498 )
1499}
1500
1501fn diff_name_status_maps_for_unique_paths<'a>(
1502 left_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
1503 right_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
1504 candidate_paths: impl Iterator<Item = &'a Vec<u8>>,
1505 options: DiffNameStatusOptions,
1506) -> Result<Vec<NameStatusEntry>> {
1507 let mut changes =
1508 raw_name_status_changes_for_unique_paths(left_entries, right_entries, candidate_paths);
1509 if options.detect_renames {
1510 changes = detect_exact_renames(changes, left_entries, right_entries, options.rename_empty);
1511 }
1512 if options.detect_copies {
1513 changes = detect_exact_copies(
1514 changes,
1515 left_entries,
1516 right_entries,
1517 options.find_copies_harder,
1518 options.rename_empty,
1519 );
1520 }
1521 Ok(changes)
1522}
1523
1524pub(crate) fn diff_name_status_maps_with_renames<'a>(
1533 left_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
1534 right_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
1535 candidate_paths: impl Iterator<Item = &'a Vec<u8>>,
1536 options: DiffNameStatusOptions,
1537 fetch_blob: impl Fn(&ObjectId) -> Option<Arc<[u8]>>,
1538) -> Result<Vec<NameStatusEntry>> {
1539 Ok(diff_name_status_maps_with_renames_and_diagnostics(
1540 left_entries,
1541 right_entries,
1542 candidate_paths,
1543 options,
1544 fetch_blob,
1545 )?
1546 .entries)
1547}
1548
1549fn diff_name_status_maps_with_renames_and_diagnostics<'a>(
1550 left_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
1551 right_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
1552 candidate_paths: impl Iterator<Item = &'a Vec<u8>>,
1553 options: DiffNameStatusOptions,
1554 fetch_blob: impl Fn(&ObjectId) -> Option<Arc<[u8]>>,
1555) -> Result<NameStatusWithRenameDiagnostics> {
1556 let paths = candidate_path_set(candidate_paths);
1557 diff_name_status_maps_with_renames_for_path_set_and_diagnostics(
1558 left_entries,
1559 right_entries,
1560 &paths,
1561 options,
1562 fetch_blob,
1563 )
1564}
1565
1566fn diff_name_status_maps_with_renames_for_path_set(
1567 left_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
1568 right_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
1569 candidate_paths: &BTreeSet<Vec<u8>>,
1570 options: DiffNameStatusOptions,
1571 fetch_blob: impl Fn(&ObjectId) -> Option<Arc<[u8]>>,
1572) -> Result<Vec<NameStatusEntry>> {
1573 Ok(
1574 diff_name_status_maps_with_renames_for_path_set_and_diagnostics(
1575 left_entries,
1576 right_entries,
1577 candidate_paths,
1578 options,
1579 fetch_blob,
1580 )?
1581 .entries,
1582 )
1583}
1584
1585fn diff_name_status_maps_with_renames_for_path_set_and_diagnostics(
1586 left_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
1587 right_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
1588 candidate_paths: &BTreeSet<Vec<u8>>,
1589 options: DiffNameStatusOptions,
1590 fetch_blob: impl Fn(&ObjectId) -> Option<Arc<[u8]>>,
1591) -> Result<NameStatusWithRenameDiagnostics> {
1592 diff_name_status_maps_with_renames_for_unique_paths_and_diagnostics(
1593 left_entries,
1594 right_entries,
1595 candidate_paths.iter(),
1596 options,
1597 fetch_blob,
1598 )
1599}
1600
1601fn diff_name_status_maps_with_renames_for_unique_paths_and_diagnostics<'a>(
1602 left_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
1603 right_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
1604 candidate_paths: impl Iterator<Item = &'a Vec<u8>>,
1605 options: DiffNameStatusOptions,
1606 fetch_blob: impl Fn(&ObjectId) -> Option<Arc<[u8]>>,
1607) -> Result<NameStatusWithRenameDiagnostics> {
1608 let mut diagnostics = RenameLimitDiagnostics::default();
1609 let mut changes =
1610 raw_name_status_changes_for_unique_paths(left_entries, right_entries, candidate_paths);
1611 if options.detect_renames {
1612 changes = detect_exact_renames(changes, left_entries, right_entries, options.rename_empty);
1613 }
1614 if options.detect_renames && options.detect_inexact {
1618 diagnostics.inexact_renames_skipped = inexact_rename_limit_exceeded(&changes, &options);
1619 changes = detect_inexact_renames(changes, &options, &fetch_blob);
1620 }
1621 if options.detect_copies {
1622 changes = detect_exact_copies(
1623 changes,
1624 left_entries,
1625 right_entries,
1626 options.find_copies_harder,
1627 options.rename_empty,
1628 );
1629 }
1630 if options.detect_copies && options.detect_inexact {
1631 match inexact_copy_limit_outcome(&changes, left_entries, &options) {
1632 InexactCopyLimitOutcome::Full => {}
1633 InexactCopyLimitOutcome::DegradedToChangedSources => {
1634 diagnostics.inexact_copies_degraded = true;
1635 }
1636 InexactCopyLimitOutcome::Skip => {
1637 diagnostics.inexact_copies_skipped = true;
1638 }
1639 }
1640 changes = detect_inexact_copies(changes, left_entries, &options, &fetch_blob);
1641 }
1642 Ok(NameStatusWithRenameDiagnostics {
1643 entries: changes,
1644 rename_limit: diagnostics,
1645 })
1646}
1647
1648fn changed_tree_name_status_entries(
1649 db: &FileObjectDatabase,
1650 format: ObjectFormat,
1651 left_tree: &ObjectId,
1652 right_tree: &ObjectId,
1653) -> Result<Vec<NameStatusEntry>> {
1654 let mut changes = Vec::new();
1655 if left_tree != right_tree {
1656 diff_tree_pair_entries(db, format, left_tree, right_tree, &[], &mut changes)?;
1657 }
1658 sort_name_status_entries_by_path(&mut changes);
1659 Ok(changes)
1660}
1661
1662fn diff_tree_pair_entries(
1663 db: &FileObjectDatabase,
1664 format: ObjectFormat,
1665 left_tree: &ObjectId,
1666 right_tree: &ObjectId,
1667 prefix: &[u8],
1668 changes: &mut Vec<NameStatusEntry>,
1669) -> Result<()> {
1670 let left_entries = read_tree_object(db, format, left_tree)?.entries;
1671 let right_entries = read_tree_object(db, format, right_tree)?.entries;
1672 let mut left_idx = 0usize;
1673 let mut right_idx = 0usize;
1674
1675 while left_idx < left_entries.len() || right_idx < right_entries.len() {
1676 match (left_entries.get(left_idx), right_entries.get(right_idx)) {
1677 (Some(left_entry), Some(right_entry)) => match left_entry.name.cmp(&right_entry.name) {
1678 std::cmp::Ordering::Equal => {
1679 let left_name = left_entry.name.clone();
1680 let right_name = right_entry.name.clone();
1681 let left_start = left_idx;
1682 let right_start = right_idx;
1683 while left_idx < left_entries.len() && left_entries[left_idx].name == left_name
1684 {
1685 left_idx += 1;
1686 }
1687 while right_idx < right_entries.len()
1688 && right_entries[right_idx].name == right_name
1689 {
1690 right_idx += 1;
1691 }
1692
1693 let left_group = &left_entries[left_start..left_idx];
1694 let right_group = &right_entries[right_start..right_idx];
1695 let paired = left_group.len().min(right_group.len());
1696 for idx in 0..paired {
1697 diff_tree_entry_pair_entries(
1698 db,
1699 format,
1700 prefix,
1701 Some(&left_group[idx]),
1702 Some(&right_group[idx]),
1703 changes,
1704 )?;
1705 }
1706 for entry in &left_group[paired..] {
1707 diff_tree_entry_pair_entries(
1708 db,
1709 format,
1710 prefix,
1711 Some(entry),
1712 None,
1713 changes,
1714 )?;
1715 }
1716 for entry in &right_group[paired..] {
1717 diff_tree_entry_pair_entries(
1718 db,
1719 format,
1720 prefix,
1721 None,
1722 Some(entry),
1723 changes,
1724 )?;
1725 }
1726 }
1727 std::cmp::Ordering::Less => {
1728 let left_name = left_entry.name.clone();
1729 while left_idx < left_entries.len() && left_entries[left_idx].name == left_name
1730 {
1731 diff_tree_entry_pair_entries(
1732 db,
1733 format,
1734 prefix,
1735 Some(&left_entries[left_idx]),
1736 None,
1737 changes,
1738 )?;
1739 left_idx += 1;
1740 }
1741 }
1742 std::cmp::Ordering::Greater => {
1743 let right_name = right_entry.name.clone();
1744 while right_idx < right_entries.len()
1745 && right_entries[right_idx].name == right_name
1746 {
1747 diff_tree_entry_pair_entries(
1748 db,
1749 format,
1750 prefix,
1751 None,
1752 Some(&right_entries[right_idx]),
1753 changes,
1754 )?;
1755 right_idx += 1;
1756 }
1757 }
1758 },
1759 (Some(left_entry), None) => {
1760 let left_name = left_entry.name.clone();
1761 while left_idx < left_entries.len() && left_entries[left_idx].name == left_name {
1762 diff_tree_entry_pair_entries(
1763 db,
1764 format,
1765 prefix,
1766 Some(&left_entries[left_idx]),
1767 None,
1768 changes,
1769 )?;
1770 left_idx += 1;
1771 }
1772 }
1773 (None, Some(right_entry)) => {
1774 let right_name = right_entry.name.clone();
1775 while right_idx < right_entries.len() && right_entries[right_idx].name == right_name
1776 {
1777 diff_tree_entry_pair_entries(
1778 db,
1779 format,
1780 prefix,
1781 None,
1782 Some(&right_entries[right_idx]),
1783 changes,
1784 )?;
1785 right_idx += 1;
1786 }
1787 }
1788 (None, None) => break,
1789 }
1790 }
1791
1792 Ok(())
1793}
1794
1795fn diff_tree_entry_pair_entries(
1796 db: &FileObjectDatabase,
1797 format: ObjectFormat,
1798 prefix: &[u8],
1799 left_entry: Option<&TreeEntry>,
1800 right_entry: Option<&TreeEntry>,
1801 changes: &mut Vec<NameStatusEntry>,
1802) -> Result<()> {
1803 let left_is_tree = left_entry.is_some_and(|entry| entry.mode == TREE_ENTRY_MODE);
1804 let right_is_tree = right_entry.is_some_and(|entry| entry.mode == TREE_ENTRY_MODE);
1805
1806 if let (Some(left_entry), Some(right_entry)) = (left_entry, right_entry) {
1807 if left_is_tree && right_is_tree {
1808 if left_entry.oid == right_entry.oid {
1809 return Ok(());
1810 }
1811 let path = join_tree_path(prefix, left_entry.name.as_bytes());
1812 return diff_tree_pair_entries(
1813 db,
1814 format,
1815 &left_entry.oid,
1816 &right_entry.oid,
1817 &path,
1818 changes,
1819 );
1820 }
1821 if !left_is_tree && !right_is_tree {
1822 if left_entry.mode == right_entry.mode && left_entry.oid == right_entry.oid {
1823 return Ok(());
1824 }
1825 let path = join_tree_path(prefix, left_entry.name.as_bytes());
1826 changes.push(NameStatusEntry {
1827 status: modify_or_type_change(left_entry.mode, right_entry.mode),
1828 path: path.into(),
1829 old_path: None,
1830 old_mode: Some(left_entry.mode),
1831 new_mode: Some(right_entry.mode),
1832 old_oid: Some(left_entry.oid),
1833 new_oid: Some(right_entry.oid),
1834 });
1835 return Ok(());
1836 }
1837 }
1838
1839 if let Some(left_entry) = left_entry {
1840 let path = join_tree_path(prefix, left_entry.name.as_bytes());
1841 collect_tree_side_changes(
1842 db,
1843 format,
1844 path,
1845 left_entry.mode,
1846 left_entry.oid,
1847 NameStatus::Deleted,
1848 changes,
1849 )?;
1850 }
1851 if let Some(right_entry) = right_entry {
1852 let path = join_tree_path(prefix, right_entry.name.as_bytes());
1853 collect_tree_side_changes(
1854 db,
1855 format,
1856 path,
1857 right_entry.mode,
1858 right_entry.oid,
1859 NameStatus::Added,
1860 changes,
1861 )?;
1862 }
1863 Ok(())
1864}
1865
1866fn collect_tree_side_changes(
1867 db: &FileObjectDatabase,
1868 format: ObjectFormat,
1869 path: Vec<u8>,
1870 mode: u32,
1871 oid: ObjectId,
1872 status: NameStatus,
1873 changes: &mut Vec<NameStatusEntry>,
1874) -> Result<()> {
1875 if mode == TREE_ENTRY_MODE {
1876 for entry in read_tree_object(db, format, &oid)?.entries {
1877 let child_path = join_tree_path(&path, entry.name.as_bytes());
1878 collect_tree_side_changes(
1879 db, format, child_path, entry.mode, entry.oid, status, changes,
1880 )?;
1881 }
1882 return Ok(());
1883 }
1884
1885 changes.push(NameStatusEntry {
1886 status,
1887 path: path.into(),
1888 old_path: None,
1889 old_mode: (status == NameStatus::Deleted).then_some(mode),
1890 new_mode: (status == NameStatus::Added).then_some(mode),
1891 old_oid: (status == NameStatus::Deleted).then_some(oid),
1892 new_oid: (status == NameStatus::Added).then_some(oid),
1893 });
1894 Ok(())
1895}
1896
1897fn sort_name_status_entries_by_path(changes: &mut [NameStatusEntry]) {
1898 changes.sort_by(|left, right| diff_entry_sort_path(left).cmp(diff_entry_sort_path(right)));
1899}
1900
1901fn detect_exact_renames(
1902 changes: Vec<NameStatusEntry>,
1903 left_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
1904 right_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
1905 rename_empty: bool,
1906) -> Vec<NameStatusEntry> {
1907 let added = changes
1908 .iter()
1909 .enumerate()
1910 .filter(|(_, entry)| entry.status == NameStatus::Added)
1911 .map(|(idx, entry)| (idx, entry.path.clone()))
1912 .collect::<Vec<_>>();
1913 let mut sources = changes
1916 .iter()
1917 .filter(|entry| entry.status == NameStatus::Deleted)
1918 .filter_map(|entry| {
1919 left_entries
1920 .get(entry.path.as_bytes())
1921 .map(|left| (entry.path.clone(), left.oid))
1922 })
1923 .collect::<Vec<_>>();
1924 sources.sort_by(|a, b| a.0.cmp(&b.0));
1925 let mut src_used = vec![false; sources.len()];
1926 let mut consumed = BTreeSet::new();
1927 let mut renamed_old_paths = BTreeSet::new();
1928 let mut result = Vec::new();
1929
1930 for (idx, new_path) in &added {
1936 let Some(right) = right_entries.get(new_path.as_bytes()) else {
1937 continue;
1938 };
1939 if !rename_empty && is_empty_blob_oid(&right.oid) {
1940 continue;
1941 }
1942 let mut best: Option<usize> = None;
1943 let mut best_score = -1i32;
1944 for (si, (src_path, src_oid)) in sources.iter().enumerate() {
1945 if src_used[si] || *src_oid != right.oid {
1946 continue;
1947 }
1948 let score = 1 + i32::from(path_basename(src_path) == path_basename(new_path));
1949 if score > best_score {
1950 best = Some(si);
1951 best_score = score;
1952 if score == 2 {
1953 break;
1954 }
1955 }
1956 }
1957 if let Some(si) = best {
1958 src_used[si] = true;
1959 consumed.insert(*idx);
1960 let old_path = sources[si].0.clone();
1961 let left = &left_entries[old_path.as_bytes()];
1962 renamed_old_paths.insert(old_path.clone());
1963 result.push(NameStatusEntry {
1964 status: NameStatus::Renamed(100),
1965 path: new_path.clone(),
1966 old_path: Some(old_path),
1967 old_mode: Some(left.mode),
1968 new_mode: Some(right.mode),
1969 old_oid: Some(left.oid),
1970 new_oid: Some(right.oid),
1971 });
1972 }
1973 }
1974
1975 for (idx, entry) in changes.into_iter().enumerate() {
1976 if entry.status == NameStatus::Added && consumed.contains(&idx) {
1977 continue;
1978 }
1979 if entry.status == NameStatus::Deleted && renamed_old_paths.contains(&entry.path) {
1980 continue;
1981 }
1982 result.push(entry);
1983 }
1984 result.sort_by(|left, right| diff_entry_sort_path(left).cmp(diff_entry_sort_path(right)));
1985 result
1986}
1987
1988fn detect_exact_copies(
1989 changes: Vec<NameStatusEntry>,
1990 left_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
1991 right_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
1992 find_copies_harder: bool,
1993 rename_empty: bool,
1994) -> Vec<NameStatusEntry> {
1995 let changed_sources = changes
1996 .iter()
1997 .filter(|entry| matches!(entry.status, NameStatus::Deleted | NameStatus::Modified))
1998 .map(|entry| entry.path.clone())
1999 .collect::<BTreeSet<_>>();
2000 let source_paths = left_entries
2001 .keys()
2002 .filter(|path| find_copies_harder || changed_sources.contains(path.as_slice()))
2003 .cloned()
2004 .collect::<Vec<_>>();
2005
2006 let mut result = Vec::new();
2007 for entry in changes {
2008 if entry.status != NameStatus::Added {
2009 result.push(entry);
2010 continue;
2011 }
2012 let Some(right) = right_entries.get(entry.path.as_bytes()) else {
2013 result.push(entry);
2014 continue;
2015 };
2016 if let Some(old_path) = source_paths.iter().find(|old_path| {
2017 old_path.as_slice() != entry.path.as_bytes()
2018 && left_entries.get(*old_path).is_some_and(|left| {
2019 left.oid == right.oid && (rename_empty || !is_empty_blob_oid(&left.oid))
2020 })
2021 }) {
2022 result.push(NameStatusEntry {
2023 status: NameStatus::Copied(100),
2024 path: entry.path,
2025 old_path: Some(old_path.clone().into()),
2026 old_mode: left_entries
2027 .get(old_path.as_slice())
2028 .map(|entry| entry.mode),
2029 new_mode: entry.new_mode,
2030 old_oid: left_entries.get(old_path.as_slice()).map(|entry| entry.oid),
2031 new_oid: entry.new_oid,
2032 });
2033 } else {
2034 result.push(entry);
2035 }
2036 }
2037 result.sort_by(|left, right| diff_entry_sort_path(left).cmp(diff_entry_sort_path(right)));
2038 result
2039}
2040
2041#[derive(Debug, Clone)]
2044struct RenameSourceMeta {
2045 path: BString,
2046 mode: Option<u32>,
2047 oid: Option<ObjectId>,
2048}
2049
2050struct ScoredPair {
2053 src: usize,
2055 dst: usize,
2057 score: u8,
2059}
2060
2061fn rename_limit_exceeded(source_count: usize, dest_count: usize, rename_limit: usize) -> bool {
2062 rename_limit > 0
2063 && source_count
2064 .saturating_mul(dest_count)
2065 .gt(&rename_limit.saturating_mul(rename_limit))
2066}
2067
2068#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2069enum InexactCopyLimitOutcome {
2070 Full,
2071 DegradedToChangedSources,
2072 Skip,
2073}
2074
2075fn inexact_rename_candidates(
2076 changes: &[NameStatusEntry],
2077 options: &DiffNameStatusOptions,
2078) -> (usize, usize) {
2079 let mut deleted = 0usize;
2080 let mut added = 0usize;
2081 for entry in changes {
2082 match entry.status {
2083 NameStatus::Deleted => {
2084 if entry
2085 .old_oid
2086 .as_ref()
2087 .is_some_and(|oid| options.rename_empty || !is_empty_blob_oid(oid))
2088 {
2089 deleted += 1;
2090 }
2091 }
2092 NameStatus::Added => {
2093 if entry
2094 .new_oid
2095 .as_ref()
2096 .is_some_and(|oid| options.rename_empty || !is_empty_blob_oid(oid))
2097 {
2098 added += 1;
2099 }
2100 }
2101 _ => {}
2102 }
2103 }
2104 (deleted, added)
2105}
2106
2107fn inexact_rename_limit_exceeded(
2108 changes: &[NameStatusEntry],
2109 options: &DiffNameStatusOptions,
2110) -> bool {
2111 let (source_count, dest_count) = inexact_rename_candidates(changes, options);
2112 rename_limit_exceeded(source_count, dest_count, options.rename_limit)
2113}
2114
2115fn changed_copy_sources(changes: &[NameStatusEntry]) -> BTreeSet<BString> {
2116 changes
2117 .iter()
2118 .filter(|entry| matches!(entry.status, NameStatus::Deleted | NameStatus::Modified))
2119 .map(|entry| entry.path.clone())
2120 .collect()
2121}
2122
2123fn inexact_copy_source_count(
2124 changed_sources: &BTreeSet<BString>,
2125 left_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
2126 options: &DiffNameStatusOptions,
2127 find_copies_harder: bool,
2128) -> usize {
2129 left_entries
2130 .iter()
2131 .filter(|(path, _)| find_copies_harder || changed_sources.contains(path.as_slice()))
2132 .filter(|(_, tracked)| options.rename_empty || !is_empty_blob_oid(&tracked.oid))
2133 .count()
2134}
2135
2136fn inexact_copy_dest_count(changes: &[NameStatusEntry], options: &DiffNameStatusOptions) -> usize {
2137 changes
2138 .iter()
2139 .filter(|entry| entry.status == NameStatus::Added)
2140 .filter(|entry| {
2141 entry
2142 .new_oid
2143 .as_ref()
2144 .is_some_and(|oid| options.rename_empty || !is_empty_blob_oid(oid))
2145 })
2146 .count()
2147}
2148
2149fn inexact_copy_limit_outcome(
2150 changes: &[NameStatusEntry],
2151 left_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
2152 options: &DiffNameStatusOptions,
2153) -> InexactCopyLimitOutcome {
2154 let changed_sources = changed_copy_sources(changes);
2155 let dest_count = inexact_copy_dest_count(changes, options);
2156 let source_count = inexact_copy_source_count(
2157 &changed_sources,
2158 left_entries,
2159 options,
2160 options.find_copies_harder,
2161 );
2162 if !rename_limit_exceeded(source_count, dest_count, options.rename_limit) {
2163 return InexactCopyLimitOutcome::Full;
2164 }
2165 if options.find_copies_harder {
2166 let changed_source_count =
2167 inexact_copy_source_count(&changed_sources, left_entries, options, false);
2168 if !rename_limit_exceeded(changed_source_count, dest_count, options.rename_limit) {
2169 return InexactCopyLimitOutcome::DegradedToChangedSources;
2170 }
2171 }
2172 InexactCopyLimitOutcome::Skip
2173}
2174
2175fn detect_inexact_renames(
2186 changes: Vec<NameStatusEntry>,
2187 options: &DiffNameStatusOptions,
2188 fetch_blob: &impl Fn(&ObjectId) -> Option<Arc<[u8]>>,
2189) -> Vec<NameStatusEntry> {
2190 let threshold = options.rename_threshold;
2191 if threshold > 100 {
2193 return changes;
2194 }
2195
2196 let mut deleted_indices = Vec::new();
2197 let mut added_indices = Vec::new();
2198 for (idx, entry) in changes.iter().enumerate() {
2199 match entry.status {
2200 NameStatus::Deleted => {
2201 let Some(oid) = entry.old_oid.as_ref() else {
2202 continue;
2203 };
2204 if !options.rename_empty && is_empty_blob_oid(oid) {
2205 continue;
2206 }
2207 deleted_indices.push(idx);
2208 }
2209 NameStatus::Added => {
2210 let Some(oid) = entry.new_oid.as_ref() else {
2211 continue;
2212 };
2213 if !options.rename_empty && is_empty_blob_oid(oid) {
2214 continue;
2215 }
2216 added_indices.push(idx);
2217 }
2218 _ => {}
2219 }
2220 }
2221
2222 if deleted_indices.is_empty() || added_indices.is_empty() {
2223 return changes;
2224 }
2225
2226 if rename_limit_exceeded(
2230 deleted_indices.len(),
2231 added_indices.len(),
2232 options.rename_limit,
2233 ) {
2234 return changes;
2235 }
2236
2237 let mut deleted: Vec<(usize, Arc<[u8]>)> = Vec::new();
2240 for idx in deleted_indices {
2241 let Some(oid) = changes[idx].old_oid.as_ref() else {
2242 continue;
2243 };
2244 if let Some(bytes) = fetch_blob(oid) {
2245 deleted.push((idx, bytes));
2246 }
2247 }
2248 let mut added: Vec<(usize, Arc<[u8]>)> = Vec::new();
2249 for idx in added_indices {
2250 let Some(oid) = changes[idx].new_oid.as_ref() else {
2251 continue;
2252 };
2253 if let Some(bytes) = fetch_blob(oid) {
2254 added.push((idx, bytes));
2255 }
2256 }
2257 if deleted.is_empty() || added.is_empty() {
2258 return changes;
2259 }
2260
2261 let mut src_used = vec![false; deleted.len()];
2262 let mut dst_used = vec![false; added.len()];
2263 let mut rename_of: BTreeMap<usize, (usize, u8)> = BTreeMap::new();
2265
2266 if !options.detect_copies {
2272 let src_paths: Vec<&[u8]> = deleted
2273 .iter()
2274 .map(|(idx, _)| &changes[*idx].path[..])
2275 .collect();
2276 let dst_paths: Vec<&[u8]> = added
2277 .iter()
2278 .map(|(idx, _)| &changes[*idx].path[..])
2279 .collect();
2280 let basename_pairs = basename_rename_matches(
2281 &src_paths,
2282 &dst_paths,
2283 &src_used,
2284 &dst_used,
2285 threshold,
2286 |si, di| Some(blob_similarity(&deleted[si].1, &added[di].1)),
2287 );
2288 for (si, di, score) in basename_pairs {
2289 src_used[si] = true;
2290 dst_used[di] = true;
2291 rename_of.insert(added[di].0, (deleted[si].0, score));
2292 }
2293 }
2294
2295 let mut pairs: Vec<ScoredPair> = Vec::new();
2298 for (si, (_, src_bytes)) in deleted.iter().enumerate() {
2299 if src_used[si] {
2300 continue;
2301 }
2302 for (di, (_, dst_bytes)) in added.iter().enumerate() {
2303 if dst_used[di] {
2304 continue;
2305 }
2306 let score = blob_similarity(src_bytes, dst_bytes);
2307 if score >= threshold {
2308 pairs.push(ScoredPair {
2309 src: si,
2310 dst: di,
2311 score,
2312 });
2313 }
2314 }
2315 }
2316 pairs.sort_by(|a, b| {
2319 b.score
2320 .cmp(&a.score)
2321 .then_with(|| a.src.cmp(&b.src))
2322 .then_with(|| a.dst.cmp(&b.dst))
2323 });
2324
2325 for pair in pairs {
2326 if src_used[pair.src] || dst_used[pair.dst] {
2327 continue;
2328 }
2329 src_used[pair.src] = true;
2330 dst_used[pair.dst] = true;
2331 let src_change_idx = deleted[pair.src].0;
2332 let dst_change_idx = added[pair.dst].0;
2333 rename_of.insert(dst_change_idx, (src_change_idx, pair.score));
2334 }
2335
2336 if rename_of.is_empty() {
2337 return changes;
2338 }
2339
2340 let consumed_sources: BTreeSet<usize> =
2343 rename_of.values().map(|(src_idx, _)| *src_idx).collect();
2344 let source_meta: BTreeMap<usize, RenameSourceMeta> = consumed_sources
2345 .iter()
2346 .map(|&src_idx| {
2347 let src = &changes[src_idx];
2348 (
2349 src_idx,
2350 RenameSourceMeta {
2351 path: src.path.clone(),
2352 mode: src.old_mode,
2353 oid: src.old_oid,
2354 },
2355 )
2356 })
2357 .collect();
2358
2359 let mut result = Vec::with_capacity(changes.len());
2360 for (idx, entry) in changes.into_iter().enumerate() {
2361 if consumed_sources.contains(&idx) {
2362 continue;
2364 }
2365 if let Some((src_idx, score)) = rename_of.get(&idx) {
2366 let meta = source_meta
2370 .get(src_idx)
2371 .cloned()
2372 .unwrap_or(RenameSourceMeta {
2373 path: BString::default(),
2374 mode: None,
2375 oid: None,
2376 });
2377 result.push(NameStatusEntry {
2378 status: NameStatus::Renamed(*score),
2379 path: entry.path,
2380 old_path: Some(meta.path),
2381 old_mode: meta.mode,
2382 new_mode: entry.new_mode,
2383 old_oid: meta.oid,
2384 new_oid: entry.new_oid,
2385 });
2386 continue;
2387 }
2388 result.push(entry);
2389 }
2390
2391 result.sort_by(|left, right| diff_entry_sort_path(left).cmp(diff_entry_sort_path(right)));
2392 result
2393}
2394
2395fn detect_inexact_copies(
2405 changes: Vec<NameStatusEntry>,
2406 left_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
2407 options: &DiffNameStatusOptions,
2408 fetch_blob: &impl Fn(&ObjectId) -> Option<Arc<[u8]>>,
2409) -> Vec<NameStatusEntry> {
2410 let threshold = options.copy_threshold;
2411 if threshold > 100 {
2412 return changes;
2413 }
2414
2415 let changed_sources = changed_copy_sources(&changes);
2416 let limit_outcome = inexact_copy_limit_outcome(&changes, left_entries, options);
2417 if limit_outcome == InexactCopyLimitOutcome::Skip {
2418 return changes;
2419 }
2420 let find_copies_harder = options.find_copies_harder
2421 && limit_outcome != InexactCopyLimitOutcome::DegradedToChangedSources;
2422 let mut source_candidates: Vec<(Vec<u8>, &TrackedEntry)> = Vec::new();
2423 for (path, tracked) in left_entries {
2424 if !(find_copies_harder || changed_sources.contains(path.as_slice())) {
2425 continue;
2426 }
2427 if !options.rename_empty && is_empty_blob_oid(&tracked.oid) {
2428 continue;
2429 }
2430 source_candidates.push((path.clone(), tracked));
2431 }
2432 if source_candidates.is_empty() {
2433 return changes;
2434 }
2435
2436 let mut sources: Vec<(Vec<u8>, &TrackedEntry, Arc<[u8]>)> = Vec::new();
2437 for (path, tracked) in source_candidates {
2438 if let Some(bytes) = fetch_blob(&tracked.oid) {
2439 sources.push((path, tracked, bytes));
2440 }
2441 }
2442 if sources.is_empty() {
2443 return changes;
2444 }
2445
2446 let mut result = Vec::with_capacity(changes.len());
2447 for entry in changes {
2448 if entry.status != NameStatus::Added {
2449 result.push(entry);
2450 continue;
2451 }
2452 let Some(new_oid) = entry.new_oid.as_ref() else {
2453 result.push(entry);
2454 continue;
2455 };
2456 let Some(dst_bytes) = fetch_blob(new_oid) else {
2457 result.push(entry);
2458 continue;
2459 };
2460
2461 let mut best: Option<(usize, u8)> = None;
2465 for (i, (src_path, _, src_bytes)) in sources.iter().enumerate() {
2466 if src_path.as_slice() == entry.path.as_bytes() {
2467 continue;
2468 }
2469 let score = blob_similarity(src_bytes, &dst_bytes);
2470 if score < threshold {
2471 continue;
2472 }
2473 match best {
2474 Some((_, best_score)) if best_score >= score => {}
2475 _ => best = Some((i, score)),
2476 }
2477 }
2478
2479 if let Some((src_idx, score)) = best {
2480 let (src_path, src_tracked, _) = &sources[src_idx];
2481 result.push(NameStatusEntry {
2482 status: NameStatus::Copied(score),
2483 path: entry.path,
2484 old_path: Some(src_path.clone().into()),
2485 old_mode: Some(src_tracked.mode),
2486 new_mode: entry.new_mode,
2487 old_oid: Some(src_tracked.oid),
2488 new_oid: entry.new_oid,
2489 });
2490 } else {
2491 result.push(entry);
2492 }
2493 }
2494 result.sort_by(|left, right| diff_entry_sort_path(left).cmp(diff_entry_sort_path(right)));
2495 result
2496}
2497
2498fn is_empty_blob_oid(oid: &ObjectId) -> bool {
2499 object_id_for_bytes(oid.format(), "blob", b"").is_ok_and(|empty| empty == *oid)
2500}
2501
2502const MAX_SPAN_BYTES: usize = 64;
2543
2544pub fn blob_similarity(a: &[u8], b: &[u8]) -> u8 {
2556 if a == b {
2558 return 100;
2559 }
2560 let max_size = a.len().max(b.len());
2561 if max_size == 0 {
2562 return 100;
2565 }
2566
2567 let src = span_hash_counts(a, blob_is_text(a));
2568 let dst = span_hash_counts(b, blob_is_text(b));
2569 let common = common_span_bytes(&src, &dst);
2570
2571 const MAX_SCORE: u64 = 60000;
2579 let internal = (common as u64 * MAX_SCORE) / max_size as u64;
2580 let score = internal * 100 / MAX_SCORE;
2581 score.min(100) as u8
2582}
2583
2584pub fn path_basename(path: &[u8]) -> &[u8] {
2587 match path.iter().rposition(|&byte| byte == b'/') {
2588 Some(slash) => &path[slash + 1..],
2589 None => path,
2590 }
2591}
2592
2593pub fn basename_min_score(threshold: u8) -> u8 {
2597 let threshold = threshold.min(100);
2598 threshold + (100 - threshold) / 2
2599}
2600
2601pub fn basename_rename_matches(
2615 src_paths: &[&[u8]],
2616 dst_paths: &[&[u8]],
2617 src_used: &[bool],
2618 dst_used: &[bool],
2619 threshold: u8,
2620 mut similarity: impl FnMut(usize, usize) -> Option<u8>,
2621) -> Vec<(usize, usize, u8)> {
2622 let min_score = basename_min_score(threshold);
2623 let mut src_by_base: HashMap<&[u8], Option<usize>> = HashMap::new();
2626 for (si, path) in src_paths.iter().enumerate() {
2627 if src_used.get(si).copied().unwrap_or(false) {
2628 continue;
2629 }
2630 src_by_base
2631 .entry(path_basename(path))
2632 .and_modify(|slot| *slot = None)
2633 .or_insert(Some(si));
2634 }
2635 let mut dst_by_base: HashMap<&[u8], Option<usize>> = HashMap::new();
2636 for (di, path) in dst_paths.iter().enumerate() {
2637 if dst_used.get(di).copied().unwrap_or(false) {
2638 continue;
2639 }
2640 dst_by_base
2641 .entry(path_basename(path))
2642 .and_modify(|slot| *slot = None)
2643 .or_insert(Some(di));
2644 }
2645 let mut matches = Vec::new();
2646 let mut dst_taken = vec![false; dst_paths.len()];
2647 for (si, path) in src_paths.iter().enumerate() {
2648 if src_used.get(si).copied().unwrap_or(false) {
2649 continue;
2650 }
2651 let base = path_basename(path);
2652 let Some(Some(src_idx)) = src_by_base.get(base).copied() else {
2654 continue;
2655 };
2656 if src_idx != si {
2657 continue;
2658 }
2659 let Some(Some(dst_idx)) = dst_by_base.get(base).copied() else {
2660 continue;
2661 };
2662 if dst_used.get(dst_idx).copied().unwrap_or(false) || dst_taken[dst_idx] {
2663 continue;
2664 }
2665 let Some(score) = similarity(si, dst_idx) else {
2666 continue;
2667 };
2668 if score < min_score {
2669 continue;
2670 }
2671 dst_taken[dst_idx] = true;
2672 matches.push((si, dst_idx, score));
2673 }
2674 matches
2675}
2676
2677fn span_hash_counts(data: &[u8], is_text: bool) -> BTreeMap<u64, usize> {
2684 let mut counts: BTreeMap<u64, usize> = BTreeMap::new();
2685 let mut idx = 0usize;
2686 let len = data.len();
2687 while idx < len {
2688 let mut accum1: u32 = 0;
2693 let mut accum2: u32 = 0;
2694 let mut span_len = 0usize;
2695 loop {
2696 let c = data[idx] as u32;
2697 idx += 1;
2698 if is_text && c == u32::from(b'\r') && idx < len && data[idx] == b'\n' {
2703 continue;
2704 }
2705 span_len += 1;
2706 accum1 = (accum1 << 7) ^ (accum2 >> 25);
2707 accum2 = (accum2 << 7) ^ (accum1 >> 25);
2708 accum1 = accum1.wrapping_add(c);
2709 let newline = c == u32::from(b'\n');
2710 if span_len >= MAX_SPAN_BYTES || newline || idx >= len {
2711 break;
2712 }
2713 }
2714 let hash = ((accum1 as u64) << 32) ^ (accum2 as u64) ^ ((span_len as u64) << 1);
2718 *counts.entry(hash).or_insert(0) += span_len;
2719 }
2720 counts
2721}
2722
2723pub fn count_changes(src: &[u8], dst: &[u8]) -> (usize, usize) {
2732 let src_counts = span_hash_counts(src, blob_is_text(src));
2733 let dst_counts = span_hash_counts(dst, blob_is_text(dst));
2734 let copied = common_span_bytes(&src_counts, &dst_counts);
2735 (copied, dst.len() - copied)
2736}
2737
2738fn blob_is_text(data: &[u8]) -> bool {
2742 const FIRST_FEW_BYTES: usize = 8000;
2743 !data.iter().take(FIRST_FEW_BYTES).any(|&byte| byte == 0)
2744}
2745
2746fn common_span_bytes(src: &BTreeMap<u64, usize>, dst: &BTreeMap<u64, usize>) -> usize {
2747 let mut common = 0usize;
2748 let (small, large) = if src.len() <= dst.len() {
2750 (src, dst)
2751 } else {
2752 (dst, src)
2753 };
2754 for (hash, small_bytes) in small {
2755 if let Some(large_bytes) = large.get(hash) {
2756 common += (*small_bytes).min(*large_bytes);
2757 }
2758 }
2759 common
2760}
2761
2762fn diff_entry_sort_path(entry: &NameStatusEntry) -> &[u8] {
2763 entry.path.as_bytes()
2766}
2767
2768fn mark_unstaged_worktree_oids_unresolved(
2769 changes: Vec<NameStatusEntry>,
2770 index_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
2771 worktree_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
2772) -> Vec<NameStatusEntry> {
2773 changes
2774 .into_iter()
2775 .map(|mut entry| {
2776 let worktree_entry = worktree_entries.get(entry.path.as_bytes());
2777 if worktree_entry != index_entries.get(entry.path.as_bytes()) {
2778 entry.new_oid = None;
2779 }
2780 entry
2781 })
2782 .collect()
2783}
2784
2785#[derive(Debug, Clone, PartialEq, Eq)]
2786pub(crate) struct TrackedEntry {
2787 pub(crate) mode: u32,
2788 pub(crate) oid: ObjectId,
2789}
2790
2791type TrackedEntryMap = BTreeMap<Vec<u8>, TrackedEntry>;
2794
2795type TrackedEntryPair = (TrackedEntryMap, TrackedEntryMap);
2797
2798struct IndexSnapshot {
2799 entries: BTreeMap<Vec<u8>, TrackedEntry>,
2800 stat_cache: IndexStatCache,
2801 missing_skip_worktree_entries: BTreeMap<Vec<u8>, TrackedEntry>,
2802 present_skip_worktree_entries: BTreeMap<Vec<u8>, TrackedEntry>,
2803}
2804
2805fn read_index_entries(
2806 git_dir: &Path,
2807 format: ObjectFormat,
2808) -> Result<BTreeMap<Vec<u8>, TrackedEntry>> {
2809 let index_path = sley_index::repository_index_path(git_dir);
2810 if !index_path.exists() {
2811 return Ok(BTreeMap::new());
2812 }
2813 let index = expand_sparse_index_for_worktree_diff(
2814 sley_index::read_repository_index(git_dir, format)?,
2815 git_dir,
2816 format,
2817 )?;
2818 Ok(index
2819 .entries
2820 .into_iter()
2821 .filter(|entry| entry.stage() == sley_index::Stage::Normal && !entry.is_intent_to_add())
2822 .map(|entry| {
2823 (
2824 entry.path.into_bytes(),
2825 TrackedEntry {
2826 mode: entry.mode,
2827 oid: entry.oid,
2828 },
2829 )
2830 })
2831 .collect())
2832}
2833
2834fn read_intent_to_add_paths(
2838 git_dir: &Path,
2839 format: ObjectFormat,
2840) -> Result<std::collections::HashSet<Vec<u8>>> {
2841 let index_path = sley_index::repository_index_path(git_dir);
2842 if !index_path.exists() {
2843 return Ok(std::collections::HashSet::new());
2844 }
2845 let index = expand_sparse_index_for_worktree_diff(
2846 sley_index::read_repository_index(git_dir, format)?,
2847 git_dir,
2848 format,
2849 )?;
2850 Ok(index
2851 .entries
2852 .iter()
2853 .filter(|entry| entry.stage() == sley_index::Stage::Normal && entry.is_intent_to_add())
2854 .map(|entry| entry.path.as_bytes().to_vec())
2855 .collect())
2856}
2857
2858fn read_index_snapshot(git_dir: &Path, format: ObjectFormat) -> Result<IndexSnapshot> {
2859 let index_path = sley_index::repository_index_path(git_dir);
2860 let index_metadata = match fs::metadata(&index_path) {
2861 Ok(metadata) => metadata,
2862 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
2863 return Ok(IndexSnapshot {
2864 entries: BTreeMap::new(),
2865 stat_cache: IndexStatCache::default(),
2866 missing_skip_worktree_entries: BTreeMap::new(),
2867 present_skip_worktree_entries: BTreeMap::new(),
2868 });
2869 }
2870 Err(err) => return Err(err.into()),
2871 };
2872 let raw_index = sley_index::read_repository_index(git_dir, format)?;
2873 let sparse_checkout_active = raw_index.is_sparse()
2874 || raw_index
2875 .entries
2876 .iter()
2877 .any(|entry| entry.mode == sley_index::SPARSE_DIR_MODE && entry.is_skip_worktree())
2878 || sparse_checkout_config_enabled(git_dir);
2879 let index = expand_sparse_index_for_worktree_diff(raw_index, git_dir, format)?;
2880 let stat_cache =
2881 IndexStatCache::from_index_mtime(&index, sley_index::file_mtime_parts(&index_metadata));
2882 let mut entries = BTreeMap::new();
2883 let mut missing_skip_worktree_entries = BTreeMap::new();
2884 let mut present_skip_worktree_entries = BTreeMap::new();
2885 for entry in index.entries {
2886 let is_skip_worktree = entry.stage() == sley_index::Stage::Normal
2887 && entry.is_skip_worktree()
2888 && !entry.is_intent_to_add();
2889 let path = entry.path.into_bytes();
2890 let tracked = TrackedEntry {
2891 mode: entry.mode,
2892 oid: entry.oid,
2893 };
2894 if is_skip_worktree {
2895 missing_skip_worktree_entries.insert(path.clone(), tracked.clone());
2896 if !sparse_checkout_active {
2897 present_skip_worktree_entries.insert(path.clone(), tracked.clone());
2898 }
2899 }
2900 entries.insert(path, tracked);
2901 }
2902 Ok(IndexSnapshot {
2903 entries,
2904 stat_cache,
2905 missing_skip_worktree_entries,
2906 present_skip_worktree_entries,
2907 })
2908}
2909
2910fn sparse_checkout_config_enabled(git_dir: &Path) -> bool {
2911 sley_config::GitConfig::read(git_dir.join("config"))
2912 .ok()
2913 .and_then(|config| config.get_bool("core", None, "sparseCheckout"))
2914 == Some(true)
2915 || sley_config::GitConfig::read(git_dir.join("config.worktree"))
2916 .ok()
2917 .and_then(|config| config.get_bool("core", None, "sparseCheckout"))
2918 == Some(true)
2919}
2920
2921trait WorktreeIndexEntry {
2922 fn git_path(&self) -> &[u8];
2923 fn stage(&self) -> sley_index::Stage;
2924 fn mode(&self) -> u32;
2925 fn oid(&self) -> ObjectId;
2926 fn size(&self) -> u32;
2927 fn is_intent_to_add(&self) -> bool;
2928 fn is_skip_worktree(&self) -> bool;
2929 fn reusable_with(&self, stat_cache: &IndexStatCache, metadata: &fs::Metadata) -> bool;
2930}
2931
2932impl WorktreeIndexEntry for sley_index::IndexEntry {
2933 fn git_path(&self) -> &[u8] {
2934 self.path.as_bytes()
2935 }
2936
2937 fn stage(&self) -> sley_index::Stage {
2938 sley_index::IndexEntry::stage(self)
2939 }
2940
2941 fn mode(&self) -> u32 {
2942 self.mode
2943 }
2944
2945 fn oid(&self) -> ObjectId {
2946 self.oid
2947 }
2948
2949 fn size(&self) -> u32 {
2950 self.size
2951 }
2952
2953 fn is_intent_to_add(&self) -> bool {
2954 sley_index::IndexEntry::is_intent_to_add(self)
2955 }
2956
2957 fn is_skip_worktree(&self) -> bool {
2958 sley_index::IndexEntry::is_skip_worktree(self)
2959 }
2960
2961 fn reusable_with(&self, stat_cache: &IndexStatCache, metadata: &fs::Metadata) -> bool {
2962 stat_cache.reusable_index_entry(self, metadata).is_some()
2963 }
2964}
2965
2966impl WorktreeIndexEntry for sley_index::IndexEntryRef<'_> {
2967 fn git_path(&self) -> &[u8] {
2968 self.path
2969 }
2970
2971 fn stage(&self) -> sley_index::Stage {
2972 sley_index::IndexEntryRef::stage(self)
2973 }
2974
2975 fn mode(&self) -> u32 {
2976 self.mode
2977 }
2978
2979 fn oid(&self) -> ObjectId {
2980 self.oid
2981 }
2982
2983 fn size(&self) -> u32 {
2984 self.size
2985 }
2986
2987 fn is_intent_to_add(&self) -> bool {
2988 sley_index::IndexEntryRef::is_intent_to_add(self)
2989 }
2990
2991 fn is_skip_worktree(&self) -> bool {
2992 sley_index::IndexEntryRef::is_skip_worktree(self)
2993 }
2994
2995 fn reusable_with(&self, stat_cache: &IndexStatCache, metadata: &fs::Metadata) -> bool {
2996 stat_cache.reusable_index_entry_ref(self, metadata)
2997 }
2998}
2999
3000fn tracked_entry_from_index(entry: &impl WorktreeIndexEntry) -> TrackedEntry {
3001 TrackedEntry {
3002 mode: entry.mode(),
3003 oid: entry.oid(),
3004 }
3005}
3006
3007fn head_tree_entries(
3008 git_dir: &Path,
3009 format: ObjectFormat,
3010 db: &FileObjectDatabase,
3011) -> Result<BTreeMap<Vec<u8>, TrackedEntry>> {
3012 let refs = FileRefStore::new(git_dir, format);
3013 let Some(head) = refs.read_ref("HEAD")? else {
3014 return Ok(BTreeMap::new());
3015 };
3016 let commit_oid = match head {
3017 RefTarget::Direct(oid) => Some(oid),
3018 RefTarget::Symbolic(name) => match refs.read_ref(&name)? {
3019 Some(RefTarget::Direct(oid)) => Some(oid),
3020 _ => None,
3021 },
3022 };
3023 let Some(commit_oid) = commit_oid else {
3024 return Ok(BTreeMap::new());
3025 };
3026 let object = db.read_object(&commit_oid)?;
3027 if object.object_type != ObjectType::Commit {
3028 return Err(GitError::InvalidObject(format!(
3029 "HEAD {commit_oid} is not a commit"
3030 )));
3031 }
3032 let commit = Commit::parse_ref(format, &object.body)?;
3033 let mut entries = BTreeMap::new();
3034 collect_tree_entries(db, format, &commit.tree, Vec::new(), &mut entries)?;
3035 Ok(entries)
3036}
3037
3038fn collect_tree_entries(
3047 db: &FileObjectDatabase,
3048 format: ObjectFormat,
3049 tree_oid: &ObjectId,
3050 prefix: Vec<u8>,
3051 entries: &mut BTreeMap<Vec<u8>, TrackedEntry>,
3052) -> Result<()> {
3053 for (rel_path, (mode, oid)) in flatten_tree(db, format, tree_oid)? {
3054 let path = join_tree_path(&prefix, &rel_path);
3055 entries.insert(path, TrackedEntry { mode, oid });
3056 }
3057 Ok(())
3058}
3059
3060pub(crate) const TREE_ENTRY_MODE: u32 = 0o040000;
3062
3063fn read_tree_object(
3067 db: &FileObjectDatabase,
3068 format: ObjectFormat,
3069 tree_oid: &ObjectId,
3070) -> Result<Tree> {
3071 let object = db.read_object(tree_oid)?;
3072 if object.object_type != ObjectType::Tree {
3073 return Err(GitError::InvalidObject(format!(
3074 "expected tree {tree_oid}, found {}",
3075 object.object_type.as_str()
3076 )));
3077 }
3078 Tree::parse(format, &object.body)
3079}
3080
3081fn join_tree_path(prefix: &[u8], name: &[u8]) -> Vec<u8> {
3084 let mut path = Vec::with_capacity(prefix.len() + 1 + name.len());
3085 path.extend_from_slice(prefix);
3086 if !path.is_empty() {
3087 path.push(b'/');
3088 }
3089 path.extend_from_slice(name);
3090 path
3091}
3092
3093pub(crate) fn collect_full_tree_pair(
3097 db: &FileObjectDatabase,
3098 format: ObjectFormat,
3099 left_tree: &ObjectId,
3100 right_tree: &ObjectId,
3101) -> Result<TrackedEntryPair> {
3102 let mut left = BTreeMap::new();
3103 collect_tree_entries(db, format, left_tree, Vec::new(), &mut left)?;
3104 let mut right = BTreeMap::new();
3105 collect_tree_entries(db, format, right_tree, Vec::new(), &mut right)?;
3106 Ok((left, right))
3107}
3108
3109pub(crate) fn changed_tree_entries(
3125 db: &FileObjectDatabase,
3126 format: ObjectFormat,
3127 left_tree: &ObjectId,
3128 right_tree: &ObjectId,
3129) -> Result<TrackedEntryPair> {
3130 let mut left = BTreeMap::new();
3131 let mut right = BTreeMap::new();
3132 if left_tree != right_tree {
3134 diff_tree_pair(
3135 db,
3136 format,
3137 left_tree,
3138 right_tree,
3139 &[],
3140 &mut left,
3141 &mut right,
3142 )?;
3143 }
3144 Ok((left, right))
3145}
3146
3147fn diff_tree_pair(
3151 db: &FileObjectDatabase,
3152 format: ObjectFormat,
3153 left_tree: &ObjectId,
3154 right_tree: &ObjectId,
3155 prefix: &[u8],
3156 left: &mut BTreeMap<Vec<u8>, TrackedEntry>,
3157 right: &mut BTreeMap<Vec<u8>, TrackedEntry>,
3158) -> Result<()> {
3159 let left_entries = read_tree_object(db, format, left_tree)?.entries;
3160 let right_entries = read_tree_object(db, format, right_tree)?.entries;
3161
3162 let mut right_by_name: HashMap<&[u8], &TreeEntry> = HashMap::with_capacity(right_entries.len());
3167 for entry in &right_entries {
3168 right_by_name.insert(entry.name.as_bytes(), entry);
3169 }
3170
3171 for left_entry in &left_entries {
3172 match right_by_name.remove(left_entry.name.as_bytes()) {
3173 Some(right_entry) => {
3174 merge_tree_entry(
3175 db,
3176 format,
3177 prefix,
3178 Some(left_entry),
3179 Some(right_entry),
3180 left,
3181 right,
3182 )?;
3183 }
3184 None => {
3185 merge_tree_entry(db, format, prefix, Some(left_entry), None, left, right)?;
3186 }
3187 }
3188 }
3189 for right_entry in &right_entries {
3191 if right_by_name.contains_key(right_entry.name.as_bytes()) {
3192 merge_tree_entry(db, format, prefix, None, Some(right_entry), left, right)?;
3193 }
3194 }
3195 Ok(())
3196}
3197
3198fn merge_tree_entry(
3211 db: &FileObjectDatabase,
3212 format: ObjectFormat,
3213 prefix: &[u8],
3214 left_entry: Option<&TreeEntry>,
3215 right_entry: Option<&TreeEntry>,
3216 left: &mut BTreeMap<Vec<u8>, TrackedEntry>,
3217 right: &mut BTreeMap<Vec<u8>, TrackedEntry>,
3218) -> Result<()> {
3219 let left_is_tree = left_entry.is_some_and(|entry| entry.mode == TREE_ENTRY_MODE);
3220 let right_is_tree = right_entry.is_some_and(|entry| entry.mode == TREE_ENTRY_MODE);
3221
3222 if let (Some(left_entry), Some(right_entry)) = (left_entry, right_entry) {
3223 if left_is_tree && right_is_tree {
3224 if left_entry.oid == right_entry.oid {
3226 return Ok(());
3227 }
3228 let path = join_tree_path(prefix, left_entry.name.as_bytes());
3229 return diff_tree_pair(
3230 db,
3231 format,
3232 &left_entry.oid,
3233 &right_entry.oid,
3234 &path,
3235 left,
3236 right,
3237 );
3238 }
3239 if !left_is_tree && !right_is_tree {
3240 if left_entry.mode == right_entry.mode && left_entry.oid == right_entry.oid {
3244 return Ok(());
3245 }
3246 let path = join_tree_path(prefix, left_entry.name.as_bytes());
3247 left.insert(
3248 path.clone(),
3249 TrackedEntry {
3250 mode: left_entry.mode,
3251 oid: left_entry.oid,
3252 },
3253 );
3254 right.insert(
3255 path,
3256 TrackedEntry {
3257 mode: right_entry.mode,
3258 oid: right_entry.oid,
3259 },
3260 );
3261 return Ok(());
3262 }
3263 }
3266
3267 if let Some(left_entry) = left_entry {
3269 let path = join_tree_path(prefix, left_entry.name.as_bytes());
3270 if left_is_tree {
3271 collect_tree_entries(db, format, &left_entry.oid, path, left)?;
3272 } else {
3273 left.insert(
3274 path,
3275 TrackedEntry {
3276 mode: left_entry.mode,
3277 oid: left_entry.oid,
3278 },
3279 );
3280 }
3281 }
3282 if let Some(right_entry) = right_entry {
3284 let path = join_tree_path(prefix, right_entry.name.as_bytes());
3285 if right_is_tree {
3286 collect_tree_entries(db, format, &right_entry.oid, path, right)?;
3287 } else {
3288 right.insert(
3289 path,
3290 TrackedEntry {
3291 mode: right_entry.mode,
3292 oid: right_entry.oid,
3293 },
3294 );
3295 }
3296 }
3297 Ok(())
3298}
3299
3300fn index_gitlinks(index: &BTreeMap<Vec<u8>, TrackedEntry>) -> BTreeMap<Vec<u8>, ObjectId> {
3301 index
3302 .iter()
3303 .filter(|(_, entry)| sley_index::is_gitlink(entry.mode))
3304 .map(|(path, entry)| (path.clone(), entry.oid))
3305 .collect()
3306}
3307
3308fn candidate_path_set<'a>(candidate_paths: impl Iterator<Item = &'a Vec<u8>>) -> BTreeSet<Vec<u8>> {
3309 candidate_paths.cloned().collect()
3310}
3311
3312fn worktree_entries_for_path_set(
3313 worktree_root: &Path,
3314 format: ObjectFormat,
3315 candidates: &BTreeSet<Vec<u8>>,
3316 index_gitlinks: &BTreeMap<Vec<u8>, ObjectId>,
3317 stat_cache: Option<&IndexStatCache>,
3318 missing_skip_worktree_entries: Option<&BTreeMap<Vec<u8>, TrackedEntry>>,
3319 present_skip_worktree_entries: Option<&BTreeMap<Vec<u8>, TrackedEntry>>,
3320) -> Result<BTreeMap<Vec<u8>, TrackedEntry>> {
3321 worktree_entries_for_unique_paths(
3322 worktree_root,
3323 format,
3324 candidates.iter(),
3325 index_gitlinks,
3326 stat_cache,
3327 missing_skip_worktree_entries,
3328 present_skip_worktree_entries,
3329 )
3330}
3331
3332fn worktree_entries_for_unique_paths<'a>(
3333 worktree_root: &Path,
3334 format: ObjectFormat,
3335 candidates: impl Iterator<Item = &'a Vec<u8>>,
3336 index_gitlinks: &BTreeMap<Vec<u8>, ObjectId>,
3337 stat_cache: Option<&IndexStatCache>,
3338 missing_skip_worktree_entries: Option<&BTreeMap<Vec<u8>, TrackedEntry>>,
3339 present_skip_worktree_entries: Option<&BTreeMap<Vec<u8>, TrackedEntry>>,
3340) -> Result<BTreeMap<Vec<u8>, TrackedEntry>> {
3341 let mut entries = BTreeMap::new();
3342 for git_path in candidates {
3343 if let Some(entry) = worktree_entry_for_path(
3344 worktree_root,
3345 format,
3346 git_path,
3347 index_gitlinks,
3348 stat_cache,
3349 missing_skip_worktree_entries,
3350 present_skip_worktree_entries,
3351 )? {
3352 entries.insert(git_path.clone(), entry);
3353 }
3354 }
3355 Ok(entries)
3356}
3357
3358fn worktree_entry_for_path(
3359 worktree_root: &Path,
3360 format: ObjectFormat,
3361 git_path: &[u8],
3362 index_gitlinks: &BTreeMap<Vec<u8>, ObjectId>,
3363 stat_cache: Option<&IndexStatCache>,
3364 missing_skip_worktree_entries: Option<&BTreeMap<Vec<u8>, TrackedEntry>>,
3365 present_skip_worktree_entries: Option<&BTreeMap<Vec<u8>, TrackedEntry>>,
3366) -> Result<Option<TrackedEntry>> {
3367 if let Some(entry) = present_skip_worktree_entries.and_then(|entries| {
3368 entries
3369 .get(git_path)
3370 .cloned()
3371 .filter(|entry| !sley_index::is_gitlink(entry.mode))
3372 }) {
3373 return Ok(Some(entry));
3374 }
3375 let path = worktree_path_for_repo_path(worktree_root, git_path);
3376 let metadata = match fs::symlink_metadata(&path) {
3377 Ok(metadata) => metadata,
3378 Err(err) if is_missing_worktree_path_error(&err) => {
3379 return Ok(missing_skip_worktree_entries.and_then(|entries| {
3380 entries
3381 .get(git_path)
3382 .cloned()
3383 .filter(|entry| !sley_index::is_gitlink(entry.mode))
3384 }));
3385 }
3386 Err(err) => return Err(GitError::Io(err.to_string())),
3387 };
3388 let file_type = metadata.file_type();
3389 if let Some(staged_oid) = index_gitlinks.get(git_path)
3390 && metadata.is_dir()
3391 {
3392 let oid = gitlink_head_oid(&path, format).unwrap_or(*staged_oid);
3393 return Ok(Some(TrackedEntry {
3394 mode: sley_index::GITLINK_MODE,
3395 oid,
3396 }));
3397 }
3398 if metadata.is_dir() {
3399 if let Some(oid) = gitlink_head_oid(&path, format) {
3400 return Ok(Some(TrackedEntry {
3401 mode: sley_index::GITLINK_MODE,
3402 oid,
3403 }));
3404 }
3405 return Ok(None);
3406 }
3407 if !(metadata.is_file() || file_type.is_symlink()) {
3408 return Ok(None);
3409 }
3410 if let Some(entry) = stat_cache.and_then(|cache| cache.reusable_entry(git_path, &metadata)) {
3411 return Ok(Some(tracked_entry_from_index(entry)));
3412 }
3413 Ok(Some(classify_worktree_entry(&path, &metadata, format)?))
3414}
3415
3416fn index_worktree_change_for_entry(
3417 path: &Path,
3418 format: ObjectFormat,
3419 index_entry: &impl WorktreeIndexEntry,
3420 stat_cache: &IndexStatCache,
3421 stat_clean_validator: &mut Option<&mut IndexWorktreeStatCleanValidator<'_>>,
3422) -> Result<Option<NameStatusEntry>> {
3423 let git_path = index_entry.git_path();
3424 let metadata = match fs::symlink_metadata(path) {
3425 Ok(metadata) => metadata,
3426 Err(err) if is_missing_worktree_path_error(&err) && index_entry.is_skip_worktree() => {
3427 return Ok(None);
3428 }
3429 Err(err) if is_missing_worktree_path_error(&err) => {
3430 return Ok(Some(index_worktree_deleted_entry(index_entry)));
3431 }
3432 Err(err) => return Err(GitError::Io(err.to_string())),
3433 };
3434 let file_type = metadata.file_type();
3435 let right = if metadata.is_dir() {
3436 if sley_index::is_gitlink(index_entry.mode()) {
3437 let oid = gitlink_head_oid(path, format).unwrap_or(index_entry.oid());
3438 Some(TrackedEntry {
3439 mode: sley_index::GITLINK_MODE,
3440 oid,
3441 })
3442 } else {
3443 gitlink_head_oid(path, format).map(|oid| TrackedEntry {
3444 mode: sley_index::GITLINK_MODE,
3445 oid,
3446 })
3447 }
3448 } else if metadata.is_file() || file_type.is_symlink() {
3449 let validated = if let Some(validator) = stat_clean_validator.as_deref_mut() {
3450 validator(
3451 IndexWorktreeValidationEntry {
3452 path: index_entry.git_path(),
3453 mode: index_entry.mode(),
3454 oid: index_entry.oid(),
3455 size: index_entry.size(),
3456 },
3457 path,
3458 &metadata,
3459 )?
3460 } else {
3461 None
3462 };
3463 if index_entry.reusable_with(stat_cache, &metadata) {
3464 return Ok(None);
3465 }
3466 Some(match validated {
3467 Some(entry) => TrackedEntry {
3468 mode: entry.mode,
3469 oid: entry.oid,
3470 },
3471 None => classify_worktree_entry(path, &metadata, format)?,
3472 })
3473 } else {
3474 None
3475 };
3476 let Some(right) = right else {
3477 return Ok(Some(index_worktree_deleted_entry(index_entry)));
3478 };
3479 let left = tracked_entry_from_index(index_entry);
3480 if right == left {
3481 return Ok(None);
3482 }
3483 Ok(Some(NameStatusEntry {
3484 status: modify_or_type_change(left.mode, right.mode),
3485 path: git_path.to_vec().into(),
3486 old_path: None,
3487 old_mode: Some(left.mode),
3488 new_mode: Some(right.mode),
3489 old_oid: Some(left.oid),
3490 new_oid: Some(right.oid),
3491 }))
3492}
3493
3494fn index_worktree_deleted_entry(index_entry: &impl WorktreeIndexEntry) -> NameStatusEntry {
3495 NameStatusEntry {
3496 status: NameStatus::Deleted,
3497 path: index_entry.git_path().to_vec().into(),
3498 old_path: None,
3499 old_mode: Some(index_entry.mode()),
3500 new_mode: None,
3501 old_oid: Some(index_entry.oid()),
3502 new_oid: None,
3503 }
3504}
3505
3506fn is_missing_worktree_path_error(err: &std::io::Error) -> bool {
3507 matches!(
3508 err.kind(),
3509 std::io::ErrorKind::NotFound | std::io::ErrorKind::NotADirectory
3510 )
3511}
3512
3513fn worktree_blob_cache_for_path_set(
3514 worktree_root: &Path,
3515 left_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
3516 right_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
3517 candidate_paths: &BTreeSet<Vec<u8>>,
3518 odb_backed_worktree_entries: Option<&BTreeMap<Vec<u8>, TrackedEntry>>,
3519 options: DiffNameStatusOptions,
3520) -> Result<HashMap<ObjectId, Arc<[u8]>>> {
3521 worktree_blob_cache_for_unique_paths(
3522 worktree_root,
3523 left_entries,
3524 right_entries,
3525 candidate_paths.iter(),
3526 odb_backed_worktree_entries,
3527 options,
3528 )
3529}
3530
3531fn worktree_blob_cache_for_unique_paths<'a>(
3532 worktree_root: &Path,
3533 left_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
3534 right_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
3535 candidate_paths: impl Iterator<Item = &'a Vec<u8>>,
3536 odb_backed_worktree_entries: Option<&BTreeMap<Vec<u8>, TrackedEntry>>,
3537 options: DiffNameStatusOptions,
3538) -> Result<HashMap<ObjectId, Arc<[u8]>>> {
3539 if !options.detect_inexact || !(options.detect_renames || options.detect_copies) {
3540 return Ok(HashMap::new());
3541 }
3542 let mut changes =
3543 raw_name_status_changes_for_unique_paths(left_entries, right_entries, candidate_paths);
3544 if options.detect_renames {
3545 changes = detect_exact_renames(changes, left_entries, right_entries, options.rename_empty);
3546 }
3547 if options.detect_copies {
3548 changes = detect_exact_copies(
3549 changes,
3550 left_entries,
3551 right_entries,
3552 options.find_copies_harder,
3553 options.rename_empty,
3554 );
3555 }
3556 let has_rename_source = options.detect_renames
3557 && changes.iter().any(|entry| {
3558 entry.status == NameStatus::Deleted
3559 && entry
3560 .old_oid
3561 .as_ref()
3562 .is_some_and(|oid| options.rename_empty || !is_empty_blob_oid(oid))
3563 });
3564 let has_copy_source = options.detect_copies
3565 && (options.find_copies_harder
3566 || changes
3567 .iter()
3568 .any(|entry| matches!(entry.status, NameStatus::Deleted | NameStatus::Modified)));
3569 if !has_rename_source && !has_copy_source {
3570 return Ok(HashMap::new());
3571 }
3572 let candidate_oids = changes
3573 .iter()
3574 .filter(|entry| entry.status == NameStatus::Added)
3575 .filter_map(|entry| entry.new_oid)
3576 .filter(|oid| options.rename_empty || !is_empty_blob_oid(oid))
3577 .collect::<BTreeSet<_>>();
3578 if candidate_oids.is_empty() {
3579 return Ok(HashMap::new());
3580 }
3581 let mut cache = HashMap::new();
3582 for (git_path, entry) in right_entries {
3583 if odb_backed_worktree_entries.is_some_and(|entries| entries.contains_key(git_path)) {
3584 continue;
3585 }
3586 if sley_index::is_gitlink(entry.mode) || !candidate_oids.contains(&entry.oid) {
3587 continue;
3588 }
3589 let path = worktree_path_for_repo_path(worktree_root, git_path);
3590 let body = if sley_index::is_symlink_mode(entry.mode) {
3591 symlink_target_bytes(&path)?
3592 } else {
3593 fs::read(&path)?
3594 };
3595 cache
3596 .entry(entry.oid)
3597 .or_insert_with(|| Arc::from(body.into_boxed_slice()));
3598 }
3599 Ok(cache)
3600}
3601
3602fn cache_or_odb_blob(
3605 cache: &HashMap<ObjectId, Arc<[u8]>>,
3606 db: &FileObjectDatabase,
3607 oid: &ObjectId,
3608) -> Option<Arc<[u8]>> {
3609 if let Some(bytes) = cache.get(oid) {
3610 return Some(bytes.clone());
3611 }
3612 read_blob_bytes(db, oid)
3613}
3614
3615#[cfg(unix)]
3616fn worktree_path_for_repo_path(worktree_root: &Path, path: &[u8]) -> PathBuf {
3617 use std::ffi::OsStr;
3618 use std::os::unix::ffi::OsStrExt;
3619
3620 let mut out = PathBuf::from(worktree_root);
3621 out.push(OsStr::from_bytes(path));
3622 out
3623}
3624
3625#[cfg(unix)]
3626fn worktree_path_for_repo_path_into(out: &mut PathBuf, worktree_root: &Path, path: &[u8]) {
3627 use std::ffi::OsStr;
3628 use std::os::unix::ffi::OsStrExt;
3629
3630 out.clear();
3631 out.push(worktree_root);
3632 out.push(OsStr::from_bytes(path));
3633}
3634
3635#[cfg(not(unix))]
3636fn worktree_path_for_repo_path(worktree_root: &Path, path: &[u8]) -> PathBuf {
3637 worktree_root.join(repo_path_to_path(path))
3638}
3639
3640#[cfg(not(unix))]
3641fn worktree_path_for_repo_path_into(out: &mut PathBuf, worktree_root: &Path, path: &[u8]) {
3642 out.clear();
3643 out.push(worktree_root);
3644 out.push(repo_path_to_path(path));
3645}
3646
3647#[cfg(not(unix))]
3648fn repo_path_to_path(path: &[u8]) -> PathBuf {
3649 let mut out = PathBuf::new();
3650 for component in String::from_utf8_lossy(path).split('/') {
3651 if !component.is_empty() {
3652 out.push(component);
3653 }
3654 }
3655 out
3656}
3657
3658#[cfg(unix)]
3659fn file_mode(metadata: &fs::Metadata) -> u32 {
3660 use std::os::unix::fs::PermissionsExt;
3661 if metadata.permissions().mode() & 0o111 != 0 {
3662 0o100755
3663 } else {
3664 0o100644
3665 }
3666}
3667
3668#[cfg(not(unix))]
3669fn file_mode(_metadata: &fs::Metadata) -> u32 {
3670 0o100644
3671}
3672
3673#[cfg(unix)]
3678pub fn symlink_target_bytes(path: &Path) -> Result<Vec<u8>> {
3679 use std::os::unix::ffi::OsStrExt;
3680 let target = fs::read_link(path)?;
3681 Ok(target.as_os_str().as_bytes().to_vec())
3682}
3683
3684#[cfg(not(unix))]
3686pub fn symlink_target_bytes(path: &Path) -> Result<Vec<u8>> {
3687 let target = fs::read_link(path)?;
3688 Ok(target.to_string_lossy().replace('\\', "/").into_bytes())
3689}
3690pub type MergeEntryMap = BTreeMap<Vec<u8>, (u32, ObjectId)>;
3692pub fn flatten_tree(
3695 reader: &impl ObjectReader,
3696 format: ObjectFormat,
3697 tree_oid: &ObjectId,
3698) -> Result<MergeEntryMap> {
3699 let mut entries = BTreeMap::new();
3700 if *tree_oid == empty_tree_oid(format)? {
3701 return Ok(entries);
3702 }
3703 collect_flat_tree(reader, format, tree_oid, Vec::new(), &mut entries)?;
3704 Ok(entries)
3705}
3706
3707pub fn corrupted_cache_tree_error() -> GitError {
3708 GitError::InvalidFormat("error: corrupted cache-tree has entries not present in index".into())
3709}
3710
3711pub fn tree_has_duplicate_leaf_paths(
3712 reader: &impl ObjectReader,
3713 format: ObjectFormat,
3714 tree_oid: &ObjectId,
3715) -> Result<bool> {
3716 if *tree_oid == empty_tree_oid(format)? {
3717 return Ok(false);
3718 }
3719 let mut seen = BTreeSet::new();
3720 collect_duplicate_leaf_paths(reader, format, tree_oid, Vec::new(), &mut seen)
3721}
3722
3723fn collect_duplicate_leaf_paths(
3724 reader: &impl ObjectReader,
3725 format: ObjectFormat,
3726 tree_oid: &ObjectId,
3727 prefix: Vec<u8>,
3728 seen: &mut BTreeSet<Vec<u8>>,
3729) -> Result<bool> {
3730 let object = reader.read_object(tree_oid)?;
3731 if object.object_type != ObjectType::Tree {
3732 return Err(GitError::InvalidObject(format!(
3733 "expected tree {}, found {}",
3734 tree_oid,
3735 object.object_type.as_str()
3736 )));
3737 }
3738 for entry in TreeEntries::new(format, &object.body) {
3739 let entry = entry?;
3740 let mut path = prefix.clone();
3741 if !path.is_empty() {
3742 path.push(b'/');
3743 }
3744 path.extend_from_slice(entry.name);
3745 if entry.mode == 0o040000 {
3746 if collect_duplicate_leaf_paths(reader, format, &entry.oid, path, seen)? {
3747 return Ok(true);
3748 }
3749 } else if !seen.insert(path) {
3750 return Ok(true);
3751 }
3752 }
3753 Ok(false)
3754}
3755
3756fn collect_flat_tree(
3757 reader: &impl ObjectReader,
3758 format: ObjectFormat,
3759 tree_oid: &ObjectId,
3760 prefix: Vec<u8>,
3761 entries: &mut MergeEntryMap,
3762) -> Result<()> {
3763 let object = reader.read_object(tree_oid)?;
3764 if object.object_type != ObjectType::Tree {
3765 return Err(GitError::InvalidObject(format!(
3766 "expected tree {}, found {}",
3767 tree_oid,
3768 object.object_type.as_str()
3769 )));
3770 }
3771 for entry in TreeEntries::new(format, &object.body) {
3772 let entry = entry?;
3773 let mut path = prefix.clone();
3774 if !path.is_empty() {
3775 path.push(b'/');
3776 }
3777 path.extend_from_slice(entry.name);
3778 if entry.mode == 0o040000 {
3779 collect_flat_tree(reader, format, &entry.oid, path, entries)?;
3780 } else {
3781 entries.insert(path, (entry.mode, entry.oid));
3782 }
3783 }
3784 Ok(())
3785}