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