1use sley_core::{GitError, ObjectFormat, ObjectId, RepoPath, Result, object_id_for_bytes};
2
3mod name;
4pub mod range;
5pub mod render;
6pub mod ws;
7
8pub use sley_core::BString;
9use sley_index::{BorrowedIndex, Index, IndexStatCache};
10use sley_object::{Commit, EncodedObject, ObjectType, Tree, TreeEntries, TreeEntry};
11use sley_odb::{FileObjectDatabase, ObjectReader, ObjectWriter};
12use sley_refs::{FileRefStore, RefTarget};
13use std::collections::{BTreeMap, BTreeSet, HashMap};
14use std::fs;
15use std::path::{Path, PathBuf};
16
17pub fn gitlink_git_dir(sub_root: &Path) -> Option<PathBuf> {
35 let dot_git = sub_root.join(".git");
36 let metadata = fs::symlink_metadata(&dot_git).ok()?;
37 if metadata.is_dir() {
38 return Some(dot_git);
39 }
40 if !metadata.is_file() {
41 return None;
42 }
43 let contents = fs::read_to_string(&dot_git).ok()?;
44 let target = contents.strip_prefix("gitdir:")?.trim();
45 if target.is_empty() {
46 return None;
47 }
48 let target = PathBuf::from(target);
49 let git_dir = if target.is_absolute() {
50 target
51 } else {
52 sub_root.join(target)
53 };
54 if git_dir.is_dir() {
55 Some(git_dir)
56 } else {
57 None
58 }
59}
60
61pub fn gitlink_broken_gitdir(sub_root: &Path) -> Option<PathBuf> {
69 let dot_git = sub_root.join(".git");
70 let metadata = fs::symlink_metadata(&dot_git).ok()?;
71 if !metadata.is_file() {
72 return None;
74 }
75 let contents = fs::read_to_string(&dot_git).ok()?;
76 let target = contents.strip_prefix("gitdir:")?.trim();
77 if target.is_empty() {
78 return None;
79 }
80 let target_path = if Path::new(target).is_absolute() {
81 PathBuf::from(target)
82 } else {
83 sub_root.join(target)
84 };
85 if target_path.is_dir() {
86 None
87 } else {
88 Some(target_path)
89 }
90}
91
92pub fn gitlink_head_oid(sub_root: &Path, format: ObjectFormat) -> Option<ObjectId> {
98 let git_dir = gitlink_git_dir(sub_root)?;
99 let store = FileRefStore::new(&git_dir, format);
100 let mut target = store.read_ref("HEAD").ok()??;
101 for _ in 0..10 {
103 match target {
104 RefTarget::Direct(oid) => return Some(oid),
105 RefTarget::Symbolic(name) => target = store.read_ref(&name).ok()??,
106 }
107 }
108 None
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128pub struct DiffLine<'a> {
129 pub content: &'a [u8],
131 pub has_newline: bool,
133}
134
135impl<'a> DiffLine<'a> {
136 pub fn bytes_without_newline(&self) -> &'a [u8] {
138 if self.has_newline {
139 self.content.strip_suffix(b"\n").unwrap_or(self.content)
140 } else {
141 self.content
142 }
143 }
144}
145
146pub fn split_lines(blob: &[u8]) -> Vec<DiffLine<'_>> {
153 let mut lines = Vec::new();
154 let mut start = 0usize;
155 let len = blob.len();
156 let mut idx = 0usize;
157 while idx < len {
158 if blob[idx] == b'\n' {
159 lines.push(DiffLine {
160 content: &blob[start..=idx],
161 has_newline: true,
162 });
163 idx += 1;
164 start = idx;
165 } else {
166 idx += 1;
167 }
168 }
169 if start < len {
170 lines.push(DiffLine {
171 content: &blob[start..len],
172 has_newline: false,
173 });
174 }
175 lines
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
188pub enum DiffOp {
189 Equal(usize),
191 Delete(usize),
193 Insert(usize),
195}
196
197pub fn myers_diff_lines(old: &[DiffLine<'_>], new: &[DiffLine<'_>]) -> Vec<DiffOp> {
206 let n_total = old.len();
209 let m_total = new.len();
210 let mut prefix = 0usize;
211 while prefix < n_total && prefix < m_total && old[prefix] == new[prefix] {
212 prefix += 1;
213 }
214 let mut suffix = 0usize;
215 while suffix < n_total - prefix
216 && suffix < m_total - prefix
217 && old[n_total - 1 - suffix] == new[m_total - 1 - suffix]
218 {
219 suffix += 1;
220 }
221
222 let old_mid = &old[prefix..n_total - suffix];
223 let new_mid = &new[prefix..m_total - suffix];
224
225 let mut ops: Vec<DiffOp> = Vec::new();
226 if prefix > 0 {
227 ops.push(DiffOp::Equal(prefix));
228 }
229 myers_core(old_mid, new_mid, &mut ops);
230 if suffix > 0 {
231 ops.push(DiffOp::Equal(suffix));
232 }
233 coalesce_ops(ops)
234}
235
236fn myers_core(old: &[DiffLine<'_>], new: &[DiffLine<'_>], out: &mut Vec<DiffOp>) {
244 let n = old.len() as isize;
245 let m = new.len() as isize;
246 if n == 0 {
247 if m > 0 {
248 out.push(DiffOp::Insert(m as usize));
249 }
250 return;
251 }
252 if m == 0 {
253 out.push(DiffOp::Delete(n as usize));
254 return;
255 }
256
257 let max = (n + m) as usize;
258 let offset = max as isize; let width = 2 * max + 1;
260 let mut v = vec![0isize; width];
262 let mut trace: Vec<Vec<isize>> = Vec::new();
264
265 let mut found_d: Option<usize> = None;
266 'search: for d in 0..=(max as isize) {
267 trace.push(v.clone());
268 let mut k = -d;
269 while k <= d {
270 let kidx = (k + offset) as usize;
271 let mut x = if k == -d
274 || (k != d && v[(k - 1 + offset) as usize] < v[(k + 1 + offset) as usize])
275 {
276 v[(k + 1 + offset) as usize]
278 } else {
279 v[(k - 1 + offset) as usize] + 1
281 };
282 let mut y = x - k;
283 while x < n && y < m && old[x as usize] == new[y as usize] {
285 x += 1;
286 y += 1;
287 }
288 v[kidx] = x;
289 if x >= n && y >= m {
290 found_d = Some(d as usize);
291 break 'search;
292 }
293 k += 2;
294 }
295 }
296
297 let Some(d_end) = found_d else {
300 out.push(DiffOp::Delete(n as usize));
301 out.push(DiffOp::Insert(m as usize));
302 return;
303 };
304
305 backtrack(n, m, &trace, d_end, offset, out);
306}
307
308fn backtrack(
314 n: isize,
315 m: isize,
316 trace: &[Vec<isize>],
317 d_end: usize,
318 offset: isize,
319 out: &mut Vec<DiffOp>,
320) {
321 let mut x = n;
322 let mut y = m;
323 let mut rev: Vec<DiffOp> = Vec::new();
324
325 for d in (0..=d_end).rev() {
326 let v = &trace[d];
327 let k = x - y;
328 let prev_k = if k == -(d as isize)
330 || (k != d as isize && v[(k - 1 + offset) as usize] < v[(k + 1 + offset) as usize])
331 {
332 k + 1 } else {
334 k - 1 };
336 let prev_x = v[(prev_k + offset) as usize];
337 let prev_y = prev_x - prev_k;
338
339 while x > prev_x && y > prev_y {
341 rev.push(DiffOp::Equal(1));
342 x -= 1;
343 y -= 1;
344 }
345 if d > 0 {
346 if x == prev_x {
347 rev.push(DiffOp::Insert(1));
349 } else {
350 rev.push(DiffOp::Delete(1));
352 }
353 x = prev_x;
354 y = prev_y;
355 }
356 }
357
358 rev.reverse();
359 out.extend(rev);
360}
361
362fn coalesce_ops(ops: Vec<DiffOp>) -> Vec<DiffOp> {
364 let mut out: Vec<DiffOp> = Vec::with_capacity(ops.len());
365 for op in ops {
366 match (out.last_mut(), op) {
367 (Some(DiffOp::Equal(prev)), DiffOp::Equal(n)) => *prev += n,
368 (Some(DiffOp::Delete(prev)), DiffOp::Delete(n)) => *prev += n,
369 (Some(DiffOp::Insert(prev)), DiffOp::Insert(n)) => *prev += n,
370 _ => out.push(op),
371 }
372 }
373 out
374}
375
376#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
395pub struct WsIgnore {
396 pub all_space: bool,
398 pub space_change: bool,
400 pub space_at_eol: bool,
402 pub cr_at_eol: bool,
404}
405
406impl WsIgnore {
407 pub const EMPTY: Self = Self {
409 all_space: false,
410 space_change: false,
411 space_at_eol: false,
412 cr_at_eol: false,
413 };
414
415 pub fn is_empty(&self) -> bool {
417 !(self.all_space || self.space_change || self.space_at_eol || self.cr_at_eol)
418 }
419}
420
421#[inline]
424fn xdl_isspace(c: u8) -> bool {
425 matches!(c, b' ' | b'\t' | b'\n' | b'\r' | 0x0b | 0x0c)
426}
427
428pub(crate) fn canonicalize_line_for_match(line: &[u8], ignore: WsIgnore) -> Vec<u8> {
445 canonicalize_line(line, ignore)
446}
447
448fn canonicalize_line(line: &[u8], ignore: WsIgnore) -> Vec<u8> {
449 if ignore.all_space {
450 return line.iter().copied().filter(|&c| !xdl_isspace(c)).collect();
451 }
452 if ignore.space_change {
453 let mut out = Vec::with_capacity(line.len());
454 let mut i = 0usize;
455 while i < line.len() {
456 if xdl_isspace(line[i]) {
457 while i < line.len() && xdl_isspace(line[i]) {
459 i += 1;
460 }
461 out.push(b' ');
462 } else {
463 out.push(line[i]);
464 i += 1;
465 }
466 }
467 if out.last() == Some(&b' ') {
469 out.pop();
470 }
471 return out;
472 }
473 if ignore.space_at_eol {
474 let mut end = line.len();
475 while end > 0 && xdl_isspace(line[end - 1]) {
476 end -= 1;
477 }
478 return line[..end].to_vec();
479 }
480 if ignore.cr_at_eol {
481 if let Some(stripped) = line.strip_suffix(b"\n") {
483 if let Some(without_cr) = stripped.strip_suffix(b"\r") {
484 let mut out = without_cr.to_vec();
485 out.push(b'\n');
486 return out;
487 }
488 } else if let Some(without_cr) = line.strip_suffix(b"\r") {
489 return without_cr.to_vec();
491 }
492 return line.to_vec();
493 }
494 line.to_vec()
495}
496
497fn line_is_blank(line: &[u8], ignore: WsIgnore) -> bool {
502 if ignore.is_empty() {
503 line.len() <= 1
504 } else {
505 line.iter().all(|&c| xdl_isspace(c))
506 }
507}
508
509pub fn myers_diff_lines_ws(
518 old: &[DiffLine<'_>],
519 new: &[DiffLine<'_>],
520 ignore: WsIgnore,
521 algorithm: DiffAlgorithm,
522) -> Vec<DiffOp> {
523 if ignore.is_empty() {
524 return diff_lines_with_algorithm(old, new, algorithm);
525 }
526 let old_canon: Vec<Vec<u8>> = old
527 .iter()
528 .map(|l| canonicalize_line(l.content, ignore))
529 .collect();
530 let new_canon: Vec<Vec<u8>> = new
531 .iter()
532 .map(|l| canonicalize_line(l.content, ignore))
533 .collect();
534 let old_lines: Vec<DiffLine<'_>> = old_canon
535 .iter()
536 .map(|c| DiffLine {
537 content: c.as_slice(),
538 has_newline: true,
539 })
540 .collect();
541 let new_lines: Vec<DiffLine<'_>> = new_canon
542 .iter()
543 .map(|c| DiffLine {
544 content: c.as_slice(),
545 has_newline: true,
546 })
547 .collect();
548 diff_lines_with_algorithm(&old_lines, &new_lines, algorithm)
549}
550
551type LineKey<'a> = (&'a [u8], bool);
577
578#[inline]
579fn line_key<'a>(line: &DiffLine<'a>) -> LineKey<'a> {
580 (line.content, line.has_newline)
581}
582
583pub fn patience_diff_lines(old: &[DiffLine<'_>], new: &[DiffLine<'_>]) -> Vec<DiffOp> {
597 patience_diff_lines_anchored(old, new, &[])
598}
599
600pub fn patience_diff_lines_anchored(
609 old: &[DiffLine<'_>],
610 new: &[DiffLine<'_>],
611 anchors: &[Vec<u8>],
612) -> Vec<DiffOp> {
613 let mut ops: Vec<DiffOp> = Vec::new();
614 patience_recurse(old, new, 0, old.len(), 0, new.len(), anchors, &mut ops);
615 coalesce_ops(ops)
616}
617
618pub fn histogram_diff_lines(old: &[DiffLine<'_>], new: &[DiffLine<'_>]) -> Vec<DiffOp> {
630 let mut ops: Vec<DiffOp> = Vec::new();
631 histogram_recurse(old, new, 0, old.len(), 0, new.len(), &mut ops);
632 coalesce_ops(ops)
633}
634
635pub fn diff_lines_with_algorithm(
648 old: &[DiffLine<'_>],
649 new: &[DiffLine<'_>],
650 algorithm: DiffAlgorithm,
651) -> Vec<DiffOp> {
652 match algorithm {
653 DiffAlgorithm::Myers | DiffAlgorithm::Minimal => myers_diff_lines(old, new),
654 DiffAlgorithm::Patience => patience_diff_lines(old, new),
655 DiffAlgorithm::Histogram => histogram_diff_lines(old, new),
656 }
657}
658
659fn emit_trivial_range(a0: usize, a1: usize, b0: usize, b1: usize, out: &mut Vec<DiffOp>) -> bool {
666 let old_len = a1 - a0;
667 let new_len = b1 - b0;
668 if old_len == 0 && new_len == 0 {
669 return true;
670 }
671 if old_len == 0 {
672 out.push(DiffOp::Insert(new_len));
673 return true;
674 }
675 if new_len == 0 {
676 out.push(DiffOp::Delete(old_len));
677 return true;
678 }
679 false
680}
681
682fn trim_common(
690 old: &[DiffLine<'_>],
691 new: &[DiffLine<'_>],
692 mut a0: usize,
693 mut a1: usize,
694 mut b0: usize,
695 mut b1: usize,
696 out: &mut Vec<DiffOp>,
697) -> (usize, usize, usize, usize, usize) {
698 let mut prefix = 0usize;
699 while a0 < a1 && b0 < b1 && old[a0] == new[b0] {
700 a0 += 1;
701 b0 += 1;
702 prefix += 1;
703 }
704 if prefix > 0 {
705 out.push(DiffOp::Equal(prefix));
706 }
707 let mut suffix = 0usize;
708 while a1 > a0 && b1 > b0 && old[a1 - 1] == new[b1 - 1] {
709 a1 -= 1;
710 b1 -= 1;
711 suffix += 1;
712 }
713 (a0, a1, b0, b1, suffix)
714}
715
716#[allow(clippy::too_many_arguments)]
722fn patience_recurse(
723 old: &[DiffLine<'_>],
724 new: &[DiffLine<'_>],
725 a0: usize,
726 a1: usize,
727 b0: usize,
728 b1: usize,
729 anchors: &[Vec<u8>],
730 out: &mut Vec<DiffOp>,
731) {
732 if emit_trivial_range(a0, a1, b0, b1, out) {
733 return;
734 }
735 let (a0, a1, b0, b1, suffix) = trim_common(old, new, a0, a1, b0, b1, out);
736 if !emit_trivial_range(a0, a1, b0, b1, out) {
737 match patience_anchors(old, new, a0, a1, b0, b1, anchors) {
738 Some(aligned) => {
739 let mut cur_a = a0;
742 let mut cur_b = b0;
743 for (ai, bi) in aligned {
744 patience_recurse(old, new, cur_a, ai, cur_b, bi, anchors, out);
745 out.push(DiffOp::Equal(1));
746 cur_a = ai + 1;
747 cur_b = bi + 1;
748 }
749 patience_recurse(old, new, cur_a, a1, cur_b, b1, anchors, out);
751 }
752 None => myers_core(&old[a0..a1], &new[b0..b1], out),
755 }
756 }
757 if suffix > 0 {
758 out.push(DiffOp::Equal(suffix));
759 }
760}
761
762fn patience_anchors(
771 old: &[DiffLine<'_>],
772 new: &[DiffLine<'_>],
773 a0: usize,
774 a1: usize,
775 b0: usize,
776 b1: usize,
777 anchors: &[Vec<u8>],
778) -> Option<Vec<(usize, usize)>> {
779 struct Occ {
782 count: usize,
783 pos: usize,
784 }
785 let mut in_old: HashMap<LineKey<'_>, Occ> = HashMap::new();
786 for (i, line) in old.iter().enumerate().take(a1).skip(a0) {
787 in_old
788 .entry(line_key(line))
789 .and_modify(|o| o.count += 1)
790 .or_insert(Occ { count: 1, pos: i });
791 }
792 let mut in_new: HashMap<LineKey<'_>, Occ> = HashMap::new();
793 for (j, line) in new.iter().enumerate().take(b1).skip(b0) {
794 in_new
795 .entry(line_key(line))
796 .and_modify(|o| o.count += 1)
797 .or_insert(Occ { count: 1, pos: j });
798 }
799
800 let mut pairs: Vec<(usize, usize)> = Vec::new();
802 for (i, line) in old.iter().enumerate().take(a1).skip(a0) {
803 let key = line_key(line);
804 let Some(o) = in_old.get(&key) else { continue };
805 if o.count != 1 || o.pos != i {
806 continue;
807 }
808 if let Some(n) = in_new.get(&key)
810 && n.count == 1
811 {
812 pairs.push((i, n.pos));
813 }
814 }
815 if pairs.is_empty() {
816 return None;
817 }
818
819 let lis = if anchors.is_empty() {
824 longest_increasing_by_new(&pairs)
825 } else {
826 let is_anchor: Vec<bool> = pairs
827 .iter()
828 .map(|&(_, nj)| line_matches_anchor(new[nj].content, anchors))
829 .collect();
830 longest_increasing_by_new_anchored(&pairs, &is_anchor)
831 };
832 if lis.is_empty() { None } else { Some(lis) }
833}
834
835fn line_matches_anchor(line: &[u8], anchors: &[Vec<u8>]) -> bool {
839 anchors.iter().any(|anchor| line.starts_with(anchor))
840}
841
842fn longest_increasing_by_new(pairs: &[(usize, usize)]) -> Vec<(usize, usize)> {
850 if pairs.is_empty() {
851 return Vec::new();
852 }
853 let mut tails: Vec<usize> = Vec::new();
856 let mut prev: Vec<Option<usize>> = vec![None; pairs.len()];
858
859 for i in 0..pairs.len() {
860 let val = pairs[i].1;
861 let mut lo = 0usize;
863 let mut hi = tails.len();
864 while lo < hi {
865 let mid = lo + (hi - lo) / 2;
866 if pairs[tails[mid]].1 < val {
867 lo = mid + 1;
868 } else {
869 hi = mid;
870 }
871 }
872 if lo > 0 {
873 prev[i] = Some(tails[lo - 1]);
874 }
875 if lo == tails.len() {
876 tails.push(i);
877 } else {
878 tails[lo] = i;
879 }
880 }
881
882 let mut result: Vec<(usize, usize)> = Vec::with_capacity(tails.len());
884 let mut cur = tails.last().copied();
885 while let Some(i) = cur {
886 result.push(pairs[i]);
887 cur = prev[i];
888 }
889 result.reverse();
890 result
891}
892
893fn longest_increasing_by_new_anchored(
905 pairs: &[(usize, usize)],
906 is_anchor: &[bool],
907) -> Vec<(usize, usize)> {
908 if pairs.is_empty() {
909 return Vec::new();
910 }
911 let mut sequence: Vec<usize> = Vec::with_capacity(pairs.len());
914 let mut prev: Vec<Option<usize>> = vec![None; pairs.len()];
915 let mut longest: usize = 0;
916 let mut anchor_i: isize = -1;
917 for (e, &(_, val)) in pairs.iter().enumerate() {
918 let i: isize = if longest == 0 || val > pairs[sequence[longest - 1]].1 {
921 longest as isize - 1
922 } else {
923 let mut lo = 0usize;
924 let mut hi = longest;
925 while lo < hi {
926 let mid = lo + (hi - lo) / 2;
927 if pairs[sequence[mid]].1 < val {
928 lo = mid + 1;
929 } else {
930 hi = mid;
931 }
932 }
933 lo as isize - 1
934 };
935 prev[e] = if i < 0 {
936 None
937 } else {
938 Some(sequence[i as usize])
939 };
940 let pos = (i + 1) as usize;
941 if (pos as isize) <= anchor_i {
942 continue;
943 }
944 if pos == sequence.len() {
945 sequence.push(e);
946 } else {
947 sequence[pos] = e;
948 }
949 if is_anchor[e] {
950 anchor_i = pos as isize;
951 longest = pos + 1;
952 } else if pos == longest {
953 longest += 1;
954 }
955 }
956 if longest == 0 {
957 return Vec::new();
958 }
959 let mut result: Vec<(usize, usize)> = Vec::with_capacity(longest);
960 let mut cur = Some(sequence[longest - 1]);
961 while let Some(i) = cur {
962 result.push(pairs[i]);
963 cur = prev[i];
964 }
965 result.reverse();
966 result
967}
968
969fn histogram_recurse(
971 old: &[DiffLine<'_>],
972 new: &[DiffLine<'_>],
973 a0: usize,
974 a1: usize,
975 b0: usize,
976 b1: usize,
977 out: &mut Vec<DiffOp>,
978) {
979 if emit_trivial_range(a0, a1, b0, b1, out) {
980 return;
981 }
982 let (a0, a1, b0, b1, suffix) = trim_common(old, new, a0, a1, b0, b1, out);
983 if !emit_trivial_range(a0, a1, b0, b1, out) {
984 match histogram_region(old, new, a0, a1, b0, b1) {
985 Some(region) => {
986 histogram_recurse(old, new, a0, region.old_start, b0, region.new_start, out);
989 out.push(DiffOp::Equal(region.len));
990 histogram_recurse(
991 old,
992 new,
993 region.old_start + region.len,
994 a1,
995 region.new_start + region.len,
996 b1,
997 out,
998 );
999 }
1000 None => myers_core(&old[a0..a1], &new[b0..b1], out),
1002 }
1003 }
1004 if suffix > 0 {
1005 out.push(DiffOp::Equal(suffix));
1006 }
1007}
1008
1009struct HistogramRegion {
1011 old_start: usize,
1012 new_start: usize,
1013 len: usize,
1014}
1015
1016fn histogram_region(
1026 old: &[DiffLine<'_>],
1027 new: &[DiffLine<'_>],
1028 a0: usize,
1029 a1: usize,
1030 b0: usize,
1031 b1: usize,
1032) -> Option<HistogramRegion> {
1033 let mut buckets: HashMap<LineKey<'_>, Vec<usize>> = HashMap::new();
1035 for (i, line) in old.iter().enumerate().take(a1).skip(a0) {
1036 buckets.entry(line_key(line)).or_default().push(i);
1037 }
1038
1039 let mut best: Option<HistogramRegion> = None;
1040 let mut best_count = usize::MAX;
1042 let mut best_len = 0usize;
1043
1044 let mut bj = b0;
1045 while bj < b1 {
1046 let key = line_key(&new[bj]);
1047 let Some(positions) = buckets.get(&key) else {
1048 bj += 1;
1049 continue;
1050 };
1051 let occ = positions.len();
1052 let mut next_bj = bj + 1;
1055 for &ai in positions {
1056 let mut start_a = ai;
1058 let mut start_b = bj;
1059 while start_a > a0 && start_b > b0 && old[start_a - 1] == new[start_b - 1] {
1060 start_a -= 1;
1061 start_b -= 1;
1062 }
1063 let mut len = 0usize;
1065 while start_a + len < a1
1066 && start_b + len < b1
1067 && old[start_a + len] == new[start_b + len]
1068 {
1069 len += 1;
1070 }
1071 let run_count = occ;
1074 let better = run_count < best_count || (run_count == best_count && len > best_len);
1075 if better && len > 0 {
1076 best_count = run_count;
1077 best_len = len;
1078 best = Some(HistogramRegion {
1079 old_start: start_a,
1080 new_start: start_b,
1081 len,
1082 });
1083 if start_b + len > next_bj {
1086 next_bj = start_b + len;
1087 }
1088 }
1089 }
1090 bj = next_bj.max(bj + 1);
1091 }
1092
1093 best
1094}
1095
1096#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1098pub enum ConflictStyle {
1099 #[default]
1101 Merge,
1102 Diff3,
1105}
1106
1107#[derive(Debug, Clone, Copy)]
1109pub struct MergeBlobOptions<'a> {
1110 pub ours_label: &'a str,
1112 pub theirs_label: &'a str,
1114 pub base_label: &'a str,
1116 pub style: ConflictStyle,
1118 pub favor: MergeFavor,
1122 pub ws_ignore: WsIgnore,
1129 pub marker_size: usize,
1131}
1132
1133impl Default for MergeBlobOptions<'_> {
1134 fn default() -> Self {
1135 Self {
1136 ours_label: "ours",
1137 theirs_label: "theirs",
1138 base_label: "base",
1139 style: ConflictStyle::Merge,
1140 favor: MergeFavor::None,
1141 ws_ignore: WsIgnore::EMPTY,
1142 marker_size: 7,
1143 }
1144 }
1145}
1146
1147#[derive(Debug, Clone, PartialEq, Eq)]
1149pub struct MergeBlobResult {
1150 pub content: Vec<u8>,
1152 pub conflicted: bool,
1154}
1155
1156pub fn merge_blobs(
1171 base: &[u8],
1172 ours: &[u8],
1173 theirs: &[u8],
1174 options: &MergeBlobOptions<'_>,
1175) -> MergeBlobResult {
1176 let base_lines = split_lines(base);
1177 let ours_lines = split_lines(ours);
1178 let theirs_lines = split_lines(theirs);
1179
1180 let ours_matches = matching_regions(&base_lines, &ours_lines, options.ws_ignore);
1185 let theirs_matches = matching_regions(&base_lines, &theirs_lines, options.ws_ignore);
1186
1187 let stable = common_stable_segments(&ours_matches, &theirs_matches);
1191
1192 let mut writer = MergeWriter::new(options);
1193 let mut base_idx = 0usize;
1195 let mut our_idx = 0usize;
1196 let mut their_idx = 0usize;
1197
1198 for seg in &stable {
1199 let base_region = &base_lines[base_idx..seg.base_start];
1201 let our_region = &ours_lines[our_idx..seg.ours_start];
1202 let their_region = &theirs_lines[their_idx..seg.theirs_start];
1203 emit_region(
1204 &mut writer,
1205 base_region,
1206 our_region,
1207 their_region,
1208 options.ws_ignore,
1209 );
1210
1211 writer.emit_lines(&ours_lines[seg.ours_start..seg.ours_start + seg.len]);
1215
1216 base_idx = seg.base_start + seg.len;
1217 our_idx = seg.ours_start + seg.len;
1218 their_idx = seg.theirs_start + seg.len;
1219 }
1220
1221 emit_region(
1224 &mut writer,
1225 &base_lines[base_idx..],
1226 &ours_lines[our_idx..],
1227 &theirs_lines[their_idx..],
1228 options.ws_ignore,
1229 );
1230
1231 writer.finish()
1232}
1233
1234fn emit_region(
1237 writer: &mut MergeWriter<'_>,
1238 base_region: &[DiffLine<'_>],
1239 our_region: &[DiffLine<'_>],
1240 their_region: &[DiffLine<'_>],
1241 ws_ignore: WsIgnore,
1242) {
1243 if our_region.is_empty() && their_region.is_empty() {
1244 return;
1245 }
1246 let our_changed = !regions_match(our_region, base_region, ws_ignore);
1249 let their_changed = !regions_match(their_region, base_region, ws_ignore);
1250 match (our_changed, their_changed) {
1251 (false, false) => writer.emit_lines(our_region),
1252 (true, false) => writer.emit_lines(our_region),
1253 (false, true) => writer.emit_lines(their_region),
1254 (true, true) => {
1255 if regions_match(our_region, their_region, ws_ignore) {
1256 writer.emit_lines(our_region);
1259 } else {
1260 writer.emit_conflict_refined(our_region, base_region, their_region);
1261 }
1262 }
1263 }
1264}
1265
1266fn regions_match(a: &[DiffLine<'_>], b: &[DiffLine<'_>], ws_ignore: WsIgnore) -> bool {
1269 if ws_ignore.is_empty() {
1270 return a == b;
1271 }
1272 a.len() == b.len()
1273 && a.iter().zip(b).all(|(x, y)| {
1274 canonicalize_line(x.content, ws_ignore) == canonicalize_line(y.content, ws_ignore)
1275 })
1276}
1277
1278enum RefineItem {
1282 Context(std::ops::Range<usize>),
1283 Conflict(std::ops::Range<usize>, std::ops::Range<usize>),
1284}
1285
1286fn refine_conflict_items(ours: &[DiffLine<'_>], theirs: &[DiffLine<'_>]) -> Vec<RefineItem> {
1295 let ops = myers_diff_lines(ours, theirs);
1298 let mut raw: Vec<RefineItem> = Vec::new();
1299 let mut oi = 0usize;
1300 let mut ti = 0usize;
1301 let mut pending: Option<(usize, usize, usize, usize)> = None; for op in ops {
1303 match op {
1304 DiffOp::Equal(n) => {
1305 if let Some((o0, o1, t0, t1)) = pending.take() {
1306 raw.push(RefineItem::Conflict(o0..o1, t0..t1));
1307 }
1308 raw.push(RefineItem::Context(oi..oi + n));
1309 oi += n;
1310 ti += n;
1311 }
1312 DiffOp::Delete(n) => {
1313 let entry = pending.get_or_insert((oi, oi, ti, ti));
1314 entry.1 = oi + n;
1315 oi += n;
1316 }
1317 DiffOp::Insert(n) => {
1318 let entry = pending.get_or_insert((oi, oi, ti, ti));
1319 entry.3 = ti + n;
1320 ti += n;
1321 }
1322 }
1323 }
1324 if let Some((o0, o1, t0, t1)) = pending.take() {
1325 raw.push(RefineItem::Conflict(o0..o1, t0..t1));
1326 }
1327
1328 let mut out: Vec<RefineItem> = Vec::new();
1332 let mut idx = 0usize;
1333 while idx < raw.len() {
1334 match &raw[idx] {
1335 RefineItem::Context(range) => {
1336 let small = range.len() <= 3;
1337 let prev_conflict = matches!(out.last(), Some(RefineItem::Conflict(..)));
1338 let next_conflict = matches!(raw.get(idx + 1), Some(RefineItem::Conflict(..)));
1339 if small && prev_conflict && next_conflict {
1340 let Some(RefineItem::Conflict(po, pt)) = out.pop() else {
1341 unreachable!()
1342 };
1343 let RefineItem::Conflict(no, nt) = &raw[idx + 1] else {
1344 unreachable!()
1345 };
1346 out.push(RefineItem::Conflict(po.start..no.end, pt.start..nt.end));
1347 idx += 2;
1348 } else {
1349 out.push(RefineItem::Context(range.clone()));
1350 idx += 1;
1351 }
1352 }
1353 RefineItem::Conflict(o, t) => {
1354 out.push(RefineItem::Conflict(o.clone(), t.clone()));
1355 idx += 1;
1356 }
1357 }
1358 }
1359 out
1360}
1361
1362#[derive(Debug, Clone, Copy)]
1365struct MatchRegion {
1366 base_start: usize,
1367 side_start: usize,
1368 len: usize,
1369}
1370
1371#[derive(Debug, Clone, Copy)]
1373struct StableSegment {
1374 base_start: usize,
1375 ours_start: usize,
1376 theirs_start: usize,
1377 len: usize,
1378}
1379
1380fn matching_regions(
1386 base: &[DiffLine<'_>],
1387 side: &[DiffLine<'_>],
1388 ws_ignore: WsIgnore,
1389) -> Vec<MatchRegion> {
1390 let ops = if ws_ignore.is_empty() {
1391 myers_diff_lines(base, side)
1392 } else {
1393 myers_diff_lines_ws(base, side, ws_ignore, DiffAlgorithm::Myers)
1396 };
1397 let mut regions = Vec::new();
1398 let mut base_idx = 0usize;
1399 let mut side_idx = 0usize;
1400 for op in ops {
1401 match op {
1402 DiffOp::Equal(n) => {
1403 regions.push(MatchRegion {
1404 base_start: base_idx,
1405 side_start: side_idx,
1406 len: n,
1407 });
1408 base_idx += n;
1409 side_idx += n;
1410 }
1411 DiffOp::Delete(n) => base_idx += n,
1412 DiffOp::Insert(n) => side_idx += n,
1413 }
1414 }
1415 regions
1416}
1417
1418fn common_stable_segments(ours: &[MatchRegion], theirs: &[MatchRegion]) -> Vec<StableSegment> {
1426 let mut segments = Vec::new();
1427 let mut oi = 0usize;
1428 let mut ti = 0usize;
1429 while oi < ours.len() && ti < theirs.len() {
1430 let o = ours[oi];
1431 let t = theirs[ti];
1432 let o_end = o.base_start + o.len;
1433 let t_end = t.base_start + t.len;
1434 let lo = o.base_start.max(t.base_start);
1435 let hi = o_end.min(t_end);
1436 if lo < hi {
1437 segments.push(StableSegment {
1438 base_start: lo,
1439 ours_start: o.side_start + (lo - o.base_start),
1440 theirs_start: t.side_start + (lo - t.base_start),
1441 len: hi - lo,
1442 });
1443 }
1444 if o_end <= t_end {
1446 oi += 1;
1447 } else {
1448 ti += 1;
1449 }
1450 }
1451 segments
1452}
1453
1454struct MergeWriter<'a> {
1457 out: Vec<u8>,
1458 conflicted: bool,
1459 options: &'a MergeBlobOptions<'a>,
1460}
1461
1462impl<'a> MergeWriter<'a> {
1463 fn new(options: &'a MergeBlobOptions<'a>) -> Self {
1464 Self {
1465 out: Vec::new(),
1466 conflicted: false,
1467 options,
1468 }
1469 }
1470
1471 fn emit_lines(&mut self, lines: &[DiffLine<'_>]) {
1474 for line in lines {
1475 self.out.extend_from_slice(line.content);
1476 }
1477 }
1478
1479 fn emit_conflict(
1485 &mut self,
1486 ours: &[DiffLine<'_>],
1487 base: &[DiffLine<'_>],
1488 theirs: &[DiffLine<'_>],
1489 ) {
1490 if self.options.favor == MergeFavor::Union {
1493 self.emit_section(ours);
1494 self.ensure_newline();
1495 self.emit_section(theirs);
1496 return;
1497 }
1498 self.conflicted = true;
1499 self.write_marker(b'<', self.options.ours_label);
1500 self.emit_section(ours);
1501 if self.options.style == ConflictStyle::Diff3 {
1502 self.ensure_newline();
1503 self.write_marker(b'|', self.options.base_label);
1504 self.emit_section(base);
1505 }
1506 self.ensure_newline();
1507 self.write_divider();
1508 self.emit_section(theirs);
1509 self.ensure_newline();
1510 self.write_marker(b'>', self.options.theirs_label);
1511 }
1512
1513 fn emit_conflict_refined(
1520 &mut self,
1521 ours: &[DiffLine<'_>],
1522 base: &[DiffLine<'_>],
1523 theirs: &[DiffLine<'_>],
1524 ) {
1525 if self.options.style == ConflictStyle::Diff3
1526 || self.options.favor != MergeFavor::None
1527 || ours.is_empty()
1528 || theirs.is_empty()
1529 {
1530 self.emit_conflict(ours, base, theirs);
1531 return;
1532 }
1533 for item in refine_conflict_items(ours, theirs) {
1534 match item {
1535 RefineItem::Context(range) => self.emit_lines(&ours[range]),
1536 RefineItem::Conflict(o, t) => self.emit_conflict(&ours[o], &[], &theirs[t]),
1537 }
1538 }
1539 }
1540
1541 fn emit_section(&mut self, lines: &[DiffLine<'_>]) {
1543 for line in lines {
1544 self.out.extend_from_slice(line.content);
1545 }
1546 }
1547
1548 fn ensure_newline(&mut self) {
1551 if !self.out.is_empty() && self.out.last() != Some(&b'\n') {
1552 self.out.push(b'\n');
1553 }
1554 }
1555
1556 fn write_marker(&mut self, ch: u8, label: &str) {
1560 for _ in 0..self.options.marker_size {
1561 self.out.push(ch);
1562 }
1563 if !label.is_empty() {
1564 self.out.push(b' ');
1565 self.out.extend_from_slice(label.as_bytes());
1566 }
1567 self.out.push(b'\n');
1568 }
1569
1570 fn write_divider(&mut self) {
1572 for _ in 0..self.options.marker_size {
1573 self.out.push(b'=');
1574 }
1575 self.out.push(b'\n');
1576 }
1577
1578 fn finish(self) -> MergeBlobResult {
1579 MergeBlobResult {
1580 content: self.out,
1581 conflicted: self.conflicted,
1582 }
1583 }
1584}
1585
1586#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1587pub enum DiffAlgorithm {
1588 Myers,
1589 Minimal,
1590 Patience,
1591 Histogram,
1592}
1593
1594#[derive(Debug, Clone, PartialEq, Eq)]
1595pub enum FileChange {
1596 Add { path: RepoPath },
1597 Delete { path: RepoPath },
1598 Modify { path: RepoPath },
1599 Rename { old: RepoPath, new: RepoPath },
1600 Copy { source: RepoPath, dest: RepoPath },
1601}
1602
1603#[derive(Debug, Clone, PartialEq, Eq)]
1604pub struct Conflict {
1605 pub path: RepoPath,
1606 pub ours: Vec<u8>,
1607 pub theirs: Vec<u8>,
1608}
1609
1610#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1611pub enum NameStatus {
1612 Added,
1613 Deleted,
1614 Modified,
1615 TypeChanged,
1621 Renamed(u8),
1622 Copied(u8),
1623 Unmerged,
1627}
1628
1629impl NameStatus {
1630 pub const fn code(self) -> char {
1631 match self {
1632 Self::Added => 'A',
1633 Self::Deleted => 'D',
1634 Self::Modified => 'M',
1635 Self::TypeChanged => 'T',
1636 Self::Renamed(_) => 'R',
1637 Self::Copied(_) => 'C',
1638 Self::Unmerged => 'U',
1639 }
1640 }
1641
1642 pub fn label(self) -> String {
1643 match self {
1644 Self::Renamed(score) => format!("R{score:03}"),
1645 Self::Copied(score) => format!("C{score:03}"),
1646 _ => self.code().to_string(),
1647 }
1648 }
1649}
1650
1651pub const S_IFMT: u32 = 0o170000;
1655
1656#[must_use]
1661pub const fn is_type_change(old_mode: u32, new_mode: u32) -> bool {
1662 (old_mode & S_IFMT) != (new_mode & S_IFMT)
1663}
1664
1665#[must_use]
1670pub const fn modify_or_type_change(old_mode: u32, new_mode: u32) -> NameStatus {
1671 if is_type_change(old_mode, new_mode) {
1672 NameStatus::TypeChanged
1673 } else {
1674 NameStatus::Modified
1675 }
1676}
1677
1678#[derive(Debug, Clone, PartialEq, Eq)]
1679pub struct NameStatusEntry {
1680 pub status: NameStatus,
1681 pub path: BString,
1682 pub old_path: Option<BString>,
1683 pub old_mode: Option<u32>,
1684 pub new_mode: Option<u32>,
1685 pub old_oid: Option<ObjectId>,
1686 pub new_oid: Option<ObjectId>,
1687}
1688
1689impl NameStatusEntry {
1690 pub fn line(&self) -> String {
1691 if let Some(old_path) = &self.old_path {
1692 format!(
1693 "{}\t{}\t{}",
1694 self.status.label(),
1695 String::from_utf8_lossy(old_path.as_bytes()),
1696 String::from_utf8_lossy(self.path.as_bytes())
1697 )
1698 } else {
1699 format!(
1700 "{}\t{}",
1701 self.status.label(),
1702 String::from_utf8_lossy(self.path.as_bytes())
1703 )
1704 }
1705 }
1706}
1707
1708#[derive(Debug, Clone, PartialEq, Eq)]
1709pub struct IndexGitlinkEntry {
1710 pub path: BString,
1711 pub oid: ObjectId,
1712}
1713
1714#[derive(Debug, Clone, PartialEq, Eq)]
1715pub struct IndexWorktreeDiff {
1716 pub entries: Vec<NameStatusEntry>,
1717 pub staged_gitlinks: Vec<IndexGitlinkEntry>,
1718}
1719
1720#[derive(Clone, Copy)]
1721pub struct IndexWorktreeValidationEntry<'a> {
1722 pub path: &'a [u8],
1723 pub mode: u32,
1724 pub oid: ObjectId,
1725 pub size: u32,
1726}
1727
1728pub struct IndexWorktreeValidatedEntry {
1729 pub mode: u32,
1730 pub oid: ObjectId,
1731}
1732
1733pub type IndexWorktreeStatCleanValidator<'a> = dyn FnMut(
1734 IndexWorktreeValidationEntry<'_>,
1735 &Path,
1736 &fs::Metadata,
1737 ) -> Result<Option<IndexWorktreeValidatedEntry>>
1738 + 'a;
1739
1740#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1741pub struct DiffNameStatusOptions {
1742 pub detect_renames: bool,
1743 pub detect_copies: bool,
1744 pub find_copies_harder: bool,
1745 pub rename_empty: bool,
1746}
1747
1748impl Default for DiffNameStatusOptions {
1749 fn default() -> Self {
1750 Self {
1751 detect_renames: true,
1752 detect_copies: false,
1753 find_copies_harder: false,
1754 rename_empty: true,
1755 }
1756 }
1757}
1758
1759pub const DEFAULT_RENAME_THRESHOLD: u8 = 50;
1763
1764#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1780pub struct RenameDetectionOptions {
1781 pub base: DiffNameStatusOptions,
1784 pub detect_inexact: bool,
1787 pub rename_threshold: u8,
1791 pub copy_threshold: u8,
1795 pub rename_limit: usize,
1802}
1803
1804#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1805pub struct RenameLimitDiagnostics {
1806 pub inexact_renames_skipped: bool,
1807 pub inexact_copies_skipped: bool,
1808 pub inexact_copies_degraded: bool,
1809}
1810
1811impl RenameLimitDiagnostics {
1812 pub fn any_skipped(self) -> bool {
1813 self.inexact_renames_skipped || self.inexact_copies_skipped
1814 }
1815
1816 pub fn any_warning(self) -> bool {
1817 self.any_skipped() || self.inexact_copies_degraded
1818 }
1819}
1820
1821#[derive(Debug, Clone, PartialEq, Eq)]
1822pub struct NameStatusWithRenameDiagnostics {
1823 pub entries: Vec<NameStatusEntry>,
1824 pub rename_limit: RenameLimitDiagnostics,
1825}
1826
1827impl Default for RenameDetectionOptions {
1828 fn default() -> Self {
1829 Self {
1830 base: DiffNameStatusOptions::default(),
1831 detect_inexact: false,
1832 rename_threshold: DEFAULT_RENAME_THRESHOLD,
1833 copy_threshold: DEFAULT_RENAME_THRESHOLD,
1834 rename_limit: 0,
1835 }
1836 }
1837}
1838
1839impl RenameDetectionOptions {
1840 pub fn inexact(base: DiffNameStatusOptions) -> Self {
1843 Self {
1844 base,
1845 detect_inexact: true,
1846 ..Self::default()
1847 }
1848 }
1849}
1850
1851pub fn diff_name_status_head_worktree(
1852 worktree_root: impl AsRef<Path>,
1853 git_dir: impl AsRef<Path>,
1854 format: ObjectFormat,
1855) -> Result<Vec<NameStatusEntry>> {
1856 diff_name_status_head_worktree_with_options(
1857 worktree_root,
1858 git_dir,
1859 format,
1860 DiffNameStatusOptions::default(),
1861 )
1862}
1863
1864pub fn diff_name_status_head_worktree_with_options(
1865 worktree_root: impl AsRef<Path>,
1866 git_dir: impl AsRef<Path>,
1867 format: ObjectFormat,
1868 options: DiffNameStatusOptions,
1869) -> Result<Vec<NameStatusEntry>> {
1870 let worktree_root = worktree_root.as_ref();
1871 let git_dir = git_dir.as_ref();
1872 let db = FileObjectDatabase::from_git_dir(git_dir, format);
1873 let head = head_tree_entries(git_dir, format, &db)?;
1874 let IndexSnapshot {
1875 entries: index,
1876 stat_cache,
1877 missing_skip_worktree_entries,
1878 present_skip_worktree_entries,
1879 } = read_index_snapshot(git_dir, format)?;
1880 let index_gitlinks = index_gitlinks(&index);
1881 let candidate_paths = candidate_path_set(head.keys().chain(index.keys()));
1882 let worktree = worktree_entries_for_path_set(
1883 worktree_root,
1884 format,
1885 &candidate_paths,
1886 &index_gitlinks,
1887 Some(&stat_cache),
1888 Some(&missing_skip_worktree_entries),
1889 Some(&present_skip_worktree_entries),
1890 )?;
1891 let changes = diff_name_status_maps_for_path_set(&head, &worktree, &candidate_paths, options)?;
1892 Ok(mark_unstaged_worktree_oids_unresolved(
1893 changes, &index, &worktree,
1894 ))
1895}
1896
1897pub fn diff_name_status_head_worktree_with_rename_options(
1901 worktree_root: impl AsRef<Path>,
1902 git_dir: impl AsRef<Path>,
1903 format: ObjectFormat,
1904 options: RenameDetectionOptions,
1905) -> Result<Vec<NameStatusEntry>> {
1906 let worktree_root = worktree_root.as_ref();
1907 let git_dir = git_dir.as_ref();
1908 let db = FileObjectDatabase::from_git_dir(git_dir, format);
1909 let head = head_tree_entries(git_dir, format, &db)?;
1910 let IndexSnapshot {
1911 entries: index,
1912 stat_cache,
1913 missing_skip_worktree_entries,
1914 present_skip_worktree_entries,
1915 } = read_index_snapshot(git_dir, format)?;
1916 let index_gitlinks = index_gitlinks(&index);
1917 let candidate_paths = candidate_path_set(head.keys().chain(index.keys()));
1918 let worktree = worktree_entries_for_path_set(
1919 worktree_root,
1920 format,
1921 &candidate_paths,
1922 &index_gitlinks,
1923 Some(&stat_cache),
1924 Some(&missing_skip_worktree_entries),
1925 Some(&present_skip_worktree_entries),
1926 )?;
1927 let cache = worktree_blob_cache_for_path_set(
1928 worktree_root,
1929 &head,
1930 &worktree,
1931 &candidate_paths,
1932 Some(&present_skip_worktree_entries),
1933 options,
1934 )?;
1935 let changes = diff_name_status_maps_with_renames_for_path_set(
1936 &head,
1937 &worktree,
1938 &candidate_paths,
1939 options,
1940 |oid| cache_or_odb_blob(&cache, &db, oid),
1941 )?;
1942 Ok(mark_unstaged_worktree_oids_unresolved(
1943 changes, &index, &worktree,
1944 ))
1945}
1946
1947pub fn diff_name_status_head_index(
1948 git_dir: impl AsRef<Path>,
1949 format: ObjectFormat,
1950) -> Result<Vec<NameStatusEntry>> {
1951 diff_name_status_head_index_with_options(git_dir, format, DiffNameStatusOptions::default())
1952}
1953
1954pub fn diff_name_status_head_index_with_options(
1955 git_dir: impl AsRef<Path>,
1956 format: ObjectFormat,
1957 options: DiffNameStatusOptions,
1958) -> Result<Vec<NameStatusEntry>> {
1959 let git_dir = git_dir.as_ref();
1960 let db = FileObjectDatabase::from_git_dir(git_dir, format);
1961 let head = head_tree_entries(git_dir, format, &db)?;
1962 let index = read_index_entries(git_dir, format)?;
1963 diff_name_status_maps(&head, &index, head.keys().chain(index.keys()), options)
1964}
1965
1966pub fn diff_name_status_head_index_with_rename_options(
1970 git_dir: impl AsRef<Path>,
1971 format: ObjectFormat,
1972 options: RenameDetectionOptions,
1973) -> Result<Vec<NameStatusEntry>> {
1974 Ok(
1975 diff_name_status_head_index_with_rename_options_and_diagnostics(git_dir, format, options)?
1976 .entries,
1977 )
1978}
1979
1980pub fn diff_name_status_head_index_with_rename_options_and_diagnostics(
1981 git_dir: impl AsRef<Path>,
1982 format: ObjectFormat,
1983 options: RenameDetectionOptions,
1984) -> Result<NameStatusWithRenameDiagnostics> {
1985 let git_dir = git_dir.as_ref();
1986 let db = FileObjectDatabase::from_git_dir(git_dir, format);
1987 let head = head_tree_entries(git_dir, format, &db)?;
1988 let index = read_index_entries(git_dir, format)?;
1989 diff_name_status_maps_with_renames_and_diagnostics(
1990 &head,
1991 &index,
1992 head.keys().chain(index.keys()),
1993 options,
1994 |oid| read_blob_bytes(&db, oid),
1995 )
1996}
1997
1998fn tree_entries(
2008 tree_oid: &ObjectId,
2009 format: ObjectFormat,
2010 db: &FileObjectDatabase,
2011) -> Result<BTreeMap<Vec<u8>, TrackedEntry>> {
2012 let mut entries = BTreeMap::new();
2013 if *tree_oid == empty_tree_oid(format)? {
2014 return Ok(entries);
2015 }
2016 collect_tree_entries(db, format, tree_oid, Vec::new(), &mut entries)?;
2017 Ok(entries)
2018}
2019
2020fn empty_tree_oid(format: ObjectFormat) -> Result<ObjectId> {
2023 object_id_for_bytes(format, "tree", b"")
2024}
2025
2026pub fn diff_name_status_tree_index_with_options(
2030 git_dir: impl AsRef<Path>,
2031 format: ObjectFormat,
2032 tree_oid: &ObjectId,
2033 options: DiffNameStatusOptions,
2034) -> Result<Vec<NameStatusEntry>> {
2035 let git_dir = git_dir.as_ref();
2036 let db = FileObjectDatabase::from_git_dir(git_dir, format);
2037 let tree = tree_entries(tree_oid, format, &db)?;
2038 let index = read_index_entries(git_dir, format)?;
2039 diff_name_status_maps(&tree, &index, tree.keys().chain(index.keys()), options)
2040}
2041
2042pub fn diff_name_status_tree_index_with_rename_options(
2047 git_dir: impl AsRef<Path>,
2048 format: ObjectFormat,
2049 tree_oid: &ObjectId,
2050 options: RenameDetectionOptions,
2051) -> Result<Vec<NameStatusEntry>> {
2052 Ok(
2053 diff_name_status_tree_index_with_rename_options_and_diagnostics(
2054 git_dir, format, tree_oid, options,
2055 )?
2056 .entries,
2057 )
2058}
2059
2060pub fn diff_name_status_tree_index_with_rename_options_and_diagnostics(
2061 git_dir: impl AsRef<Path>,
2062 format: ObjectFormat,
2063 tree_oid: &ObjectId,
2064 options: RenameDetectionOptions,
2065) -> Result<NameStatusWithRenameDiagnostics> {
2066 let git_dir = git_dir.as_ref();
2067 let db = FileObjectDatabase::from_git_dir(git_dir, format);
2068 let tree = tree_entries(tree_oid, format, &db)?;
2069 let index = read_index_entries(git_dir, format)?;
2070 diff_name_status_maps_with_renames_and_diagnostics(
2071 &tree,
2072 &index,
2073 tree.keys().chain(index.keys()),
2074 options,
2075 |oid| read_blob_bytes(&db, oid),
2076 )
2077}
2078
2079pub fn diff_name_status_tree_worktree_with_options(
2085 worktree_root: impl AsRef<Path>,
2086 git_dir: impl AsRef<Path>,
2087 format: ObjectFormat,
2088 tree_oid: &ObjectId,
2089 options: DiffNameStatusOptions,
2090) -> Result<Vec<NameStatusEntry>> {
2091 let worktree_root = worktree_root.as_ref();
2092 let git_dir = git_dir.as_ref();
2093 let db = FileObjectDatabase::from_git_dir(git_dir, format);
2094 let tree = tree_entries(tree_oid, format, &db)?;
2095 let IndexSnapshot {
2096 entries: index,
2097 stat_cache,
2098 missing_skip_worktree_entries,
2099 present_skip_worktree_entries,
2100 } = read_index_snapshot(git_dir, format)?;
2101 let index_gitlinks = index_gitlinks(&index);
2102 let candidate_paths = candidate_path_set(tree.keys().chain(index.keys()));
2103 let worktree = worktree_entries_for_path_set(
2104 worktree_root,
2105 format,
2106 &candidate_paths,
2107 &index_gitlinks,
2108 Some(&stat_cache),
2109 Some(&missing_skip_worktree_entries),
2110 Some(&present_skip_worktree_entries),
2111 )?;
2112 let changes = diff_name_status_maps_for_path_set(&tree, &worktree, &candidate_paths, options)?;
2113 Ok(mark_unstaged_worktree_oids_unresolved(
2114 changes, &index, &worktree,
2115 ))
2116}
2117
2118pub fn diff_name_status_tree_worktree_with_rename_options(
2124 worktree_root: impl AsRef<Path>,
2125 git_dir: impl AsRef<Path>,
2126 format: ObjectFormat,
2127 tree_oid: &ObjectId,
2128 options: RenameDetectionOptions,
2129) -> Result<Vec<NameStatusEntry>> {
2130 let worktree_root = worktree_root.as_ref();
2131 let git_dir = git_dir.as_ref();
2132 let db = FileObjectDatabase::from_git_dir(git_dir, format);
2133 let tree = tree_entries(tree_oid, format, &db)?;
2134 let IndexSnapshot {
2135 entries: index,
2136 stat_cache,
2137 missing_skip_worktree_entries,
2138 present_skip_worktree_entries,
2139 } = read_index_snapshot(git_dir, format)?;
2140 let index_gitlinks = index_gitlinks(&index);
2141 let candidate_paths = candidate_path_set(tree.keys().chain(index.keys()));
2142 let worktree = worktree_entries_for_path_set(
2143 worktree_root,
2144 format,
2145 &candidate_paths,
2146 &index_gitlinks,
2147 Some(&stat_cache),
2148 Some(&missing_skip_worktree_entries),
2149 Some(&present_skip_worktree_entries),
2150 )?;
2151 let cache = worktree_blob_cache_for_path_set(
2152 worktree_root,
2153 &tree,
2154 &worktree,
2155 &candidate_paths,
2156 Some(&present_skip_worktree_entries),
2157 options,
2158 )?;
2159 let changes = diff_name_status_maps_with_renames_for_path_set(
2160 &tree,
2161 &worktree,
2162 &candidate_paths,
2163 options,
2164 |oid| cache_or_odb_blob(&cache, &db, oid),
2165 )?;
2166 Ok(mark_unstaged_worktree_oids_unresolved(
2167 changes, &index, &worktree,
2168 ))
2169}
2170
2171pub fn diff_name_status_index_worktree(
2172 worktree_root: impl AsRef<Path>,
2173 git_dir: impl AsRef<Path>,
2174 format: ObjectFormat,
2175) -> Result<Vec<NameStatusEntry>> {
2176 diff_name_status_index_worktree_with_options(
2177 worktree_root,
2178 git_dir,
2179 format,
2180 DiffNameStatusOptions::default(),
2181 )
2182}
2183
2184pub fn diff_name_status_index_worktree_with_options(
2185 worktree_root: impl AsRef<Path>,
2186 git_dir: impl AsRef<Path>,
2187 format: ObjectFormat,
2188 options: DiffNameStatusOptions,
2189) -> Result<Vec<NameStatusEntry>> {
2190 Ok(diff_name_status_index_worktree_with_options_and_gitlinks(
2191 worktree_root,
2192 git_dir,
2193 format,
2194 options,
2195 )?
2196 .entries)
2197}
2198
2199pub fn diff_name_status_index_worktree_with_options_and_gitlinks(
2200 worktree_root: impl AsRef<Path>,
2201 git_dir: impl AsRef<Path>,
2202 format: ObjectFormat,
2203 options: DiffNameStatusOptions,
2204) -> Result<IndexWorktreeDiff> {
2205 let IndexWorktreeDiff {
2206 entries,
2207 staged_gitlinks,
2208 } = diff_name_status_index_worktree_changes(
2209 worktree_root.as_ref(),
2210 git_dir.as_ref(),
2211 format,
2212 None,
2213 )?;
2214 let entries = apply_name_status_options_to_index_worktree_changes(entries, options)?;
2215 Ok(IndexWorktreeDiff {
2216 entries,
2217 staged_gitlinks,
2218 })
2219}
2220
2221pub fn diff_name_status_index_worktree_with_options_and_gitlinks_validated(
2222 worktree_root: impl AsRef<Path>,
2223 git_dir: impl AsRef<Path>,
2224 format: ObjectFormat,
2225 options: DiffNameStatusOptions,
2226 stat_clean_validator: &mut IndexWorktreeStatCleanValidator<'_>,
2227) -> Result<IndexWorktreeDiff> {
2228 let IndexWorktreeDiff {
2229 entries,
2230 staged_gitlinks,
2231 } = diff_name_status_index_worktree_changes(
2232 worktree_root.as_ref(),
2233 git_dir.as_ref(),
2234 format,
2235 Some(stat_clean_validator),
2236 )?;
2237 let entries = apply_name_status_options_to_index_worktree_changes(entries, options)?;
2238 Ok(IndexWorktreeDiff {
2239 entries,
2240 staged_gitlinks,
2241 })
2242}
2243
2244pub fn diff_name_status_index_worktree_with_rename_options(
2248 worktree_root: impl AsRef<Path>,
2249 git_dir: impl AsRef<Path>,
2250 format: ObjectFormat,
2251 options: RenameDetectionOptions,
2252) -> Result<Vec<NameStatusEntry>> {
2253 Ok(
2254 diff_name_status_index_worktree_with_rename_options_and_gitlinks(
2255 worktree_root,
2256 git_dir,
2257 format,
2258 options,
2259 )?
2260 .entries,
2261 )
2262}
2263
2264pub fn diff_name_status_index_worktree_with_rename_options_and_gitlinks(
2265 worktree_root: impl AsRef<Path>,
2266 git_dir: impl AsRef<Path>,
2267 format: ObjectFormat,
2268 options: RenameDetectionOptions,
2269) -> Result<IndexWorktreeDiff> {
2270 let IndexWorktreeDiff {
2271 entries,
2272 staged_gitlinks,
2273 } = diff_name_status_index_worktree_changes(
2274 worktree_root.as_ref(),
2275 git_dir.as_ref(),
2276 format,
2277 None,
2278 )?;
2279 let entries = apply_name_status_options_to_index_worktree_changes(entries, options.base)?;
2283 Ok(IndexWorktreeDiff {
2284 entries,
2285 staged_gitlinks,
2286 })
2287}
2288
2289fn diff_name_status_index_worktree_changes(
2290 worktree_root: &Path,
2291 git_dir: &Path,
2292 format: ObjectFormat,
2293 mut stat_clean_validator: Option<&mut IndexWorktreeStatCleanValidator<'_>>,
2294) -> Result<IndexWorktreeDiff> {
2295 let index_path = sley_index::repository_index_path(git_dir);
2296 let index_metadata = match fs::metadata(&index_path) {
2297 Ok(metadata) => metadata,
2298 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
2299 return Ok(IndexWorktreeDiff {
2300 entries: Vec::new(),
2301 staged_gitlinks: Vec::new(),
2302 });
2303 }
2304 Err(err) => return Err(err.into()),
2305 };
2306 let index_bytes = fs::read(&index_path)?;
2307 if let Ok(index) = BorrowedIndex::parse(&index_bytes, format)
2308 && index.extension(&sley_index::INDEX_EXT_LINK)?.is_none()
2309 && !index.entries.iter().any(borrowed_entry_is_sparse_dir)
2310 {
2311 let (has_non_normal_stage, staged_gitlinks) =
2312 index_worktree_metadata_for_entries(&index.entries);
2313 if has_non_normal_stage {
2314 return diff_name_status_index_worktree_changes_from_snapshot(
2315 worktree_root,
2316 git_dir,
2317 format,
2318 );
2319 }
2320 let stat_cache =
2321 IndexStatCache::from_index_mtime_only(sley_index::file_mtime_parts(&index_metadata));
2322 let entries = diff_name_status_index_worktree_changes_for_borrowed_entries(
2323 worktree_root,
2324 format,
2325 &index.entries,
2326 &stat_cache,
2327 stat_clean_validator.as_deref_mut(),
2328 )?;
2329 return Ok(IndexWorktreeDiff {
2330 entries,
2331 staged_gitlinks,
2332 });
2333 }
2334 let index = expand_sparse_index_for_worktree_diff(
2335 sley_index::read_repository_index(git_dir, format)?,
2336 git_dir,
2337 format,
2338 )?;
2339 let (has_non_normal_stage, staged_gitlinks) =
2340 index_worktree_metadata_for_entries(&index.entries);
2341 if has_non_normal_stage {
2342 return diff_name_status_index_worktree_changes_from_snapshot(
2343 worktree_root,
2344 git_dir,
2345 format,
2346 );
2347 }
2348 let stat_cache =
2349 IndexStatCache::from_index_mtime_only(sley_index::file_mtime_parts(&index_metadata));
2350 let entries = diff_name_status_index_worktree_changes_for_entries(
2351 worktree_root,
2352 format,
2353 &index.entries,
2354 &stat_cache,
2355 stat_clean_validator.as_deref_mut(),
2356 )?;
2357 Ok(IndexWorktreeDiff {
2358 entries,
2359 staged_gitlinks,
2360 })
2361}
2362
2363fn borrowed_entry_is_sparse_dir(entry: &sley_index::IndexEntryRef<'_>) -> bool {
2364 entry.mode == sley_index::SPARSE_DIR_MODE && entry.is_skip_worktree()
2365}
2366
2367fn expand_sparse_index_for_worktree_diff(
2368 mut index: Index,
2369 git_dir: &Path,
2370 format: ObjectFormat,
2371) -> Result<Index> {
2372 if !index
2373 .entries
2374 .iter()
2375 .any(sley_index::IndexEntry::is_sparse_dir)
2376 {
2377 return Ok(index);
2378 }
2379
2380 let db = FileObjectDatabase::from_git_dir(git_dir, format);
2381 let mut expanded = Vec::with_capacity(index.entries.len());
2382 for entry in std::mem::take(&mut index.entries) {
2383 if !entry.is_sparse_dir() {
2384 expanded.push(entry);
2385 continue;
2386 }
2387
2388 let dir_prefix = entry.path.as_bytes();
2389 for (rel_path, (mode, oid)) in flatten_tree(&db, format, &entry.oid)? {
2390 let mut path = dir_prefix.to_vec();
2391 path.extend_from_slice(&rel_path);
2392 let mut expanded_entry = sley_index::IndexEntry {
2393 ctime_seconds: 0,
2394 ctime_nanoseconds: 0,
2395 mtime_seconds: 0,
2396 mtime_nanoseconds: 0,
2397 dev: 0,
2398 ino: 0,
2399 mode,
2400 uid: 0,
2401 gid: 0,
2402 size: 0,
2403 oid,
2404 flags: 0,
2405 flags_extended: 0,
2406 path: BString::from(path),
2407 };
2408 expanded_entry.set_skip_worktree(true);
2409 expanded_entry.refresh_name_length();
2410 expanded.push(expanded_entry);
2411 }
2412 }
2413
2414 expanded.sort_by(|left, right| left.path.as_bytes().cmp(right.path.as_bytes()));
2415 index.entries = expanded;
2416 index.clear_sparse_extension()?;
2417 Ok(index)
2418}
2419
2420fn diff_name_status_index_worktree_changes_for_borrowed_entries(
2421 worktree_root: &Path,
2422 format: ObjectFormat,
2423 entries: &[sley_index::IndexEntryRef<'_>],
2424 stat_cache: &IndexStatCache,
2425 stat_clean_validator: Option<&mut IndexWorktreeStatCleanValidator<'_>>,
2426) -> Result<Vec<NameStatusEntry>> {
2427 const PARALLEL_SCAN_MIN_ENTRIES: usize = 2048;
2428 let workers = std::thread::available_parallelism()
2429 .map(|count| count.get())
2430 .unwrap_or(1)
2431 .min(8);
2432 if stat_clean_validator.is_some() || workers <= 1 || entries.len() < PARALLEL_SCAN_MIN_ENTRIES {
2433 return diff_name_status_index_worktree_changes_for_borrowed_entry_chunk(
2434 worktree_root,
2435 format,
2436 entries,
2437 stat_cache,
2438 stat_clean_validator,
2439 );
2440 }
2441 let chunk_size = entries.len().div_ceil(workers);
2442 std::thread::scope(|scope| {
2443 let mut handles = Vec::new();
2444 for chunk in entries.chunks(chunk_size) {
2445 handles.push(scope.spawn(move || {
2446 diff_name_status_index_worktree_changes_for_borrowed_entry_chunk(
2447 worktree_root,
2448 format,
2449 chunk,
2450 stat_cache,
2451 None,
2452 )
2453 }));
2454 }
2455 let mut changes = Vec::new();
2456 for handle in handles {
2457 let chunk_changes = handle
2458 .join()
2459 .map_err(|_| GitError::Command("diff worker panicked".into()))??;
2460 changes.extend(chunk_changes);
2461 }
2462 Ok(changes)
2463 })
2464}
2465
2466fn diff_name_status_index_worktree_changes_for_entries(
2467 worktree_root: &Path,
2468 format: ObjectFormat,
2469 entries: &[sley_index::IndexEntry],
2470 stat_cache: &IndexStatCache,
2471 stat_clean_validator: Option<&mut IndexWorktreeStatCleanValidator<'_>>,
2472) -> Result<Vec<NameStatusEntry>> {
2473 const PARALLEL_SCAN_MIN_ENTRIES: usize = 2048;
2474 let workers = std::thread::available_parallelism()
2475 .map(|count| count.get())
2476 .unwrap_or(1)
2477 .min(8);
2478 if stat_clean_validator.is_some() || workers <= 1 || entries.len() < PARALLEL_SCAN_MIN_ENTRIES {
2479 return diff_name_status_index_worktree_changes_for_entry_chunk(
2480 worktree_root,
2481 format,
2482 entries,
2483 stat_cache,
2484 stat_clean_validator,
2485 );
2486 }
2487 let chunk_size = entries.len().div_ceil(workers);
2488 std::thread::scope(|scope| {
2489 let mut handles = Vec::new();
2490 for chunk in entries.chunks(chunk_size) {
2491 handles.push(scope.spawn(move || {
2492 diff_name_status_index_worktree_changes_for_entry_chunk(
2493 worktree_root,
2494 format,
2495 chunk,
2496 stat_cache,
2497 None,
2498 )
2499 }));
2500 }
2501 let mut changes = Vec::new();
2502 for handle in handles {
2503 let chunk_changes = handle
2504 .join()
2505 .map_err(|_| GitError::Command("diff worker panicked".into()))??;
2506 changes.extend(chunk_changes);
2507 }
2508 Ok(changes)
2509 })
2510}
2511
2512fn diff_name_status_index_worktree_changes_for_entry_chunk(
2513 worktree_root: &Path,
2514 format: ObjectFormat,
2515 entries: &[sley_index::IndexEntry],
2516 stat_cache: &IndexStatCache,
2517 mut stat_clean_validator: Option<&mut IndexWorktreeStatCleanValidator<'_>>,
2518) -> Result<Vec<NameStatusEntry>> {
2519 let mut changes = Vec::new();
2520 let mut path = PathBuf::from(worktree_root);
2521 for entry in entries {
2522 worktree_path_for_repo_path_into(&mut path, worktree_root, entry.path.as_bytes());
2523 if let Some(change) = index_worktree_change_for_entry(
2524 &path,
2525 format,
2526 entry,
2527 stat_cache,
2528 &mut stat_clean_validator,
2529 )? {
2530 changes.push(change);
2531 }
2532 }
2533 Ok(changes)
2534}
2535
2536fn diff_name_status_index_worktree_changes_for_borrowed_entry_chunk(
2537 worktree_root: &Path,
2538 format: ObjectFormat,
2539 entries: &[sley_index::IndexEntryRef<'_>],
2540 stat_cache: &IndexStatCache,
2541 mut stat_clean_validator: Option<&mut IndexWorktreeStatCleanValidator<'_>>,
2542) -> Result<Vec<NameStatusEntry>> {
2543 let mut changes = Vec::new();
2544 let mut path = PathBuf::from(worktree_root);
2545 for entry in entries {
2546 worktree_path_for_repo_path_into(&mut path, worktree_root, entry.path);
2547 if let Some(change) = index_worktree_change_for_entry(
2548 &path,
2549 format,
2550 entry,
2551 stat_cache,
2552 &mut stat_clean_validator,
2553 )? {
2554 changes.push(change);
2555 }
2556 }
2557 Ok(changes)
2558}
2559
2560fn index_worktree_metadata_for_entries(
2561 entries: &[impl WorktreeIndexEntry],
2562) -> (bool, Vec<IndexGitlinkEntry>) {
2563 let mut needs_snapshot = false;
2564 let mut staged_gitlinks = Vec::new();
2565 for entry in entries {
2566 if entry.stage() != sley_index::Stage::Normal {
2567 needs_snapshot = true;
2568 }
2569 if entry.is_intent_to_add() {
2572 needs_snapshot = true;
2573 }
2574 if sley_index::is_gitlink(entry.mode()) {
2575 staged_gitlinks.push(IndexGitlinkEntry {
2576 path: BString::from_bytes(entry.git_path()),
2577 oid: entry.oid(),
2578 });
2579 }
2580 }
2581 (needs_snapshot, staged_gitlinks)
2582}
2583
2584fn diff_name_status_index_worktree_changes_from_snapshot(
2585 worktree_root: &Path,
2586 git_dir: &Path,
2587 format: ObjectFormat,
2588) -> Result<IndexWorktreeDiff> {
2589 let IndexSnapshot {
2590 entries: index,
2591 stat_cache,
2592 ..
2593 } = read_index_snapshot(git_dir, format)?;
2594 let intent_to_add_paths = read_intent_to_add_paths(git_dir, format)?;
2599 let unmerged = read_unmerged_stages(git_dir, format)?;
2606 let index_gitlinks = index_gitlinks(&index);
2607 let staged_gitlinks = index_gitlinks
2608 .iter()
2609 .map(|(path, oid)| IndexGitlinkEntry {
2610 path: BString::from_bytes(path),
2611 oid: *oid,
2612 })
2613 .collect();
2614 let mut changes = Vec::new();
2615 for (git_path, left) in &index {
2616 let conflict_stages = unmerged.get(git_path);
2620 let right = worktree_entry_for_path(
2621 worktree_root,
2622 format,
2623 git_path,
2624 &index_gitlinks,
2625 Some(&stat_cache),
2626 None,
2627 None,
2628 )?;
2629 if conflict_stages.is_some() {
2630 changes.push(NameStatusEntry {
2634 status: NameStatus::Unmerged,
2635 path: git_path.clone().into(),
2636 old_path: None,
2637 old_mode: None,
2638 new_mode: right.as_ref().map(|entry| entry.mode),
2639 old_oid: None,
2640 new_oid: None,
2641 });
2642 }
2643 let left = match conflict_stages {
2648 Some(stages) => match stages.ours.as_ref() {
2649 Some(ours) => ours,
2650 None => continue,
2651 },
2652 None => left,
2653 };
2654 if intent_to_add_paths.contains(git_path.as_slice()) {
2659 if let Some(right) = right {
2660 changes.push(NameStatusEntry {
2661 status: NameStatus::Added,
2662 path: git_path.clone().into(),
2663 old_path: None,
2664 old_mode: None,
2665 new_mode: Some(right.mode),
2666 old_oid: None,
2667 new_oid: Some(right.oid),
2668 });
2669 }
2670 continue;
2671 }
2672 let Some(right) = right else {
2673 changes.push(NameStatusEntry {
2674 status: NameStatus::Deleted,
2675 path: git_path.clone().into(),
2676 old_path: None,
2677 old_mode: Some(left.mode),
2678 new_mode: None,
2679 old_oid: Some(left.oid),
2680 new_oid: None,
2681 });
2682 continue;
2683 };
2684 if right != *left {
2685 changes.push(NameStatusEntry {
2686 status: modify_or_type_change(left.mode, right.mode),
2687 path: git_path.clone().into(),
2688 old_path: None,
2689 old_mode: Some(left.mode),
2690 new_mode: Some(right.mode),
2691 old_oid: Some(left.oid),
2692 new_oid: Some(right.oid),
2693 });
2694 }
2695 }
2696 Ok(IndexWorktreeDiff {
2697 entries: changes,
2698 staged_gitlinks,
2699 })
2700}
2701
2702struct ConflictStages {
2704 ours: Option<TrackedEntry>,
2705}
2706
2707fn read_unmerged_stages(
2711 git_dir: &Path,
2712 format: ObjectFormat,
2713) -> Result<BTreeMap<Vec<u8>, ConflictStages>> {
2714 let index_path = sley_index::repository_index_path(git_dir);
2715 if !index_path.exists() {
2716 return Ok(BTreeMap::new());
2717 }
2718 let index = sley_index::read_repository_index(git_dir, format)?;
2719 let mut out: BTreeMap<Vec<u8>, ConflictStages> = BTreeMap::new();
2720 for entry in &index.entries {
2721 let stage = entry.stage();
2722 if stage == sley_index::Stage::Normal {
2723 continue;
2724 }
2725 let path = entry.path.clone().into_bytes();
2726 let slot = out.entry(path).or_insert(ConflictStages { ours: None });
2727 if stage == sley_index::Stage::Ours {
2728 slot.ours = Some(TrackedEntry {
2729 mode: entry.mode,
2730 oid: entry.oid,
2731 });
2732 }
2733 }
2734 Ok(out)
2735}
2736
2737fn apply_name_status_options_to_index_worktree_changes(
2738 mut changes: Vec<NameStatusEntry>,
2739 options: DiffNameStatusOptions,
2740) -> Result<Vec<NameStatusEntry>> {
2741 if options.detect_renames {
2742 changes = detect_exact_renames_from_changes(changes, options.rename_empty);
2743 } else if options.detect_copies {
2744 changes.sort_by(|left, right| diff_entry_sort_path(left).cmp(diff_entry_sort_path(right)));
2745 }
2746 Ok(changes)
2747}
2748
2749fn detect_exact_renames_from_changes(
2750 changes: Vec<NameStatusEntry>,
2751 rename_empty: bool,
2752) -> Vec<NameStatusEntry> {
2753 let added = changes
2754 .iter()
2755 .enumerate()
2756 .filter(|(_, entry)| entry.status == NameStatus::Added)
2757 .map(|(idx, entry)| (idx, entry.path.clone()))
2758 .collect::<Vec<_>>();
2759 let mut sources = changes
2760 .iter()
2761 .enumerate()
2762 .filter(|(_, entry)| entry.status == NameStatus::Deleted)
2763 .filter_map(|(idx, entry)| {
2764 entry
2765 .old_oid
2766 .map(|oid| (idx, entry.path.clone(), oid, entry.old_mode))
2767 })
2768 .collect::<Vec<_>>();
2769 sources.sort_by(|left, right| left.1.cmp(&right.1).then_with(|| left.0.cmp(&right.0)));
2770
2771 let mut src_used = vec![false; sources.len()];
2772 let mut consumed_added = BTreeSet::new();
2773 let mut consumed_deleted = BTreeSet::new();
2774 let mut result = Vec::new();
2775
2776 for (idx, new_path) in &added {
2777 let Some(right_oid) = changes[*idx].new_oid else {
2778 continue;
2779 };
2780 if !rename_empty && is_empty_blob_oid(&right_oid) {
2781 continue;
2782 };
2783 let mut best: Option<usize> = None;
2784 let mut best_score = -1i32;
2785 for (source_idx, (_, src_path, src_oid, _)) in sources.iter().enumerate() {
2786 if src_used[source_idx] || *src_oid != right_oid {
2787 continue;
2788 }
2789 let score = 1 + i32::from(path_basename(src_path) == path_basename(new_path));
2790 if score > best_score {
2791 best = Some(source_idx);
2792 best_score = score;
2793 if score == 2 {
2794 break;
2795 }
2796 }
2797 }
2798 if let Some(source_idx) = best {
2799 src_used[source_idx] = true;
2800 consumed_added.insert(*idx);
2801 let (old_idx, old_path, old_oid, old_mode) = &sources[source_idx];
2802 consumed_deleted.insert(*old_idx);
2803 if old_path.as_bytes() == new_path.as_bytes()
2804 && *old_mode == changes[*idx].new_mode
2805 && *old_oid == right_oid
2806 {
2807 continue;
2808 }
2809 result.push(NameStatusEntry {
2810 status: NameStatus::Renamed(100),
2811 path: new_path.clone(),
2812 old_path: Some(old_path.clone()),
2813 old_mode: *old_mode,
2814 new_mode: changes[*idx].new_mode,
2815 old_oid: Some(*old_oid),
2816 new_oid: Some(right_oid),
2817 });
2818 }
2819 }
2820
2821 for (index, entry) in changes.into_iter().enumerate() {
2822 if consumed_added.contains(&index) || consumed_deleted.contains(&index) {
2823 continue;
2824 }
2825 result.push(entry);
2826 }
2827 result.sort_by(|left, right| diff_entry_sort_path(left).cmp(diff_entry_sort_path(right)));
2828 result
2829}
2830
2831pub fn diff_name_status_index_worktree_for_diff_files_with_options(
2855 worktree_root: impl AsRef<Path>,
2856 git_dir: impl AsRef<Path>,
2857 format: ObjectFormat,
2858 options: DiffNameStatusOptions,
2859) -> Result<Vec<NameStatusEntry>> {
2860 let worktree_root = worktree_root.as_ref();
2861 let git_dir = git_dir.as_ref();
2862 let changes =
2863 diff_name_status_index_worktree_with_options(worktree_root, git_dir, format, options)?;
2864 augment_with_stat_dirty_entries(worktree_root, git_dir, format, changes)
2865}
2866
2867pub fn diff_name_status_index_worktree_for_diff_files_with_rename_options(
2871 worktree_root: impl AsRef<Path>,
2872 git_dir: impl AsRef<Path>,
2873 format: ObjectFormat,
2874 options: RenameDetectionOptions,
2875) -> Result<Vec<NameStatusEntry>> {
2876 let worktree_root = worktree_root.as_ref();
2877 let git_dir = git_dir.as_ref();
2878 let changes = diff_name_status_index_worktree_with_rename_options(
2879 worktree_root,
2880 git_dir,
2881 format,
2882 options,
2883 )?;
2884 augment_with_stat_dirty_entries(worktree_root, git_dir, format, changes)
2885}
2886
2887fn augment_with_stat_dirty_entries(
2894 worktree_root: &Path,
2895 git_dir: &Path,
2896 format: ObjectFormat,
2897 mut content_changes: Vec<NameStatusEntry>,
2898) -> Result<Vec<NameStatusEntry>> {
2899 let IndexSnapshot {
2900 entries: index,
2901 stat_cache,
2902 ..
2903 } = read_index_snapshot(git_dir, format)?;
2904 let already_reported: BTreeSet<&[u8]> = content_changes
2907 .iter()
2908 .map(|entry| entry.path.as_bytes())
2909 .collect();
2910 let mut extras = Vec::new();
2911 for (git_path, tracked) in &index {
2912 if already_reported.contains(git_path.as_slice()) {
2913 continue;
2914 }
2915 let Some(cached) = stat_cache.entry_for_git_path(git_path) else {
2916 continue;
2917 };
2918 if sley_index::is_gitlink(tracked.mode) {
2921 continue;
2922 }
2923 let path = worktree_path_for_repo_path(worktree_root, git_path);
2924 let Ok(metadata) = fs::symlink_metadata(&path) else {
2925 continue;
2928 };
2929 if !(metadata.is_file() || metadata.file_type().is_symlink()) {
2930 continue;
2931 }
2932 match stat_cache.index_entry_worktree_stat_verdict(cached, &metadata) {
2933 sley_index::StatVerdict::Clean => continue,
2934 sley_index::StatVerdict::Dirty => {}
2935 sley_index::StatVerdict::RacyNeedsContentCheck => {
2940 if worktree_oid_matches_index(worktree_root, git_path, &metadata, tracked, format)?
2941 {
2942 continue;
2943 }
2944 }
2945 }
2946 extras.push(NameStatusEntry {
2947 status: NameStatus::Modified,
2948 path: git_path.clone().into(),
2949 old_path: None,
2950 old_mode: Some(tracked.mode),
2951 new_mode: Some(tracked.mode),
2952 old_oid: Some(tracked.oid),
2953 new_oid: None,
2954 });
2955 }
2956 if !extras.is_empty() {
2957 content_changes.extend(extras);
2958 content_changes
2959 .sort_by(|left, right| diff_entry_sort_path(left).cmp(diff_entry_sort_path(right)));
2960 }
2961 Ok(content_changes)
2962}
2963
2964fn classify_worktree_entry(
2974 path: &Path,
2975 metadata: &fs::Metadata,
2976 format: ObjectFormat,
2977) -> Result<TrackedEntry> {
2978 let is_symlink = metadata.file_type().is_symlink();
2979 let body = if is_symlink {
2980 symlink_target_bytes(path)?
2981 } else {
2982 fs::read(path)?
2983 };
2984 let oid = EncodedObject::new(ObjectType::Blob, body).object_id(format)?;
2985 let mode = if is_symlink {
2986 sley_index::SYMLINK_MODE
2987 } else {
2988 file_mode(metadata)
2989 };
2990 Ok(TrackedEntry { mode, oid })
2991}
2992
2993fn worktree_oid_matches_index(
2999 worktree_root: &Path,
3000 git_path: &[u8],
3001 metadata: &fs::Metadata,
3002 index_entry: &TrackedEntry,
3003 format: ObjectFormat,
3004) -> Result<bool> {
3005 let path = worktree_path_for_repo_path(worktree_root, git_path);
3006 let entry = classify_worktree_entry(&path, metadata, format)?;
3007 Ok(entry.oid == index_entry.oid && entry.mode == index_entry.mode)
3008}
3009
3010pub fn diff_name_status_trees_with_options(
3011 db: &FileObjectDatabase,
3012 format: ObjectFormat,
3013 left_tree: &ObjectId,
3014 right_tree: &ObjectId,
3015 options: DiffNameStatusOptions,
3016) -> Result<Vec<NameStatusEntry>> {
3017 if !options.detect_copies {
3018 let mut changes = changed_tree_name_status_entries(db, format, left_tree, right_tree)?;
3019 if options.detect_renames {
3020 changes = detect_exact_renames_from_changes(changes, options.rename_empty);
3021 }
3022 return Ok(changes);
3023 }
3024
3025 let needs_full_maps = options.detect_copies && options.find_copies_harder;
3030 let (left_entries, right_entries) = if needs_full_maps {
3031 collect_full_tree_pair(db, format, left_tree, right_tree)?
3032 } else {
3033 changed_tree_entries(db, format, left_tree, right_tree)?
3034 };
3035 diff_name_status_maps(
3036 &left_entries,
3037 &right_entries,
3038 left_entries.keys().chain(right_entries.keys()),
3039 options,
3040 )
3041}
3042
3043pub fn diff_name_status_empty_tree_with_options(
3044 db: &FileObjectDatabase,
3045 format: ObjectFormat,
3046 right_tree: &ObjectId,
3047 options: DiffNameStatusOptions,
3048) -> Result<Vec<NameStatusEntry>> {
3049 let left_entries = BTreeMap::new();
3050 let mut right_entries = BTreeMap::new();
3051 collect_tree_entries(db, format, right_tree, Vec::new(), &mut right_entries)?;
3052 diff_name_status_maps(&left_entries, &right_entries, right_entries.keys(), options)
3053}
3054
3055pub fn diff_name_status_trees_with_rename_options(
3063 db: &FileObjectDatabase,
3064 format: ObjectFormat,
3065 left_tree: &ObjectId,
3066 right_tree: &ObjectId,
3067 options: RenameDetectionOptions,
3068) -> Result<Vec<NameStatusEntry>> {
3069 Ok(diff_name_status_trees_with_rename_options_and_diagnostics(
3070 db, format, left_tree, right_tree, options,
3071 )?
3072 .entries)
3073}
3074
3075pub fn diff_name_status_trees_with_rename_options_and_diagnostics(
3076 db: &FileObjectDatabase,
3077 format: ObjectFormat,
3078 left_tree: &ObjectId,
3079 right_tree: &ObjectId,
3080 options: RenameDetectionOptions,
3081) -> Result<NameStatusWithRenameDiagnostics> {
3082 let base = options.base;
3083 if !base.detect_copies {
3084 let mut diagnostics = RenameLimitDiagnostics::default();
3085 let mut changes = changed_tree_name_status_entries(db, format, left_tree, right_tree)?;
3086 if base.detect_renames {
3087 changes = detect_exact_renames_from_changes(changes, base.rename_empty);
3088 }
3089 if base.detect_renames && options.detect_inexact {
3090 diagnostics.inexact_renames_skipped = inexact_rename_limit_exceeded(&changes, &options);
3091 changes = detect_inexact_renames(changes, &options, &|oid| read_blob_bytes(db, oid));
3092 }
3093 return Ok(NameStatusWithRenameDiagnostics {
3094 entries: changes,
3095 rename_limit: diagnostics,
3096 });
3097 }
3098
3099 let needs_full_maps = options.base.detect_copies && options.base.find_copies_harder;
3103 let (left_entries, right_entries) = if needs_full_maps {
3104 collect_full_tree_pair(db, format, left_tree, right_tree)?
3105 } else {
3106 changed_tree_entries(db, format, left_tree, right_tree)?
3107 };
3108 diff_name_status_maps_with_renames_and_diagnostics(
3109 &left_entries,
3110 &right_entries,
3111 left_entries.keys().chain(right_entries.keys()),
3112 options,
3113 |oid| read_blob_bytes(db, oid),
3114 )
3115}
3116
3117pub fn diff_name_status_empty_tree_with_rename_options(
3124 db: &FileObjectDatabase,
3125 format: ObjectFormat,
3126 right_tree: &ObjectId,
3127 options: RenameDetectionOptions,
3128) -> Result<Vec<NameStatusEntry>> {
3129 let left_entries = BTreeMap::new();
3130 let mut right_entries = BTreeMap::new();
3131 collect_tree_entries(db, format, right_tree, Vec::new(), &mut right_entries)?;
3132 diff_name_status_maps_with_renames(
3133 &left_entries,
3134 &right_entries,
3135 right_entries.keys(),
3136 options,
3137 |oid| read_blob_bytes(db, oid),
3138 )
3139}
3140
3141fn read_blob_bytes(db: &FileObjectDatabase, oid: &ObjectId) -> Option<Vec<u8>> {
3146 match db.read_object(oid) {
3147 Ok(object) if object.object_type == ObjectType::Blob => Some(object.body.clone()),
3148 _ => None,
3149 }
3150}
3151
3152fn raw_name_status_changes_for_unique_paths<'a>(
3155 left_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
3156 right_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
3157 paths: impl Iterator<Item = &'a Vec<u8>>,
3158) -> Vec<NameStatusEntry> {
3159 let mut changes = Vec::new();
3160 for path in paths {
3161 let left = left_entries.get(path);
3162 let right = right_entries.get(path);
3163 let status = match (left, right) {
3164 (None, Some(_)) => Some(NameStatus::Added),
3165 (Some(_), None) => Some(NameStatus::Deleted),
3166 (Some(left), Some(right)) if left != right => {
3167 Some(modify_or_type_change(left.mode, right.mode))
3168 }
3169 _ => None,
3170 };
3171 if let Some(status) = status {
3172 changes.push(NameStatusEntry {
3173 status,
3174 path: path.clone().into(),
3175 old_path: None,
3176 old_mode: left.map(|entry| entry.mode),
3177 new_mode: right.map(|entry| entry.mode),
3178 old_oid: left.map(|entry| entry.oid),
3179 new_oid: right.map(|entry| entry.oid),
3180 });
3181 }
3182 }
3183 changes
3184}
3185
3186fn diff_name_status_maps<'a>(
3187 left_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
3188 right_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
3189 candidate_paths: impl Iterator<Item = &'a Vec<u8>>,
3190 options: DiffNameStatusOptions,
3191) -> Result<Vec<NameStatusEntry>> {
3192 let paths = candidate_path_set(candidate_paths);
3193 diff_name_status_maps_for_path_set(left_entries, right_entries, &paths, options)
3194}
3195
3196fn diff_name_status_maps_for_path_set(
3197 left_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
3198 right_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
3199 candidate_paths: &BTreeSet<Vec<u8>>,
3200 options: DiffNameStatusOptions,
3201) -> Result<Vec<NameStatusEntry>> {
3202 diff_name_status_maps_for_unique_paths(
3203 left_entries,
3204 right_entries,
3205 candidate_paths.iter(),
3206 options,
3207 )
3208}
3209
3210fn diff_name_status_maps_for_unique_paths<'a>(
3211 left_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
3212 right_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
3213 candidate_paths: impl Iterator<Item = &'a Vec<u8>>,
3214 options: DiffNameStatusOptions,
3215) -> Result<Vec<NameStatusEntry>> {
3216 let mut changes =
3217 raw_name_status_changes_for_unique_paths(left_entries, right_entries, candidate_paths);
3218 if options.detect_renames {
3219 changes = detect_exact_renames(changes, left_entries, right_entries, options.rename_empty);
3220 }
3221 if options.detect_copies {
3222 changes = detect_exact_copies(
3223 changes,
3224 left_entries,
3225 right_entries,
3226 options.find_copies_harder,
3227 options.rename_empty,
3228 );
3229 }
3230 Ok(changes)
3231}
3232
3233fn diff_name_status_maps_with_renames<'a>(
3242 left_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
3243 right_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
3244 candidate_paths: impl Iterator<Item = &'a Vec<u8>>,
3245 options: RenameDetectionOptions,
3246 fetch_blob: impl Fn(&ObjectId) -> Option<Vec<u8>>,
3247) -> Result<Vec<NameStatusEntry>> {
3248 Ok(diff_name_status_maps_with_renames_and_diagnostics(
3249 left_entries,
3250 right_entries,
3251 candidate_paths,
3252 options,
3253 fetch_blob,
3254 )?
3255 .entries)
3256}
3257
3258fn diff_name_status_maps_with_renames_and_diagnostics<'a>(
3259 left_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
3260 right_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
3261 candidate_paths: impl Iterator<Item = &'a Vec<u8>>,
3262 options: RenameDetectionOptions,
3263 fetch_blob: impl Fn(&ObjectId) -> Option<Vec<u8>>,
3264) -> Result<NameStatusWithRenameDiagnostics> {
3265 let paths = candidate_path_set(candidate_paths);
3266 diff_name_status_maps_with_renames_for_path_set_and_diagnostics(
3267 left_entries,
3268 right_entries,
3269 &paths,
3270 options,
3271 fetch_blob,
3272 )
3273}
3274
3275fn diff_name_status_maps_with_renames_for_path_set(
3276 left_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
3277 right_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
3278 candidate_paths: &BTreeSet<Vec<u8>>,
3279 options: RenameDetectionOptions,
3280 fetch_blob: impl Fn(&ObjectId) -> Option<Vec<u8>>,
3281) -> Result<Vec<NameStatusEntry>> {
3282 Ok(
3283 diff_name_status_maps_with_renames_for_path_set_and_diagnostics(
3284 left_entries,
3285 right_entries,
3286 candidate_paths,
3287 options,
3288 fetch_blob,
3289 )?
3290 .entries,
3291 )
3292}
3293
3294fn diff_name_status_maps_with_renames_for_path_set_and_diagnostics(
3295 left_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
3296 right_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
3297 candidate_paths: &BTreeSet<Vec<u8>>,
3298 options: RenameDetectionOptions,
3299 fetch_blob: impl Fn(&ObjectId) -> Option<Vec<u8>>,
3300) -> Result<NameStatusWithRenameDiagnostics> {
3301 diff_name_status_maps_with_renames_for_unique_paths_and_diagnostics(
3302 left_entries,
3303 right_entries,
3304 candidate_paths.iter(),
3305 options,
3306 fetch_blob,
3307 )
3308}
3309
3310fn diff_name_status_maps_with_renames_for_unique_paths_and_diagnostics<'a>(
3311 left_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
3312 right_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
3313 candidate_paths: impl Iterator<Item = &'a Vec<u8>>,
3314 options: RenameDetectionOptions,
3315 fetch_blob: impl Fn(&ObjectId) -> Option<Vec<u8>>,
3316) -> Result<NameStatusWithRenameDiagnostics> {
3317 let base = options.base;
3318 let mut diagnostics = RenameLimitDiagnostics::default();
3319 let mut changes =
3320 raw_name_status_changes_for_unique_paths(left_entries, right_entries, candidate_paths);
3321 if base.detect_renames {
3322 changes = detect_exact_renames(changes, left_entries, right_entries, base.rename_empty);
3323 }
3324 if base.detect_renames && options.detect_inexact {
3328 diagnostics.inexact_renames_skipped = inexact_rename_limit_exceeded(&changes, &options);
3329 changes = detect_inexact_renames(changes, &options, &fetch_blob);
3330 }
3331 if base.detect_copies {
3332 changes = detect_exact_copies(
3333 changes,
3334 left_entries,
3335 right_entries,
3336 base.find_copies_harder,
3337 base.rename_empty,
3338 );
3339 }
3340 if base.detect_copies && options.detect_inexact {
3341 match inexact_copy_limit_outcome(&changes, left_entries, &options) {
3342 InexactCopyLimitOutcome::Full => {}
3343 InexactCopyLimitOutcome::DegradedToChangedSources => {
3344 diagnostics.inexact_copies_degraded = true;
3345 }
3346 InexactCopyLimitOutcome::Skip => {
3347 diagnostics.inexact_copies_skipped = true;
3348 }
3349 }
3350 changes = detect_inexact_copies(changes, left_entries, &options, &fetch_blob);
3351 }
3352 Ok(NameStatusWithRenameDiagnostics {
3353 entries: changes,
3354 rename_limit: diagnostics,
3355 })
3356}
3357
3358fn changed_tree_name_status_entries(
3359 db: &FileObjectDatabase,
3360 format: ObjectFormat,
3361 left_tree: &ObjectId,
3362 right_tree: &ObjectId,
3363) -> Result<Vec<NameStatusEntry>> {
3364 let mut changes = Vec::new();
3365 if left_tree != right_tree {
3366 diff_tree_pair_entries(db, format, left_tree, right_tree, &[], &mut changes)?;
3367 }
3368 sort_name_status_entries_by_path(&mut changes);
3369 Ok(changes)
3370}
3371
3372fn diff_tree_pair_entries(
3373 db: &FileObjectDatabase,
3374 format: ObjectFormat,
3375 left_tree: &ObjectId,
3376 right_tree: &ObjectId,
3377 prefix: &[u8],
3378 changes: &mut Vec<NameStatusEntry>,
3379) -> Result<()> {
3380 let left_entries = read_tree_object(db, format, left_tree)?.entries;
3381 let right_entries = read_tree_object(db, format, right_tree)?.entries;
3382 let mut left_idx = 0usize;
3383 let mut right_idx = 0usize;
3384
3385 while left_idx < left_entries.len() || right_idx < right_entries.len() {
3386 match (left_entries.get(left_idx), right_entries.get(right_idx)) {
3387 (Some(left_entry), Some(right_entry)) => match left_entry.name.cmp(&right_entry.name) {
3388 std::cmp::Ordering::Equal => {
3389 let left_name = left_entry.name.clone();
3390 let right_name = right_entry.name.clone();
3391 let left_start = left_idx;
3392 let right_start = right_idx;
3393 while left_idx < left_entries.len() && left_entries[left_idx].name == left_name
3394 {
3395 left_idx += 1;
3396 }
3397 while right_idx < right_entries.len()
3398 && right_entries[right_idx].name == right_name
3399 {
3400 right_idx += 1;
3401 }
3402
3403 let left_group = &left_entries[left_start..left_idx];
3404 let right_group = &right_entries[right_start..right_idx];
3405 let paired = left_group.len().min(right_group.len());
3406 for idx in 0..paired {
3407 diff_tree_entry_pair_entries(
3408 db,
3409 format,
3410 prefix,
3411 Some(&left_group[idx]),
3412 Some(&right_group[idx]),
3413 changes,
3414 )?;
3415 }
3416 for entry in &left_group[paired..] {
3417 diff_tree_entry_pair_entries(
3418 db,
3419 format,
3420 prefix,
3421 Some(entry),
3422 None,
3423 changes,
3424 )?;
3425 }
3426 for entry in &right_group[paired..] {
3427 diff_tree_entry_pair_entries(
3428 db,
3429 format,
3430 prefix,
3431 None,
3432 Some(entry),
3433 changes,
3434 )?;
3435 }
3436 }
3437 std::cmp::Ordering::Less => {
3438 let left_name = left_entry.name.clone();
3439 while left_idx < left_entries.len() && left_entries[left_idx].name == left_name
3440 {
3441 diff_tree_entry_pair_entries(
3442 db,
3443 format,
3444 prefix,
3445 Some(&left_entries[left_idx]),
3446 None,
3447 changes,
3448 )?;
3449 left_idx += 1;
3450 }
3451 }
3452 std::cmp::Ordering::Greater => {
3453 let right_name = right_entry.name.clone();
3454 while right_idx < right_entries.len()
3455 && right_entries[right_idx].name == right_name
3456 {
3457 diff_tree_entry_pair_entries(
3458 db,
3459 format,
3460 prefix,
3461 None,
3462 Some(&right_entries[right_idx]),
3463 changes,
3464 )?;
3465 right_idx += 1;
3466 }
3467 }
3468 },
3469 (Some(left_entry), None) => {
3470 let left_name = left_entry.name.clone();
3471 while left_idx < left_entries.len() && left_entries[left_idx].name == left_name {
3472 diff_tree_entry_pair_entries(
3473 db,
3474 format,
3475 prefix,
3476 Some(&left_entries[left_idx]),
3477 None,
3478 changes,
3479 )?;
3480 left_idx += 1;
3481 }
3482 }
3483 (None, Some(right_entry)) => {
3484 let right_name = right_entry.name.clone();
3485 while right_idx < right_entries.len() && right_entries[right_idx].name == right_name
3486 {
3487 diff_tree_entry_pair_entries(
3488 db,
3489 format,
3490 prefix,
3491 None,
3492 Some(&right_entries[right_idx]),
3493 changes,
3494 )?;
3495 right_idx += 1;
3496 }
3497 }
3498 (None, None) => break,
3499 }
3500 }
3501
3502 Ok(())
3503}
3504
3505fn diff_tree_entry_pair_entries(
3506 db: &FileObjectDatabase,
3507 format: ObjectFormat,
3508 prefix: &[u8],
3509 left_entry: Option<&TreeEntry>,
3510 right_entry: Option<&TreeEntry>,
3511 changes: &mut Vec<NameStatusEntry>,
3512) -> Result<()> {
3513 let left_is_tree = left_entry.is_some_and(|entry| entry.mode == TREE_ENTRY_MODE);
3514 let right_is_tree = right_entry.is_some_and(|entry| entry.mode == TREE_ENTRY_MODE);
3515
3516 if let (Some(left_entry), Some(right_entry)) = (left_entry, right_entry) {
3517 if left_is_tree && right_is_tree {
3518 if left_entry.oid == right_entry.oid {
3519 return Ok(());
3520 }
3521 let path = join_tree_path(prefix, left_entry.name.as_bytes());
3522 return diff_tree_pair_entries(
3523 db,
3524 format,
3525 &left_entry.oid,
3526 &right_entry.oid,
3527 &path,
3528 changes,
3529 );
3530 }
3531 if !left_is_tree && !right_is_tree {
3532 if left_entry.mode == right_entry.mode && left_entry.oid == right_entry.oid {
3533 return Ok(());
3534 }
3535 let path = join_tree_path(prefix, left_entry.name.as_bytes());
3536 changes.push(NameStatusEntry {
3537 status: modify_or_type_change(left_entry.mode, right_entry.mode),
3538 path: path.into(),
3539 old_path: None,
3540 old_mode: Some(left_entry.mode),
3541 new_mode: Some(right_entry.mode),
3542 old_oid: Some(left_entry.oid),
3543 new_oid: Some(right_entry.oid),
3544 });
3545 return Ok(());
3546 }
3547 }
3548
3549 if let Some(left_entry) = left_entry {
3550 let path = join_tree_path(prefix, left_entry.name.as_bytes());
3551 collect_tree_side_changes(
3552 db,
3553 format,
3554 path,
3555 left_entry.mode,
3556 left_entry.oid,
3557 NameStatus::Deleted,
3558 changes,
3559 )?;
3560 }
3561 if let Some(right_entry) = right_entry {
3562 let path = join_tree_path(prefix, right_entry.name.as_bytes());
3563 collect_tree_side_changes(
3564 db,
3565 format,
3566 path,
3567 right_entry.mode,
3568 right_entry.oid,
3569 NameStatus::Added,
3570 changes,
3571 )?;
3572 }
3573 Ok(())
3574}
3575
3576fn collect_tree_side_changes(
3577 db: &FileObjectDatabase,
3578 format: ObjectFormat,
3579 path: Vec<u8>,
3580 mode: u32,
3581 oid: ObjectId,
3582 status: NameStatus,
3583 changes: &mut Vec<NameStatusEntry>,
3584) -> Result<()> {
3585 if mode == TREE_ENTRY_MODE {
3586 for entry in read_tree_object(db, format, &oid)?.entries {
3587 let child_path = join_tree_path(&path, entry.name.as_bytes());
3588 collect_tree_side_changes(
3589 db, format, child_path, entry.mode, entry.oid, status, changes,
3590 )?;
3591 }
3592 return Ok(());
3593 }
3594
3595 changes.push(NameStatusEntry {
3596 status,
3597 path: path.into(),
3598 old_path: None,
3599 old_mode: (status == NameStatus::Deleted).then_some(mode),
3600 new_mode: (status == NameStatus::Added).then_some(mode),
3601 old_oid: (status == NameStatus::Deleted).then_some(oid),
3602 new_oid: (status == NameStatus::Added).then_some(oid),
3603 });
3604 Ok(())
3605}
3606
3607fn sort_name_status_entries_by_path(changes: &mut [NameStatusEntry]) {
3608 changes.sort_by(|left, right| diff_entry_sort_path(left).cmp(diff_entry_sort_path(right)));
3609}
3610
3611fn detect_exact_renames(
3612 changes: Vec<NameStatusEntry>,
3613 left_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
3614 right_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
3615 rename_empty: bool,
3616) -> Vec<NameStatusEntry> {
3617 let added = changes
3618 .iter()
3619 .enumerate()
3620 .filter(|(_, entry)| entry.status == NameStatus::Added)
3621 .map(|(idx, entry)| (idx, entry.path.clone()))
3622 .collect::<Vec<_>>();
3623 let mut sources = changes
3626 .iter()
3627 .filter(|entry| entry.status == NameStatus::Deleted)
3628 .filter_map(|entry| {
3629 left_entries
3630 .get(entry.path.as_bytes())
3631 .map(|left| (entry.path.clone(), left.oid))
3632 })
3633 .collect::<Vec<_>>();
3634 sources.sort_by(|a, b| a.0.cmp(&b.0));
3635 let mut src_used = vec![false; sources.len()];
3636 let mut consumed = BTreeSet::new();
3637 let mut renamed_old_paths = BTreeSet::new();
3638 let mut result = Vec::new();
3639
3640 for (idx, new_path) in &added {
3646 let Some(right) = right_entries.get(new_path.as_bytes()) else {
3647 continue;
3648 };
3649 if !rename_empty && is_empty_blob_oid(&right.oid) {
3650 continue;
3651 }
3652 let mut best: Option<usize> = None;
3653 let mut best_score = -1i32;
3654 for (si, (src_path, src_oid)) in sources.iter().enumerate() {
3655 if src_used[si] || *src_oid != right.oid {
3656 continue;
3657 }
3658 let score = 1 + i32::from(path_basename(src_path) == path_basename(new_path));
3659 if score > best_score {
3660 best = Some(si);
3661 best_score = score;
3662 if score == 2 {
3663 break;
3664 }
3665 }
3666 }
3667 if let Some(si) = best {
3668 src_used[si] = true;
3669 consumed.insert(*idx);
3670 let old_path = sources[si].0.clone();
3671 let left = &left_entries[old_path.as_bytes()];
3672 renamed_old_paths.insert(old_path.clone());
3673 result.push(NameStatusEntry {
3674 status: NameStatus::Renamed(100),
3675 path: new_path.clone(),
3676 old_path: Some(old_path),
3677 old_mode: Some(left.mode),
3678 new_mode: Some(right.mode),
3679 old_oid: Some(left.oid),
3680 new_oid: Some(right.oid),
3681 });
3682 }
3683 }
3684
3685 for (idx, entry) in changes.into_iter().enumerate() {
3686 if entry.status == NameStatus::Added && consumed.contains(&idx) {
3687 continue;
3688 }
3689 if entry.status == NameStatus::Deleted && renamed_old_paths.contains(&entry.path) {
3690 continue;
3691 }
3692 result.push(entry);
3693 }
3694 result.sort_by(|left, right| diff_entry_sort_path(left).cmp(diff_entry_sort_path(right)));
3695 result
3696}
3697
3698fn detect_exact_copies(
3699 changes: Vec<NameStatusEntry>,
3700 left_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
3701 right_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
3702 find_copies_harder: bool,
3703 rename_empty: bool,
3704) -> Vec<NameStatusEntry> {
3705 let changed_sources = changes
3706 .iter()
3707 .filter(|entry| matches!(entry.status, NameStatus::Deleted | NameStatus::Modified))
3708 .map(|entry| entry.path.clone())
3709 .collect::<BTreeSet<_>>();
3710 let source_paths = left_entries
3711 .keys()
3712 .filter(|path| find_copies_harder || changed_sources.contains(path.as_slice()))
3713 .cloned()
3714 .collect::<Vec<_>>();
3715
3716 let mut result = Vec::new();
3717 for entry in changes {
3718 if entry.status != NameStatus::Added {
3719 result.push(entry);
3720 continue;
3721 }
3722 let Some(right) = right_entries.get(entry.path.as_bytes()) else {
3723 result.push(entry);
3724 continue;
3725 };
3726 if let Some(old_path) = source_paths.iter().find(|old_path| {
3727 old_path.as_slice() != entry.path.as_bytes()
3728 && left_entries.get(*old_path).is_some_and(|left| {
3729 left.oid == right.oid && (rename_empty || !is_empty_blob_oid(&left.oid))
3730 })
3731 }) {
3732 result.push(NameStatusEntry {
3733 status: NameStatus::Copied(100),
3734 path: entry.path,
3735 old_path: Some(old_path.clone().into()),
3736 old_mode: left_entries
3737 .get(old_path.as_slice())
3738 .map(|entry| entry.mode),
3739 new_mode: entry.new_mode,
3740 old_oid: left_entries.get(old_path.as_slice()).map(|entry| entry.oid),
3741 new_oid: entry.new_oid,
3742 });
3743 } else {
3744 result.push(entry);
3745 }
3746 }
3747 result.sort_by(|left, right| diff_entry_sort_path(left).cmp(diff_entry_sort_path(right)));
3748 result
3749}
3750
3751#[derive(Debug, Clone)]
3754struct RenameSourceMeta {
3755 path: BString,
3756 mode: Option<u32>,
3757 oid: Option<ObjectId>,
3758}
3759
3760struct ScoredPair {
3763 src: usize,
3765 dst: usize,
3767 score: u8,
3769}
3770
3771fn rename_limit_exceeded(source_count: usize, dest_count: usize, rename_limit: usize) -> bool {
3772 rename_limit > 0
3773 && source_count
3774 .saturating_mul(dest_count)
3775 .gt(&rename_limit.saturating_mul(rename_limit))
3776}
3777
3778#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3779enum InexactCopyLimitOutcome {
3780 Full,
3781 DegradedToChangedSources,
3782 Skip,
3783}
3784
3785fn inexact_rename_candidates(
3786 changes: &[NameStatusEntry],
3787 options: &RenameDetectionOptions,
3788) -> (usize, usize) {
3789 let mut deleted = 0usize;
3790 let mut added = 0usize;
3791 for entry in changes {
3792 match entry.status {
3793 NameStatus::Deleted => {
3794 if entry
3795 .old_oid
3796 .as_ref()
3797 .is_some_and(|oid| options.base.rename_empty || !is_empty_blob_oid(oid))
3798 {
3799 deleted += 1;
3800 }
3801 }
3802 NameStatus::Added => {
3803 if entry
3804 .new_oid
3805 .as_ref()
3806 .is_some_and(|oid| options.base.rename_empty || !is_empty_blob_oid(oid))
3807 {
3808 added += 1;
3809 }
3810 }
3811 _ => {}
3812 }
3813 }
3814 (deleted, added)
3815}
3816
3817fn inexact_rename_limit_exceeded(
3818 changes: &[NameStatusEntry],
3819 options: &RenameDetectionOptions,
3820) -> bool {
3821 let (source_count, dest_count) = inexact_rename_candidates(changes, options);
3822 rename_limit_exceeded(source_count, dest_count, options.rename_limit)
3823}
3824
3825fn changed_copy_sources(changes: &[NameStatusEntry]) -> BTreeSet<BString> {
3826 changes
3827 .iter()
3828 .filter(|entry| matches!(entry.status, NameStatus::Deleted | NameStatus::Modified))
3829 .map(|entry| entry.path.clone())
3830 .collect()
3831}
3832
3833fn inexact_copy_source_count(
3834 changed_sources: &BTreeSet<BString>,
3835 left_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
3836 options: &RenameDetectionOptions,
3837 find_copies_harder: bool,
3838) -> usize {
3839 left_entries
3840 .iter()
3841 .filter(|(path, _)| find_copies_harder || changed_sources.contains(path.as_slice()))
3842 .filter(|(_, tracked)| options.base.rename_empty || !is_empty_blob_oid(&tracked.oid))
3843 .count()
3844}
3845
3846fn inexact_copy_dest_count(changes: &[NameStatusEntry], options: &RenameDetectionOptions) -> usize {
3847 changes
3848 .iter()
3849 .filter(|entry| entry.status == NameStatus::Added)
3850 .filter(|entry| {
3851 entry
3852 .new_oid
3853 .as_ref()
3854 .is_some_and(|oid| options.base.rename_empty || !is_empty_blob_oid(oid))
3855 })
3856 .count()
3857}
3858
3859fn inexact_copy_limit_outcome(
3860 changes: &[NameStatusEntry],
3861 left_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
3862 options: &RenameDetectionOptions,
3863) -> InexactCopyLimitOutcome {
3864 let changed_sources = changed_copy_sources(changes);
3865 let dest_count = inexact_copy_dest_count(changes, options);
3866 let source_count = inexact_copy_source_count(
3867 &changed_sources,
3868 left_entries,
3869 options,
3870 options.base.find_copies_harder,
3871 );
3872 if !rename_limit_exceeded(source_count, dest_count, options.rename_limit) {
3873 return InexactCopyLimitOutcome::Full;
3874 }
3875 if options.base.find_copies_harder {
3876 let changed_source_count =
3877 inexact_copy_source_count(&changed_sources, left_entries, options, false);
3878 if !rename_limit_exceeded(changed_source_count, dest_count, options.rename_limit) {
3879 return InexactCopyLimitOutcome::DegradedToChangedSources;
3880 }
3881 }
3882 InexactCopyLimitOutcome::Skip
3883}
3884
3885fn detect_inexact_renames(
3896 changes: Vec<NameStatusEntry>,
3897 options: &RenameDetectionOptions,
3898 fetch_blob: &impl Fn(&ObjectId) -> Option<Vec<u8>>,
3899) -> Vec<NameStatusEntry> {
3900 let threshold = options.rename_threshold;
3901 if threshold > 100 {
3903 return changes;
3904 }
3905
3906 let mut deleted_indices = Vec::new();
3907 let mut added_indices = Vec::new();
3908 for (idx, entry) in changes.iter().enumerate() {
3909 match entry.status {
3910 NameStatus::Deleted => {
3911 let Some(oid) = entry.old_oid.as_ref() else {
3912 continue;
3913 };
3914 if !options.base.rename_empty && is_empty_blob_oid(oid) {
3915 continue;
3916 }
3917 deleted_indices.push(idx);
3918 }
3919 NameStatus::Added => {
3920 let Some(oid) = entry.new_oid.as_ref() else {
3921 continue;
3922 };
3923 if !options.base.rename_empty && is_empty_blob_oid(oid) {
3924 continue;
3925 }
3926 added_indices.push(idx);
3927 }
3928 _ => {}
3929 }
3930 }
3931
3932 if deleted_indices.is_empty() || added_indices.is_empty() {
3933 return changes;
3934 }
3935
3936 if rename_limit_exceeded(
3940 deleted_indices.len(),
3941 added_indices.len(),
3942 options.rename_limit,
3943 ) {
3944 return changes;
3945 }
3946
3947 let mut deleted: Vec<(usize, Vec<u8>)> = Vec::new();
3950 for idx in deleted_indices {
3951 let Some(oid) = changes[idx].old_oid.as_ref() else {
3952 continue;
3953 };
3954 if let Some(bytes) = fetch_blob(oid) {
3955 deleted.push((idx, bytes));
3956 }
3957 }
3958 let mut added: Vec<(usize, Vec<u8>)> = Vec::new();
3959 for idx in added_indices {
3960 let Some(oid) = changes[idx].new_oid.as_ref() else {
3961 continue;
3962 };
3963 if let Some(bytes) = fetch_blob(oid) {
3964 added.push((idx, bytes));
3965 }
3966 }
3967 if deleted.is_empty() || added.is_empty() {
3968 return changes;
3969 }
3970
3971 let mut src_used = vec![false; deleted.len()];
3972 let mut dst_used = vec![false; added.len()];
3973 let mut rename_of: BTreeMap<usize, (usize, u8)> = BTreeMap::new();
3975
3976 if !options.base.detect_copies {
3982 let src_paths: Vec<&[u8]> = deleted
3983 .iter()
3984 .map(|(idx, _)| &changes[*idx].path[..])
3985 .collect();
3986 let dst_paths: Vec<&[u8]> = added
3987 .iter()
3988 .map(|(idx, _)| &changes[*idx].path[..])
3989 .collect();
3990 let basename_pairs = basename_rename_matches(
3991 &src_paths,
3992 &dst_paths,
3993 &src_used,
3994 &dst_used,
3995 threshold,
3996 |si, di| Some(blob_similarity(&deleted[si].1, &added[di].1)),
3997 );
3998 for (si, di, score) in basename_pairs {
3999 src_used[si] = true;
4000 dst_used[di] = true;
4001 rename_of.insert(added[di].0, (deleted[si].0, score));
4002 }
4003 }
4004
4005 let mut pairs: Vec<ScoredPair> = Vec::new();
4008 for (si, (_, src_bytes)) in deleted.iter().enumerate() {
4009 if src_used[si] {
4010 continue;
4011 }
4012 for (di, (_, dst_bytes)) in added.iter().enumerate() {
4013 if dst_used[di] {
4014 continue;
4015 }
4016 let score = blob_similarity(src_bytes, dst_bytes);
4017 if score >= threshold {
4018 pairs.push(ScoredPair {
4019 src: si,
4020 dst: di,
4021 score,
4022 });
4023 }
4024 }
4025 }
4026 pairs.sort_by(|a, b| {
4029 b.score
4030 .cmp(&a.score)
4031 .then_with(|| a.src.cmp(&b.src))
4032 .then_with(|| a.dst.cmp(&b.dst))
4033 });
4034
4035 for pair in pairs {
4036 if src_used[pair.src] || dst_used[pair.dst] {
4037 continue;
4038 }
4039 src_used[pair.src] = true;
4040 dst_used[pair.dst] = true;
4041 let src_change_idx = deleted[pair.src].0;
4042 let dst_change_idx = added[pair.dst].0;
4043 rename_of.insert(dst_change_idx, (src_change_idx, pair.score));
4044 }
4045
4046 if rename_of.is_empty() {
4047 return changes;
4048 }
4049
4050 let consumed_sources: BTreeSet<usize> =
4053 rename_of.values().map(|(src_idx, _)| *src_idx).collect();
4054 let source_meta: BTreeMap<usize, RenameSourceMeta> = consumed_sources
4055 .iter()
4056 .map(|&src_idx| {
4057 let src = &changes[src_idx];
4058 (
4059 src_idx,
4060 RenameSourceMeta {
4061 path: src.path.clone(),
4062 mode: src.old_mode,
4063 oid: src.old_oid,
4064 },
4065 )
4066 })
4067 .collect();
4068
4069 let mut result = Vec::with_capacity(changes.len());
4070 for (idx, entry) in changes.into_iter().enumerate() {
4071 if consumed_sources.contains(&idx) {
4072 continue;
4074 }
4075 if let Some((src_idx, score)) = rename_of.get(&idx) {
4076 let meta = source_meta
4080 .get(src_idx)
4081 .cloned()
4082 .unwrap_or(RenameSourceMeta {
4083 path: BString::default(),
4084 mode: None,
4085 oid: None,
4086 });
4087 result.push(NameStatusEntry {
4088 status: NameStatus::Renamed(*score),
4089 path: entry.path,
4090 old_path: Some(meta.path),
4091 old_mode: meta.mode,
4092 new_mode: entry.new_mode,
4093 old_oid: meta.oid,
4094 new_oid: entry.new_oid,
4095 });
4096 continue;
4097 }
4098 result.push(entry);
4099 }
4100
4101 result.sort_by(|left, right| diff_entry_sort_path(left).cmp(diff_entry_sort_path(right)));
4102 result
4103}
4104
4105fn detect_inexact_copies(
4115 changes: Vec<NameStatusEntry>,
4116 left_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
4117 options: &RenameDetectionOptions,
4118 fetch_blob: &impl Fn(&ObjectId) -> Option<Vec<u8>>,
4119) -> Vec<NameStatusEntry> {
4120 let threshold = options.copy_threshold;
4121 if threshold > 100 {
4122 return changes;
4123 }
4124
4125 let changed_sources = changed_copy_sources(&changes);
4126 let limit_outcome = inexact_copy_limit_outcome(&changes, left_entries, options);
4127 if limit_outcome == InexactCopyLimitOutcome::Skip {
4128 return changes;
4129 }
4130 let find_copies_harder = options.base.find_copies_harder
4131 && limit_outcome != InexactCopyLimitOutcome::DegradedToChangedSources;
4132 let mut source_candidates: Vec<(Vec<u8>, &TrackedEntry)> = Vec::new();
4133 for (path, tracked) in left_entries {
4134 if !(find_copies_harder || changed_sources.contains(path.as_slice())) {
4135 continue;
4136 }
4137 if !options.base.rename_empty && is_empty_blob_oid(&tracked.oid) {
4138 continue;
4139 }
4140 source_candidates.push((path.clone(), tracked));
4141 }
4142 if source_candidates.is_empty() {
4143 return changes;
4144 }
4145
4146 let mut sources: Vec<(Vec<u8>, &TrackedEntry, Vec<u8>)> = Vec::new();
4147 for (path, tracked) in source_candidates {
4148 if let Some(bytes) = fetch_blob(&tracked.oid) {
4149 sources.push((path, tracked, bytes));
4150 }
4151 }
4152 if sources.is_empty() {
4153 return changes;
4154 }
4155
4156 let mut result = Vec::with_capacity(changes.len());
4157 for entry in changes {
4158 if entry.status != NameStatus::Added {
4159 result.push(entry);
4160 continue;
4161 }
4162 let Some(new_oid) = entry.new_oid.as_ref() else {
4163 result.push(entry);
4164 continue;
4165 };
4166 let Some(dst_bytes) = fetch_blob(new_oid) else {
4167 result.push(entry);
4168 continue;
4169 };
4170
4171 let mut best: Option<(usize, u8)> = None;
4175 for (i, (src_path, _, src_bytes)) in sources.iter().enumerate() {
4176 if src_path.as_slice() == entry.path.as_bytes() {
4177 continue;
4178 }
4179 let score = blob_similarity(src_bytes, &dst_bytes);
4180 if score < threshold {
4181 continue;
4182 }
4183 match best {
4184 Some((_, best_score)) if best_score >= score => {}
4185 _ => best = Some((i, score)),
4186 }
4187 }
4188
4189 if let Some((src_idx, score)) = best {
4190 let (src_path, src_tracked, _) = &sources[src_idx];
4191 result.push(NameStatusEntry {
4192 status: NameStatus::Copied(score),
4193 path: entry.path,
4194 old_path: Some(src_path.clone().into()),
4195 old_mode: Some(src_tracked.mode),
4196 new_mode: entry.new_mode,
4197 old_oid: Some(src_tracked.oid),
4198 new_oid: entry.new_oid,
4199 });
4200 } else {
4201 result.push(entry);
4202 }
4203 }
4204 result.sort_by(|left, right| diff_entry_sort_path(left).cmp(diff_entry_sort_path(right)));
4205 result
4206}
4207
4208fn is_empty_blob_oid(oid: &ObjectId) -> bool {
4209 object_id_for_bytes(oid.format(), "blob", b"").is_ok_and(|empty| empty == *oid)
4210}
4211
4212const MAX_SPAN_BYTES: usize = 64;
4253
4254pub fn blob_similarity(a: &[u8], b: &[u8]) -> u8 {
4266 if a == b {
4268 return 100;
4269 }
4270 let max_size = a.len().max(b.len());
4271 if max_size == 0 {
4272 return 100;
4275 }
4276
4277 let src = span_hash_counts(a, blob_is_text(a));
4278 let dst = span_hash_counts(b, blob_is_text(b));
4279 let common = common_span_bytes(&src, &dst);
4280
4281 const MAX_SCORE: u64 = 60000;
4289 let internal = (common as u64 * MAX_SCORE) / max_size as u64;
4290 let score = internal * 100 / MAX_SCORE;
4291 score.min(100) as u8
4292}
4293
4294pub fn path_basename(path: &[u8]) -> &[u8] {
4297 match path.iter().rposition(|&byte| byte == b'/') {
4298 Some(slash) => &path[slash + 1..],
4299 None => path,
4300 }
4301}
4302
4303pub fn basename_min_score(threshold: u8) -> u8 {
4307 let threshold = threshold.min(100);
4308 threshold + (100 - threshold) / 2
4309}
4310
4311pub fn basename_rename_matches(
4325 src_paths: &[&[u8]],
4326 dst_paths: &[&[u8]],
4327 src_used: &[bool],
4328 dst_used: &[bool],
4329 threshold: u8,
4330 mut similarity: impl FnMut(usize, usize) -> Option<u8>,
4331) -> Vec<(usize, usize, u8)> {
4332 let min_score = basename_min_score(threshold);
4333 let mut src_by_base: HashMap<&[u8], Option<usize>> = HashMap::new();
4336 for (si, path) in src_paths.iter().enumerate() {
4337 if src_used.get(si).copied().unwrap_or(false) {
4338 continue;
4339 }
4340 src_by_base
4341 .entry(path_basename(path))
4342 .and_modify(|slot| *slot = None)
4343 .or_insert(Some(si));
4344 }
4345 let mut dst_by_base: HashMap<&[u8], Option<usize>> = HashMap::new();
4346 for (di, path) in dst_paths.iter().enumerate() {
4347 if dst_used.get(di).copied().unwrap_or(false) {
4348 continue;
4349 }
4350 dst_by_base
4351 .entry(path_basename(path))
4352 .and_modify(|slot| *slot = None)
4353 .or_insert(Some(di));
4354 }
4355 let mut matches = Vec::new();
4356 let mut dst_taken = vec![false; dst_paths.len()];
4357 for (si, path) in src_paths.iter().enumerate() {
4358 if src_used.get(si).copied().unwrap_or(false) {
4359 continue;
4360 }
4361 let base = path_basename(path);
4362 let Some(Some(src_idx)) = src_by_base.get(base).copied() else {
4364 continue;
4365 };
4366 if src_idx != si {
4367 continue;
4368 }
4369 let Some(Some(dst_idx)) = dst_by_base.get(base).copied() else {
4370 continue;
4371 };
4372 if dst_used.get(dst_idx).copied().unwrap_or(false) || dst_taken[dst_idx] {
4373 continue;
4374 }
4375 let Some(score) = similarity(si, dst_idx) else {
4376 continue;
4377 };
4378 if score < min_score {
4379 continue;
4380 }
4381 dst_taken[dst_idx] = true;
4382 matches.push((si, dst_idx, score));
4383 }
4384 matches
4385}
4386
4387fn span_hash_counts(data: &[u8], is_text: bool) -> BTreeMap<u64, usize> {
4394 let mut counts: BTreeMap<u64, usize> = BTreeMap::new();
4395 let mut idx = 0usize;
4396 let len = data.len();
4397 while idx < len {
4398 let mut accum1: u32 = 0;
4403 let mut accum2: u32 = 0;
4404 let mut span_len = 0usize;
4405 loop {
4406 let c = data[idx] as u32;
4407 idx += 1;
4408 if is_text && c == u32::from(b'\r') && idx < len && data[idx] == b'\n' {
4413 continue;
4414 }
4415 span_len += 1;
4416 accum1 = (accum1 << 7) ^ (accum2 >> 25);
4417 accum2 = (accum2 << 7) ^ (accum1 >> 25);
4418 accum1 = accum1.wrapping_add(c);
4419 let newline = c == u32::from(b'\n');
4420 if span_len >= MAX_SPAN_BYTES || newline || idx >= len {
4421 break;
4422 }
4423 }
4424 let hash = ((accum1 as u64) << 32) ^ (accum2 as u64) ^ ((span_len as u64) << 1);
4428 *counts.entry(hash).or_insert(0) += span_len;
4429 }
4430 counts
4431}
4432
4433pub fn count_changes(src: &[u8], dst: &[u8]) -> (usize, usize) {
4442 let src_counts = span_hash_counts(src, blob_is_text(src));
4443 let dst_counts = span_hash_counts(dst, blob_is_text(dst));
4444 let copied = common_span_bytes(&src_counts, &dst_counts);
4445 (copied, dst.len() - copied)
4446}
4447
4448fn blob_is_text(data: &[u8]) -> bool {
4452 const FIRST_FEW_BYTES: usize = 8000;
4453 !data.iter().take(FIRST_FEW_BYTES).any(|&byte| byte == 0)
4454}
4455
4456fn common_span_bytes(src: &BTreeMap<u64, usize>, dst: &BTreeMap<u64, usize>) -> usize {
4457 let mut common = 0usize;
4458 let (small, large) = if src.len() <= dst.len() {
4460 (src, dst)
4461 } else {
4462 (dst, src)
4463 };
4464 for (hash, small_bytes) in small {
4465 if let Some(large_bytes) = large.get(hash) {
4466 common += (*small_bytes).min(*large_bytes);
4467 }
4468 }
4469 common
4470}
4471
4472fn diff_entry_sort_path(entry: &NameStatusEntry) -> &[u8] {
4473 entry.path.as_bytes()
4476}
4477
4478fn mark_unstaged_worktree_oids_unresolved(
4479 changes: Vec<NameStatusEntry>,
4480 index_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
4481 worktree_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
4482) -> Vec<NameStatusEntry> {
4483 changes
4484 .into_iter()
4485 .map(|mut entry| {
4486 let worktree_entry = worktree_entries.get(entry.path.as_bytes());
4487 if worktree_entry != index_entries.get(entry.path.as_bytes()) {
4488 entry.new_oid = None;
4489 }
4490 entry
4491 })
4492 .collect()
4493}
4494
4495#[derive(Debug, Clone, PartialEq, Eq)]
4496struct TrackedEntry {
4497 mode: u32,
4498 oid: ObjectId,
4499}
4500
4501type TrackedEntryMap = BTreeMap<Vec<u8>, TrackedEntry>;
4504
4505type TrackedEntryPair = (TrackedEntryMap, TrackedEntryMap);
4507
4508struct IndexSnapshot {
4509 entries: BTreeMap<Vec<u8>, TrackedEntry>,
4510 stat_cache: IndexStatCache,
4511 missing_skip_worktree_entries: BTreeMap<Vec<u8>, TrackedEntry>,
4512 present_skip_worktree_entries: BTreeMap<Vec<u8>, TrackedEntry>,
4513}
4514
4515fn read_index_entries(
4516 git_dir: &Path,
4517 format: ObjectFormat,
4518) -> Result<BTreeMap<Vec<u8>, TrackedEntry>> {
4519 let index_path = sley_index::repository_index_path(git_dir);
4520 if !index_path.exists() {
4521 return Ok(BTreeMap::new());
4522 }
4523 let index = expand_sparse_index_for_worktree_diff(
4524 sley_index::read_repository_index(git_dir, format)?,
4525 git_dir,
4526 format,
4527 )?;
4528 Ok(index
4529 .entries
4530 .into_iter()
4531 .filter(|entry| entry.stage() == sley_index::Stage::Normal && !entry.is_intent_to_add())
4532 .map(|entry| {
4533 (
4534 entry.path.into_bytes(),
4535 TrackedEntry {
4536 mode: entry.mode,
4537 oid: entry.oid,
4538 },
4539 )
4540 })
4541 .collect())
4542}
4543
4544fn read_intent_to_add_paths(
4548 git_dir: &Path,
4549 format: ObjectFormat,
4550) -> Result<std::collections::HashSet<Vec<u8>>> {
4551 let index_path = sley_index::repository_index_path(git_dir);
4552 if !index_path.exists() {
4553 return Ok(std::collections::HashSet::new());
4554 }
4555 let index = expand_sparse_index_for_worktree_diff(
4556 sley_index::read_repository_index(git_dir, format)?,
4557 git_dir,
4558 format,
4559 )?;
4560 Ok(index
4561 .entries
4562 .iter()
4563 .filter(|entry| entry.stage() == sley_index::Stage::Normal && entry.is_intent_to_add())
4564 .map(|entry| entry.path.as_bytes().to_vec())
4565 .collect())
4566}
4567
4568fn read_index_snapshot(git_dir: &Path, format: ObjectFormat) -> Result<IndexSnapshot> {
4569 let index_path = sley_index::repository_index_path(git_dir);
4570 let index_metadata = match fs::metadata(&index_path) {
4571 Ok(metadata) => metadata,
4572 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
4573 return Ok(IndexSnapshot {
4574 entries: BTreeMap::new(),
4575 stat_cache: IndexStatCache::default(),
4576 missing_skip_worktree_entries: BTreeMap::new(),
4577 present_skip_worktree_entries: BTreeMap::new(),
4578 });
4579 }
4580 Err(err) => return Err(err.into()),
4581 };
4582 let raw_index = sley_index::read_repository_index(git_dir, format)?;
4583 let sparse_checkout_active = raw_index.is_sparse()
4584 || raw_index
4585 .entries
4586 .iter()
4587 .any(|entry| entry.mode == sley_index::SPARSE_DIR_MODE && entry.is_skip_worktree())
4588 || sparse_checkout_config_enabled(git_dir);
4589 let index = expand_sparse_index_for_worktree_diff(raw_index, git_dir, format)?;
4590 let stat_cache =
4591 IndexStatCache::from_index_mtime(&index, sley_index::file_mtime_parts(&index_metadata));
4592 let mut entries = BTreeMap::new();
4593 let mut missing_skip_worktree_entries = BTreeMap::new();
4594 let mut present_skip_worktree_entries = BTreeMap::new();
4595 for entry in index.entries {
4596 let is_skip_worktree = entry.stage() == sley_index::Stage::Normal
4597 && entry.is_skip_worktree()
4598 && !entry.is_intent_to_add();
4599 let path = entry.path.into_bytes();
4600 let tracked = TrackedEntry {
4601 mode: entry.mode,
4602 oid: entry.oid,
4603 };
4604 if is_skip_worktree {
4605 missing_skip_worktree_entries.insert(path.clone(), tracked.clone());
4606 if !sparse_checkout_active {
4607 present_skip_worktree_entries.insert(path.clone(), tracked.clone());
4608 }
4609 }
4610 entries.insert(path, tracked);
4611 }
4612 Ok(IndexSnapshot {
4613 entries,
4614 stat_cache,
4615 missing_skip_worktree_entries,
4616 present_skip_worktree_entries,
4617 })
4618}
4619
4620fn sparse_checkout_config_enabled(git_dir: &Path) -> bool {
4621 sley_config::GitConfig::read(git_dir.join("config"))
4622 .ok()
4623 .and_then(|config| config.get_bool("core", None, "sparseCheckout"))
4624 == Some(true)
4625 || sley_config::GitConfig::read(git_dir.join("config.worktree"))
4626 .ok()
4627 .and_then(|config| config.get_bool("core", None, "sparseCheckout"))
4628 == Some(true)
4629}
4630
4631trait WorktreeIndexEntry {
4632 fn git_path(&self) -> &[u8];
4633 fn stage(&self) -> sley_index::Stage;
4634 fn mode(&self) -> u32;
4635 fn oid(&self) -> ObjectId;
4636 fn size(&self) -> u32;
4637 fn is_intent_to_add(&self) -> bool;
4638 fn is_skip_worktree(&self) -> bool;
4639 fn reusable_with(&self, stat_cache: &IndexStatCache, metadata: &fs::Metadata) -> bool;
4640}
4641
4642impl WorktreeIndexEntry for sley_index::IndexEntry {
4643 fn git_path(&self) -> &[u8] {
4644 self.path.as_bytes()
4645 }
4646
4647 fn stage(&self) -> sley_index::Stage {
4648 sley_index::IndexEntry::stage(self)
4649 }
4650
4651 fn mode(&self) -> u32 {
4652 self.mode
4653 }
4654
4655 fn oid(&self) -> ObjectId {
4656 self.oid
4657 }
4658
4659 fn size(&self) -> u32 {
4660 self.size
4661 }
4662
4663 fn is_intent_to_add(&self) -> bool {
4664 sley_index::IndexEntry::is_intent_to_add(self)
4665 }
4666
4667 fn is_skip_worktree(&self) -> bool {
4668 sley_index::IndexEntry::is_skip_worktree(self)
4669 }
4670
4671 fn reusable_with(&self, stat_cache: &IndexStatCache, metadata: &fs::Metadata) -> bool {
4672 stat_cache.reusable_index_entry(self, metadata).is_some()
4673 }
4674}
4675
4676impl WorktreeIndexEntry for sley_index::IndexEntryRef<'_> {
4677 fn git_path(&self) -> &[u8] {
4678 self.path
4679 }
4680
4681 fn stage(&self) -> sley_index::Stage {
4682 sley_index::IndexEntryRef::stage(self)
4683 }
4684
4685 fn mode(&self) -> u32 {
4686 self.mode
4687 }
4688
4689 fn oid(&self) -> ObjectId {
4690 self.oid
4691 }
4692
4693 fn size(&self) -> u32 {
4694 self.size
4695 }
4696
4697 fn is_intent_to_add(&self) -> bool {
4698 sley_index::IndexEntryRef::is_intent_to_add(self)
4699 }
4700
4701 fn is_skip_worktree(&self) -> bool {
4702 sley_index::IndexEntryRef::is_skip_worktree(self)
4703 }
4704
4705 fn reusable_with(&self, stat_cache: &IndexStatCache, metadata: &fs::Metadata) -> bool {
4706 stat_cache.reusable_index_entry_ref(self, metadata)
4707 }
4708}
4709
4710fn tracked_entry_from_index(entry: &impl WorktreeIndexEntry) -> TrackedEntry {
4711 TrackedEntry {
4712 mode: entry.mode(),
4713 oid: entry.oid(),
4714 }
4715}
4716
4717fn head_tree_entries(
4718 git_dir: &Path,
4719 format: ObjectFormat,
4720 db: &FileObjectDatabase,
4721) -> Result<BTreeMap<Vec<u8>, TrackedEntry>> {
4722 let refs = FileRefStore::new(git_dir, format);
4723 let Some(head) = refs.read_ref("HEAD")? else {
4724 return Ok(BTreeMap::new());
4725 };
4726 let commit_oid = match head {
4727 RefTarget::Direct(oid) => Some(oid),
4728 RefTarget::Symbolic(name) => match refs.read_ref(&name)? {
4729 Some(RefTarget::Direct(oid)) => Some(oid),
4730 _ => None,
4731 },
4732 };
4733 let Some(commit_oid) = commit_oid else {
4734 return Ok(BTreeMap::new());
4735 };
4736 let object = db.read_object(&commit_oid)?;
4737 if object.object_type != ObjectType::Commit {
4738 return Err(GitError::InvalidObject(format!(
4739 "HEAD {commit_oid} is not a commit"
4740 )));
4741 }
4742 let commit = Commit::parse_ref(format, &object.body)?;
4743 let mut entries = BTreeMap::new();
4744 collect_tree_entries(db, format, &commit.tree, Vec::new(), &mut entries)?;
4745 Ok(entries)
4746}
4747
4748fn collect_tree_entries(
4757 db: &FileObjectDatabase,
4758 format: ObjectFormat,
4759 tree_oid: &ObjectId,
4760 prefix: Vec<u8>,
4761 entries: &mut BTreeMap<Vec<u8>, TrackedEntry>,
4762) -> Result<()> {
4763 for (rel_path, (mode, oid)) in flatten_tree(db, format, tree_oid)? {
4764 let path = join_tree_path(&prefix, &rel_path);
4765 entries.insert(path, TrackedEntry { mode, oid });
4766 }
4767 Ok(())
4768}
4769
4770const TREE_ENTRY_MODE: u32 = 0o040000;
4772
4773fn read_tree_object(
4777 db: &FileObjectDatabase,
4778 format: ObjectFormat,
4779 tree_oid: &ObjectId,
4780) -> Result<Tree> {
4781 let object = db.read_object(tree_oid)?;
4782 if object.object_type != ObjectType::Tree {
4783 return Err(GitError::InvalidObject(format!(
4784 "expected tree {tree_oid}, found {}",
4785 object.object_type.as_str()
4786 )));
4787 }
4788 Tree::parse(format, &object.body)
4789}
4790
4791fn join_tree_path(prefix: &[u8], name: &[u8]) -> Vec<u8> {
4794 let mut path = Vec::with_capacity(prefix.len() + 1 + name.len());
4795 path.extend_from_slice(prefix);
4796 if !path.is_empty() {
4797 path.push(b'/');
4798 }
4799 path.extend_from_slice(name);
4800 path
4801}
4802
4803fn collect_full_tree_pair(
4807 db: &FileObjectDatabase,
4808 format: ObjectFormat,
4809 left_tree: &ObjectId,
4810 right_tree: &ObjectId,
4811) -> Result<TrackedEntryPair> {
4812 let mut left = BTreeMap::new();
4813 collect_tree_entries(db, format, left_tree, Vec::new(), &mut left)?;
4814 let mut right = BTreeMap::new();
4815 collect_tree_entries(db, format, right_tree, Vec::new(), &mut right)?;
4816 Ok((left, right))
4817}
4818
4819fn changed_tree_entries(
4835 db: &FileObjectDatabase,
4836 format: ObjectFormat,
4837 left_tree: &ObjectId,
4838 right_tree: &ObjectId,
4839) -> Result<TrackedEntryPair> {
4840 let mut left = BTreeMap::new();
4841 let mut right = BTreeMap::new();
4842 if left_tree != right_tree {
4844 diff_tree_pair(
4845 db,
4846 format,
4847 left_tree,
4848 right_tree,
4849 &[],
4850 &mut left,
4851 &mut right,
4852 )?;
4853 }
4854 Ok((left, right))
4855}
4856
4857fn diff_tree_pair(
4861 db: &FileObjectDatabase,
4862 format: ObjectFormat,
4863 left_tree: &ObjectId,
4864 right_tree: &ObjectId,
4865 prefix: &[u8],
4866 left: &mut BTreeMap<Vec<u8>, TrackedEntry>,
4867 right: &mut BTreeMap<Vec<u8>, TrackedEntry>,
4868) -> Result<()> {
4869 let left_entries = read_tree_object(db, format, left_tree)?.entries;
4870 let right_entries = read_tree_object(db, format, right_tree)?.entries;
4871
4872 let mut right_by_name: HashMap<&[u8], &TreeEntry> = HashMap::with_capacity(right_entries.len());
4877 for entry in &right_entries {
4878 right_by_name.insert(entry.name.as_bytes(), entry);
4879 }
4880
4881 for left_entry in &left_entries {
4882 match right_by_name.remove(left_entry.name.as_bytes()) {
4883 Some(right_entry) => {
4884 merge_tree_entry(
4885 db,
4886 format,
4887 prefix,
4888 Some(left_entry),
4889 Some(right_entry),
4890 left,
4891 right,
4892 )?;
4893 }
4894 None => {
4895 merge_tree_entry(db, format, prefix, Some(left_entry), None, left, right)?;
4896 }
4897 }
4898 }
4899 for right_entry in &right_entries {
4901 if right_by_name.contains_key(right_entry.name.as_bytes()) {
4902 merge_tree_entry(db, format, prefix, None, Some(right_entry), left, right)?;
4903 }
4904 }
4905 Ok(())
4906}
4907
4908fn merge_tree_entry(
4921 db: &FileObjectDatabase,
4922 format: ObjectFormat,
4923 prefix: &[u8],
4924 left_entry: Option<&TreeEntry>,
4925 right_entry: Option<&TreeEntry>,
4926 left: &mut BTreeMap<Vec<u8>, TrackedEntry>,
4927 right: &mut BTreeMap<Vec<u8>, TrackedEntry>,
4928) -> Result<()> {
4929 let left_is_tree = left_entry.is_some_and(|entry| entry.mode == TREE_ENTRY_MODE);
4930 let right_is_tree = right_entry.is_some_and(|entry| entry.mode == TREE_ENTRY_MODE);
4931
4932 if let (Some(left_entry), Some(right_entry)) = (left_entry, right_entry) {
4933 if left_is_tree && right_is_tree {
4934 if left_entry.oid == right_entry.oid {
4936 return Ok(());
4937 }
4938 let path = join_tree_path(prefix, left_entry.name.as_bytes());
4939 return diff_tree_pair(
4940 db,
4941 format,
4942 &left_entry.oid,
4943 &right_entry.oid,
4944 &path,
4945 left,
4946 right,
4947 );
4948 }
4949 if !left_is_tree && !right_is_tree {
4950 if left_entry.mode == right_entry.mode && left_entry.oid == right_entry.oid {
4954 return Ok(());
4955 }
4956 let path = join_tree_path(prefix, left_entry.name.as_bytes());
4957 left.insert(
4958 path.clone(),
4959 TrackedEntry {
4960 mode: left_entry.mode,
4961 oid: left_entry.oid,
4962 },
4963 );
4964 right.insert(
4965 path,
4966 TrackedEntry {
4967 mode: right_entry.mode,
4968 oid: right_entry.oid,
4969 },
4970 );
4971 return Ok(());
4972 }
4973 }
4976
4977 if let Some(left_entry) = left_entry {
4979 let path = join_tree_path(prefix, left_entry.name.as_bytes());
4980 if left_is_tree {
4981 collect_tree_entries(db, format, &left_entry.oid, path, left)?;
4982 } else {
4983 left.insert(
4984 path,
4985 TrackedEntry {
4986 mode: left_entry.mode,
4987 oid: left_entry.oid,
4988 },
4989 );
4990 }
4991 }
4992 if let Some(right_entry) = right_entry {
4994 let path = join_tree_path(prefix, right_entry.name.as_bytes());
4995 if right_is_tree {
4996 collect_tree_entries(db, format, &right_entry.oid, path, right)?;
4997 } else {
4998 right.insert(
4999 path,
5000 TrackedEntry {
5001 mode: right_entry.mode,
5002 oid: right_entry.oid,
5003 },
5004 );
5005 }
5006 }
5007 Ok(())
5008}
5009
5010fn index_gitlinks(index: &BTreeMap<Vec<u8>, TrackedEntry>) -> BTreeMap<Vec<u8>, ObjectId> {
5011 index
5012 .iter()
5013 .filter(|(_, entry)| sley_index::is_gitlink(entry.mode))
5014 .map(|(path, entry)| (path.clone(), entry.oid))
5015 .collect()
5016}
5017
5018fn candidate_path_set<'a>(candidate_paths: impl Iterator<Item = &'a Vec<u8>>) -> BTreeSet<Vec<u8>> {
5019 candidate_paths.cloned().collect()
5020}
5021
5022fn worktree_entries_for_path_set(
5023 worktree_root: &Path,
5024 format: ObjectFormat,
5025 candidates: &BTreeSet<Vec<u8>>,
5026 index_gitlinks: &BTreeMap<Vec<u8>, ObjectId>,
5027 stat_cache: Option<&IndexStatCache>,
5028 missing_skip_worktree_entries: Option<&BTreeMap<Vec<u8>, TrackedEntry>>,
5029 present_skip_worktree_entries: Option<&BTreeMap<Vec<u8>, TrackedEntry>>,
5030) -> Result<BTreeMap<Vec<u8>, TrackedEntry>> {
5031 worktree_entries_for_unique_paths(
5032 worktree_root,
5033 format,
5034 candidates.iter(),
5035 index_gitlinks,
5036 stat_cache,
5037 missing_skip_worktree_entries,
5038 present_skip_worktree_entries,
5039 )
5040}
5041
5042fn worktree_entries_for_unique_paths<'a>(
5043 worktree_root: &Path,
5044 format: ObjectFormat,
5045 candidates: impl Iterator<Item = &'a Vec<u8>>,
5046 index_gitlinks: &BTreeMap<Vec<u8>, ObjectId>,
5047 stat_cache: Option<&IndexStatCache>,
5048 missing_skip_worktree_entries: Option<&BTreeMap<Vec<u8>, TrackedEntry>>,
5049 present_skip_worktree_entries: Option<&BTreeMap<Vec<u8>, TrackedEntry>>,
5050) -> Result<BTreeMap<Vec<u8>, TrackedEntry>> {
5051 let mut entries = BTreeMap::new();
5052 for git_path in candidates {
5053 if let Some(entry) = worktree_entry_for_path(
5054 worktree_root,
5055 format,
5056 git_path,
5057 index_gitlinks,
5058 stat_cache,
5059 missing_skip_worktree_entries,
5060 present_skip_worktree_entries,
5061 )? {
5062 entries.insert(git_path.clone(), entry);
5063 }
5064 }
5065 Ok(entries)
5066}
5067
5068fn worktree_entry_for_path(
5069 worktree_root: &Path,
5070 format: ObjectFormat,
5071 git_path: &[u8],
5072 index_gitlinks: &BTreeMap<Vec<u8>, ObjectId>,
5073 stat_cache: Option<&IndexStatCache>,
5074 missing_skip_worktree_entries: Option<&BTreeMap<Vec<u8>, TrackedEntry>>,
5075 present_skip_worktree_entries: Option<&BTreeMap<Vec<u8>, TrackedEntry>>,
5076) -> Result<Option<TrackedEntry>> {
5077 if let Some(entry) = present_skip_worktree_entries.and_then(|entries| {
5078 entries
5079 .get(git_path)
5080 .cloned()
5081 .filter(|entry| !sley_index::is_gitlink(entry.mode))
5082 }) {
5083 return Ok(Some(entry));
5084 }
5085 let path = worktree_path_for_repo_path(worktree_root, git_path);
5086 let metadata = match fs::symlink_metadata(&path) {
5087 Ok(metadata) => metadata,
5088 Err(err) if is_missing_worktree_path_error(&err) => {
5089 return Ok(missing_skip_worktree_entries.and_then(|entries| {
5090 entries
5091 .get(git_path)
5092 .cloned()
5093 .filter(|entry| !sley_index::is_gitlink(entry.mode))
5094 }));
5095 }
5096 Err(err) => return Err(GitError::Io(err.to_string())),
5097 };
5098 let file_type = metadata.file_type();
5099 if let Some(staged_oid) = index_gitlinks.get(git_path)
5100 && metadata.is_dir()
5101 {
5102 let oid = gitlink_head_oid(&path, format).unwrap_or(*staged_oid);
5103 return Ok(Some(TrackedEntry {
5104 mode: sley_index::GITLINK_MODE,
5105 oid,
5106 }));
5107 }
5108 if metadata.is_dir() {
5109 if let Some(oid) = gitlink_head_oid(&path, format) {
5110 return Ok(Some(TrackedEntry {
5111 mode: sley_index::GITLINK_MODE,
5112 oid,
5113 }));
5114 }
5115 return Ok(None);
5116 }
5117 if !(metadata.is_file() || file_type.is_symlink()) {
5118 return Ok(None);
5119 }
5120 if let Some(entry) = stat_cache.and_then(|cache| cache.reusable_entry(git_path, &metadata)) {
5121 return Ok(Some(tracked_entry_from_index(entry)));
5122 }
5123 Ok(Some(classify_worktree_entry(&path, &metadata, format)?))
5124}
5125
5126fn index_worktree_change_for_entry(
5127 path: &Path,
5128 format: ObjectFormat,
5129 index_entry: &impl WorktreeIndexEntry,
5130 stat_cache: &IndexStatCache,
5131 stat_clean_validator: &mut Option<&mut IndexWorktreeStatCleanValidator<'_>>,
5132) -> Result<Option<NameStatusEntry>> {
5133 let git_path = index_entry.git_path();
5134 let metadata = match fs::symlink_metadata(path) {
5135 Ok(metadata) => metadata,
5136 Err(err) if is_missing_worktree_path_error(&err) && index_entry.is_skip_worktree() => {
5137 return Ok(None);
5138 }
5139 Err(err) if is_missing_worktree_path_error(&err) => {
5140 return Ok(Some(index_worktree_deleted_entry(index_entry)));
5141 }
5142 Err(err) => return Err(GitError::Io(err.to_string())),
5143 };
5144 let file_type = metadata.file_type();
5145 let right = if metadata.is_dir() {
5146 if sley_index::is_gitlink(index_entry.mode()) {
5147 let oid = gitlink_head_oid(path, format).unwrap_or(index_entry.oid());
5148 Some(TrackedEntry {
5149 mode: sley_index::GITLINK_MODE,
5150 oid,
5151 })
5152 } else {
5153 gitlink_head_oid(path, format).map(|oid| TrackedEntry {
5154 mode: sley_index::GITLINK_MODE,
5155 oid,
5156 })
5157 }
5158 } else if metadata.is_file() || file_type.is_symlink() {
5159 let validated = if let Some(validator) = stat_clean_validator.as_deref_mut() {
5160 validator(
5161 IndexWorktreeValidationEntry {
5162 path: index_entry.git_path(),
5163 mode: index_entry.mode(),
5164 oid: index_entry.oid(),
5165 size: index_entry.size(),
5166 },
5167 path,
5168 &metadata,
5169 )?
5170 } else {
5171 None
5172 };
5173 if index_entry.reusable_with(stat_cache, &metadata) {
5174 return Ok(None);
5175 }
5176 Some(match validated {
5177 Some(entry) => TrackedEntry {
5178 mode: entry.mode,
5179 oid: entry.oid,
5180 },
5181 None => classify_worktree_entry(path, &metadata, format)?,
5182 })
5183 } else {
5184 None
5185 };
5186 let Some(right) = right else {
5187 return Ok(Some(index_worktree_deleted_entry(index_entry)));
5188 };
5189 let left = tracked_entry_from_index(index_entry);
5190 if right == left {
5191 return Ok(None);
5192 }
5193 Ok(Some(NameStatusEntry {
5194 status: modify_or_type_change(left.mode, right.mode),
5195 path: git_path.to_vec().into(),
5196 old_path: None,
5197 old_mode: Some(left.mode),
5198 new_mode: Some(right.mode),
5199 old_oid: Some(left.oid),
5200 new_oid: Some(right.oid),
5201 }))
5202}
5203
5204fn index_worktree_deleted_entry(index_entry: &impl WorktreeIndexEntry) -> NameStatusEntry {
5205 NameStatusEntry {
5206 status: NameStatus::Deleted,
5207 path: index_entry.git_path().to_vec().into(),
5208 old_path: None,
5209 old_mode: Some(index_entry.mode()),
5210 new_mode: None,
5211 old_oid: Some(index_entry.oid()),
5212 new_oid: None,
5213 }
5214}
5215
5216fn is_missing_worktree_path_error(err: &std::io::Error) -> bool {
5217 matches!(
5218 err.kind(),
5219 std::io::ErrorKind::NotFound | std::io::ErrorKind::NotADirectory
5220 )
5221}
5222
5223fn worktree_blob_cache_for_path_set(
5224 worktree_root: &Path,
5225 left_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
5226 right_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
5227 candidate_paths: &BTreeSet<Vec<u8>>,
5228 odb_backed_worktree_entries: Option<&BTreeMap<Vec<u8>, TrackedEntry>>,
5229 options: RenameDetectionOptions,
5230) -> Result<HashMap<ObjectId, Vec<u8>>> {
5231 worktree_blob_cache_for_unique_paths(
5232 worktree_root,
5233 left_entries,
5234 right_entries,
5235 candidate_paths.iter(),
5236 odb_backed_worktree_entries,
5237 options,
5238 )
5239}
5240
5241fn worktree_blob_cache_for_unique_paths<'a>(
5242 worktree_root: &Path,
5243 left_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
5244 right_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
5245 candidate_paths: impl Iterator<Item = &'a Vec<u8>>,
5246 odb_backed_worktree_entries: Option<&BTreeMap<Vec<u8>, TrackedEntry>>,
5247 options: RenameDetectionOptions,
5248) -> Result<HashMap<ObjectId, Vec<u8>>> {
5249 if !options.detect_inexact || !(options.base.detect_renames || options.base.detect_copies) {
5250 return Ok(HashMap::new());
5251 }
5252 let base = options.base;
5253 let mut changes =
5254 raw_name_status_changes_for_unique_paths(left_entries, right_entries, candidate_paths);
5255 if base.detect_renames {
5256 changes = detect_exact_renames(changes, left_entries, right_entries, base.rename_empty);
5257 }
5258 if base.detect_copies {
5259 changes = detect_exact_copies(
5260 changes,
5261 left_entries,
5262 right_entries,
5263 base.find_copies_harder,
5264 base.rename_empty,
5265 );
5266 }
5267 let has_rename_source = base.detect_renames
5268 && changes.iter().any(|entry| {
5269 entry.status == NameStatus::Deleted
5270 && entry
5271 .old_oid
5272 .as_ref()
5273 .is_some_and(|oid| base.rename_empty || !is_empty_blob_oid(oid))
5274 });
5275 let has_copy_source = base.detect_copies
5276 && (base.find_copies_harder
5277 || changes
5278 .iter()
5279 .any(|entry| matches!(entry.status, NameStatus::Deleted | NameStatus::Modified)));
5280 if !has_rename_source && !has_copy_source {
5281 return Ok(HashMap::new());
5282 }
5283 let candidate_oids = changes
5284 .iter()
5285 .filter(|entry| entry.status == NameStatus::Added)
5286 .filter_map(|entry| entry.new_oid)
5287 .filter(|oid| base.rename_empty || !is_empty_blob_oid(oid))
5288 .collect::<BTreeSet<_>>();
5289 if candidate_oids.is_empty() {
5290 return Ok(HashMap::new());
5291 }
5292 let mut cache = HashMap::new();
5293 for (git_path, entry) in right_entries {
5294 if odb_backed_worktree_entries.is_some_and(|entries| entries.contains_key(git_path)) {
5295 continue;
5296 }
5297 if sley_index::is_gitlink(entry.mode) || !candidate_oids.contains(&entry.oid) {
5298 continue;
5299 }
5300 let path = worktree_path_for_repo_path(worktree_root, git_path);
5301 let body = if sley_index::is_symlink_mode(entry.mode) {
5302 symlink_target_bytes(&path)?
5303 } else {
5304 fs::read(&path)?
5305 };
5306 cache.entry(entry.oid).or_insert(body);
5307 }
5308 Ok(cache)
5309}
5310
5311fn cache_or_odb_blob(
5314 cache: &HashMap<ObjectId, Vec<u8>>,
5315 db: &FileObjectDatabase,
5316 oid: &ObjectId,
5317) -> Option<Vec<u8>> {
5318 if let Some(bytes) = cache.get(oid) {
5319 return Some(bytes.clone());
5320 }
5321 read_blob_bytes(db, oid)
5322}
5323
5324#[cfg(unix)]
5325fn worktree_path_for_repo_path(worktree_root: &Path, path: &[u8]) -> PathBuf {
5326 use std::ffi::OsStr;
5327 use std::os::unix::ffi::OsStrExt;
5328
5329 let mut out = PathBuf::from(worktree_root);
5330 out.push(OsStr::from_bytes(path));
5331 out
5332}
5333
5334#[cfg(unix)]
5335fn worktree_path_for_repo_path_into(out: &mut PathBuf, worktree_root: &Path, path: &[u8]) {
5336 use std::ffi::OsStr;
5337 use std::os::unix::ffi::OsStrExt;
5338
5339 out.clear();
5340 out.push(worktree_root);
5341 out.push(OsStr::from_bytes(path));
5342}
5343
5344#[cfg(not(unix))]
5345fn worktree_path_for_repo_path(worktree_root: &Path, path: &[u8]) -> PathBuf {
5346 worktree_root.join(repo_path_to_path(path))
5347}
5348
5349#[cfg(not(unix))]
5350fn worktree_path_for_repo_path_into(out: &mut PathBuf, worktree_root: &Path, path: &[u8]) {
5351 out.clear();
5352 out.push(worktree_root);
5353 out.push(repo_path_to_path(path));
5354}
5355
5356#[cfg(not(unix))]
5357fn repo_path_to_path(path: &[u8]) -> PathBuf {
5358 let mut out = PathBuf::new();
5359 for component in String::from_utf8_lossy(path).split('/') {
5360 if !component.is_empty() {
5361 out.push(component);
5362 }
5363 }
5364 out
5365}
5366
5367#[cfg(unix)]
5368fn file_mode(metadata: &fs::Metadata) -> u32 {
5369 use std::os::unix::fs::PermissionsExt;
5370 if metadata.permissions().mode() & 0o111 != 0 {
5371 0o100755
5372 } else {
5373 0o100644
5374 }
5375}
5376
5377#[cfg(not(unix))]
5378fn file_mode(_metadata: &fs::Metadata) -> u32 {
5379 0o100644
5380}
5381
5382#[cfg(unix)]
5387pub fn symlink_target_bytes(path: &Path) -> Result<Vec<u8>> {
5388 use std::os::unix::ffi::OsStrExt;
5389 let target = fs::read_link(path)?;
5390 Ok(target.as_os_str().as_bytes().to_vec())
5391}
5392
5393#[cfg(not(unix))]
5395pub fn symlink_target_bytes(path: &Path) -> Result<Vec<u8>> {
5396 let target = fs::read_link(path)?;
5397 Ok(target.to_string_lossy().replace('\\', "/").into_bytes())
5398}
5399
5400#[derive(Debug, Clone, PartialEq, Eq)]
5417pub enum HunkLine {
5418 Context(Vec<u8>),
5420 Insert(Vec<u8>),
5422 Delete(Vec<u8>),
5424}
5425
5426impl HunkLine {
5427 pub fn content(&self) -> &[u8] {
5429 match self {
5430 Self::Context(bytes) | Self::Insert(bytes) | Self::Delete(bytes) => bytes,
5431 }
5432 }
5433}
5434
5435#[derive(Debug, Clone, PartialEq, Eq)]
5442pub struct Hunk {
5443 pub old_start: usize,
5444 pub old_len: usize,
5445 pub new_start: usize,
5446 pub new_len: usize,
5447 pub lines: Vec<HunkLine>,
5448 pub old_no_newline: bool,
5450 pub new_no_newline: bool,
5452 pub line_input_lines: Vec<usize>,
5456}
5457
5458#[derive(Debug, Clone, PartialEq, Eq)]
5460pub struct FilePatch {
5461 pub old_path: Option<Vec<u8>>,
5463 pub new_path: Option<Vec<u8>>,
5465 pub old_mode: Option<u32>,
5467 pub new_mode: Option<u32>,
5469 pub hunks: Vec<Hunk>,
5470 pub is_new: bool,
5472 pub is_delete: bool,
5474 pub is_rename: bool,
5476 pub is_copy: bool,
5478 pub similarity: Option<u8>,
5480 pub dissimilarity: Option<u8>,
5482 pub old_oid_hex: Option<Vec<u8>>,
5486 pub new_oid_hex: Option<Vec<u8>>,
5487 pub is_binary: bool,
5491 pub binary: Option<BinaryPatch>,
5496 pub is_toplevel_relative: bool,
5501}
5502
5503#[derive(Debug, Clone, PartialEq, Eq)]
5507pub struct BinaryPatch {
5508 pub forward: BinaryHunk,
5509 pub reverse: Option<BinaryHunk>,
5510}
5511
5512#[derive(Debug, Clone, PartialEq, Eq)]
5515pub struct BinaryHunk {
5516 pub method: BinaryMethod,
5517 pub origlen: usize,
5520 pub deflated: Vec<u8>,
5522}
5523
5524#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5526pub enum BinaryMethod {
5527 Literal,
5529 Delta,
5531}
5532
5533#[derive(Debug, Clone, PartialEq, Eq)]
5535pub enum ApplyOutcome {
5536 Applied(Vec<u8>),
5538 Rejected,
5540}
5541
5542const MIN_FUZZ_CONTEXT: usize = usize::MAX;
5553
5554pub fn parse_unified_patch(input: &[u8]) -> Result<Vec<FilePatch>> {
5562 parse_unified_patch_with_recount(input, false)
5563}
5564
5565pub fn parse_unified_patch_with_recount(input: &[u8], recount: bool) -> Result<Vec<FilePatch>> {
5568 parse_unified_patch_with_options(input, recount, &PatchPathOptions::default())
5569}
5570
5571#[derive(Clone)]
5574pub struct PatchPathOptions {
5575 pub p_value: usize,
5577 pub p_value_known: bool,
5580 pub root: Vec<u8>,
5582 pub prefix: Vec<u8>,
5587}
5588
5589impl Default for PatchPathOptions {
5590 fn default() -> Self {
5591 PatchPathOptions {
5592 p_value: 1,
5593 p_value_known: false,
5594 root: Vec::new(),
5595 prefix: Vec::new(),
5596 }
5597 }
5598}
5599
5600pub fn parse_unified_patch_with_options(
5603 input: &[u8],
5604 recount: bool,
5605 options: &PatchPathOptions,
5606) -> Result<Vec<FilePatch>> {
5607 let lines = split_patch_lines(input);
5608 let mut parser = PatchParser {
5609 lines: &lines,
5610 index: 0,
5611 recount,
5612 p_value: options.p_value,
5613 p_value_known: options.p_value_known,
5614 root: options.root.clone(),
5615 prefix: options.prefix.clone(),
5616 };
5617 parser.parse()
5618}
5619
5620pub fn apply_file_patch(base: &[u8], patch: &FilePatch) -> ApplyOutcome {
5645 apply_file_patch_with_options(base, patch, &ApplyFileOptions::default())
5646}
5647
5648#[derive(Clone, Default)]
5651pub struct ApplyFileOptions {
5652 pub unidiff_zero: bool,
5655}
5656
5657pub fn reverse_file_patch(patch: &FilePatch) -> FilePatch {
5661 let hunks = patch
5662 .hunks
5663 .iter()
5664 .map(|hunk| {
5665 let lines = hunk
5666 .lines
5667 .iter()
5668 .map(|line| match line {
5669 HunkLine::Context(b) => HunkLine::Context(b.clone()),
5670 HunkLine::Insert(b) => HunkLine::Delete(b.clone()),
5671 HunkLine::Delete(b) => HunkLine::Insert(b.clone()),
5672 })
5673 .collect();
5674 Hunk {
5675 old_start: hunk.new_start,
5676 old_len: hunk.new_len,
5677 new_start: hunk.old_start,
5678 new_len: hunk.old_len,
5679 lines,
5680 old_no_newline: hunk.new_no_newline,
5681 new_no_newline: hunk.old_no_newline,
5682 line_input_lines: hunk.line_input_lines.clone(),
5685 }
5686 })
5687 .collect();
5688 let (old_mode, new_mode) = if patch.new_mode.is_some() || patch.is_delete {
5692 (patch.new_mode, patch.old_mode)
5693 } else {
5694 (patch.old_mode, patch.new_mode)
5695 };
5696 FilePatch {
5697 old_path: patch.new_path.clone(),
5698 new_path: patch.old_path.clone(),
5699 old_mode,
5700 new_mode,
5701 hunks,
5702 is_new: patch.is_delete,
5703 is_delete: patch.is_new,
5704 is_rename: patch.is_rename,
5705 is_copy: patch.is_copy,
5706 similarity: patch.similarity,
5707 dissimilarity: patch.dissimilarity,
5708 old_oid_hex: patch.new_oid_hex.clone(),
5711 new_oid_hex: patch.old_oid_hex.clone(),
5712 is_binary: patch.is_binary,
5713 binary: patch.binary.as_ref().map(|binary| BinaryPatch {
5714 forward: binary
5716 .reverse
5717 .clone()
5718 .unwrap_or_else(|| binary.forward.clone()),
5719 reverse: Some(binary.forward.clone()),
5720 }),
5721 is_toplevel_relative: patch.is_toplevel_relative,
5722 }
5723}
5724
5725pub fn apply_file_patch_with_options(
5727 base: &[u8],
5728 patch: &FilePatch,
5729 options: &ApplyFileOptions,
5730) -> ApplyOutcome {
5731 if patch.is_delete && patch.hunks.is_empty() {
5733 return ApplyOutcome::Applied(Vec::new());
5734 }
5735 let base_for_match: &[u8] = if patch.is_new { b"" } else { base };
5738
5739 let mut image = split_blob_lines(base_for_match);
5743
5744 let mut running_offset: isize = 0;
5747
5748 for hunk in &patch.hunks {
5749 match apply_one_hunk(&mut image, hunk, running_offset, options.unidiff_zero) {
5750 Some(drift) => running_offset += drift,
5751 None => return ApplyOutcome::Rejected,
5752 }
5753 }
5754
5755 ApplyOutcome::Applied(join_lines(&image))
5756}
5757
5758#[derive(Debug, Clone, PartialEq, Eq)]
5760pub struct RejectApply {
5761 pub content: Vec<u8>,
5763 pub rejected: Vec<usize>,
5765}
5766
5767pub fn apply_file_patch_rejecting(
5776 base: &[u8],
5777 patch: &FilePatch,
5778 options: &ApplyFileOptions,
5779) -> RejectApply {
5780 if patch.is_delete && patch.hunks.is_empty() {
5781 return RejectApply {
5782 content: Vec::new(),
5783 rejected: Vec::new(),
5784 };
5785 }
5786 let base_for_match: &[u8] = if patch.is_new { b"" } else { base };
5787 let mut image = split_blob_lines(base_for_match);
5788 let mut running_offset: isize = 0;
5789 let mut rejected = Vec::new();
5790 for (index, hunk) in patch.hunks.iter().enumerate() {
5791 match apply_one_hunk(&mut image, hunk, running_offset, options.unidiff_zero) {
5792 Some(drift) => running_offset += drift,
5793 None => rejected.push(index),
5794 }
5795 }
5796 RejectApply {
5797 content: join_lines(&image),
5798 rejected,
5799 }
5800}
5801
5802pub fn render_reject_hunk(hunk: &Hunk) -> Vec<u8> {
5808 fn range(start: usize, count: usize) -> String {
5809 if count == 1 {
5810 start.to_string()
5811 } else {
5812 format!("{start},{count}")
5813 }
5814 }
5815 let mut out = Vec::new();
5816 out.extend_from_slice(b"@@ -");
5817 out.extend_from_slice(range(hunk.old_start, hunk.old_len).as_bytes());
5818 out.extend_from_slice(b" +");
5819 out.extend_from_slice(range(hunk.new_start, hunk.new_len).as_bytes());
5820 out.extend_from_slice(b" @@\n");
5821 let last_old = hunk
5824 .lines
5825 .iter()
5826 .rposition(|line| matches!(line, HunkLine::Context(_) | HunkLine::Delete(_)));
5827 let last_new = hunk
5828 .lines
5829 .iter()
5830 .rposition(|line| matches!(line, HunkLine::Context(_) | HunkLine::Insert(_)));
5831 for (index, line) in hunk.lines.iter().enumerate() {
5832 let (prefix, content) = match line {
5833 HunkLine::Context(bytes) => (b' ', bytes),
5834 HunkLine::Insert(bytes) => (b'+', bytes),
5835 HunkLine::Delete(bytes) => (b'-', bytes),
5836 };
5837 out.push(prefix);
5838 out.extend_from_slice(content);
5839 out.push(b'\n');
5840 let old_incomplete = hunk.old_no_newline && Some(index) == last_old;
5841 let new_incomplete = hunk.new_no_newline && Some(index) == last_new;
5842 if old_incomplete || new_incomplete {
5843 out.extend_from_slice(b"\\ No newline at end of file\n");
5844 }
5845 }
5846 out
5847}
5848
5849#[derive(Clone, Copy)]
5864pub struct WsApplyOptions {
5865 pub unidiff_zero: bool,
5867 pub ws_rule: ws::WsRule,
5869 pub ws_fix: bool,
5871 pub ws_ignore_change: bool,
5873}
5874
5875pub enum WsApplyOutcome {
5877 Applied {
5879 content: Vec<u8>,
5880 blank_at_eof_removed: usize,
5881 },
5882 Rejected,
5884}
5885
5886#[derive(Clone)]
5889struct WsImageLine {
5890 content: Vec<u8>,
5891 no_newline: bool,
5892 common: bool,
5893}
5894
5895impl WsImageLine {
5896 fn bytes(&self) -> Vec<u8> {
5897 let mut out = self.content.clone();
5898 if !self.no_newline {
5899 out.push(b'\n');
5900 }
5901 out
5902 }
5903}
5904
5905fn line_bytes(line: &Line) -> Vec<u8> {
5906 let mut out = line.content.clone();
5907 if !line.no_newline {
5908 out.push(b'\n');
5909 }
5910 out
5911}
5912
5913fn ws_line_from_bytes(bytes: Vec<u8>, common: bool) -> WsImageLine {
5916 if bytes.last() == Some(&b'\n') {
5917 WsImageLine {
5918 content: bytes[..bytes.len() - 1].to_vec(),
5919 no_newline: false,
5920 common,
5921 }
5922 } else {
5923 WsImageLine {
5924 content: bytes,
5925 no_newline: true,
5926 common,
5927 }
5928 }
5929}
5930
5931pub fn apply_file_patch_ws(
5933 base: &[u8],
5934 patch: &FilePatch,
5935 opts: &WsApplyOptions,
5936) -> WsApplyOutcome {
5937 if patch.is_delete && patch.hunks.is_empty() {
5938 return WsApplyOutcome::Applied {
5939 content: Vec::new(),
5940 blank_at_eof_removed: 0,
5941 };
5942 }
5943 let base_for_match: &[u8] = if patch.is_new { b"" } else { base };
5944 let mut image = split_blob_lines(base_for_match);
5945 let mut running_offset: isize = 0;
5946 let mut blank_removed = 0usize;
5947 for hunk in &patch.hunks {
5948 match apply_one_fragment_ws(&mut image, hunk, running_offset, opts, &mut blank_removed) {
5949 Some(drift) => running_offset += drift,
5950 None => return WsApplyOutcome::Rejected,
5951 }
5952 }
5953 WsApplyOutcome::Applied {
5954 content: join_lines(&image),
5955 blank_at_eof_removed: blank_removed,
5956 }
5957}
5958
5959fn apply_one_fragment_ws(
5960 image: &mut Vec<Line>,
5961 hunk: &Hunk,
5962 running_offset: isize,
5963 opts: &WsApplyOptions,
5964 blank_removed: &mut usize,
5965) -> Option<isize> {
5966 let blank_eof = opts.ws_rule & ws::WS_BLANK_AT_EOF != 0;
5967 let mut preimage: Vec<WsImageLine> = Vec::new();
5968 let mut postimage: Vec<WsImageLine> = Vec::new();
5969 let mut leading = 0usize;
5970 let mut trailing = 0usize;
5971 let mut seen_change = false;
5972 let mut new_blank_lines_at_end = 0usize;
5975 for hl in &hunk.lines {
5976 let mut added_blank_line = false;
5977 let mut is_blank_context = false;
5978 match hl {
5979 HunkLine::Context(bytes) => {
5980 if blank_eof && ws::ws_blank_line(bytes) {
5981 is_blank_context = true;
5982 }
5983 preimage.push(WsImageLine {
5984 content: bytes.clone(),
5985 no_newline: false,
5986 common: true,
5987 });
5988 postimage.push(WsImageLine {
5989 content: bytes.clone(),
5990 no_newline: false,
5991 common: true,
5992 });
5993 if !seen_change {
5994 leading += 1;
5995 }
5996 trailing += 1;
5997 }
5998 HunkLine::Delete(bytes) => {
5999 preimage.push(WsImageLine {
6000 content: bytes.clone(),
6001 no_newline: false,
6002 common: false,
6003 });
6004 seen_change = true;
6005 trailing = 0;
6006 }
6007 HunkLine::Insert(bytes) => {
6008 postimage.push(WsImageLine {
6009 content: bytes.clone(),
6010 no_newline: false,
6011 common: false,
6012 });
6013 if blank_eof && ws::ws_blank_line(bytes) {
6014 added_blank_line = true;
6015 }
6016 seen_change = true;
6017 trailing = 0;
6018 }
6019 }
6020 if added_blank_line {
6021 new_blank_lines_at_end += 1;
6022 } else if is_blank_context {
6023 } else {
6025 new_blank_lines_at_end = 0;
6026 }
6027 }
6028 if hunk.old_no_newline
6029 && let Some(last) = preimage.last_mut()
6030 {
6031 last.no_newline = true;
6032 }
6033 if hunk.new_no_newline
6034 && let Some(last) = postimage.last_mut()
6035 {
6036 last.no_newline = true;
6037 }
6038
6039 let mut match_beginning = hunk.old_start == 0 || (hunk.old_start == 1 && !opts.unidiff_zero);
6040 let mut match_end = !opts.unidiff_zero && trailing == 0;
6041
6042 let mut expected = if preimage.is_empty() {
6043 new_side_position(hunk, running_offset)
6044 } else {
6045 expected_position(hunk, running_offset)
6046 };
6047 let hunk_expected = expected;
6048 let mut leading_v = leading;
6049 let mut trailing_v = trailing;
6050
6051 let applied_pos = loop {
6052 if let Some(pos) = find_pos_ws(
6053 image,
6054 &mut preimage,
6055 &mut postimage,
6056 expected,
6057 opts,
6058 match_beginning,
6059 match_end,
6060 ) {
6061 break pos;
6062 }
6063 #[allow(clippy::absurd_extreme_comparisons)]
6064 if leading_v <= MIN_FUZZ_CONTEXT && trailing_v <= MIN_FUZZ_CONTEXT {
6065 return None;
6066 }
6067 if match_beginning || match_end {
6068 match_beginning = false;
6069 match_end = false;
6070 continue;
6071 }
6072 if leading_v >= trailing_v {
6073 preimage.remove(0);
6074 postimage.remove(0);
6075 expected -= 1;
6076 leading_v -= 1;
6077 }
6078 if trailing_v > leading_v {
6079 preimage.pop();
6080 postimage.pop();
6081 trailing_v -= 1;
6082 }
6083 };
6084
6085 if new_blank_lines_at_end > 0
6088 && preimage.len() + applied_pos >= image.len()
6089 && blank_eof
6090 && opts.ws_fix
6091 {
6092 for _ in 0..new_blank_lines_at_end {
6093 postimage.pop();
6094 }
6095 *blank_removed += new_blank_lines_at_end;
6096 }
6097
6098 let preimage_limit = preimage.len().min(image.len() - applied_pos);
6101 let replacement: Vec<Line> = postimage
6102 .iter()
6103 .map(|line| Line {
6104 content: line.content.clone(),
6105 no_newline: line.no_newline,
6106 })
6107 .collect();
6108 image.splice(applied_pos..applied_pos + preimage_limit, replacement);
6109 Some(applied_pos as isize - hunk_expected)
6110}
6111
6112fn find_pos_ws(
6116 image: &[Line],
6117 preimage: &mut Vec<WsImageLine>,
6118 postimage: &mut Vec<WsImageLine>,
6119 expected: isize,
6120 opts: &WsApplyOptions,
6121 match_beginning: bool,
6122 match_end: bool,
6123) -> Option<usize> {
6124 let line_nr = image.len();
6125 let pre_nr = preimage.len();
6126 let mut line: isize = if match_beginning {
6127 0
6128 } else if match_end {
6129 line_nr as isize - pre_nr as isize
6130 } else {
6131 expected
6132 };
6133 if line < 0 {
6134 line = 0;
6135 }
6136 if line as usize > line_nr {
6137 line = line_nr as isize;
6138 }
6139 let start = line as usize;
6140 let mut backwards = start;
6141 let mut forwards = start;
6142 let mut current = start;
6143 let mut i: u64 = 0;
6144 loop {
6145 if match_fragment_ws(
6146 image,
6147 preimage,
6148 postimage,
6149 current,
6150 opts,
6151 match_beginning,
6152 match_end,
6153 ) {
6154 return Some(current);
6155 }
6156 loop {
6157 if backwards == 0 && forwards == line_nr {
6158 return None;
6159 }
6160 if i & 1 == 1 {
6161 if backwards == 0 {
6162 i += 1;
6163 continue;
6164 }
6165 backwards -= 1;
6166 current = backwards;
6167 } else {
6168 if forwards == line_nr {
6169 i += 1;
6170 continue;
6171 }
6172 forwards += 1;
6173 current = forwards;
6174 }
6175 break;
6176 }
6177 i += 1;
6178 }
6179}
6180
6181fn match_fragment_ws(
6187 image: &[Line],
6188 preimage: &mut Vec<WsImageLine>,
6189 postimage: &mut Vec<WsImageLine>,
6190 current_lno: usize,
6191 opts: &WsApplyOptions,
6192 match_beginning: bool,
6193 match_end: bool,
6194) -> bool {
6195 let blank_eof = opts.ws_rule & ws::WS_BLANK_AT_EOF != 0;
6196 let preimage_limit: usize;
6197 if preimage.len() + current_lno <= image.len() {
6198 preimage_limit = preimage.len();
6199 if match_end && (preimage.len() + current_lno != image.len()) {
6200 return false;
6201 }
6202 } else if opts.ws_fix && blank_eof {
6203 preimage_limit = image.len() - current_lno;
6206 } else {
6207 return false;
6208 }
6209
6210 if match_beginning && current_lno != 0 {
6211 return false;
6212 }
6213
6214 if preimage_limit == preimage.len() {
6215 let mut exact = true;
6217 if match_end && current_lno + preimage_limit != image.len() {
6218 exact = false;
6219 }
6220 if exact {
6221 for i in 0..preimage_limit {
6222 let img = &image[current_lno + i];
6223 let pre = &preimage[i];
6224 if img.content != pre.content || img.no_newline != pre.no_newline {
6225 exact = false;
6226 break;
6227 }
6228 }
6229 }
6230 if exact {
6231 return true;
6232 }
6233 } else {
6234 let mut all_blank = true;
6237 for line in preimage.iter().take(preimage_limit) {
6238 if !line.content.iter().all(|&b| ws::is_space(b)) {
6239 all_blank = false;
6240 break;
6241 }
6242 }
6243 if all_blank {
6244 return false;
6245 }
6246 }
6247
6248 if opts.ws_ignore_change {
6250 return fuzzy_match_ws(image, preimage, postimage, current_lno, preimage_limit);
6251 }
6252 if !opts.ws_fix {
6253 return false;
6254 }
6255
6256 let mut fixed: Vec<WsImageLine> = Vec::with_capacity(preimage.len());
6259 for i in 0..preimage_limit {
6260 let fixed_pre = ws::ws_fix_bytes(&preimage[i].bytes(), opts.ws_rule);
6261 let fixed_tgt = ws::ws_fix_bytes(&line_bytes(&image[current_lno + i]), opts.ws_rule);
6262 if fixed_pre != fixed_tgt {
6263 return false;
6264 }
6265 fixed.push(ws_line_from_bytes(fixed_pre, preimage[i].common));
6266 }
6267 for line in preimage.iter().skip(preimage_limit) {
6268 let fixed_pre = ws::ws_fix_bytes(&line.bytes(), opts.ws_rule);
6269 if !fixed_pre.iter().all(|&b| ws::is_space(b)) {
6270 return false;
6271 }
6272 fixed.push(ws_line_from_bytes(fixed_pre, line.common));
6273 }
6274 update_pre_post_images_ws(preimage, postimage, fixed);
6275 true
6276}
6277
6278fn fuzzy_match_ws(
6282 image: &[Line],
6283 preimage: &mut Vec<WsImageLine>,
6284 postimage: &mut Vec<WsImageLine>,
6285 current_lno: usize,
6286 preimage_limit: usize,
6287) -> bool {
6288 for i in 0..preimage_limit {
6289 if !fuzzy_matchlines(&line_bytes(&image[current_lno + i]), &preimage[i].bytes()) {
6290 return false;
6291 }
6292 }
6293 for line in preimage.iter().skip(preimage_limit) {
6295 if !line.bytes().iter().all(|&b| ws::is_space(b)) {
6296 return false;
6297 }
6298 }
6299 let mut fixed: Vec<WsImageLine> = Vec::with_capacity(preimage.len());
6302 for i in 0..preimage_limit {
6303 let img = &image[current_lno + i];
6304 fixed.push(WsImageLine {
6305 content: img.content.clone(),
6306 no_newline: img.no_newline,
6307 common: preimage[i].common,
6308 });
6309 }
6310 for line in preimage.iter().skip(preimage_limit) {
6311 fixed.push(line.clone());
6312 }
6313 update_pre_post_images_ws(preimage, postimage, fixed);
6314 true
6315}
6316
6317fn fuzzy_matchlines(s1: &[u8], s2: &[u8]) -> bool {
6320 let trim = |s: &[u8]| {
6321 let mut end = s.len();
6322 while end > 0 && (s[end - 1] == b'\r' || s[end - 1] == b'\n') {
6323 end -= 1;
6324 }
6325 end
6326 };
6327 let end1 = trim(s1);
6328 let end2 = trim(s2);
6329 let (mut i, mut j) = (0usize, 0usize);
6330 while i < end1 && j < end2 {
6331 if ws::is_space(s1[i]) {
6332 if !ws::is_space(s2[j]) {
6333 return false;
6334 }
6335 while i < end1 && ws::is_space(s1[i]) {
6336 i += 1;
6337 }
6338 while j < end2 && ws::is_space(s2[j]) {
6339 j += 1;
6340 }
6341 } else if s1[i] != s2[j] {
6342 return false;
6343 } else {
6344 i += 1;
6345 j += 1;
6346 }
6347 }
6348 i == end1 && j == end2
6349}
6350
6351fn update_pre_post_images_ws(
6357 preimage: &mut Vec<WsImageLine>,
6358 postimage: &mut Vec<WsImageLine>,
6359 fixed: Vec<WsImageLine>,
6360) {
6361 *preimage = fixed;
6362 let mut new_post: Vec<WsImageLine> = Vec::with_capacity(postimage.len());
6363 let mut ctx = 0usize;
6364 for line in postimage.iter() {
6365 if !line.common {
6366 new_post.push(line.clone());
6367 continue;
6368 }
6369 while ctx < preimage.len() && !preimage[ctx].common {
6370 ctx += 1;
6371 }
6372 if ctx >= preimage.len() {
6373 continue;
6375 }
6376 new_post.push(WsImageLine {
6377 content: preimage[ctx].content.clone(),
6378 no_newline: preimage[ctx].no_newline,
6379 common: true,
6380 });
6381 ctx += 1;
6382 }
6383 *postimage = new_post;
6384}
6385
6386fn apply_one_hunk(
6394 image: &mut Vec<Line>,
6395 hunk: &Hunk,
6396 running_offset: isize,
6397 unidiff_zero: bool,
6398) -> Option<isize> {
6399 let mut preimage: Vec<Line> = Vec::new();
6403 let mut postimage: Vec<Line> = Vec::new();
6404 let mut leading = 0usize; let mut trailing = 0usize; let mut seen_change = false;
6407 for hl in &hunk.lines {
6408 match hl {
6409 HunkLine::Context(bytes) => {
6410 preimage.push(Line {
6411 content: bytes.clone(),
6412 no_newline: false,
6413 });
6414 postimage.push(Line {
6415 content: bytes.clone(),
6416 no_newline: false,
6417 });
6418 if !seen_change {
6419 leading += 1;
6420 }
6421 trailing += 1;
6422 }
6423 HunkLine::Delete(bytes) => {
6424 preimage.push(Line {
6425 content: bytes.clone(),
6426 no_newline: false,
6427 });
6428 seen_change = true;
6429 trailing = 0;
6430 }
6431 HunkLine::Insert(bytes) => {
6432 postimage.push(Line {
6433 content: bytes.clone(),
6434 no_newline: false,
6435 });
6436 seen_change = true;
6437 trailing = 0;
6438 }
6439 }
6440 }
6441
6442 if hunk.old_no_newline
6446 && let Some(last) = preimage.last_mut()
6447 {
6448 last.no_newline = true;
6449 }
6450 if hunk.new_no_newline
6451 && let Some(last) = postimage.last_mut()
6452 {
6453 last.no_newline = true;
6454 }
6455
6456 let mut match_beginning = hunk.old_start == 0 || (hunk.old_start == 1 && !unidiff_zero);
6462 let mut match_end = !unidiff_zero && trailing == 0;
6463
6464 let mut expected = if preimage.is_empty() {
6473 new_side_position(hunk, running_offset)
6474 } else {
6475 expected_position(hunk, running_offset)
6476 };
6477 let hunk_expected = expected;
6480
6481 loop {
6482 if let Some(pos) = find_hunk_pos(image, &preimage, expected, match_beginning, match_end) {
6483 let take = preimage.len();
6485 let replacement: Vec<Line> = postimage.clone();
6486 image.splice(pos..pos + take, replacement);
6487 return Some(pos as isize - hunk_expected);
6488 }
6489
6490 #[allow(clippy::absurd_extreme_comparisons)]
6499 if leading <= MIN_FUZZ_CONTEXT && trailing <= MIN_FUZZ_CONTEXT {
6500 return None;
6501 }
6502
6503 if match_beginning || match_end {
6506 match_beginning = false;
6507 match_end = false;
6508 continue;
6509 }
6510
6511 if leading >= trailing {
6513 preimage.remove(0);
6515 postimage.remove(0);
6516 expected -= 1;
6517 leading -= 1;
6518 }
6519 if trailing > leading {
6520 preimage.pop();
6521 postimage.pop();
6522 trailing -= 1;
6523 }
6524 }
6525}
6526
6527#[derive(Debug, Clone, PartialEq, Eq)]
6529struct Line {
6530 content: Vec<u8>,
6531 no_newline: bool,
6532}
6533
6534fn split_blob_lines(data: &[u8]) -> Vec<Line> {
6539 let mut lines = Vec::new();
6540 let mut start = 0usize;
6541 while start < data.len() {
6542 match data[start..].iter().position(|&b| b == b'\n') {
6543 Some(rel) => {
6544 let end = start + rel;
6545 lines.push(Line {
6546 content: data[start..end].to_vec(),
6547 no_newline: false,
6548 });
6549 start = end + 1;
6550 }
6551 None => {
6552 lines.push(Line {
6553 content: data[start..].to_vec(),
6554 no_newline: true,
6555 });
6556 start = data.len();
6557 }
6558 }
6559 }
6560 lines
6561}
6562
6563fn join_lines(lines: &[Line]) -> Vec<u8> {
6565 let mut out = Vec::new();
6566 for line in lines {
6567 out.extend_from_slice(&line.content);
6568 if !line.no_newline {
6569 out.push(b'\n');
6570 }
6571 }
6572 out
6573}
6574
6575fn expected_position(hunk: &Hunk, running_offset: isize) -> isize {
6578 let base = if hunk.old_start == 0 {
6580 0
6581 } else {
6582 hunk.old_start as isize - 1
6583 };
6584 base + running_offset
6585}
6586
6587fn new_side_position(hunk: &Hunk, running_offset: isize) -> isize {
6590 let base = if hunk.new_start == 0 {
6591 0
6592 } else {
6593 hunk.new_start as isize - 1
6594 };
6595 base + running_offset
6596}
6597
6598fn find_hunk_pos(
6606 image: &[Line],
6607 preimage: &[Line],
6608 expected: isize,
6609 match_beginning: bool,
6610 match_end: bool,
6611) -> Option<usize> {
6612 let line_nr = image.len();
6613 let pre_nr = preimage.len();
6614
6615 let mut line: isize = if match_beginning {
6618 0
6619 } else if match_end {
6620 line_nr as isize - pre_nr as isize
6621 } else {
6622 expected
6623 };
6624 if line < 0 {
6625 line = 0;
6626 }
6627 if line as usize > line_nr {
6628 line = line_nr as isize;
6629 }
6630
6631 let start = line as usize;
6632 let mut backwards = start;
6633 let mut forwards = start;
6634 let mut current = start;
6635
6636 let mut i: u64 = 0;
6637 loop {
6638 if preimage_matches_at(image, preimage, current, match_beginning, match_end) {
6639 return Some(current);
6640 }
6641
6642 loop {
6643 if backwards == 0 && forwards == line_nr {
6645 return None;
6646 }
6647 if i & 1 == 1 {
6648 if backwards == 0 {
6650 i += 1;
6651 continue;
6652 }
6653 backwards -= 1;
6654 current = backwards;
6655 } else {
6656 if forwards == line_nr {
6658 i += 1;
6659 continue;
6660 }
6661 forwards += 1;
6662 current = forwards;
6663 }
6664 break;
6665 }
6666 i += 1;
6667 }
6668}
6669
6670fn preimage_matches_at(
6679 image: &[Line],
6680 preimage: &[Line],
6681 pos: usize,
6682 match_beginning: bool,
6683 match_end: bool,
6684) -> bool {
6685 if match_beginning && pos != 0 {
6686 return false;
6687 }
6688 if pos + preimage.len() > image.len() {
6690 return false;
6691 }
6692 if match_end && pos + preimage.len() != image.len() {
6693 return false;
6694 }
6695 for (i, pre) in preimage.iter().enumerate() {
6696 let img = &image[pos + i];
6697 if img.content != pre.content {
6698 return false;
6699 }
6700 if pre.no_newline != img.no_newline {
6706 return false;
6707 }
6708 }
6709 true
6710}
6711
6712fn split_patch_lines(input: &[u8]) -> Vec<&[u8]> {
6716 let mut lines = Vec::new();
6717 let mut start = 0usize;
6718 while start < input.len() {
6719 match input[start..].iter().position(|&b| b == b'\n') {
6720 Some(rel) => {
6721 let end = start + rel;
6722 lines.push(&input[start..end]);
6723 start = end + 1;
6724 }
6725 None => {
6726 lines.push(&input[start..]);
6727 start = input.len();
6728 }
6729 }
6730 }
6731 lines
6732}
6733
6734struct PatchParser<'a> {
6735 lines: &'a [&'a [u8]],
6736 index: usize,
6737 recount: bool,
6738 p_value: usize,
6741 p_value_known: bool,
6742 root: Vec<u8>,
6744 prefix: Vec<u8>,
6747}
6748
6749impl<'a> PatchParser<'a> {
6750 fn parse(&mut self) -> Result<Vec<FilePatch>> {
6751 let mut patches = Vec::new();
6752 while self.index < self.lines.len() {
6753 let line = self.lines[self.index];
6754 if line.starts_with(b"diff --git ") {
6755 patches.push(self.parse_file(Some(line))?);
6756 } else if line.starts_with(b"--- ") {
6757 patches.push(self.parse_file(None)?);
6759 } else if line.starts_with(b"@@ ") {
6760 return Err(GitError::InvalidFormat(
6761 "hunk header encountered before any file header".to_string(),
6762 ));
6763 } else {
6764 self.index += 1;
6766 }
6767 }
6768 Ok(patches)
6769 }
6770
6771 fn parse_file(&mut self, diff_line: Option<&[u8]>) -> Result<FilePatch> {
6775 match diff_line {
6776 Some(diff_line) => self.parse_git_file(diff_line),
6777 None => self.parse_traditional_file(),
6778 }
6779 }
6780
6781 fn p_minus_one(&self) -> usize {
6785 self.p_value.saturating_sub(1)
6786 }
6787
6788 fn parse_git_file(&mut self, diff_line: &[u8]) -> Result<FilePatch> {
6791 let mut patch = empty_file_patch();
6792 let rest = &diff_line[b"diff --git ".len()..];
6795 let mut def_name = name::git_header_name(self.p_value, rest);
6796 if let (Some(d), false) = (def_name.as_mut(), self.root.is_empty()) {
6797 let mut s = self.root.clone();
6798 s.extend_from_slice(d);
6799 *d = s;
6800 }
6801 self.index += 1;
6802
6803 patch.is_toplevel_relative = true;
6806
6807 let mut binary_seen = false;
6810
6811 while self.index < self.lines.len() {
6813 let line = self.lines[self.index];
6814 if line.starts_with(b"--- ") {
6815 self.parse_git_old_header(&line[b"--- ".len()..], &mut patch);
6816 self.index += 1;
6817 break;
6818 } else if line.starts_with(b"@@ ") {
6819 break;
6821 } else if line.starts_with(b"diff --git ") {
6822 break;
6824 } else if let Some(rest) = strip_prefix(line, b"old mode ") {
6825 patch.old_mode = Some(self.parse_mode_line(rest)?);
6826 } else if let Some(rest) = strip_prefix(line, b"new mode ") {
6827 patch.new_mode = Some(self.parse_mode_line(rest)?);
6828 } else if let Some(rest) = strip_prefix(line, b"new file mode ") {
6829 patch.is_new = true;
6830 patch.new_mode = Some(self.parse_mode_line(rest)?);
6831 patch.new_path = def_name.clone();
6832 } else if let Some(rest) = strip_prefix(line, b"deleted file mode ") {
6833 patch.is_delete = true;
6834 patch.old_mode = Some(self.parse_mode_line(rest)?);
6835 patch.old_path = def_name.clone();
6836 } else if let Some(rest) = strip_prefix(line, b"index ") {
6837 self.parse_index_line(rest, &mut patch)?;
6841 } else if let Some(rest) = strip_prefix(line, b"rename from ") {
6842 patch.is_rename = true;
6843 patch.old_path = name::find_name(rest, None, self.p_minus_one(), 0, &self.root);
6844 } else if let Some(rest) = strip_prefix(line, b"rename to ") {
6845 patch.is_rename = true;
6846 patch.new_path = name::find_name(rest, None, self.p_minus_one(), 0, &self.root);
6847 } else if let Some(rest) = strip_prefix(line, b"copy from ") {
6848 patch.is_copy = true;
6849 patch.old_path = name::find_name(rest, None, self.p_minus_one(), 0, &self.root);
6850 } else if let Some(rest) = strip_prefix(line, b"copy to ") {
6851 patch.is_copy = true;
6852 patch.new_path = name::find_name(rest, None, self.p_minus_one(), 0, &self.root);
6853 } else if let Some(rest) = strip_prefix(line, b"similarity index ") {
6854 patch.similarity = parse_percent(rest);
6855 } else if let Some(rest) = strip_prefix(line, b"dissimilarity index ") {
6856 patch.dissimilarity = parse_percent(rest);
6857 } else if line == b"GIT binary patch" {
6858 let gitbin_line = self.index + 1;
6860 patch.is_binary = true;
6861 patch.binary = Some(self.parse_binary_block(gitbin_line)?);
6862 binary_seen = true;
6863 break;
6864 } else if apply_is_binary_files_differ(line) {
6865 patch.is_binary = true;
6868 binary_seen = true;
6869 self.index += 1;
6870 break;
6871 } else {
6872 self.index += 1;
6874 continue;
6875 }
6876 self.index += 1;
6877 }
6878
6879 if !binary_seen
6881 && self.index < self.lines.len()
6882 && self.lines[self.index].starts_with(b"+++ ")
6883 {
6884 let line = self.lines[self.index];
6885 self.parse_git_new_header(&line[b"+++ ".len()..], &mut patch);
6886 self.index += 1;
6887 }
6888
6889 if patch.old_path.is_none() && patch.new_path.is_none() {
6892 match &def_name {
6893 Some(d) => {
6894 patch.old_path = Some(d.clone());
6895 patch.new_path = Some(d.clone());
6896 }
6897 None => {
6898 return Err(GitError::InvalidFormat(format!(
6899 "git diff header lacks filename information when removing {} \
6900 leading pathname components",
6901 self.p_value
6902 )));
6903 }
6904 }
6905 }
6906
6907 if !binary_seen {
6909 self.parse_hunks(&mut patch)?;
6910 }
6911 Ok(patch)
6912 }
6913
6914 fn parse_index_line(&self, rest: &[u8], patch: &mut FilePatch) -> Result<()> {
6917 let Some(dotdot) = find_subslice(rest, b"..") else {
6918 return Ok(());
6919 };
6920 let old = &rest[..dotdot];
6921 let after = &rest[dotdot + 2..];
6922 let (new, mode_part) = match after.iter().position(|&b| b == b' ') {
6924 Some(space) => (&after[..space], Some(&after[space + 1..])),
6925 None => (after, None),
6926 };
6927 if !old.is_empty() {
6928 patch.old_oid_hex = Some(old.to_vec());
6929 }
6930 if !new.is_empty() {
6931 patch.new_oid_hex = Some(new.to_vec());
6932 }
6933 if let Some(mode) = mode_part
6934 && !mode.is_empty()
6935 {
6936 patch.old_mode = Some(self.parse_mode_line(mode)?);
6937 }
6938 Ok(())
6939 }
6940
6941 fn parse_mode_line(&self, rest: &[u8]) -> Result<u32> {
6944 let mut value: u32 = 0;
6945 let mut i = 0;
6946 while i < rest.len() && (b'0'..=b'7').contains(&rest[i]) {
6947 value = value
6948 .checked_mul(8)
6949 .and_then(|value| value.checked_add((rest[i] - b'0') as u32))
6950 .ok_or_else(|| self.invalid_mode_error(rest))?;
6951 i += 1;
6952 }
6953 if i == 0 || (i < rest.len() && !rest[i].is_ascii_whitespace()) {
6954 return Err(self.invalid_mode_error(rest));
6955 }
6956 Ok(value)
6957 }
6958
6959 fn invalid_mode_error(&self, rest: &[u8]) -> GitError {
6960 GitError::InvalidFormat(format!(
6961 "invalid mode on line {}: {}",
6962 self.index + 1,
6963 lossy(rest)
6964 ))
6965 }
6966
6967 fn parse_binary_block(&mut self, gitbin_line: usize) -> Result<BinaryPatch> {
6972 self.index += 1;
6974 let forward = match self.parse_binary_hunk()? {
6975 Some(hunk) => hunk,
6976 None => {
6977 return Err(GitError::InvalidFormat(format!(
6978 "binary-unrecognized:{gitbin_line}"
6979 )));
6980 }
6981 };
6982 let reverse = self.parse_binary_hunk()?;
6983 Ok(BinaryPatch { forward, reverse })
6984 }
6985
6986 fn parse_binary_hunk(&mut self) -> Result<Option<BinaryHunk>> {
6989 if self.index >= self.lines.len() {
6990 return Ok(None);
6991 }
6992 let line = self.lines[self.index];
6993 let (method, num) = if let Some(rest) = strip_prefix(line, b"delta ") {
6994 (BinaryMethod::Delta, rest)
6995 } else if let Some(rest) = strip_prefix(line, b"literal ") {
6996 (BinaryMethod::Literal, rest)
6997 } else {
6998 return Ok(None);
6999 };
7000 let origlen = parse_leading_usize(num);
7001 self.index += 1;
7002
7003 let mut deflated = Vec::new();
7004 loop {
7005 if self.index >= self.lines.len() {
7006 return Err(self.corrupt_binary_error());
7008 }
7009 let data = self.lines[self.index];
7010 if data.is_empty() {
7011 self.index += 1;
7013 break;
7014 }
7015 let len = data.len();
7018 if len < 6 || !(len - 1).is_multiple_of(5) {
7019 return Err(self.corrupt_binary_error());
7020 }
7021 let max_byte_length = (len - 1) / 5 * 4;
7022 let byte_length = match data[0] {
7023 b'A'..=b'Z' => (data[0] - b'A') as usize + 1,
7024 b'a'..=b'z' => (data[0] - b'a') as usize + 27,
7025 _ => return Err(self.corrupt_binary_error()),
7026 };
7027 if max_byte_length < byte_length || byte_length <= max_byte_length.saturating_sub(4) {
7028 return Err(self.corrupt_binary_error());
7029 }
7030 let decoded = decode_base85(&data[1..], byte_length)
7031 .ok_or_else(|| self.corrupt_binary_error())?;
7032 deflated.extend_from_slice(&decoded);
7033 self.index += 1;
7034 }
7035 Ok(Some(BinaryHunk {
7036 method,
7037 origlen,
7038 deflated,
7039 }))
7040 }
7041
7042 fn corrupt_binary_error(&self) -> GitError {
7043 GitError::InvalidFormat(format!("binary-corrupt:{}", self.index + 1))
7044 }
7045
7046 fn parse_git_old_header(&self, rest: &[u8], patch: &mut FilePatch) {
7047 if name::is_dev_null(rest) {
7048 patch.is_new = true;
7049 patch.old_path = None;
7050 } else if patch.old_path.is_none() {
7051 patch.old_path = name::find_name(rest, None, self.p_value, name::TERM_TAB, &self.root);
7052 }
7053 }
7054
7055 fn parse_git_new_header(&self, rest: &[u8], patch: &mut FilePatch) {
7056 if name::is_dev_null(rest) {
7057 patch.is_delete = true;
7058 patch.new_path = None;
7059 } else if patch.new_path.is_none() {
7060 patch.new_path = name::find_name(rest, None, self.p_value, name::TERM_TAB, &self.root);
7061 }
7062 }
7063
7064 fn parse_traditional_file(&mut self) -> Result<FilePatch> {
7068 let mut patch = empty_file_patch();
7069 let first_line = self.lines[self.index];
7070 let first = first_line[b"--- ".len()..].to_vec();
7071 self.index += 1;
7072 let second = if self.index < self.lines.len() && self.lines[self.index].starts_with(b"+++ ")
7073 {
7074 let s = self.lines[self.index][b"+++ ".len()..].to_vec();
7075 self.index += 1;
7076 Some(s)
7077 } else {
7078 None
7079 };
7080
7081 if let Some(second) = &second {
7082 if !self.p_value_known {
7083 let p0 = name::guess_p_value(&first, &self.root, &self.prefix);
7084 let q0 = name::guess_p_value(second, &self.root, &self.prefix);
7085 let p = if p0.is_none() { q0 } else { p0 };
7086 if let Some(pv) = p
7087 && Some(pv) == q0
7088 {
7089 self.p_value = pv;
7090 self.p_value_known = true;
7091 }
7092 }
7093
7094 let name = if name::is_dev_null(&first) {
7095 patch.is_new = true;
7096 let name = name::find_name_traditional(second, None, self.p_value, &self.root);
7097 patch.new_path = name.clone();
7098 name
7099 } else if name::is_dev_null(second) {
7100 patch.is_delete = true;
7101 let name = name::find_name_traditional(&first, None, self.p_value, &self.root);
7102 patch.old_path = name.clone();
7103 name
7104 } else {
7105 let first_name =
7106 name::find_name_traditional(&first, None, self.p_value, &self.root);
7107 let name = name::find_name_traditional(
7108 second,
7109 first_name.as_deref(),
7110 self.p_value,
7111 &self.root,
7112 );
7113 if name::has_epoch_timestamp(&first) {
7114 patch.is_new = true;
7115 patch.new_path = name.clone();
7116 } else if name::has_epoch_timestamp(second) {
7117 patch.is_delete = true;
7118 patch.old_path = name.clone();
7119 } else {
7120 patch.old_path = name.clone();
7121 patch.new_path = name.clone();
7122 }
7123 name
7124 };
7125 if name.is_none() {
7130 return Err(GitError::InvalidFormat(format!(
7131 "unable to find filename in patch at line {}",
7132 self.index
7133 )));
7134 }
7135 }
7136
7137 self.parse_hunks(&mut patch)?;
7138 Ok(patch)
7139 }
7140
7141 fn parse_hunks(&mut self, patch: &mut FilePatch) -> Result<()> {
7144 while self.index < self.lines.len() {
7145 let line = self.lines[self.index];
7146 if line.starts_with(b"@@ -") {
7152 let hunk = self.parse_hunk()?;
7153 patch.hunks.push(hunk);
7154 } else if line.starts_with(b"diff --git ") {
7155 break;
7156 } else if line.starts_with(b"--- ") {
7157 break;
7159 } else {
7160 self.index += 1;
7162 }
7163 }
7164 Ok(())
7165 }
7166
7167 fn parse_hunk(&mut self) -> Result<Hunk> {
7168 let header = self.lines[self.index];
7169 let (old_start, old_len, new_start, new_len) = parse_hunk_header(header)?;
7170 self.index += 1;
7171
7172 let mut hunk = Hunk {
7173 old_start,
7174 old_len,
7175 new_start,
7176 new_len,
7177 lines: Vec::new(),
7178 old_no_newline: false,
7179 new_no_newline: false,
7180 line_input_lines: Vec::new(),
7181 };
7182 let mut old_seen = 0usize;
7183 let mut new_seen = 0usize;
7184
7185 while self.index < self.lines.len() {
7186 if !self.recount && old_seen >= old_len && new_seen >= new_len {
7190 break;
7191 }
7192 let line = self.lines[self.index];
7193 if self.recount
7194 && (line.starts_with(b"@@ ")
7195 || line.starts_with(b"diff --git ")
7196 || line.starts_with(b"diff a/")
7197 || line.starts_with(b"--- "))
7198 {
7199 break;
7200 }
7201 if line.is_empty() {
7202 hunk.lines.push(HunkLine::Context(Vec::new()));
7206 hunk.line_input_lines.push(self.index + 1);
7207 old_seen += 1;
7208 new_seen += 1;
7209 self.index += 1;
7210 continue;
7211 }
7212 match line[0] {
7213 b' ' => {
7214 hunk.lines.push(HunkLine::Context(line[1..].to_vec()));
7215 hunk.line_input_lines.push(self.index + 1);
7216 old_seen += 1;
7217 new_seen += 1;
7218 }
7219 b'+' => {
7220 hunk.lines.push(HunkLine::Insert(line[1..].to_vec()));
7221 hunk.line_input_lines.push(self.index + 1);
7222 new_seen += 1;
7223 }
7224 b'-' => {
7225 hunk.lines.push(HunkLine::Delete(line[1..].to_vec()));
7226 hunk.line_input_lines.push(self.index + 1);
7227 old_seen += 1;
7228 }
7229 b'\\' => {
7230 self.mark_no_newline(&mut hunk);
7233 self.index += 1;
7234 continue;
7235 }
7236 _ => {
7237 return Err(GitError::InvalidFormat(format!(
7238 "corrupt-hunk-body:{}",
7239 self.index + 1
7240 )));
7241 }
7242 }
7243 self.index += 1;
7244 }
7245
7246 if self.index < self.lines.len() && self.lines[self.index].starts_with(b"\\") {
7249 self.mark_no_newline(&mut hunk);
7250 self.index += 1;
7251 }
7252
7253 if self.recount {
7254 hunk.old_len = old_seen;
7255 hunk.new_len = new_seen;
7256 } else if old_seen != old_len || new_seen != new_len {
7257 return Err(GitError::InvalidFormat(format!(
7258 "hunk body line counts mismatch: header declared -{old_len},+{new_len} \
7259 but body had -{old_seen},+{new_seen}"
7260 )));
7261 }
7262
7263 Ok(hunk)
7264 }
7265
7266 fn mark_no_newline(&self, hunk: &mut Hunk) {
7269 match hunk.lines.last() {
7270 Some(HunkLine::Context(_)) => {
7271 hunk.old_no_newline = true;
7272 hunk.new_no_newline = true;
7273 }
7274 Some(HunkLine::Insert(_)) => hunk.new_no_newline = true,
7275 Some(HunkLine::Delete(_)) => hunk.old_no_newline = true,
7276 None => {}
7277 }
7278 }
7279}
7280
7281fn empty_file_patch() -> FilePatch {
7283 FilePatch {
7284 old_path: None,
7285 new_path: None,
7286 old_mode: None,
7287 new_mode: None,
7288 hunks: Vec::new(),
7289 is_new: false,
7290 is_delete: false,
7291 is_rename: false,
7292 is_copy: false,
7293 similarity: None,
7294 dissimilarity: None,
7295 old_oid_hex: None,
7296 new_oid_hex: None,
7297 is_binary: false,
7298 binary: None,
7299 is_toplevel_relative: false,
7300 }
7301}
7302
7303fn parse_hunk_header(line: &[u8]) -> Result<(usize, usize, usize, usize)> {
7306 let err = || GitError::InvalidFormat(format!("malformed hunk header: {}", lossy(line)));
7307 let rest = strip_prefix(line, b"@@ ").ok_or_else(err)?;
7308 let close = find_subslice(rest, b" @@").ok_or_else(err)?;
7310 let ranges = &rest[..close];
7311 let mut parts = ranges.split(|&b| b == b' ').filter(|p| !p.is_empty());
7312 let old = parts.next().ok_or_else(err)?;
7313 let new = parts.next().ok_or_else(err)?;
7314 let old = strip_prefix(old, b"-").ok_or_else(err)?;
7315 let new = strip_prefix(new, b"+").ok_or_else(err)?;
7316 let (old_start, old_len) = parse_range(old).ok_or_else(err)?;
7317 let (new_start, new_len) = parse_range(new).ok_or_else(err)?;
7318 Ok((old_start, old_len, new_start, new_len))
7319}
7320
7321fn parse_range(range: &[u8]) -> Option<(usize, usize)> {
7323 match range.iter().position(|&b| b == b',') {
7324 Some(comma) => {
7325 let start = parse_usize(&range[..comma])?;
7326 let len = parse_usize(&range[comma + 1..])?;
7327 Some((start, len))
7328 }
7329 None => Some((parse_usize(range)?, 1)),
7330 }
7331}
7332
7333fn parse_usize(bytes: &[u8]) -> Option<usize> {
7334 if bytes.is_empty() {
7335 return None;
7336 }
7337 let mut value: usize = 0;
7338 for &b in bytes {
7339 if !b.is_ascii_digit() {
7340 return None;
7341 }
7342 value = value.checked_mul(10)?.checked_add((b - b'0') as usize)?;
7343 }
7344 Some(value)
7345}
7346
7347fn parse_percent(bytes: &[u8]) -> Option<u8> {
7348 let trimmed = trim_ascii_end(bytes)
7349 .strip_suffix(b"%")
7350 .unwrap_or(trim_ascii_end(bytes));
7351 let value = parse_usize(trimmed)?;
7352 u8::try_from(value).ok().filter(|value| *value <= 100)
7353}
7354
7355fn strip_prefix<'b>(line: &'b [u8], prefix: &[u8]) -> Option<&'b [u8]> {
7356 if line.starts_with(prefix) {
7357 Some(&line[prefix.len()..])
7358 } else {
7359 None
7360 }
7361}
7362
7363fn apply_is_binary_files_differ(line: &[u8]) -> bool {
7366 line.ends_with(b" differ")
7367 && (line.starts_with(b"Binary files ") || line.starts_with(b"Files "))
7368}
7369
7370fn parse_leading_usize(bytes: &[u8]) -> usize {
7373 let mut value = 0usize;
7374 for &b in bytes {
7375 if !b.is_ascii_digit() {
7376 break;
7377 }
7378 value = value.saturating_mul(10).saturating_add((b - b'0') as usize);
7379 }
7380 value
7381}
7382
7383const BASE85_ALPHABET: &[u8; 85] =
7385 b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~";
7386
7387fn base85_value(ch: u8) -> Option<u32> {
7388 BASE85_ALPHABET
7389 .iter()
7390 .position(|&c| c == ch)
7391 .map(|index| index as u32)
7392}
7393
7394fn decode_base85(buffer: &[u8], len: usize) -> Option<Vec<u8>> {
7398 let mut out = Vec::with_capacity(len);
7399 let mut pos = 0usize;
7400 let mut remaining = len;
7401 while remaining > 0 {
7402 let mut acc: u32 = 0;
7403 for _ in 0..4 {
7405 let de = base85_value(*buffer.get(pos)?)?;
7406 pos += 1;
7407 acc = acc * 85 + de;
7408 }
7409 let de = base85_value(*buffer.get(pos)?)?;
7410 pos += 1;
7411 if 0xffff_ffffu32 / 85 < acc {
7413 return None;
7414 }
7415 acc *= 85;
7416 if 0xffff_ffffu32 - de < acc {
7417 return None;
7418 }
7419 acc += de;
7420
7421 let cnt = remaining.min(4);
7422 remaining -= cnt;
7423 let bytes = acc.to_be_bytes();
7424 out.extend_from_slice(&bytes[..cnt]);
7425 }
7426 Some(out)
7427}
7428
7429pub fn git_patch_delta(base: &[u8], delta: &[u8]) -> Option<Vec<u8>> {
7434 let mut data = 0usize;
7435 let read_hdr_size = |data: &mut usize| -> Option<usize> {
7436 let mut size = 0usize;
7437 let mut shift = 0u32;
7438 loop {
7439 let cmd = *delta.get(*data)?;
7440 *data += 1;
7441 size |= ((cmd & 0x7f) as usize).checked_shl(shift)?;
7442 shift += 7;
7443 if cmd & 0x80 == 0 {
7444 break;
7445 }
7446 }
7447 Some(size)
7448 };
7449
7450 let base_size = read_hdr_size(&mut data)?;
7451 if base_size != base.len() {
7452 return None;
7453 }
7454 let result_size = read_hdr_size(&mut data)?;
7455 let mut out = Vec::with_capacity(result_size);
7456
7457 while data < delta.len() {
7458 let cmd = delta[data];
7459 data += 1;
7460 if cmd & 0x80 != 0 {
7461 let mut cp_off = 0usize;
7463 let mut cp_size = 0usize;
7464 if cmd & 0x01 != 0 {
7465 cp_off = *delta.get(data)? as usize;
7466 data += 1;
7467 }
7468 if cmd & 0x02 != 0 {
7469 cp_off |= (*delta.get(data)? as usize) << 8;
7470 data += 1;
7471 }
7472 if cmd & 0x04 != 0 {
7473 cp_off |= (*delta.get(data)? as usize) << 16;
7474 data += 1;
7475 }
7476 if cmd & 0x08 != 0 {
7477 cp_off |= (*delta.get(data)? as usize) << 24;
7478 data += 1;
7479 }
7480 if cmd & 0x10 != 0 {
7481 cp_size = *delta.get(data)? as usize;
7482 data += 1;
7483 }
7484 if cmd & 0x20 != 0 {
7485 cp_size |= (*delta.get(data)? as usize) << 8;
7486 data += 1;
7487 }
7488 if cmd & 0x40 != 0 {
7489 cp_size |= (*delta.get(data)? as usize) << 16;
7490 data += 1;
7491 }
7492 if cp_size == 0 {
7493 cp_size = 0x10000;
7494 }
7495 let end = cp_off.checked_add(cp_size)?;
7496 if end > base.len() || cp_size > result_size {
7497 return None;
7498 }
7499 out.extend_from_slice(&base[cp_off..end]);
7500 } else if cmd != 0 {
7501 let len = cmd as usize;
7503 let end = data.checked_add(len)?;
7504 if end > delta.len() {
7505 return None;
7506 }
7507 out.extend_from_slice(&delta[data..end]);
7508 data = end;
7509 } else {
7510 return None;
7512 }
7513 }
7514
7515 if data != delta.len() || out.len() != result_size {
7516 return None;
7517 }
7518 Some(out)
7519}
7520
7521fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> {
7522 if needle.is_empty() || needle.len() > haystack.len() {
7523 return None;
7524 }
7525 haystack
7526 .windows(needle.len())
7527 .position(|window| window == needle)
7528}
7529
7530fn trim_ascii_end(bytes: &[u8]) -> &[u8] {
7531 let mut end = bytes.len();
7532 while end > 0 && (bytes[end - 1] == b' ' || bytes[end - 1] == b'\r') {
7533 end -= 1;
7534 }
7535 &bytes[..end]
7536}
7537
7538fn lossy(bytes: &[u8]) -> String {
7539 String::from_utf8_lossy(bytes).into_owned()
7540}
7541
7542pub type MergeEntryMap = BTreeMap<Vec<u8>, (u32, ObjectId)>;
7565
7566#[derive(Clone, Copy, PartialEq, Eq, Debug)]
7569pub enum MergeFavor {
7570 None,
7572 Ours,
7574 Theirs,
7576 Union,
7579}
7580
7581pub struct MergeTreesOptions<'a> {
7583 pub ours_label: &'a str,
7585 pub theirs_label: &'a str,
7587 pub ancestor_label: &'a str,
7590 pub favor: MergeFavor,
7592 pub path_favor: Option<&'a dyn Fn(&[u8]) -> MergeFavor>,
7596 pub path_marker_size: Option<&'a dyn Fn(&[u8]) -> usize>,
7599 pub detect_renames: bool,
7603 pub rename_threshold: u8,
7605 pub rename_limit: usize,
7609 pub directory_renames: DirectoryRenames,
7615 pub style: ConflictStyle,
7617 pub ws_ignore: WsIgnore,
7620}
7621
7622#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
7625pub enum DirectoryRenames {
7626 #[default]
7628 False,
7629 True,
7631 Conflict,
7634}
7635
7636impl Default for MergeTreesOptions<'_> {
7637 fn default() -> Self {
7638 Self {
7639 ours_label: "ours",
7640 theirs_label: "theirs",
7641 ancestor_label: "merged common ancestors",
7642 favor: MergeFavor::None,
7643 path_favor: None,
7644 path_marker_size: None,
7645 detect_renames: false,
7646 rename_threshold: DEFAULT_RENAME_THRESHOLD,
7647 rename_limit: 0,
7648 directory_renames: DirectoryRenames::False,
7649 style: ConflictStyle::Merge,
7650 ws_ignore: WsIgnore::EMPTY,
7651 }
7652 }
7653}
7654
7655fn merge_favor_for_path(options: &MergeTreesOptions<'_>, path: &[u8]) -> MergeFavor {
7656 if options.favor != MergeFavor::None {
7657 return options.favor;
7658 }
7659 options
7660 .path_favor
7661 .map(|resolver| resolver(path))
7662 .unwrap_or(MergeFavor::None)
7663}
7664
7665fn merge_marker_size_for_path(options: &MergeTreesOptions<'_>, path: &[u8]) -> usize {
7666 options
7667 .path_marker_size
7668 .map(|resolver| resolver(path))
7669 .unwrap_or(7)
7670}
7671
7672#[derive(Debug, Clone, PartialEq, Eq)]
7675pub enum MergeConflictKind {
7676 Content { add_add: bool },
7679 ModifyDelete {
7681 deleted_in: String,
7683 modified_in: String,
7685 },
7686 RenameContent {
7689 old_path: Vec<u8>,
7691 },
7692 RenameRenameTwoToOne {
7695 ours_path: Vec<u8>,
7697 theirs_path: Vec<u8>,
7699 },
7700 RenameRenameOneToTwo {
7703 old_path: Vec<u8>,
7705 ours_path: Vec<u8>,
7707 theirs_path: Vec<u8>,
7709 ours_label: String,
7711 theirs_label: String,
7713 },
7714 RenameRenameOneToTwoStage,
7717 DirRenameSplit {
7720 source_dir: Vec<u8>,
7722 },
7723 RenameDelete {
7725 old_path: Vec<u8>,
7727 renamed_in: String,
7729 deleted_in: String,
7731 },
7732 FileDirectory {
7738 original_path: Vec<u8>,
7740 moved_from: String,
7742 },
7743 DirRenameLocation {
7749 old_path: Vec<u8>,
7752 renamed_from: Option<Vec<u8>>,
7756 added_in: String,
7758 dir_renamed_in: String,
7760 back_to_self: bool,
7766 },
7767 DirRenameImplicitCollision {
7774 sources: Vec<Vec<u8>>,
7776 },
7777 DistinctTypes {
7787 original_path: Vec<u8>,
7789 ours_renamed: Option<Vec<u8>>,
7792 theirs_renamed: Option<Vec<u8>>,
7795 },
7796 DistinctTypesStage,
7799}
7800
7801#[derive(Debug, Clone)]
7803pub struct MergedPath {
7804 pub path: Vec<u8>,
7806 pub stages: MergeStages,
7809 pub result: Option<(u32, ObjectId)>,
7812 pub worktree: Option<(u32, Vec<u8>)>,
7816 pub conflict: Option<MergeConflictKind>,
7818 pub auto_merged: bool,
7823}
7824
7825impl MergedPath {
7826 pub fn is_clean(&self) -> bool {
7828 self.conflict.is_none()
7829 }
7830}
7831
7832#[derive(Debug, Clone, Default)]
7834pub struct MergeStages {
7835 pub base: Option<(u32, ObjectId)>,
7836 pub ours: Option<(u32, ObjectId)>,
7837 pub theirs: Option<(u32, ObjectId)>,
7838}
7839
7840#[derive(Debug, Clone)]
7843pub struct MergeTreesResult {
7844 pub tree: ObjectId,
7847 pub paths: Vec<MergedPath>,
7849 pub clean: bool,
7851 pub cleanup_paths: Vec<Vec<u8>>,
7855 pub info_messages: Vec<MergeInfoMessage>,
7857}
7858
7859impl MergeTreesResult {
7860 pub fn conflicts(&self) -> impl Iterator<Item = &MergedPath> {
7862 self.paths.iter().filter(|entry| entry.conflict.is_some())
7863 }
7864}
7865
7866#[derive(Debug, Clone, PartialEq, Eq)]
7868pub enum MergeInfoMessage {
7869 DirRenameSkippedDueToRerename {
7872 old_dir: Vec<u8>,
7873 path: Vec<u8>,
7874 new_dir: Vec<u8>,
7875 },
7876 DirRenameApplied {
7879 old_path: Vec<u8>,
7880 new_path: Vec<u8>,
7881 renamed_from: Option<Vec<u8>>,
7882 added_in: String,
7883 dir_renamed_in: String,
7884 },
7885 DirRenameLocationConflict {
7890 old_path: Vec<u8>,
7891 new_path: Vec<u8>,
7892 renamed_from: Option<Vec<u8>>,
7893 added_in: String,
7894 dir_renamed_in: String,
7895 },
7896 RenameDeleteConflict {
7900 old_path: Vec<u8>,
7901 new_path: Vec<u8>,
7902 renamed_in: String,
7903 deleted_in: String,
7904 },
7905}
7906
7907pub fn flatten_tree(
7910 reader: &impl ObjectReader,
7911 format: ObjectFormat,
7912 tree_oid: &ObjectId,
7913) -> Result<MergeEntryMap> {
7914 let mut entries = BTreeMap::new();
7915 if *tree_oid == empty_tree_oid(format)? {
7916 return Ok(entries);
7917 }
7918 collect_flat_tree(reader, format, tree_oid, Vec::new(), &mut entries)?;
7919 Ok(entries)
7920}
7921
7922pub fn corrupted_cache_tree_error() -> GitError {
7923 GitError::InvalidFormat("error: corrupted cache-tree has entries not present in index".into())
7924}
7925
7926pub fn tree_has_duplicate_leaf_paths(
7927 reader: &impl ObjectReader,
7928 format: ObjectFormat,
7929 tree_oid: &ObjectId,
7930) -> Result<bool> {
7931 if *tree_oid == empty_tree_oid(format)? {
7932 return Ok(false);
7933 }
7934 let mut seen = BTreeSet::new();
7935 collect_duplicate_leaf_paths(reader, format, tree_oid, Vec::new(), &mut seen)
7936}
7937
7938fn collect_duplicate_leaf_paths(
7939 reader: &impl ObjectReader,
7940 format: ObjectFormat,
7941 tree_oid: &ObjectId,
7942 prefix: Vec<u8>,
7943 seen: &mut BTreeSet<Vec<u8>>,
7944) -> Result<bool> {
7945 let object = reader.read_object(tree_oid)?;
7946 if object.object_type != ObjectType::Tree {
7947 return Err(GitError::InvalidObject(format!(
7948 "expected tree {}, found {}",
7949 tree_oid,
7950 object.object_type.as_str()
7951 )));
7952 }
7953 for entry in TreeEntries::new(format, &object.body) {
7954 let entry = entry?;
7955 let mut path = prefix.clone();
7956 if !path.is_empty() {
7957 path.push(b'/');
7958 }
7959 path.extend_from_slice(entry.name);
7960 if entry.mode == 0o040000 {
7961 if collect_duplicate_leaf_paths(reader, format, &entry.oid, path, seen)? {
7962 return Ok(true);
7963 }
7964 } else if !seen.insert(path) {
7965 return Ok(true);
7966 }
7967 }
7968 Ok(false)
7969}
7970
7971fn collect_flat_tree(
7972 reader: &impl ObjectReader,
7973 format: ObjectFormat,
7974 tree_oid: &ObjectId,
7975 prefix: Vec<u8>,
7976 entries: &mut MergeEntryMap,
7977) -> Result<()> {
7978 let object = reader.read_object(tree_oid)?;
7979 if object.object_type != ObjectType::Tree {
7980 return Err(GitError::InvalidObject(format!(
7981 "expected tree {}, found {}",
7982 tree_oid,
7983 object.object_type.as_str()
7984 )));
7985 }
7986 for entry in TreeEntries::new(format, &object.body) {
7987 let entry = entry?;
7988 let mut path = prefix.clone();
7989 if !path.is_empty() {
7990 path.push(b'/');
7991 }
7992 path.extend_from_slice(entry.name);
7993 if entry.mode == 0o040000 {
7994 collect_flat_tree(reader, format, &entry.oid, path, entries)?;
7995 } else {
7996 entries.insert(path, (entry.mode, entry.oid));
7997 }
7998 }
7999 Ok(())
8000}
8001
8002pub fn is_mergeable_file_mode(mode: u32) -> bool {
8005 mode == 0o100644 || mode == 0o100755
8006}
8007
8008pub fn merge_trees(
8021 db: &FileObjectDatabase,
8022 format: ObjectFormat,
8023 base: Option<&ObjectId>,
8024 ours: &ObjectId,
8025 theirs: &ObjectId,
8026 options: &MergeTreesOptions<'_>,
8027) -> Result<MergeTreesResult> {
8028 let base_map = match base {
8029 Some(tree) => flatten_tree(db, format, tree)?,
8030 None => MergeEntryMap::new(),
8031 };
8032 let ours_map = flatten_tree(db, format, ours)?;
8033 let theirs_map = flatten_tree(db, format, theirs)?;
8034 merge_entry_maps(db, format, &base_map, &ours_map, &theirs_map, options)
8035}
8036
8037pub fn merge_entry_maps(
8041 db: &FileObjectDatabase,
8042 format: ObjectFormat,
8043 base_map: &MergeEntryMap,
8044 ours_map: &MergeEntryMap,
8045 theirs_map: &MergeEntryMap,
8046 options: &MergeTreesOptions<'_>,
8047) -> Result<MergeTreesResult> {
8048 let (mut renames, side_renames) = if options.detect_renames {
8055 let (renames, ours_side, theirs_side) =
8056 detect_merge_renames(db, format, base_map, ours_map, theirs_map, options)?;
8057 (renames, Some((ours_side, theirs_side)))
8058 } else {
8059 (MergeRenames::default(), None)
8060 };
8061
8062 let (mut eff_base, mut eff_ours, mut eff_theirs) =
8064 apply_merge_renames(base_map, ours_map, theirs_map, &renames);
8065
8066 let mut dir_rename_dirty = false;
8074 let mut rehomed_paths: BTreeMap<Vec<u8>, RehomeSides> = BTreeMap::new();
8075 let mut dir_rename_two_to_one: Vec<DirRenameTwoToOne> = Vec::new();
8076 let mut dir_rename_collisions: Vec<DirRenameCollision> = Vec::new();
8077 let mut dir_rename_splits: BTreeSet<Vec<u8>> = BTreeSet::new();
8078 let mut dir_rename_back_to_self: BTreeSet<Vec<u8>> = BTreeSet::new();
8079 let mut info_messages = Vec::new();
8080 let mut cleanup_paths: BTreeSet<Vec<u8>> = renames
8081 .dest_to_source
8082 .values()
8083 .map(|rename| rename.source.clone())
8084 .collect();
8085 if options.directory_renames != DirectoryRenames::False
8086 && let Some((ours_side, theirs_side)) = &side_renames
8087 {
8088 let dir_renames = compute_directory_renames(ours_map, theirs_map, ours_side, theirs_side);
8089 let outcome = apply_directory_renames(
8090 base_map,
8091 &eff_base,
8092 &eff_ours,
8093 &eff_theirs,
8094 ours_side,
8095 theirs_side,
8096 &dir_renames,
8097 &renames.dest_to_source,
8098 );
8099 eff_base = outcome.base;
8100 eff_ours = outcome.ours;
8101 eff_theirs = outcome.theirs;
8102 rehomed_paths = outcome.rehomed;
8103 dir_rename_collisions = outcome.collisions;
8104 dir_rename_splits = outcome.splits;
8105 dir_rename_back_to_self = outcome.back_to_self;
8106 info_messages = outcome.info_messages;
8107 dir_rename_dirty = outcome.dirty;
8108 remap_rename_destinations(&mut renames, &rehomed_paths);
8109 drop_collapsed_rename_rename_conflicts(&mut renames);
8110 dir_rename_two_to_one = collect_dir_rename_two_to_one(&renames, &rehomed_paths);
8111 }
8112 for info in rehomed_paths
8113 .values()
8114 .flat_map(|sides| [&sides.ours, &sides.theirs])
8115 .flatten()
8116 {
8117 cleanup_paths.insert(info.old_path.clone());
8118 }
8119 if options.directory_renames == DirectoryRenames::True {
8120 for (dest, sides) in &rehomed_paths {
8121 for info in [&sides.ours, &sides.theirs].into_iter().flatten() {
8122 let (added_in, dir_renamed_in) = if info.added_on_ours {
8123 (
8124 options.ours_label.to_string(),
8125 options.theirs_label.to_string(),
8126 )
8127 } else {
8128 (
8129 options.theirs_label.to_string(),
8130 options.ours_label.to_string(),
8131 )
8132 };
8133 info_messages.push(MergeInfoMessage::DirRenameApplied {
8134 old_path: info.old_path.clone(),
8135 new_path: dest.clone(),
8136 renamed_from: info.renamed_from.clone(),
8137 added_in,
8138 dir_renamed_in,
8139 });
8140 }
8141 }
8142 }
8143 let dir_rename_conflict_paths: BTreeMap<Vec<u8>, RehomeSides> =
8146 if options.directory_renames == DirectoryRenames::Conflict {
8147 rehomed_paths.clone()
8148 } else {
8149 BTreeMap::new()
8150 };
8151
8152 let mut all_paths = BTreeSet::new();
8153 all_paths.extend(eff_base.keys().cloned());
8154 all_paths.extend(eff_ours.keys().cloned());
8155 all_paths.extend(eff_theirs.keys().cloned());
8156
8157 let mut paths: Vec<MergedPath> = Vec::new();
8158 let mut leaves: MergeEntryMap = BTreeMap::new();
8159 let mut clean = true;
8160
8161 for path in all_paths {
8162 let base = eff_base.get(&path).cloned();
8163 let ours = eff_ours.get(&path).cloned();
8164 let theirs = eff_theirs.get(&path).cloned();
8165 let rename = renames.dest_to_source.get(&path);
8166 let old_path = rename.map(|r| r.source.clone());
8167 let favor = merge_favor_for_path(options, &path);
8168
8169 if ours == theirs {
8171 if let Some(entry) = ours {
8172 leaves.insert(path.clone(), entry);
8173 }
8174 paths.push(clean_path(path, ours));
8175 continue;
8176 }
8177 if ours == base {
8178 if let Some(entry) = &theirs {
8179 leaves.insert(path.clone(), *entry);
8180 }
8181 paths.push(clean_path(path, theirs));
8182 continue;
8183 }
8184 if theirs == base {
8185 if let Some(entry) = &ours {
8186 leaves.insert(path.clone(), *entry);
8187 }
8188 paths.push(clean_path(path, ours));
8189 continue;
8190 }
8191
8192 let content_mergeable = matches!(&ours, Some((mode, _)) if is_mergeable_file_mode(*mode))
8194 && matches!(&theirs, Some((mode, _)) if is_mergeable_file_mode(*mode))
8195 && match &base {
8196 Some((mode, _)) => is_mergeable_file_mode(*mode),
8197 None => true,
8198 };
8199
8200 if let (true, Some((ours_mode, ours_oid)), Some((theirs_mode, theirs_oid))) =
8201 (content_mergeable, &ours, &theirs)
8202 {
8203 let add_add = base.is_none();
8204 let base_bytes = match &base {
8205 Some((_, oid)) => merge_blob_bytes(db, oid)?,
8206 None => Vec::new(),
8207 };
8208 let ours_bytes = merge_blob_bytes(db, ours_oid)?;
8209 let theirs_bytes = merge_blob_bytes(db, theirs_oid)?;
8210 let rehome = rehomed_paths.get(&path);
8215 let (base_label_owned, ours_label, theirs_label) = match rename {
8220 Some(MergeRename { source, side }) => {
8221 let (ours_path, theirs_path) = match side {
8222 RenameSide::Theirs => (source.as_slice(), path.as_slice()),
8224 RenameSide::Ours => (path.as_slice(), source.as_slice()),
8226 };
8227 (
8228 qualify_label(options.ancestor_label, source.as_slice()),
8229 qualify_label(options.ours_label, ours_path),
8230 qualify_label(options.theirs_label, theirs_path),
8231 )
8232 }
8233 None => {
8234 let ours_path = rehome
8235 .and_then(|info| info.ours.as_ref())
8236 .map_or(path.as_slice(), |info| info.old_path.as_slice());
8237 let theirs_path = rehome
8238 .and_then(|info| info.theirs.as_ref())
8239 .map_or(path.as_slice(), |info| info.old_path.as_slice());
8240 if ours_path != path.as_slice() || theirs_path != path.as_slice() {
8241 (
8242 qualify_label(options.ancestor_label, path.as_slice()),
8243 qualify_label(options.ours_label, ours_path),
8244 qualify_label(options.theirs_label, theirs_path),
8245 )
8246 } else {
8247 (
8248 options.ancestor_label.to_string(),
8249 options.ours_label.to_string(),
8250 options.theirs_label.to_string(),
8251 )
8252 }
8253 }
8254 };
8255 let result = merge_blobs(
8256 &base_bytes,
8257 &ours_bytes,
8258 &theirs_bytes,
8259 &MergeBlobOptions {
8260 ours_label: &ours_label,
8261 theirs_label: &theirs_label,
8262 base_label: &base_label_owned,
8263 style: options.style,
8264 favor,
8265 ws_ignore: options.ws_ignore,
8266 marker_size: merge_marker_size_for_path(options, &path),
8267 },
8268 );
8269
8270 let base_mode = base.as_ref().map(|(mode, _)| *mode);
8271 let (resolved_mode, mode_conflict) =
8272 merge_file_modes(base_mode, *ours_mode, *theirs_mode);
8273
8274 if !result.conflicted && !mode_conflict {
8275 let oid = db.write_object(EncodedObject::new(ObjectType::Blob, result.content))?;
8276 leaves.insert(path.clone(), (resolved_mode, oid));
8277 paths.push(clean_path_auto(path, Some((resolved_mode, oid)), true));
8278 } else if favor != MergeFavor::None && !mode_conflict {
8279 let chosen = if favor == MergeFavor::Ours {
8280 ours
8281 } else {
8282 theirs
8283 };
8284 if let Some(entry) = chosen {
8285 leaves.insert(path.clone(), entry);
8286 }
8287 paths.push(clean_path_auto(path, chosen, true));
8288 } else {
8289 clean = false;
8290 let oid =
8291 db.write_object(EncodedObject::new(ObjectType::Blob, result.content.clone()))?;
8292 leaves.insert(path.clone(), (resolved_mode, oid));
8293 let worktree_mode = if *ours_mode == *theirs_mode {
8294 *ours_mode
8295 } else {
8296 0o100644
8297 };
8298 let conflict = if let Some(old) = &old_path {
8299 MergeConflictKind::RenameContent {
8300 old_path: old.clone(),
8301 }
8302 } else if add_add {
8303 match rehome.and_then(|info| Some((info.ours.as_ref()?, info.theirs.as_ref()?)))
8304 {
8305 Some((ours_info, theirs_info)) => MergeConflictKind::RenameRenameTwoToOne {
8306 ours_path: ours_info.old_path.clone(),
8307 theirs_path: theirs_info.old_path.clone(),
8308 },
8309 None => MergeConflictKind::Content { add_add },
8310 }
8311 } else {
8312 MergeConflictKind::Content { add_add }
8313 };
8314 paths.push(MergedPath {
8315 path: path.clone(),
8316 stages: stages_for(&base, &ours, &theirs),
8317 result: Some((resolved_mode, oid)),
8318 worktree: Some((worktree_mode, result.content)),
8319 conflict: Some(conflict),
8320 auto_merged: true,
8321 });
8322 }
8323 } else if base.is_some() && (ours.is_none() || theirs.is_none()) {
8324 clean = false;
8326 let (deleted_in, modified_in, surviving) = if ours.is_none() {
8327 (
8328 options.ours_label.to_string(),
8329 options.theirs_label.to_string(),
8330 theirs,
8331 )
8332 } else {
8333 (
8334 options.theirs_label.to_string(),
8335 options.ours_label.to_string(),
8336 ours,
8337 )
8338 };
8339 let worktree = match &surviving {
8340 Some((mode, oid)) => Some((*mode, merge_worktree_bytes(db, *mode, oid)?)),
8341 None => None,
8342 };
8343 if let Some(entry) = surviving {
8344 leaves.insert(path.clone(), entry);
8345 }
8346 paths.push(MergedPath {
8347 path: path.clone(),
8348 stages: stages_for(&base, &ours, &theirs),
8349 result: surviving,
8350 worktree,
8351 conflict: Some(MergeConflictKind::ModifyDelete {
8352 deleted_in,
8353 modified_in,
8354 }),
8355 auto_merged: false,
8356 });
8357 } else if let (Some(&(ours_mode, ours_oid)), Some(&(theirs_mode, theirs_oid))) =
8358 (ours.as_ref(), theirs.as_ref())
8359 && sley_index::is_symlink_mode(ours_mode)
8360 && sley_index::is_symlink_mode(theirs_mode)
8361 {
8362 match favor {
8369 MergeFavor::Ours => {
8370 leaves.insert(path.clone(), (ours_mode, ours_oid));
8371 paths.push(clean_path_auto(
8372 path.clone(),
8373 Some((ours_mode, ours_oid)),
8374 false,
8375 ));
8376 }
8377 MergeFavor::Theirs => {
8378 leaves.insert(path.clone(), (theirs_mode, theirs_oid));
8379 paths.push(clean_path_auto(
8380 path.clone(),
8381 Some((theirs_mode, theirs_oid)),
8382 false,
8383 ));
8384 }
8385 MergeFavor::None | MergeFavor::Union => {
8386 clean = false;
8387 leaves.insert(path.clone(), (ours_mode, ours_oid));
8388 let worktree =
8389 Some((ours_mode, merge_worktree_bytes(db, ours_mode, &ours_oid)?));
8390 paths.push(MergedPath {
8391 path: path.clone(),
8392 stages: stages_for(&base, &ours, &theirs),
8393 result: Some((ours_mode, ours_oid)),
8394 worktree,
8395 conflict: Some(MergeConflictKind::Content {
8396 add_add: base.is_none(),
8397 }),
8398 auto_merged: false,
8399 });
8400 }
8401 }
8402 } else if let (Some((ours_mode, ours_oid)), Some((theirs_mode, theirs_oid))) =
8403 (ours, theirs)
8404 && is_type_change(ours_mode, theirs_mode)
8405 {
8406 clean = false;
8413 let (rename_ours, rename_theirs) = if is_mergeable_file_mode(ours_mode) {
8416 (true, false)
8417 } else if is_mergeable_file_mode(theirs_mode) {
8418 (false, true)
8419 } else {
8420 (true, true)
8421 };
8422 let ours_base = base.filter(|(mode, _)| !is_type_change(*mode, ours_mode));
8425 let theirs_base = base.filter(|(mode, _)| !is_type_change(*mode, theirs_mode));
8426 let ours_path = if rename_ours {
8429 unique_df_path(&path, options.ours_label, &leaves, &paths)
8430 } else {
8431 path.clone()
8432 };
8433 leaves.insert(ours_path.clone(), (ours_mode, ours_oid));
8434 let theirs_path = if rename_theirs {
8435 unique_df_path(&path, options.theirs_label, &leaves, &paths)
8436 } else {
8437 path.clone()
8438 };
8439 leaves.insert(theirs_path.clone(), (theirs_mode, theirs_oid));
8440
8441 let ours_carries_message = !rename_ours || rename_theirs;
8444 let distinct = MergeConflictKind::DistinctTypes {
8445 original_path: path.clone(),
8446 ours_renamed: rename_ours.then(|| ours_path.clone()),
8447 theirs_renamed: rename_theirs.then(|| theirs_path.clone()),
8448 };
8449 let ours_worktree = Some((ours_mode, merge_worktree_bytes(db, ours_mode, &ours_oid)?));
8450 paths.push(MergedPath {
8451 path: ours_path,
8452 stages: MergeStages {
8453 base: ours_base,
8454 ours: Some((ours_mode, ours_oid)),
8455 theirs: None,
8456 },
8457 result: Some((ours_mode, ours_oid)),
8458 worktree: ours_worktree,
8459 conflict: Some(if ours_carries_message {
8460 distinct.clone()
8461 } else {
8462 MergeConflictKind::DistinctTypesStage
8463 }),
8464 auto_merged: false,
8465 });
8466 let theirs_worktree = Some((
8467 theirs_mode,
8468 merge_worktree_bytes(db, theirs_mode, &theirs_oid)?,
8469 ));
8470 paths.push(MergedPath {
8471 path: theirs_path,
8472 stages: MergeStages {
8473 base: theirs_base,
8474 ours: None,
8475 theirs: Some((theirs_mode, theirs_oid)),
8476 },
8477 result: Some((theirs_mode, theirs_oid)),
8478 worktree: theirs_worktree,
8479 conflict: Some(if ours_carries_message {
8480 MergeConflictKind::DistinctTypesStage
8481 } else {
8482 distinct
8483 }),
8484 auto_merged: false,
8485 });
8486 } else {
8487 clean = false;
8490 let add_add = base.is_none();
8491 let surviving = ours.or(theirs);
8492 let worktree = match &surviving {
8493 Some((mode, oid)) => Some((*mode, merge_worktree_bytes(db, *mode, oid)?)),
8494 None => None,
8495 };
8496 if let Some(entry) = surviving {
8497 leaves.insert(path.clone(), entry);
8498 }
8499 paths.push(MergedPath {
8500 path: path.clone(),
8501 stages: stages_for(&base, &ours, &theirs),
8502 result: surviving,
8503 worktree,
8504 conflict: Some(MergeConflictKind::Content { add_add }),
8505 auto_merged: false,
8506 });
8507 }
8508 }
8509
8510 if !renames.rename_rename_one_to_two.is_empty() {
8511 apply_rename_rename_one_to_two_conflicts(
8512 db,
8513 base_map,
8514 &eff_ours,
8515 &eff_theirs,
8516 &renames.rename_rename_one_to_two,
8517 &mut paths,
8518 &mut leaves,
8519 options,
8520 )?;
8521 clean = false;
8522 }
8523
8524 if !dir_rename_two_to_one.is_empty() {
8525 apply_dir_rename_two_to_one_conflicts(
8526 db,
8527 &eff_ours,
8528 &eff_theirs,
8529 &dir_rename_two_to_one,
8530 &mut paths,
8531 &mut leaves,
8532 options,
8533 )?;
8534 clean = false;
8535 }
8536
8537 if !renames.rename_rename_two_to_one.is_empty() || !renames.rename_adds.is_empty() {
8542 apply_rename_two_to_one_and_add_conflicts(
8543 db,
8544 base_map,
8545 ours_map,
8546 theirs_map,
8547 &renames,
8548 &mut paths,
8549 &mut leaves,
8550 options,
8551 )?;
8552 clean = false;
8553 }
8554
8555 if !renames.rename_deletes.is_empty() {
8560 for (dest, rd) in &renames.rename_deletes {
8561 let Some(slot) = paths.iter_mut().find(|p| &p.path == dest) else {
8563 continue;
8564 };
8565 if slot.conflict.is_some() {
8566 continue;
8567 }
8568 let base_entry = base_map.get(&rd.source).copied();
8569 let renamed_entry = slot.result;
8570 let (ours_stage, theirs_stage) = match rd.side {
8573 RenameSide::Ours => (renamed_entry, None),
8574 RenameSide::Theirs => (None, renamed_entry),
8575 };
8576 let (renamed_in, deleted_in) = match rd.side {
8577 RenameSide::Ours => (
8578 options.ours_label.to_string(),
8579 options.theirs_label.to_string(),
8580 ),
8581 RenameSide::Theirs => (
8582 options.theirs_label.to_string(),
8583 options.ours_label.to_string(),
8584 ),
8585 };
8586 let worktree = match &renamed_entry {
8587 Some((mode, oid)) => Some((*mode, merge_worktree_bytes(db, *mode, oid)?)),
8588 None => None,
8589 };
8590 slot.stages = MergeStages {
8591 base: base_entry,
8592 ours: ours_stage,
8593 theirs: theirs_stage,
8594 };
8595 slot.worktree = worktree;
8596 slot.conflict = Some(MergeConflictKind::RenameDelete {
8597 old_path: rd.source.clone(),
8598 renamed_in,
8599 deleted_in,
8600 });
8601 clean = false;
8602 }
8603 }
8604
8605 if dir_rename_dirty {
8612 clean = false;
8613 }
8614 for collision in &dir_rename_collisions {
8620 clean = false;
8621 if let Some(slot) = paths.iter_mut().find(|p| p.path == collision.dest)
8622 && slot.conflict.is_none()
8623 {
8624 slot.conflict = Some(MergeConflictKind::DirRenameImplicitCollision {
8625 sources: collision.sources.clone(),
8626 });
8627 } else if !paths.iter().any(|p| p.path == collision.dest) {
8628 paths.push(MergedPath {
8629 path: collision.dest.clone(),
8630 stages: MergeStages::default(),
8631 result: None,
8632 worktree: None,
8633 conflict: Some(MergeConflictKind::DirRenameImplicitCollision {
8634 sources: collision.sources.clone(),
8635 }),
8636 auto_merged: false,
8637 });
8638 }
8639 }
8640 for source_dir in &dir_rename_splits {
8641 clean = false;
8642 paths.push(MergedPath {
8643 path: source_dir.clone(),
8644 stages: MergeStages::default(),
8645 result: None,
8646 worktree: None,
8647 conflict: Some(MergeConflictKind::DirRenameSplit {
8648 source_dir: source_dir.clone(),
8649 }),
8650 auto_merged: false,
8651 });
8652 }
8653 if !dir_rename_conflict_paths.is_empty() {
8654 clean = false;
8655 for (dest, infos) in &dir_rename_conflict_paths {
8656 for info in [&infos.ours, &infos.theirs].into_iter().flatten() {
8657 let (added_in, dir_renamed_in) = if info.added_on_ours {
8658 (
8660 options.ours_label.to_string(),
8661 options.theirs_label.to_string(),
8662 )
8663 } else {
8664 (
8665 options.theirs_label.to_string(),
8666 options.ours_label.to_string(),
8667 )
8668 };
8669 let back_to_self = dir_rename_back_to_self.contains(dest);
8676 if let Some(slot) = paths.iter_mut().find(|p| &p.path == dest)
8677 && slot.conflict.is_none()
8678 {
8679 if back_to_self {
8680 slot.stages = MergeStages {
8681 base: eff_base.get(dest).copied(),
8682 ours: eff_ours.get(dest).copied(),
8683 theirs: eff_theirs.get(dest).copied(),
8684 };
8685 slot.worktree = match &slot.result {
8686 Some((mode, oid)) => {
8687 Some((*mode, merge_worktree_bytes(db, *mode, oid)?))
8688 }
8689 None => slot.worktree.clone(),
8690 };
8691 }
8692 slot.conflict = Some(MergeConflictKind::DirRenameLocation {
8693 old_path: info.old_path.clone(),
8694 renamed_from: info.renamed_from.clone(),
8695 added_in,
8696 dir_renamed_in,
8697 back_to_self,
8698 });
8699 } else {
8700 info_messages.push(MergeInfoMessage::DirRenameLocationConflict {
8701 old_path: info.old_path.clone(),
8702 new_path: dest.clone(),
8703 renamed_from: info.renamed_from.clone(),
8704 added_in,
8705 dir_renamed_in,
8706 });
8707 }
8708 }
8709 }
8710 }
8711
8712 resolve_directory_file_conflicts(
8720 db,
8721 &mut paths,
8722 &mut leaves,
8723 &mut clean,
8724 &eff_ours,
8725 &eff_theirs,
8726 options,
8727 &mut info_messages,
8728 )?;
8729
8730 let tree = write_merged_tree(db, &leaves)?;
8731
8732 cleanup_paths.retain(|path| !leaves.contains_key(path));
8733
8734 Ok(MergeTreesResult {
8735 tree,
8736 paths,
8737 clean,
8738 cleanup_paths: cleanup_paths.into_iter().collect(),
8739 info_messages,
8740 })
8741}
8742
8743fn flatten_branch_label(branch: &str) -> String {
8747 branch.replace('/', "_")
8748}
8749
8750fn unique_df_path(
8754 path: &[u8],
8755 branch: &str,
8756 leaves: &MergeEntryMap,
8757 paths: &[MergedPath],
8758) -> Vec<u8> {
8759 let mut base = path.to_vec();
8760 base.push(b'~');
8761 base.extend_from_slice(flatten_branch_label(branch).as_bytes());
8762 let taken = |candidate: &[u8]| {
8763 leaves.contains_key(candidate) || paths.iter().any(|p| p.path == candidate)
8764 };
8765 if !taken(&base) {
8766 return base;
8767 }
8768 let mut suffix = 0usize;
8769 loop {
8770 let mut candidate = base.clone();
8771 candidate.push(b'_');
8772 candidate.extend_from_slice(suffix.to_string().as_bytes());
8773 if !taken(&candidate) {
8774 return candidate;
8775 }
8776 suffix += 1;
8777 }
8778}
8779
8780#[allow(clippy::too_many_arguments)]
8784fn resolve_directory_file_conflicts(
8785 db: &FileObjectDatabase,
8786 paths: &mut Vec<MergedPath>,
8787 leaves: &mut MergeEntryMap,
8788 clean: &mut bool,
8789 eff_ours: &MergeEntryMap,
8790 eff_theirs: &MergeEntryMap,
8791 options: &MergeTreesOptions<'_>,
8792 info_messages: &mut Vec<MergeInfoMessage>,
8793) -> Result<()> {
8794 let mut directory_prefixes: BTreeSet<Vec<u8>> = BTreeSet::new();
8797 for key in leaves.keys() {
8798 let mut idx = 0;
8799 while let Some(pos) = key[idx..].iter().position(|b| *b == b'/') {
8800 let end = idx + pos;
8801 directory_prefixes.insert(key[..end].to_vec());
8802 idx = end + 1;
8803 }
8804 }
8805 if directory_prefixes.is_empty() {
8806 return Ok(());
8807 }
8808
8809 let colliding: Vec<Vec<u8>> = leaves
8811 .keys()
8812 .filter(|key| directory_prefixes.contains(*key))
8813 .cloned()
8814 .collect();
8815
8816 for original in colliding {
8817 let Some(entry) = leaves.remove(&original) else {
8818 continue;
8819 };
8820 let moved_bytes = merge_worktree_bytes(db, entry.0, &entry.1)?;
8823 let ours_has_file = eff_ours.contains_key(&original);
8829 let theirs_has_file = eff_theirs.contains_key(&original);
8830 let from_ours = ours_has_file || !theirs_has_file;
8831 let branch = if from_ours {
8832 options.ours_label
8833 } else {
8834 options.theirs_label
8835 };
8836 let new_path = unique_df_path(&original, branch, leaves, paths);
8837 leaves.insert(new_path.clone(), entry);
8838 *clean = false;
8839
8840 if let Some(slot) = paths.iter_mut().find(|p| p.path == original) {
8843 if let Some(MergeConflictKind::RenameDelete {
8844 old_path,
8845 renamed_in,
8846 deleted_in,
8847 }) = &slot.conflict
8848 {
8849 info_messages.push(MergeInfoMessage::RenameDeleteConflict {
8850 old_path: old_path.clone(),
8851 new_path: original.clone(),
8852 renamed_in: renamed_in.clone(),
8853 deleted_in: deleted_in.clone(),
8854 });
8855 }
8856 slot.path = new_path.clone();
8857 slot.result = Some(entry);
8858 if slot.stages.base.is_none()
8861 && slot.stages.ours.is_none()
8862 && slot.stages.theirs.is_none()
8863 {
8864 slot.stages = MergeStages {
8865 base: None,
8866 ours: if from_ours { Some(entry) } else { None },
8867 theirs: if from_ours { None } else { Some(entry) },
8868 };
8869 }
8870 slot.worktree = Some((entry.0, moved_bytes));
8876 slot.conflict = Some(MergeConflictKind::FileDirectory {
8877 original_path: original.clone(),
8878 moved_from: branch.to_string(),
8879 });
8880 } else {
8881 paths.push(MergedPath {
8882 path: new_path.clone(),
8883 stages: MergeStages {
8884 base: None,
8885 ours: if from_ours { Some(entry) } else { None },
8886 theirs: if from_ours { None } else { Some(entry) },
8887 },
8888 result: Some(entry),
8889 worktree: Some((entry.0, moved_bytes)),
8890 conflict: Some(MergeConflictKind::FileDirectory {
8891 original_path: original.clone(),
8892 moved_from: branch.to_string(),
8893 }),
8894 auto_merged: false,
8895 });
8896 }
8897 }
8898
8899 paths.sort_by(|a, b| a.path.cmp(&b.path));
8901 Ok(())
8902}
8903
8904fn clean_path(path: Vec<u8>, result: Option<(u32, ObjectId)>) -> MergedPath {
8906 clean_path_auto(path, result, false)
8907}
8908
8909fn clean_path_auto(
8912 path: Vec<u8>,
8913 result: Option<(u32, ObjectId)>,
8914 auto_merged: bool,
8915) -> MergedPath {
8916 MergedPath {
8917 path,
8918 stages: MergeStages::default(),
8919 result,
8920 worktree: None,
8921 conflict: None,
8922 auto_merged,
8923 }
8924}
8925
8926fn stages_for(
8928 base: &Option<(u32, ObjectId)>,
8929 ours: &Option<(u32, ObjectId)>,
8930 theirs: &Option<(u32, ObjectId)>,
8931) -> MergeStages {
8932 MergeStages {
8933 base: *base,
8934 ours: *ours,
8935 theirs: *theirs,
8936 }
8937}
8938
8939fn merge_blob_bytes(reader: &impl ObjectReader, oid: &ObjectId) -> Result<Vec<u8>> {
8941 let object = reader.read_object(oid)?;
8942 if object.object_type != ObjectType::Blob {
8943 return Err(GitError::InvalidObject(format!(
8944 "expected blob {}, found {}",
8945 oid,
8946 object.object_type.as_str()
8947 )));
8948 }
8949 Ok(object.body.clone())
8950}
8951
8952fn merge_worktree_bytes(reader: &impl ObjectReader, mode: u32, oid: &ObjectId) -> Result<Vec<u8>> {
8953 if sley_index::is_gitlink(mode) {
8954 Ok(Vec::new())
8955 } else {
8956 merge_blob_bytes(reader, oid)
8957 }
8958}
8959
8960fn merge_file_modes(base: Option<u32>, ours: u32, theirs: u32) -> (u32, bool) {
8963 if ours == theirs {
8964 return (ours, false);
8965 }
8966 match base {
8967 Some(base) if ours == base => (theirs, false),
8968 Some(base) if theirs == base => (ours, false),
8969 _ => (ours, true),
8970 }
8971}
8972
8973fn write_merged_tree(db: &FileObjectDatabase, leaves: &MergeEntryMap) -> Result<ObjectId> {
8976 let mut root = MergeTreeNode::default();
8977 for (path, (mode, oid)) in leaves {
8978 root.insert(path, *mode, *oid);
8979 }
8980 root.write(db)
8981}
8982
8983#[derive(Default)]
8984struct MergeTreeNode {
8985 blobs: BTreeMap<Vec<u8>, (u32, ObjectId)>,
8986 subtrees: BTreeMap<Vec<u8>, MergeTreeNode>,
8987}
8988
8989impl MergeTreeNode {
8990 fn insert(&mut self, path: &[u8], mode: u32, oid: ObjectId) {
8991 match path.iter().position(|byte| *byte == b'/') {
8992 Some(slash) => {
8993 let component = path[..slash].to_vec();
8994 let rest = &path[slash + 1..];
8995 self.subtrees
8996 .entry(component)
8997 .or_default()
8998 .insert(rest, mode, oid);
8999 }
9000 None => {
9001 self.blobs.insert(path.to_vec(), (mode, oid));
9002 }
9003 }
9004 }
9005
9006 fn write(&self, db: &FileObjectDatabase) -> Result<ObjectId> {
9007 let mut entries: Vec<TreeEntry> = Vec::new();
9008 for (name, (mode, oid)) in &self.blobs {
9009 entries.push(TreeEntry {
9010 mode: *mode,
9011 name: BString::from(name.clone()),
9012 oid: *oid,
9013 });
9014 }
9015 for (name, subtree) in &self.subtrees {
9016 let oid = subtree.write(db)?;
9017 entries.push(TreeEntry {
9018 mode: 0o040000,
9019 name: BString::from(name.clone()),
9020 oid,
9021 });
9022 }
9023 entries.sort_by_key(merge_tree_sort_key);
9024 let tree = Tree { entries };
9025 db.write_object(EncodedObject::new(ObjectType::Tree, tree.write()))
9026 }
9027}
9028
9029fn merge_tree_sort_key(entry: &TreeEntry) -> Vec<u8> {
9030 let mut key = entry.name.as_bytes().to_vec();
9031 if entry.mode == 0o040000 {
9032 key.push(b'/');
9033 }
9034 key
9035}
9036
9037#[derive(Clone, Copy, PartialEq, Eq)]
9041enum RenameSide {
9042 Ours,
9043 Theirs,
9044}
9045
9046#[derive(Clone)]
9048struct MergeRename {
9049 source: Vec<u8>,
9050 side: RenameSide,
9051}
9052
9053#[derive(Clone)]
9057struct RenameDelete {
9058 source: Vec<u8>,
9060 side: RenameSide,
9062}
9063
9064#[derive(Default)]
9068struct MergeRenames {
9069 dest_to_source: BTreeMap<Vec<u8>, MergeRename>,
9073 rename_deletes: BTreeMap<Vec<u8>, RenameDelete>,
9076 rename_rename_one_to_two: BTreeMap<Vec<u8>, RenameRenameOneToTwo>,
9078 rename_rename_two_to_one: BTreeMap<Vec<u8>, RenameRenameTwoToOne>,
9081 rename_adds: BTreeMap<Vec<u8>, RenameAdd>,
9084}
9085
9086#[derive(Clone)]
9087struct RenameRenameOneToTwo {
9088 ours_dest: Vec<u8>,
9089 theirs_dest: Vec<u8>,
9090}
9091
9092#[derive(Clone)]
9096struct RenameRenameTwoToOne {
9097 ours_source: Vec<u8>,
9099 theirs_source: Vec<u8>,
9101}
9102
9103#[derive(Clone)]
9107struct RenameAdd {
9108 source: Vec<u8>,
9110 side: RenameSide,
9112}
9113
9114struct SideRenames {
9119 pairs: Vec<(Vec<u8>, Vec<u8>)>,
9120}
9121
9122fn detect_merge_renames(
9132 db: &FileObjectDatabase,
9133 format: ObjectFormat,
9134 base_map: &MergeEntryMap,
9135 ours_map: &MergeEntryMap,
9136 theirs_map: &MergeEntryMap,
9137 options: &MergeTreesOptions<'_>,
9138) -> Result<(MergeRenames, SideRenames, SideRenames)> {
9139 let mut renames = MergeRenames::default();
9140
9141 let ours_side = collect_side_renames(
9143 db,
9144 format,
9145 base_map,
9146 ours_map,
9147 theirs_map,
9148 RenameSide::Ours,
9149 options.rename_threshold,
9150 options.rename_limit,
9151 &mut renames,
9152 )?;
9153 let theirs_side = collect_side_renames(
9155 db,
9156 format,
9157 base_map,
9158 theirs_map,
9159 ours_map,
9160 RenameSide::Theirs,
9161 options.rename_threshold,
9162 options.rename_limit,
9163 &mut renames,
9164 )?;
9165
9166 collect_rename_rename_one_to_two(&mut renames, &ours_side, &theirs_side);
9167 collect_rename_rename_two_to_one_and_adds(
9168 &mut renames,
9169 &ours_side,
9170 &theirs_side,
9171 base_map,
9172 ours_map,
9173 theirs_map,
9174 );
9175
9176 Ok((renames, ours_side, theirs_side))
9177}
9178
9179fn collect_rename_rename_two_to_one_and_adds(
9188 renames: &mut MergeRenames,
9189 ours_side: &SideRenames,
9190 theirs_side: &SideRenames,
9191 base_map: &MergeEntryMap,
9192 ours_map: &MergeEntryMap,
9193 theirs_map: &MergeEntryMap,
9194) {
9195 let ours_by_dest: BTreeMap<&[u8], &[u8]> = ours_side
9196 .pairs
9197 .iter()
9198 .map(|(old, new)| (new.as_slice(), old.as_slice()))
9199 .collect();
9200 let theirs_by_dest: BTreeMap<&[u8], &[u8]> = theirs_side
9201 .pairs
9202 .iter()
9203 .map(|(old, new)| (new.as_slice(), old.as_slice()))
9204 .collect();
9205
9206 for (dest, ours_src) in &ours_by_dest {
9210 let Some(theirs_src) = theirs_by_dest.get(dest) else {
9211 continue;
9212 };
9213 if ours_src == theirs_src {
9214 continue;
9215 }
9216 if renames.rename_rename_one_to_two.contains_key(*dest) {
9218 continue;
9219 }
9220 renames.rename_rename_two_to_one.insert(
9221 dest.to_vec(),
9222 RenameRenameTwoToOne {
9223 ours_source: ours_src.to_vec(),
9224 theirs_source: theirs_src.to_vec(),
9225 },
9226 );
9227 }
9228
9229 for (dest, ours_src) in &ours_by_dest {
9232 if renames.rename_rename_two_to_one.contains_key(*dest)
9233 || renames.rename_rename_one_to_two.contains_key(*dest)
9234 {
9235 continue;
9236 }
9237 if theirs_map.contains_key(*dest)
9238 && !base_map.contains_key(*dest)
9239 && !theirs_by_dest.contains_key(dest)
9240 {
9241 renames.rename_adds.insert(
9242 dest.to_vec(),
9243 RenameAdd {
9244 source: ours_src.to_vec(),
9245 side: RenameSide::Ours,
9246 },
9247 );
9248 }
9249 }
9250 for (dest, theirs_src) in &theirs_by_dest {
9252 if renames.rename_rename_two_to_one.contains_key(*dest)
9253 || renames.rename_rename_one_to_two.contains_key(*dest)
9254 || renames.rename_adds.contains_key(*dest)
9255 {
9256 continue;
9257 }
9258 if ours_map.contains_key(*dest)
9259 && !base_map.contains_key(*dest)
9260 && !ours_by_dest.contains_key(dest)
9261 {
9262 renames.rename_adds.insert(
9263 dest.to_vec(),
9264 RenameAdd {
9265 source: theirs_src.to_vec(),
9266 side: RenameSide::Theirs,
9267 },
9268 );
9269 }
9270 }
9271}
9272
9273fn collect_rename_rename_one_to_two(
9274 renames: &mut MergeRenames,
9275 ours_side: &SideRenames,
9276 theirs_side: &SideRenames,
9277) {
9278 let ours_by_source: BTreeMap<&[u8], &[u8]> = ours_side
9279 .pairs
9280 .iter()
9281 .map(|(old, new)| (old.as_slice(), new.as_slice()))
9282 .collect();
9283 for (old, theirs_new) in &theirs_side.pairs {
9284 let Some(ours_new) = ours_by_source.get(old.as_slice()) else {
9285 continue;
9286 };
9287 if *ours_new == theirs_new.as_slice() {
9288 continue;
9289 }
9290 renames.rename_deletes.remove(*ours_new);
9291 renames.rename_deletes.remove(theirs_new);
9292 renames.dest_to_source.remove(*ours_new);
9293 renames.dest_to_source.remove(theirs_new);
9294 renames.rename_rename_one_to_two.insert(
9295 old.clone(),
9296 RenameRenameOneToTwo {
9297 ours_dest: (*ours_new).to_vec(),
9298 theirs_dest: theirs_new.clone(),
9299 },
9300 );
9301 }
9302}
9303
9304#[allow(clippy::too_many_arguments)]
9309fn collect_side_renames(
9310 db: &FileObjectDatabase,
9311 format: ObjectFormat,
9312 base_map: &MergeEntryMap,
9313 side_map: &MergeEntryMap,
9314 other_map: &MergeEntryMap,
9315 side: RenameSide,
9316 threshold: u8,
9317 rename_limit: usize,
9318 renames: &mut MergeRenames,
9319) -> Result<SideRenames> {
9320 let base_tree = entry_map_as_tracked(base_map);
9323 let side_tree = entry_map_as_tracked(side_map);
9324 let options = RenameDetectionOptions {
9325 base: DiffNameStatusOptions {
9326 detect_renames: true,
9327 detect_copies: false,
9328 find_copies_harder: false,
9329 rename_empty: false,
9330 },
9331 detect_inexact: true,
9332 rename_threshold: threshold,
9333 copy_threshold: threshold,
9334 rename_limit,
9335 };
9336 let changes = diff_name_status_maps_with_renames(
9337 &base_tree,
9338 &side_tree,
9339 base_tree.keys().chain(side_tree.keys()),
9340 options,
9341 |oid| merge_blob_bytes(db, oid).ok(),
9342 )?;
9343
9344 let mut pairs = Vec::new();
9345 for change in changes {
9346 let NameStatus::Renamed(_) = change.status else {
9347 continue;
9348 };
9349 let Some(old_path) = change.old_path.as_ref() else {
9350 continue;
9351 };
9352 let old = old_path.as_bytes().to_vec();
9353 let new = change.path.as_bytes().to_vec();
9354 pairs.push((old.clone(), new.clone()));
9356
9357 if !other_map.contains_key(&old) {
9362 if base_map.contains_key(&old) && !other_map.contains_key(&new) {
9367 renames
9368 .rename_deletes
9369 .entry(new.clone())
9370 .or_insert(RenameDelete {
9371 source: old.clone(),
9372 side,
9373 });
9374 }
9375 continue;
9376 }
9377 if other_map.contains_key(&new) {
9380 continue;
9381 }
9382 renames
9386 .dest_to_source
9387 .entry(new)
9388 .or_insert(MergeRename { source: old, side });
9389 }
9390
9391 let _ = format;
9392 Ok(SideRenames { pairs })
9393}
9394
9395fn apply_merge_renames(
9401 base_map: &MergeEntryMap,
9402 ours_map: &MergeEntryMap,
9403 theirs_map: &MergeEntryMap,
9404 renames: &MergeRenames,
9405) -> (MergeEntryMap, MergeEntryMap, MergeEntryMap) {
9406 if renames.dest_to_source.is_empty() {
9407 return (base_map.clone(), ours_map.clone(), theirs_map.clone());
9408 }
9409 let mut base = base_map.clone();
9410 let mut ours = ours_map.clone();
9411 let mut theirs = theirs_map.clone();
9412
9413 for (new, rename) in &renames.dest_to_source {
9414 let old = &rename.source;
9415 if let Some(entry) = base.remove(old) {
9417 base.entry(new.clone()).or_insert(entry);
9418 }
9419 for side in [&mut ours, &mut theirs] {
9421 if let Some(entry) = side.remove(old) {
9422 side.entry(new.clone()).or_insert(entry);
9423 }
9424 }
9425 }
9426 (base, ours, theirs)
9427}
9428
9429fn parent_dir(path: &[u8]) -> Option<&[u8]> {
9433 path.iter().rposition(|b| *b == b'/').map(|i| &path[..i])
9434}
9435
9436fn apply_dir_rename(old_dir: &[u8], new_dir: &[u8], path: &[u8]) -> Vec<u8> {
9440 let rest_start = if new_dir.is_empty() {
9443 old_dir.len() + 1
9444 } else {
9445 old_dir.len()
9446 };
9447 let mut out = new_dir.to_vec();
9448 out.extend_from_slice(&path[rest_start..]);
9449 out
9450}
9451
9452fn check_dir_renamed<'a>(
9456 path: &[u8],
9457 dir_renames: &'a BTreeMap<Vec<u8>, Vec<u8>>,
9458) -> Option<(&'a [u8], &'a [u8])> {
9459 let mut cur = parent_dir(path);
9460 while let Some(dir) = cur {
9461 if let Some((old_dir, new_dir)) = dir_renames.get_key_value(dir) {
9462 return Some((old_dir.as_slice(), new_dir.as_slice()));
9463 }
9464 cur = parent_dir(dir);
9465 }
9466 None
9467}
9468
9469struct DirectoryRenameMaps {
9472 ours: BTreeMap<Vec<u8>, Vec<u8>>,
9475 theirs: BTreeMap<Vec<u8>, Vec<u8>>,
9477 ours_split: BTreeSet<Vec<u8>>,
9481 theirs_split: BTreeSet<Vec<u8>>,
9483}
9484
9485fn compute_directory_renames(
9494 ours_map: &MergeEntryMap,
9495 theirs_map: &MergeEntryMap,
9496 ours_side: &SideRenames,
9497 theirs_side: &SideRenames,
9498) -> DirectoryRenameMaps {
9499 let ours = compute_side_dir_renames(&ours_side.pairs, ours_map);
9500 let theirs = compute_side_dir_renames(&theirs_side.pairs, theirs_map);
9501
9502 let mut ours_map_out = ours.renames;
9506 let mut theirs_map_out = theirs.renames;
9507 let dup: Vec<Vec<u8>> = ours_map_out
9508 .keys()
9509 .filter(|k| theirs_map_out.contains_key(*k))
9510 .cloned()
9511 .collect();
9512 for k in dup {
9513 ours_map_out.remove(&k);
9514 theirs_map_out.remove(&k);
9515 }
9516
9517 DirectoryRenameMaps {
9518 ours: ours_map_out,
9519 theirs: theirs_map_out,
9520 ours_split: ours.split,
9521 theirs_split: theirs.split,
9522 }
9523}
9524
9525struct SideDirRenames {
9527 renames: BTreeMap<Vec<u8>, Vec<u8>>,
9528 split: BTreeSet<Vec<u8>>,
9529}
9530
9531fn compute_side_dir_renames(
9534 pairs: &[(Vec<u8>, Vec<u8>)],
9535 side_map: &MergeEntryMap,
9536) -> SideDirRenames {
9537 let mut counts: BTreeMap<Vec<u8>, BTreeMap<Vec<u8>, usize>> = BTreeMap::new();
9545 for (old, new) in pairs {
9546 update_dir_rename_counts(&mut counts, old, new);
9547 }
9548
9549 let mut renames = BTreeMap::new();
9550 let mut split = BTreeSet::new();
9551 for (old_dir, targets) in counts {
9552 let mut max = 0usize;
9553 let mut bad_max = 0usize;
9554 let mut best: Option<Vec<u8>> = None;
9555 for (target, count) in &targets {
9556 if *count == max {
9557 bad_max = max;
9558 } else if *count > max {
9559 max = *count;
9560 best = Some(target.clone());
9561 }
9562 }
9563 if max == 0 {
9564 continue;
9565 }
9566 if bad_max == max {
9567 split.insert(old_dir);
9568 continue;
9569 }
9570 if let Some(best) = best
9575 && directory_fully_removed(&old_dir, side_map)
9576 {
9577 renames.insert(old_dir, best);
9578 }
9579 }
9580
9581 SideDirRenames { renames, split }
9582}
9583
9584fn update_dir_rename_counts(
9590 counts: &mut BTreeMap<Vec<u8>, BTreeMap<Vec<u8>, usize>>,
9591 old: &[u8],
9592 new: &[u8],
9593) {
9594 let mut old_dir = old.to_vec();
9596 let mut new_dir = new.to_vec();
9597 let mut first = true;
9598 loop {
9599 let old_has = dir_munge(&mut old_dir);
9602 let new_has = dir_munge(&mut new_dir);
9603
9604 if !first {
9608 let old_sub = trailing_component(old, &old_dir);
9609 let new_sub = trailing_component(new, &new_dir);
9610 if old_sub != new_sub {
9611 break;
9612 }
9613 }
9614
9615 if old_dir == new_dir {
9616 break;
9619 }
9620 *counts
9621 .entry(old_dir.clone())
9622 .or_default()
9623 .entry(new_dir.clone())
9624 .or_default() += 1;
9625
9626 first = false;
9627 if old_dir.is_empty() || new_dir.is_empty() {
9629 break;
9630 }
9631 if !old_has || !new_has {
9634 break;
9635 }
9636 }
9637}
9638
9639fn dir_munge(buf: &mut Vec<u8>) -> bool {
9643 match buf.iter().rposition(|b| *b == b'/') {
9644 Some(i) => {
9645 buf.truncate(i);
9646 true
9647 }
9648 None => {
9649 buf.clear();
9650 false
9651 }
9652 }
9653}
9654
9655fn trailing_component<'a>(full: &'a [u8], dir: &[u8]) -> &'a [u8] {
9659 if dir.is_empty() {
9660 full
9661 } else {
9662 &full[dir.len() + 1..]
9664 }
9665}
9666
9667fn directory_fully_removed(dir: &[u8], side_map: &MergeEntryMap) -> bool {
9670 let mut prefix = dir.to_vec();
9671 prefix.push(b'/');
9672 for path in side_map.keys() {
9673 if path.starts_with(&prefix) {
9674 return false;
9675 }
9676 }
9677 true
9678}
9679
9680struct DirRenameMove {
9684 from: Vec<u8>,
9687 to: Vec<u8>,
9689 renamed_from: Option<Vec<u8>>,
9693}
9694
9695struct DirRenameTwoToOne {
9696 dest: Vec<u8>,
9697 ours_source: Vec<u8>,
9698 theirs_source: Vec<u8>,
9699 ours_label_path: Vec<u8>,
9700 theirs_label_path: Vec<u8>,
9701}
9702
9703#[derive(Clone)]
9706struct RehomeInfo {
9707 old_path: Vec<u8>,
9709 renamed_from: Option<Vec<u8>>,
9711 added_on_ours: bool,
9714}
9715
9716#[derive(Clone, Default)]
9718struct RehomeSides {
9719 ours: Option<RehomeInfo>,
9720 theirs: Option<RehomeInfo>,
9721}
9722
9723struct DirRenameCollision {
9728 dest: Vec<u8>,
9730 sources: Vec<Vec<u8>>,
9732}
9733
9734struct DirRenameOutcome {
9736 base: MergeEntryMap,
9740 ours: MergeEntryMap,
9741 theirs: MergeEntryMap,
9742 rehomed: BTreeMap<Vec<u8>, RehomeSides>,
9744 collisions: Vec<DirRenameCollision>,
9748 splits: BTreeSet<Vec<u8>>,
9750 back_to_self: BTreeSet<Vec<u8>>,
9756 dirty: bool,
9759 info_messages: Vec<MergeInfoMessage>,
9760}
9761
9762#[allow(clippy::too_many_arguments)]
9777fn apply_directory_renames(
9778 base_map: &MergeEntryMap,
9779 eff_base: &MergeEntryMap,
9780 eff_ours: &MergeEntryMap,
9781 eff_theirs: &MergeEntryMap,
9782 ours_side: &SideRenames,
9783 theirs_side: &SideRenames,
9784 dir_renames: &DirectoryRenameMaps,
9785 file_rename_dests: &BTreeMap<Vec<u8>, MergeRename>,
9786) -> DirRenameOutcome {
9787 let mut base = eff_base.clone();
9788 let mut ours = eff_ours.clone();
9789 let mut theirs = eff_theirs.clone();
9790 let mut rehomed = BTreeMap::new();
9791 let mut collisions = Vec::new();
9792 let mut splits = BTreeSet::new();
9793 let mut back_to_self = BTreeSet::new();
9794 let mut info_messages = Vec::new();
9795 let mut dirty = false;
9796
9797 let ours_excl = exclusion_dirs(&dir_renames.ours);
9801 let theirs_excl = exclusion_dirs(&dir_renames.theirs);
9802
9803 let ours_moves = plan_rehome(
9807 base_map,
9808 &ours,
9809 ours_side,
9810 &dir_renames.theirs,
9811 &ours_excl,
9812 &dir_renames.theirs_split,
9813 &mut collisions,
9814 &mut splits,
9815 &mut info_messages,
9816 &mut dirty,
9817 );
9818 let theirs_moves = plan_rehome(
9819 base_map,
9820 &theirs,
9821 theirs_side,
9822 &dir_renames.ours,
9823 &theirs_excl,
9824 &dir_renames.ours_split,
9825 &mut collisions,
9826 &mut splits,
9827 &mut info_messages,
9828 &mut dirty,
9829 );
9830
9831 apply_rehome_moves(
9832 base_map,
9833 file_rename_dests,
9834 &mut base,
9835 &mut ours,
9836 &mut theirs,
9837 ours_moves,
9838 true,
9839 &mut rehomed,
9840 &mut collisions,
9841 &mut back_to_self,
9842 &mut dirty,
9843 );
9844 apply_rehome_moves(
9845 base_map,
9846 file_rename_dests,
9847 &mut base,
9848 &mut ours,
9849 &mut theirs,
9850 theirs_moves,
9851 false,
9852 &mut rehomed,
9853 &mut collisions,
9854 &mut back_to_self,
9855 &mut dirty,
9856 );
9857
9858 DirRenameOutcome {
9859 base,
9860 ours,
9861 theirs,
9862 rehomed,
9863 collisions,
9864 splits,
9865 back_to_self,
9866 dirty,
9867 info_messages,
9868 }
9869}
9870
9871fn exclusion_dirs(side_dir_renames: &BTreeMap<Vec<u8>, Vec<u8>>) -> BTreeSet<Vec<u8>> {
9875 side_dir_renames.keys().cloned().collect()
9876}
9877
9878#[allow(clippy::too_many_arguments)]
9893fn plan_rehome(
9894 base_map: &MergeEntryMap,
9895 side: &MergeEntryMap,
9896 side_renames: &SideRenames,
9897 renamer_dirs: &BTreeMap<Vec<u8>, Vec<u8>>,
9898 exclusions: &BTreeSet<Vec<u8>>,
9899 split_dirs: &BTreeSet<Vec<u8>>,
9900 collisions: &mut Vec<DirRenameCollision>,
9901 splits: &mut BTreeSet<Vec<u8>>,
9902 info_messages: &mut Vec<MergeInfoMessage>,
9903 dirty: &mut bool,
9904) -> Vec<DirRenameMove> {
9905 if renamer_dirs.is_empty() && split_dirs.is_empty() {
9906 return Vec::new();
9907 }
9908
9909 let side_rename_src: BTreeMap<&[u8], &[u8]> = side_renames
9912 .pairs
9913 .iter()
9914 .map(|(o, n)| (n.as_slice(), o.as_slice()))
9915 .collect();
9916
9917 let candidates: Vec<Vec<u8>> = side
9918 .keys()
9919 .filter(|p| !base_map.contains_key(*p) || side_rename_src.contains_key(p.as_slice()))
9920 .cloned()
9921 .collect();
9922
9923 let mut planned: BTreeMap<Vec<u8>, Vec<DirRenameMove>> = BTreeMap::new();
9925 for path in candidates {
9926 if let Some(split_dir) = check_dir_split(&path, split_dirs) {
9927 splits.insert(split_dir.to_vec());
9928 *dirty = true;
9929 continue;
9930 }
9931 let Some((old_dir, new_dir)) = check_dir_renamed(&path, renamer_dirs) else {
9932 continue;
9933 };
9934 let new_dir_is_exclusion = exclusions.contains(new_dir);
9938 let new_dir_inside_exclusion = exclusions
9939 .iter()
9940 .any(|dir| directory_contains_proper(dir, new_dir));
9941 if new_dir_is_exclusion
9942 || (new_dir_inside_exclusion
9943 && !side_has_pure_add_under_dir(side, base_map, &side_rename_src, old_dir))
9944 {
9945 info_messages.push(MergeInfoMessage::DirRenameSkippedDueToRerename {
9946 old_dir: old_dir.to_vec(),
9947 path: path.clone(),
9948 new_dir: new_dir.to_vec(),
9949 });
9950 continue;
9951 }
9952 let dest = apply_dir_rename(old_dir, new_dir, &path);
9953 if dest == path {
9954 continue;
9956 }
9957 let renamed_from = side_rename_src.get(path.as_slice()).map(|s| s.to_vec());
9958 planned
9959 .entry(dest.clone())
9960 .or_default()
9961 .push(DirRenameMove {
9962 from: path,
9963 to: dest,
9964 renamed_from,
9965 });
9966 }
9967
9968 let mut moves = Vec::new();
9969 for (dest, group) in planned {
9970 if group.len() > 1 {
9971 *dirty = true;
9974 collisions.push(DirRenameCollision {
9975 dest,
9976 sources: group.into_iter().map(|m| m.from).collect(),
9977 });
9978 continue;
9979 }
9980 moves.push(group.into_iter().next().expect("non-empty"));
9981 }
9982 moves
9983}
9984
9985fn check_dir_split<'a>(path: &[u8], split_dirs: &'a BTreeSet<Vec<u8>>) -> Option<&'a [u8]> {
9986 let mut dir = parent_dir(path)?;
9987 loop {
9988 if let Some(split_dir) = split_dirs.get(dir) {
9989 return Some(split_dir);
9990 }
9991 dir = parent_dir(dir)?;
9992 }
9993}
9994
9995fn directory_contains_proper(parent: &[u8], child: &[u8]) -> bool {
9996 !parent.is_empty()
9997 && child.len() > parent.len()
9998 && child.starts_with(parent)
9999 && child[parent.len()] == b'/'
10000}
10001
10002fn side_has_pure_add_under_dir(
10003 side: &MergeEntryMap,
10004 base_map: &MergeEntryMap,
10005 side_rename_src: &BTreeMap<&[u8], &[u8]>,
10006 dir: &[u8],
10007) -> bool {
10008 side.keys().any(|path| {
10009 path_is_under_dir(path, dir)
10010 && !base_map.contains_key(path)
10011 && !side_rename_src.contains_key(path.as_slice())
10012 })
10013}
10014
10015fn path_is_under_dir(path: &[u8], dir: &[u8]) -> bool {
10016 !dir.is_empty() && path.len() > dir.len() && path.starts_with(dir) && path[dir.len()] == b'/'
10017}
10018
10019#[allow(clippy::too_many_arguments)]
10028fn apply_rehome_moves(
10029 original_base: &MergeEntryMap,
10030 file_rename_dests: &BTreeMap<Vec<u8>, MergeRename>,
10031 base: &mut MergeEntryMap,
10032 ours: &mut MergeEntryMap,
10033 theirs: &mut MergeEntryMap,
10034 moves: Vec<DirRenameMove>,
10035 side_is_ours: bool,
10036 rehomed: &mut BTreeMap<Vec<u8>, RehomeSides>,
10037 collisions: &mut Vec<DirRenameCollision>,
10038 back_to_self: &mut BTreeSet<Vec<u8>>,
10039 dirty: &mut bool,
10040) {
10041 for mv in moves {
10042 let occupied_on_this_side = if side_is_ours {
10047 ours.contains_key(&mv.to) || map_has_directory_at(ours, &mv.to)
10048 } else {
10049 theirs.contains_key(&mv.to) || map_has_directory_at(theirs, &mv.to)
10050 };
10051 let occupied_by_cross_rename =
10052 file_rename_dests
10053 .get(&mv.to)
10054 .is_some_and(|rename| match (side_is_ours, rename.side) {
10055 (true, RenameSide::Theirs) | (false, RenameSide::Ours) => true,
10056 (true, RenameSide::Ours) | (false, RenameSide::Theirs) => false,
10057 });
10058 let base_entry_at_dest = original_base.get(&mv.to).copied();
10059 let base_entry_at_source = original_base.get(&mv.from).copied();
10060 let other_side_entry_at_dest = if side_is_ours {
10061 theirs.get(&mv.to).copied()
10062 } else {
10063 ours.get(&mv.to).copied()
10064 };
10065 let other_side_entry_at_source = if side_is_ours {
10066 theirs.get(&mv.from).copied()
10067 } else {
10068 ours.get(&mv.from).copied()
10069 };
10070 let base_entry_for_shifted_source = base_entry_at_source.or(base_entry_at_dest);
10071 let rename_back_to_modified_source = mv
10072 .renamed_from
10073 .as_ref()
10074 .is_some_and(|source| source == &mv.to)
10075 && base_entry_at_dest.is_some()
10076 && (other_side_entry_at_dest.is_some_and(|entry| Some(entry) != base_entry_at_dest)
10077 || other_side_entry_at_source
10078 .is_some_and(|entry| Some(entry) != base_entry_for_shifted_source));
10079 if ((base_entry_at_dest.is_some() && !rename_back_to_modified_source)
10080 || (occupied_on_this_side && !occupied_by_cross_rename))
10081 && mv.to != mv.from
10082 {
10083 *dirty = true;
10084 collisions.push(DirRenameCollision {
10085 dest: mv.to.clone(),
10086 sources: vec![mv.from.clone()],
10087 });
10088 continue;
10089 }
10090 let mut moved = false;
10091 if occupied_by_cross_rename {
10092 base.remove(&mv.from);
10093 if side_is_ours {
10094 if let Some(entry) = ours.remove(&mv.from) {
10095 ours.insert(mv.to.clone(), entry);
10096 moved = true;
10097 }
10098 theirs.remove(&mv.from);
10099 } else {
10100 ours.remove(&mv.from);
10101 if let Some(entry) = theirs.remove(&mv.from) {
10102 theirs.insert(mv.to.clone(), entry);
10103 moved = true;
10104 }
10105 }
10106 } else {
10107 for m in [&mut *base, &mut *ours, &mut *theirs] {
10111 if let Some(entry) = m.remove(&mv.from) {
10112 m.insert(mv.to.clone(), entry);
10113 moved = true;
10114 }
10115 }
10116 }
10117 if moved {
10118 if rename_back_to_modified_source {
10119 back_to_self.insert(mv.to.clone());
10120 }
10121 let info = RehomeInfo {
10122 old_path: mv.from.clone(),
10123 renamed_from: mv.renamed_from.clone(),
10124 added_on_ours: side_is_ours,
10125 };
10126 let entry = rehomed.entry(mv.to.clone()).or_default();
10127 if side_is_ours {
10128 entry.ours = Some(info);
10129 } else {
10130 entry.theirs = Some(info);
10131 }
10132 }
10133 }
10134}
10135
10136fn collect_dir_rename_two_to_one(
10137 renames: &MergeRenames,
10138 rehomed: &BTreeMap<Vec<u8>, RehomeSides>,
10139) -> Vec<DirRenameTwoToOne> {
10140 let mut conflicts = Vec::new();
10141 for (dest, sides) in rehomed {
10142 let Some(file_rename) = renames.dest_to_source.get(dest) else {
10143 continue;
10144 };
10145 match file_rename.side {
10146 RenameSide::Ours => {
10147 let Some(info) = sides.theirs.as_ref() else {
10148 continue;
10149 };
10150 let Some(theirs_source) = info.renamed_from.as_ref() else {
10151 continue;
10152 };
10153 conflicts.push(DirRenameTwoToOne {
10154 dest: dest.clone(),
10155 ours_source: file_rename.source.clone(),
10156 theirs_source: theirs_source.clone(),
10157 ours_label_path: dest.clone(),
10158 theirs_label_path: info.old_path.clone(),
10159 });
10160 }
10161 RenameSide::Theirs => {
10162 let Some(info) = sides.ours.as_ref() else {
10163 continue;
10164 };
10165 let Some(ours_source) = info.renamed_from.as_ref() else {
10166 continue;
10167 };
10168 conflicts.push(DirRenameTwoToOne {
10169 dest: dest.clone(),
10170 ours_source: ours_source.clone(),
10171 theirs_source: file_rename.source.clone(),
10172 ours_label_path: info.old_path.clone(),
10173 theirs_label_path: dest.clone(),
10174 });
10175 }
10176 }
10177 }
10178 conflicts
10179}
10180
10181fn map_has_directory_at(map: &MergeEntryMap, path: &[u8]) -> bool {
10182 let mut prefix = path.to_vec();
10183 prefix.push(b'/');
10184 map.keys().any(|candidate| candidate.starts_with(&prefix))
10185}
10186
10187fn remap_rename_destinations(renames: &mut MergeRenames, rehomed: &BTreeMap<Vec<u8>, RehomeSides>) {
10188 if rehomed.is_empty() {
10189 return;
10190 }
10191 let mut remapped_deletes = BTreeMap::new();
10192 for (dest, rd) in std::mem::take(&mut renames.rename_deletes) {
10193 let new_dest = rehomed
10194 .iter()
10195 .find_map(|(new_dest, sides)| {
10196 let moved = sides
10197 .ours
10198 .as_ref()
10199 .is_some_and(|info| info.old_path == dest)
10200 || sides
10201 .theirs
10202 .as_ref()
10203 .is_some_and(|info| info.old_path == dest);
10204 moved.then(|| new_dest.clone())
10205 })
10206 .unwrap_or(dest);
10207 remapped_deletes.insert(new_dest, rd);
10208 }
10209 renames.rename_deletes = remapped_deletes;
10210
10211 for rename in renames.rename_rename_one_to_two.values_mut() {
10212 for (dest, sides) in rehomed {
10213 if sides
10214 .ours
10215 .as_ref()
10216 .is_some_and(|info| info.old_path == rename.ours_dest)
10217 {
10218 rename.ours_dest = dest.clone();
10219 }
10220 if sides
10221 .theirs
10222 .as_ref()
10223 .is_some_and(|info| info.old_path == rename.theirs_dest)
10224 {
10225 rename.theirs_dest = dest.clone();
10226 }
10227 }
10228 }
10229}
10230
10231fn drop_collapsed_rename_rename_conflicts(renames: &mut MergeRenames) {
10232 renames
10233 .rename_rename_one_to_two
10234 .retain(|_, rename| rename.ours_dest != rename.theirs_dest);
10235}
10236
10237fn apply_dir_rename_two_to_one_conflicts(
10238 db: &FileObjectDatabase,
10239 eff_ours: &MergeEntryMap,
10240 eff_theirs: &MergeEntryMap,
10241 conflicts: &[DirRenameTwoToOne],
10242 paths: &mut [MergedPath],
10243 leaves: &mut MergeEntryMap,
10244 options: &MergeTreesOptions<'_>,
10245) -> Result<()> {
10246 for conflict in conflicts {
10247 let Some(slot) = paths.iter_mut().find(|path| path.path == conflict.dest) else {
10248 continue;
10249 };
10250 let ours_entry = eff_ours.get(&conflict.dest).copied();
10251 let theirs_entry = eff_theirs.get(&conflict.dest).copied();
10252 let (Some((ours_mode, ours_oid)), Some((theirs_mode, theirs_oid))) =
10253 (ours_entry, theirs_entry)
10254 else {
10255 continue;
10256 };
10257 let ours_bytes = merge_blob_bytes(db, &ours_oid)?;
10258 let theirs_bytes = merge_blob_bytes(db, &theirs_oid)?;
10259 let (resolved_mode, mode_conflict) = merge_file_modes(None, ours_mode, theirs_mode);
10260 let favor = merge_favor_for_path(options, &conflict.dest);
10261 let result = if is_mergeable_file_mode(ours_mode) && is_mergeable_file_mode(theirs_mode) {
10262 merge_blobs(
10263 &[],
10264 &ours_bytes,
10265 &theirs_bytes,
10266 &MergeBlobOptions {
10267 ours_label: &qualify_label(options.ours_label, &conflict.ours_label_path),
10268 theirs_label: &qualify_label(options.theirs_label, &conflict.theirs_label_path),
10269 base_label: options.ancestor_label,
10270 style: options.style,
10271 favor,
10272 ws_ignore: options.ws_ignore,
10273 marker_size: merge_marker_size_for_path(options, &conflict.dest),
10274 },
10275 )
10276 } else {
10277 MergeBlobResult {
10278 content: ours_bytes.clone(),
10279 conflicted: true,
10280 }
10281 };
10282 let oid = db.write_object(EncodedObject::new(ObjectType::Blob, result.content.clone()))?;
10283 leaves.insert(conflict.dest.clone(), (resolved_mode, oid));
10284 slot.stages = MergeStages {
10285 base: None,
10286 ours: ours_entry,
10287 theirs: theirs_entry,
10288 };
10289 slot.result = Some((resolved_mode, oid));
10290 slot.worktree = Some((
10291 if ours_mode == theirs_mode {
10292 ours_mode
10293 } else {
10294 0o100644
10295 },
10296 result.content,
10297 ));
10298 slot.conflict = Some(MergeConflictKind::RenameRenameTwoToOne {
10299 ours_path: conflict.ours_source.clone(),
10300 theirs_path: conflict.theirs_source.clone(),
10301 });
10302 slot.auto_merged = !mode_conflict;
10303 }
10304 Ok(())
10305}
10306
10307fn rename_merged_leaf(
10313 db: &FileObjectDatabase,
10314 base: Option<(u32, ObjectId)>,
10315 ours: Option<(u32, ObjectId)>,
10316 theirs: Option<(u32, ObjectId)>,
10317 path: &[u8],
10318 options: &MergeTreesOptions<'_>,
10319) -> Result<Option<(u32, ObjectId)>> {
10320 match (ours, theirs) {
10321 (None, None) => Ok(None),
10322 (Some(entry), None) | (None, Some(entry)) => Ok(Some(entry)),
10323 (Some((ours_mode, ours_oid)), Some((theirs_mode, theirs_oid))) => {
10324 if (ours_mode, ours_oid) == (theirs_mode, theirs_oid) {
10325 return Ok(Some((ours_mode, ours_oid)));
10326 }
10327 if !is_mergeable_file_mode(ours_mode) || !is_mergeable_file_mode(theirs_mode) {
10328 return Ok(Some((ours_mode, ours_oid)));
10329 }
10330 let base_bytes = match base {
10331 Some((_, oid)) => merge_blob_bytes(db, &oid)?,
10332 None => Vec::new(),
10333 };
10334 let favor = merge_favor_for_path(options, path);
10335 let result = merge_blobs(
10336 &base_bytes,
10337 &merge_blob_bytes(db, &ours_oid)?,
10338 &merge_blob_bytes(db, &theirs_oid)?,
10339 &MergeBlobOptions {
10340 ours_label: options.ours_label,
10341 theirs_label: options.theirs_label,
10342 base_label: options.ancestor_label,
10343 style: options.style,
10344 favor,
10345 ws_ignore: options.ws_ignore,
10346 marker_size: merge_marker_size_for_path(options, path),
10347 },
10348 );
10349 let (mode, _) = merge_file_modes(base.map(|(mode, _)| mode), ours_mode, theirs_mode);
10350 let oid = db.write_object(EncodedObject::new(ObjectType::Blob, result.content))?;
10351 Ok(Some((mode, oid)))
10352 }
10353 }
10354}
10355
10356#[allow(clippy::too_many_arguments)]
10364fn apply_rename_two_to_one_and_add_conflicts(
10365 db: &FileObjectDatabase,
10366 base_map: &MergeEntryMap,
10367 ours_map: &MergeEntryMap,
10368 theirs_map: &MergeEntryMap,
10369 renames: &MergeRenames,
10370 paths: &mut Vec<MergedPath>,
10371 leaves: &mut MergeEntryMap,
10372 options: &MergeTreesOptions<'_>,
10373) -> Result<()> {
10374 let mut consumed_sources: Vec<Vec<u8>> = Vec::new();
10375
10376 for (dest, conflict) in &renames.rename_rename_two_to_one {
10377 let ours_leaf = rename_merged_leaf(
10380 db,
10381 base_map.get(&conflict.ours_source).copied(),
10382 ours_map.get(dest).copied(),
10383 theirs_map.get(&conflict.ours_source).copied(),
10384 dest,
10385 options,
10386 )?;
10387 let theirs_leaf = rename_merged_leaf(
10388 db,
10389 base_map.get(&conflict.theirs_source).copied(),
10390 ours_map.get(&conflict.theirs_source).copied(),
10391 theirs_map.get(dest).copied(),
10392 dest,
10393 options,
10394 )?;
10395 write_two_sided_dest_conflict(
10396 db,
10397 dest,
10398 ours_leaf,
10399 theirs_leaf,
10400 MergeConflictKind::RenameRenameTwoToOne {
10401 ours_path: conflict.ours_source.clone(),
10402 theirs_path: conflict.theirs_source.clone(),
10403 },
10404 options,
10405 paths,
10406 leaves,
10407 )?;
10408 consumed_sources.push(conflict.ours_source.clone());
10409 consumed_sources.push(conflict.theirs_source.clone());
10410 }
10411
10412 for (dest, add) in &renames.rename_adds {
10413 let (ours_leaf, theirs_leaf) = match add.side {
10414 RenameSide::Ours => (
10415 rename_merged_leaf(
10416 db,
10417 base_map.get(&add.source).copied(),
10418 ours_map.get(dest).copied(),
10419 theirs_map.get(&add.source).copied(),
10420 dest,
10421 options,
10422 )?,
10423 theirs_map.get(dest).copied(),
10424 ),
10425 RenameSide::Theirs => (
10426 ours_map.get(dest).copied(),
10427 rename_merged_leaf(
10428 db,
10429 base_map.get(&add.source).copied(),
10430 ours_map.get(&add.source).copied(),
10431 theirs_map.get(dest).copied(),
10432 dest,
10433 options,
10434 )?,
10435 ),
10436 };
10437 write_two_sided_dest_conflict(
10438 db,
10439 dest,
10440 ours_leaf,
10441 theirs_leaf,
10442 MergeConflictKind::Content { add_add: true },
10443 options,
10444 paths,
10445 leaves,
10446 )?;
10447 consumed_sources.push(add.source.clone());
10448 }
10449
10450 for source in &consumed_sources {
10456 leaves.remove(source);
10457 if let Some(slot) = paths.iter_mut().find(|path| &path.path == source) {
10458 slot.stages = MergeStages::default();
10459 slot.result = None;
10460 slot.worktree = None;
10461 slot.conflict = None;
10462 slot.auto_merged = false;
10463 } else {
10464 paths.push(MergedPath {
10465 path: source.clone(),
10466 stages: MergeStages::default(),
10467 result: None,
10468 worktree: None,
10469 conflict: None,
10470 auto_merged: false,
10471 });
10472 }
10473 }
10474 Ok(())
10475}
10476
10477#[allow(clippy::too_many_arguments)]
10482fn write_two_sided_dest_conflict(
10483 db: &FileObjectDatabase,
10484 dest: &[u8],
10485 ours_leaf: Option<(u32, ObjectId)>,
10486 theirs_leaf: Option<(u32, ObjectId)>,
10487 kind: MergeConflictKind,
10488 options: &MergeTreesOptions<'_>,
10489 paths: &mut Vec<MergedPath>,
10490 leaves: &mut MergeEntryMap,
10491) -> Result<()> {
10492 let ours_bytes = match ours_leaf {
10493 Some((mode, oid)) => Some((mode, merge_worktree_bytes(db, mode, &oid)?)),
10494 None => None,
10495 };
10496 let theirs_bytes = match theirs_leaf {
10497 Some((mode, oid)) => Some((mode, merge_worktree_bytes(db, mode, &oid)?)),
10498 None => None,
10499 };
10500 let (worktree_mode, worktree_content, result_leaf) = match (&ours_bytes, &theirs_bytes) {
10501 (Some((ours_mode, ours_content)), Some((theirs_mode, theirs_content))) => {
10502 let favor = merge_favor_for_path(options, dest);
10503 let merged = merge_blobs(
10504 &[],
10505 ours_content,
10506 theirs_content,
10507 &MergeBlobOptions {
10508 ours_label: options.ours_label,
10509 theirs_label: options.theirs_label,
10510 base_label: options.ancestor_label,
10511 style: options.style,
10512 favor,
10513 ws_ignore: options.ws_ignore,
10514 marker_size: merge_marker_size_for_path(options, dest),
10515 },
10516 );
10517 let mode = if ours_mode == theirs_mode {
10518 *ours_mode
10519 } else {
10520 0o100644
10521 };
10522 let oid =
10523 db.write_object(EncodedObject::new(ObjectType::Blob, merged.content.clone()))?;
10524 (mode, merged.content, Some((mode, oid)))
10525 }
10526 (Some((mode, content)), None) | (None, Some((mode, content))) => {
10527 (*mode, content.clone(), ours_leaf.or(theirs_leaf))
10528 }
10529 (None, None) => (0o100644, Vec::new(), None),
10530 };
10531
10532 let slot = MergedPath {
10533 path: dest.to_vec(),
10534 stages: MergeStages {
10535 base: None,
10536 ours: ours_leaf,
10537 theirs: theirs_leaf,
10538 },
10539 result: result_leaf,
10540 worktree: Some((worktree_mode, worktree_content)),
10541 conflict: Some(kind),
10542 auto_merged: true,
10543 };
10544 if let Some(existing) = paths.iter_mut().find(|path| path.path == dest) {
10545 *existing = slot;
10546 } else {
10547 paths.push(slot);
10548 }
10549 if let Some(leaf) = result_leaf {
10550 leaves.insert(dest.to_vec(), leaf);
10551 } else {
10552 leaves.remove(dest);
10553 }
10554 Ok(())
10555}
10556
10557#[allow(clippy::too_many_arguments)]
10558fn apply_rename_rename_one_to_two_conflicts(
10559 db: &FileObjectDatabase,
10560 base_map: &MergeEntryMap,
10561 eff_ours: &MergeEntryMap,
10562 eff_theirs: &MergeEntryMap,
10563 conflicts: &BTreeMap<Vec<u8>, RenameRenameOneToTwo>,
10564 paths: &mut Vec<MergedPath>,
10565 leaves: &mut MergeEntryMap,
10566 options: &MergeTreesOptions<'_>,
10567) -> Result<()> {
10568 for (old_path, conflict) in conflicts {
10569 let base_entry = base_map.get(old_path).copied();
10570 let ours_entry = eff_ours.get(&conflict.ours_dest).copied();
10571 let theirs_entry = eff_theirs.get(&conflict.theirs_dest).copied();
10572 let theirs_add_at_ours_dest = eff_theirs.get(&conflict.ours_dest).copied();
10573 let ours_add_at_theirs_dest = eff_ours.get(&conflict.theirs_dest).copied();
10574
10575 leaves.remove(old_path);
10576 leaves.remove(&conflict.ours_dest);
10577 leaves.remove(&conflict.theirs_dest);
10578 paths.retain(|path| {
10579 path.path != *old_path
10580 && path.path != conflict.ours_dest
10581 && path.path != conflict.theirs_dest
10582 });
10583
10584 paths.push(MergedPath {
10585 path: old_path.clone(),
10586 stages: MergeStages {
10587 base: base_entry,
10588 ours: None,
10589 theirs: None,
10590 },
10591 result: None,
10592 worktree: None,
10593 conflict: Some(MergeConflictKind::RenameRenameOneToTwo {
10594 old_path: old_path.clone(),
10595 ours_path: conflict.ours_dest.clone(),
10596 theirs_path: conflict.theirs_dest.clone(),
10597 ours_label: options.ours_label.to_string(),
10598 theirs_label: options.theirs_label.to_string(),
10599 }),
10600 auto_merged: false,
10601 });
10602
10603 let ours_worktree = match ours_entry {
10604 Some((mode, oid)) => Some((mode, merge_worktree_bytes(db, mode, &oid)?)),
10605 None => None,
10606 };
10607 paths.push(MergedPath {
10608 path: conflict.ours_dest.clone(),
10609 stages: MergeStages {
10610 base: None,
10611 ours: ours_entry,
10612 theirs: theirs_add_at_ours_dest,
10613 },
10614 result: None,
10615 worktree: ours_worktree,
10616 conflict: Some(MergeConflictKind::RenameRenameOneToTwoStage),
10617 auto_merged: false,
10618 });
10619
10620 let theirs_worktree = match theirs_entry {
10621 Some((mode, oid)) => Some((mode, merge_worktree_bytes(db, mode, &oid)?)),
10622 None => None,
10623 };
10624 paths.push(MergedPath {
10625 path: conflict.theirs_dest.clone(),
10626 stages: MergeStages {
10627 base: None,
10628 ours: ours_add_at_theirs_dest,
10629 theirs: theirs_entry,
10630 },
10631 result: None,
10632 worktree: theirs_worktree,
10633 conflict: Some(MergeConflictKind::RenameRenameOneToTwoStage),
10634 auto_merged: false,
10635 });
10636 }
10637 Ok(())
10638}
10639
10640fn qualify_label(label: &str, path: &[u8]) -> String {
10643 format!("{label}:{}", String::from_utf8_lossy(path))
10644}
10645
10646fn entry_map_as_tracked(map: &MergeEntryMap) -> BTreeMap<Vec<u8>, TrackedEntry> {
10649 map.iter()
10650 .map(|(path, (mode, oid))| {
10651 (
10652 path.clone(),
10653 TrackedEntry {
10654 mode: *mode,
10655 oid: *oid,
10656 },
10657 )
10658 })
10659 .collect()
10660}
10661
10662#[cfg(test)]
10663mod tests {
10664 use super::*;
10665 use sley_formats::RepositoryLayout;
10666 use sley_object::TreeEntry;
10667 use sley_odb::ObjectWriter;
10668 use std::path::PathBuf;
10669 use std::sync::atomic::{AtomicU64, Ordering};
10670
10671 static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
10672
10673 #[test]
10674 fn name_status_reports_added_from_index() {
10675 let root = temp_root();
10676 let layout = RepositoryLayout::init_at(&root, ObjectFormat::Sha1, false)
10677 .expect("test operation should succeed");
10678 let db = FileObjectDatabase::from_git_dir(&layout.git_dir, ObjectFormat::Sha1);
10679 let oid = db
10680 .write_object(EncodedObject::new(ObjectType::Blob, b"hello\n".to_vec()))
10681 .expect("test operation should succeed");
10682 let index = Index {
10683 version: 2,
10684 entries: vec![sley_index::IndexEntry {
10685 ctime_seconds: 0,
10686 ctime_nanoseconds: 0,
10687 mtime_seconds: 0,
10688 mtime_nanoseconds: 0,
10689 dev: 0,
10690 ino: 0,
10691 mode: 0o100644,
10692 uid: 0,
10693 gid: 0,
10694 size: 6,
10695 oid,
10696 flags: "hello.txt".len() as u16,
10697 flags_extended: 0,
10698 path: BString::from(b"hello.txt"),
10699 }],
10700 extensions: Vec::new(),
10701 checksum: None,
10702 };
10703 fs::write(
10704 layout.git_dir.join("index"),
10705 index
10706 .write_v2_sha1()
10707 .expect("test operation should succeed"),
10708 )
10709 .expect("test operation should succeed");
10710 fs::write(root.join("hello.txt"), b"hello\n").expect("test operation should succeed");
10711 let changes = diff_name_status_head_worktree(&root, &layout.git_dir, ObjectFormat::Sha1)
10712 .expect("test operation should succeed");
10713 assert_eq!(changes[0].line(), "A\thello.txt");
10714 fs::remove_dir_all(root).expect("test operation should succeed");
10715 }
10716
10717 #[test]
10718 fn tree_worktree_diff_treats_absent_skip_worktree_entries_as_clean() {
10719 let root = temp_root();
10720 let layout = RepositoryLayout::init_at(&root, ObjectFormat::Sha1, false)
10721 .expect("test operation should succeed");
10722 let mut db = FileObjectDatabase::from_git_dir(&layout.git_dir, ObjectFormat::Sha1);
10723 let oid = write_blob(&mut db, b"clean\n");
10724 let tree = write_tree(
10725 &mut db,
10726 &[(b"removeme", 0o100644, oid), (b"untouched", 0o100644, oid)],
10727 );
10728 write_index(
10729 &layout.git_dir,
10730 vec![
10731 skip_worktree_entry(b"removeme", oid),
10732 skip_worktree_entry(b"untouched", oid),
10733 ],
10734 );
10735
10736 let changes = diff_name_status_tree_worktree_with_options(
10737 &root,
10738 &layout.git_dir,
10739 ObjectFormat::Sha1,
10740 &tree,
10741 DiffNameStatusOptions::default(),
10742 )
10743 .expect("test operation should succeed");
10744
10745 assert!(
10746 changes.is_empty(),
10747 "absent sparse entries should not appear as deletes: {changes:?}"
10748 );
10749 fs::remove_dir_all(root).expect("test operation should succeed");
10750 }
10751
10752 #[test]
10753 fn tree_worktree_diff_trusts_present_skip_worktree_entry_when_sparse_disabled() {
10754 let root = temp_root();
10755 let layout = RepositoryLayout::init_at(&root, ObjectFormat::Sha1, false)
10756 .expect("test operation should succeed");
10757 let mut db = FileObjectDatabase::from_git_dir(&layout.git_dir, ObjectFormat::Sha1);
10758 let oid = write_blob(&mut db, b"clean\n");
10759 let tree = write_tree(&mut db, &[(b"modified", 0o100644, oid)]);
10760 write_index(&layout.git_dir, vec![skip_worktree_entry(b"modified", oid)]);
10761 fs::write(root.join("modified"), b"dirty\n").expect("test operation should succeed");
10762
10763 let changes = diff_name_status_tree_worktree_with_options(
10764 &root,
10765 &layout.git_dir,
10766 ObjectFormat::Sha1,
10767 &tree,
10768 DiffNameStatusOptions::default(),
10769 )
10770 .expect("test operation should succeed");
10771
10772 assert!(
10773 changes.is_empty(),
10774 "present skip-worktree dirt should be ignored when sparse checkout is disabled: {changes:?}"
10775 );
10776 fs::remove_dir_all(root).expect("test operation should succeed");
10777 }
10778
10779 #[test]
10780 fn tree_worktree_diff_adds_index_blob_for_present_skip_worktree_entry() {
10781 let root = temp_root();
10782 let layout = RepositoryLayout::init_at(&root, ObjectFormat::Sha1, false)
10783 .expect("test operation should succeed");
10784 let mut db = FileObjectDatabase::from_git_dir(&layout.git_dir, ObjectFormat::Sha1);
10785 let oid = write_blob(&mut db, b"");
10786 let tree = write_tree(&mut db, &[]);
10787 write_index(&layout.git_dir, vec![skip_worktree_entry(b"added", oid)]);
10788 fs::write(root.join("added"), b"dirty\n").expect("test operation should succeed");
10789
10790 let changes = diff_name_status_tree_worktree_with_options(
10791 &root,
10792 &layout.git_dir,
10793 ObjectFormat::Sha1,
10794 &tree,
10795 DiffNameStatusOptions::default(),
10796 )
10797 .expect("test operation should succeed");
10798
10799 assert_eq!(changes.len(), 1);
10800 assert_eq!(changes[0].line(), "A\tadded");
10801 assert_eq!(changes[0].new_oid, Some(oid));
10802 fs::remove_dir_all(root).expect("test operation should succeed");
10803 }
10804
10805 #[test]
10806 fn index_worktree_diff_returns_staged_gitlinks() {
10807 let root = temp_root();
10808 let layout = RepositoryLayout::init_at(&root, ObjectFormat::Sha1, false)
10809 .expect("test operation should succeed");
10810 let oid = ObjectId::from_hex(
10811 ObjectFormat::Sha1,
10812 "1111111111111111111111111111111111111111",
10813 )
10814 .expect("test operation should succeed");
10815 let index = Index {
10816 version: 2,
10817 entries: vec![sley_index::IndexEntry {
10818 ctime_seconds: 0,
10819 ctime_nanoseconds: 0,
10820 mtime_seconds: 0,
10821 mtime_nanoseconds: 0,
10822 dev: 0,
10823 ino: 0,
10824 mode: sley_index::GITLINK_MODE,
10825 uid: 0,
10826 gid: 0,
10827 size: 0,
10828 oid,
10829 flags: "deps/sub".len() as u16,
10830 flags_extended: 0,
10831 path: BString::from(b"deps/sub"),
10832 }],
10833 extensions: Vec::new(),
10834 checksum: None,
10835 };
10836 fs::write(
10837 layout.git_dir.join("index"),
10838 index
10839 .write_v2_sha1()
10840 .expect("test operation should succeed"),
10841 )
10842 .expect("test operation should succeed");
10843
10844 let diff = diff_name_status_index_worktree_with_options_and_gitlinks(
10845 &root,
10846 &layout.git_dir,
10847 ObjectFormat::Sha1,
10848 DiffNameStatusOptions::default(),
10849 )
10850 .expect("test operation should succeed");
10851
10852 assert_eq!(diff.entries.len(), 1);
10853 let gitlinks = diff.staged_gitlinks;
10854 assert_eq!(gitlinks.len(), 1);
10855 assert_eq!(gitlinks[0].path.as_bytes(), b"deps/sub");
10856 assert_eq!(gitlinks[0].oid, oid);
10857 fs::remove_dir_all(root).expect("test operation should succeed");
10858 }
10859
10860 #[cfg(unix)]
10861 #[test]
10862 fn index_worktree_diff_ignores_untracked_dangling_symlink() {
10863 use std::os::unix::fs::symlink;
10864
10865 let root = temp_root();
10866 let layout = RepositoryLayout::init_at(&root, ObjectFormat::Sha1, false)
10867 .expect("test operation should succeed");
10868 let db = FileObjectDatabase::from_git_dir(&layout.git_dir, ObjectFormat::Sha1);
10869 let oid = db
10870 .write_object(EncodedObject::new(ObjectType::Blob, b"clean\n".to_vec()))
10871 .expect("test operation should succeed");
10872 let index = Index {
10873 version: 2,
10874 entries: vec![sley_index::IndexEntry {
10875 ctime_seconds: 0,
10876 ctime_nanoseconds: 0,
10877 mtime_seconds: 0,
10878 mtime_nanoseconds: 0,
10879 dev: 0,
10880 ino: 0,
10881 mode: 0o100644,
10882 uid: 0,
10883 gid: 0,
10884 size: 6,
10885 oid,
10886 flags: "tracked.txt".len() as u16,
10887 flags_extended: 0,
10888 path: BString::from(b"tracked.txt"),
10889 }],
10890 extensions: Vec::new(),
10891 checksum: None,
10892 };
10893 fs::write(
10894 layout.git_dir.join("index"),
10895 index
10896 .write_v2_sha1()
10897 .expect("test operation should succeed"),
10898 )
10899 .expect("test operation should succeed");
10900 fs::write(root.join("tracked.txt"), b"clean\n").expect("test operation should succeed");
10901 symlink("missing-target", root.join("untracked-link"))
10902 .expect("test operation should succeed");
10903
10904 let changes = diff_name_status_index_worktree_with_options(
10905 &root,
10906 &layout.git_dir,
10907 ObjectFormat::Sha1,
10908 DiffNameStatusOptions {
10909 detect_renames: false,
10910 detect_copies: false,
10911 find_copies_harder: false,
10912 rename_empty: true,
10913 },
10914 )
10915 .expect("untracked dangling symlink should be ignored");
10916 assert!(changes.is_empty());
10917 fs::remove_dir_all(root).expect("test operation should succeed");
10918 }
10919
10920 #[test]
10921 fn index_worktree_diff_trusts_non_racy_stat_cache() {
10922 let root = temp_root();
10923 let layout = RepositoryLayout::init_at(&root, ObjectFormat::Sha1, false)
10924 .expect("test operation should succeed");
10925 let worktree_path = root.join("tracked.txt");
10926 fs::write(&worktree_path, b"clean\n").expect("test operation should succeed");
10927 let metadata = fs::symlink_metadata(&worktree_path).expect("test operation should succeed");
10928 let (mtime_seconds, mtime_nanoseconds) =
10929 sley_index::file_mtime_parts(&metadata).expect("test operation should succeed");
10930 let bogus_oid = ObjectId::from_hex(
10931 ObjectFormat::Sha1,
10932 "1111111111111111111111111111111111111111",
10933 )
10934 .expect("test operation should succeed");
10935 let index = Index {
10936 version: 2,
10937 entries: vec![sley_index::IndexEntry {
10938 ctime_seconds: 0,
10939 ctime_nanoseconds: 0,
10940 mtime_seconds: mtime_seconds as u32,
10941 mtime_nanoseconds: mtime_nanoseconds as u32,
10942 dev: 0,
10943 ino: 0,
10944 mode: sley_index::worktree_metadata_mode(&metadata),
10945 uid: 0,
10946 gid: 0,
10947 size: metadata.len() as u32,
10948 oid: bogus_oid,
10949 flags: "tracked.txt".len() as u16,
10950 flags_extended: 0,
10951 path: BString::from(b"tracked.txt"),
10952 }],
10953 extensions: Vec::new(),
10954 checksum: None,
10955 };
10956 std::thread::sleep(std::time::Duration::from_millis(1100));
10957 fs::write(
10958 layout.git_dir.join("index"),
10959 index
10960 .write_v2_sha1()
10961 .expect("test operation should succeed"),
10962 )
10963 .expect("test operation should succeed");
10964
10965 let changes = diff_name_status_index_worktree(&root, &layout.git_dir, ObjectFormat::Sha1)
10966 .expect("test operation should succeed");
10967 assert!(
10968 changes.is_empty(),
10969 "a clean non-racy stat match must reuse the cached index oid"
10970 );
10971 fs::remove_dir_all(root).expect("test operation should succeed");
10972 }
10973
10974 fn temp_root() -> PathBuf {
10975 let path = std::env::temp_dir().join(format!(
10976 "sley-diff-{}-{}",
10977 std::process::id(),
10978 TEMP_COUNTER.fetch_add(1, Ordering::Relaxed)
10979 ));
10980 fs::create_dir_all(&path).expect("test operation should succeed");
10981 path
10982 }
10983
10984 fn merge_opts() -> MergeBlobOptions<'static> {
10987 MergeBlobOptions {
10988 ours_label: "ours",
10989 theirs_label: "theirs",
10990 base_label: "base",
10991 style: ConflictStyle::Merge,
10992 favor: MergeFavor::None,
10993 ws_ignore: WsIgnore::EMPTY,
10994 marker_size: 7,
10995 }
10996 }
10997
10998 #[test]
10999 fn split_lines_preserves_content_and_newlines() {
11000 let lines = split_lines(b"a\nb\nc\n");
11001 assert_eq!(lines.len(), 3);
11002 assert_eq!(lines[0].content, b"a\n");
11003 assert!(lines[0].has_newline);
11004 assert_eq!(lines[2].content, b"c\n");
11005 assert!(lines[2].has_newline);
11006 assert!(split_lines(b"").is_empty());
11007 }
11008
11009 #[test]
11010 fn split_lines_tracks_missing_final_newline() {
11011 let lines = split_lines(b"a\nb");
11012 assert_eq!(lines.len(), 2);
11013 assert!(lines[0].has_newline);
11014 assert!(!lines[1].has_newline);
11015 assert_eq!(lines[1].content, b"b");
11016 assert_eq!(lines[1].bytes_without_newline(), b"b");
11017 let with_nl = split_lines(b"b\n");
11019 assert_ne!(lines[1], with_nl[0]);
11020 }
11021
11022 #[test]
11023 fn myers_replace_single_line() {
11024 let old = split_lines(b"a\nb\nc\n");
11025 let new = split_lines(b"a\nx\nc\n");
11026 assert_eq!(
11027 myers_diff_lines(&old, &new),
11028 vec![
11029 DiffOp::Equal(1),
11030 DiffOp::Delete(1),
11031 DiffOp::Insert(1),
11032 DiffOp::Equal(1),
11033 ]
11034 );
11035 }
11036
11037 #[test]
11038 fn myers_identical_is_single_equal() {
11039 let old = split_lines(b"a\nb\nc\n");
11040 let new = split_lines(b"a\nb\nc\n");
11041 assert_eq!(myers_diff_lines(&old, &new), vec![DiffOp::Equal(3)]);
11042 }
11043
11044 #[test]
11045 fn myers_pure_insert_and_delete() {
11046 let empty = split_lines(b"");
11047 let two = split_lines(b"a\nb\n");
11048 assert_eq!(myers_diff_lines(&empty, &two), vec![DiffOp::Insert(2)]);
11049 assert_eq!(myers_diff_lines(&two, &empty), vec![DiffOp::Delete(2)]);
11050
11051 let old = split_lines(b"a\nb\nc\nd\n");
11052 let new = split_lines(b"a\nc\nd\n");
11053 assert_eq!(
11054 myers_diff_lines(&old, &new),
11055 vec![DiffOp::Equal(1), DiffOp::Delete(1), DiffOp::Equal(2)]
11056 );
11057 }
11058
11059 #[test]
11060 fn myers_reconstructs_new_and_is_minimal() {
11061 let old = split_lines(b"the\nquick\nbrown\nfox\n");
11063 let new = split_lines(b"the\nlazy\nbrown\ncat\n");
11064 let ops = myers_diff_lines(&old, &new);
11065 let mut oi = 0usize;
11066 let mut ni = 0usize;
11067 let mut edits = 0usize;
11068 let mut rebuilt: Vec<u8> = Vec::new();
11069 for op in &ops {
11070 match *op {
11071 DiffOp::Equal(n) => {
11072 for _ in 0..n {
11073 assert_eq!(old[oi], new[ni]);
11074 rebuilt.extend_from_slice(old[oi].content);
11075 oi += 1;
11076 ni += 1;
11077 }
11078 }
11079 DiffOp::Delete(n) => {
11080 oi += n;
11081 edits += n;
11082 }
11083 DiffOp::Insert(n) => {
11084 for _ in 0..n {
11085 rebuilt.extend_from_slice(new[ni].content);
11086 ni += 1;
11087 }
11088 edits += n;
11089 }
11090 }
11091 }
11092 assert_eq!(rebuilt, b"the\nlazy\nbrown\ncat\n");
11093 assert_eq!(edits, 4);
11095 }
11096
11097 #[test]
11098 fn merge_non_overlapping_changes_is_clean() {
11099 let base = b"a\nb\nc\nd\ne\n";
11100 let ours = b"A\nb\nc\nd\ne\n";
11101 let theirs = b"a\nb\nc\nd\nE\n";
11102 let result = merge_blobs(base, ours, theirs, &merge_opts());
11103 assert!(!result.conflicted);
11104 assert_eq!(result.content, b"A\nb\nc\nd\nE\n");
11105 }
11106
11107 #[test]
11108 fn merge_identical_changes_no_conflict() {
11109 let base = b"a\nb\nc\n";
11110 let ours = b"a\nX\nc\n";
11111 let theirs = b"a\nX\nc\n";
11112 let result = merge_blobs(base, ours, theirs, &merge_opts());
11113 assert!(!result.conflicted);
11114 assert_eq!(result.content, b"a\nX\nc\n");
11115 }
11116
11117 #[test]
11118 fn merge_overlapping_change_emits_exact_markers() {
11119 let base = b"a\nb\nc\n";
11120 let ours = b"a\nOURS\nc\n";
11121 let theirs = b"a\nTHEIRS\nc\n";
11122 let result = merge_blobs(base, ours, theirs, &merge_opts());
11123 assert!(result.conflicted);
11124 assert_eq!(
11125 result.content,
11126 b"a\n<<<<<<< ours\nOURS\n=======\nTHEIRS\n>>>>>>> theirs\nc\n".to_vec(),
11127 );
11128 }
11129
11130 #[test]
11131 fn merge_diff3_style_includes_base_section() {
11132 let base = b"a\nb\nc\n";
11133 let ours = b"a\nOURS\nc\n";
11134 let theirs = b"a\nTHEIRS\nc\n";
11135 let options = MergeBlobOptions {
11136 style: ConflictStyle::Diff3,
11137 ..merge_opts()
11138 };
11139 let result = merge_blobs(base, ours, theirs, &options);
11140 assert!(result.conflicted);
11141 assert_eq!(
11142 result.content,
11143 b"a\n<<<<<<< ours\nOURS\n||||||| base\nb\n=======\nTHEIRS\n>>>>>>> theirs\nc\n"
11144 .to_vec(),
11145 );
11146 }
11147
11148 #[test]
11149 fn merge_empty_label_omits_trailing_space() {
11150 let base = b"a\nb\nc\n";
11151 let ours = b"a\nOURS\nc\n";
11152 let theirs = b"a\nTHEIRS\nc\n";
11153 let options = MergeBlobOptions {
11154 ours_label: "",
11155 theirs_label: "",
11156 base_label: "",
11157 style: ConflictStyle::Merge,
11158 favor: MergeFavor::None,
11159 ws_ignore: WsIgnore::EMPTY,
11160 marker_size: 7,
11161 };
11162 let result = merge_blobs(base, ours, theirs, &options);
11163 assert!(result.conflicted);
11164 assert_eq!(
11166 result.content,
11167 b"a\n<<<<<<<\nOURS\n=======\nTHEIRS\n>>>>>>>\nc\n".to_vec(),
11168 );
11169 }
11170
11171 #[test]
11172 fn merge_add_add_empty_base_conflicts() {
11173 let result = merge_blobs(b"", b"x\ny\n", b"p\nq\n", &merge_opts());
11174 assert!(result.conflicted);
11175 assert_eq!(
11176 result.content,
11177 b"<<<<<<< ours\nx\ny\n=======\np\nq\n>>>>>>> theirs\n".to_vec(),
11178 );
11179 }
11180
11181 #[test]
11182 fn merge_ignore_space_change_resolves_clean_keeping_ours() {
11183 let base = b"alpha beta\nsecond line\n";
11188 let ours = b"alpha beta\nsecond line\n"; let theirs = b"alpha beta\nsecond CHANGED\n"; let options = MergeBlobOptions {
11191 ws_ignore: WsIgnore {
11192 space_change: true,
11193 ..WsIgnore::EMPTY
11194 },
11195 ..merge_opts()
11196 };
11197 let result = merge_blobs(base, ours, theirs, &options);
11198 assert!(
11199 !result.conflicted,
11200 "whitespace-only divergence is not a conflict"
11201 );
11202 assert_eq!(result.content, b"alpha beta\nsecond CHANGED\n".to_vec());
11203 }
11204
11205 #[test]
11206 fn merge_ignore_space_change_still_conflicts_on_real_divergence() {
11207 let base = b"one\n";
11210 let ours = b"OURS\n";
11211 let theirs = b"THEIRS\n";
11212 let options = MergeBlobOptions {
11213 ws_ignore: WsIgnore {
11214 space_change: true,
11215 ..WsIgnore::EMPTY
11216 },
11217 ..merge_opts()
11218 };
11219 let result = merge_blobs(base, ours, theirs, &options);
11220 assert!(result.conflicted);
11221 }
11222
11223 #[test]
11224 fn merge_add_add_empty_base_identical_is_clean() {
11225 let result = merge_blobs(b"", b"x\ny\n", b"x\ny\n", &merge_opts());
11226 assert!(!result.conflicted);
11227 assert_eq!(result.content, b"x\ny\n");
11228 }
11229
11230 #[test]
11231 fn merge_deletion_one_side_takes_deletion() {
11232 let result = merge_blobs(b"a\nb\nc\n", b"a\nc\n", b"a\nb\nc\n", &merge_opts());
11234 assert!(!result.conflicted);
11235 assert_eq!(result.content, b"a\nc\n");
11236 }
11237
11238 #[test]
11239 fn merge_deletion_vs_modification_conflicts() {
11240 let result = merge_blobs(b"a\nb\nc\n", b"a\nc\n", b"a\nB!\nc\n", &merge_opts());
11242 assert!(result.conflicted);
11243 assert_eq!(
11245 result.content,
11246 b"a\n<<<<<<< ours\n=======\nB!\n>>>>>>> theirs\nc\n".to_vec(),
11247 );
11248 }
11249
11250 #[test]
11251 fn merge_missing_final_newline_marker_starts_on_own_line() {
11252 let base = b"a\nb";
11255 let ours = b"a\nOURS";
11256 let theirs = b"a\nTHEIRS";
11257 let result = merge_blobs(base, ours, theirs, &merge_opts());
11258 assert!(result.conflicted);
11259 assert_eq!(
11260 result.content,
11261 b"a\n<<<<<<< ours\nOURS\n=======\nTHEIRS\n>>>>>>> theirs\n".to_vec(),
11262 );
11263 }
11264
11265 #[test]
11266 fn merge_clean_preserves_missing_final_newline() {
11267 let result = merge_blobs(b"a\nb\n", b"a\nb", b"a\nb\n", &merge_opts());
11270 assert!(!result.conflicted);
11271 assert_eq!(result.content, b"a\nb");
11272 }
11273
11274 #[test]
11275 fn merge_both_append_identical_tail_is_clean() {
11276 let result = merge_blobs(b"a\n", b"a\nz\n", b"a\nz\n", &merge_opts());
11277 assert!(!result.conflicted);
11278 assert_eq!(result.content, b"a\nz\n");
11279 }
11280
11281 #[test]
11282 fn merge_when_ours_equals_base_yields_theirs() {
11283 let base = b"b\na\n";
11286 let theirs = b"b\nb\nc\na\nc\n";
11287 let result = merge_blobs(base, base, theirs, &merge_opts());
11288 assert!(!result.conflicted);
11289 assert_eq!(result.content, theirs.to_vec());
11290 }
11291 fn applied(outcome: ApplyOutcome) -> Vec<u8> {
11292 match outcome {
11293 ApplyOutcome::Applied(bytes) => bytes,
11294 ApplyOutcome::Rejected => panic!("expected Applied, got Rejected"),
11295 }
11296 }
11297
11298 #[test]
11299 fn parse_multi_file_patch() {
11300 let patch = b"\
11301diff --git a/one.txt b/one.txt
11302index aaaaaaa..bbbbbbb 100644
11303--- a/one.txt
11304+++ b/one.txt
11305@@ -1,3 +1,3 @@
11306 alpha
11307-beta
11308+BETA
11309 gamma
11310diff --git a/two.txt b/two.txt
11311index ccccccc..ddddddd 100644
11312--- a/two.txt
11313+++ b/two.txt
11314@@ -1,2 +1,3 @@
11315 first
11316+inserted
11317 second
11318";
11319 let patches = parse_unified_patch(patch).expect("test operation should succeed");
11320 assert_eq!(patches.len(), 2);
11321
11322 assert_eq!(patches[0].old_path.as_deref(), Some(b"one.txt".as_slice()));
11323 assert_eq!(patches[0].new_path.as_deref(), Some(b"one.txt".as_slice()));
11324 assert_eq!(patches[0].old_mode, Some(0o100644));
11327 assert_eq!(
11328 patches[0].old_oid_hex.as_deref(),
11329 Some(b"aaaaaaa".as_slice())
11330 );
11331 assert_eq!(
11332 patches[0].new_oid_hex.as_deref(),
11333 Some(b"bbbbbbb".as_slice())
11334 );
11335 assert_eq!(patches[0].hunks.len(), 1);
11336 let h = &patches[0].hunks[0];
11337 assert_eq!(
11338 (h.old_start, h.old_len, h.new_start, h.new_len),
11339 (1, 3, 1, 3)
11340 );
11341 assert_eq!(
11342 h.lines,
11343 vec![
11344 HunkLine::Context(b"alpha".to_vec()),
11345 HunkLine::Delete(b"beta".to_vec()),
11346 HunkLine::Insert(b"BETA".to_vec()),
11347 HunkLine::Context(b"gamma".to_vec()),
11348 ]
11349 );
11350
11351 assert_eq!(patches[1].new_path.as_deref(), Some(b"two.txt".as_slice()));
11352 assert_eq!(patches[1].hunks[0].new_len, 3);
11353 }
11354
11355 #[test]
11356 fn parse_default_hunk_range_length() {
11357 let patch = b"\
11359--- a/x
11360+++ b/x
11361@@ -1 +1,2 @@
11362 line
11363+added
11364";
11365 let patches = parse_unified_patch(patch).expect("test operation should succeed");
11366 let h = &patches[0].hunks[0];
11367 assert_eq!(
11368 (h.old_start, h.old_len, h.new_start, h.new_len),
11369 (1, 1, 1, 2)
11370 );
11371 }
11372
11373 #[test]
11374 fn parse_hunk_header_before_file_errors() {
11375 let patch = b"@@ -1,1 +1,1 @@\n context\n";
11376 assert!(parse_unified_patch(patch).is_err());
11377 }
11378
11379 #[test]
11380 fn parse_mismatched_counts_errors() {
11381 let patch = b"--- a/x\n+++ b/x\n@@ -1,2 +1,2 @@\n only\n+new\n";
11383 assert!(parse_unified_patch(patch).is_err());
11384 }
11385
11386 #[test]
11387 fn apply_clean_hunk() {
11388 let base = b"alpha\nbeta\ngamma\n";
11389 let patch = parse_unified_patch(
11390 b"--- a/x\n+++ b/x\n@@ -1,3 +1,3 @@\n alpha\n-beta\n+BETA\n gamma\n",
11391 )
11392 .expect("test operation should succeed");
11393 let out = applied(apply_file_patch(base, &patch[0]));
11394 assert_eq!(out, b"alpha\nBETA\ngamma\n");
11395 }
11396
11397 #[test]
11398 fn apply_with_line_offset() {
11399 let base = b"pre1\npre2\npre3\nalpha\nbeta\ngamma\ntail\n";
11406 let patch = parse_unified_patch(
11407 b"--- a/x\n+++ b/x\n@@ -2,4 +2,4 @@\n alpha\n-beta\n+BETA\n gamma\n tail\n",
11408 )
11409 .expect("test operation should succeed");
11410 let out = applied(apply_file_patch(base, &patch[0]));
11411 assert_eq!(out, b"pre1\npre2\npre3\nalpha\nBETA\ngamma\ntail\n");
11412 }
11413
11414 #[test]
11415 fn apply_with_negative_line_offset() {
11416 let base = b"alpha\nbeta\ngamma\n";
11418 let patch = parse_unified_patch(
11419 b"--- a/x\n+++ b/x\n@@ -50,3 +50,3 @@\n alpha\n-beta\n+BETA\n gamma\n",
11420 )
11421 .expect("test operation should succeed");
11422 let out = applied(apply_file_patch(base, &patch[0]));
11423 assert_eq!(out, b"alpha\nBETA\ngamma\n");
11424 }
11425
11426 #[test]
11427 fn apply_multiple_hunks() {
11428 let base = b"a\nb\nc\nd\ne\nf\ng\nh\n";
11429 let patch = parse_unified_patch(
11430 b"--- a/x\n+++ b/x\n\
11431@@ -1,3 +1,3 @@\n a\n-b\n+B\n c\n\
11432@@ -6,3 +6,3 @@\n f\n-g\n+G\n h\n",
11433 )
11434 .expect("test operation should succeed");
11435 let out = applied(apply_file_patch(base, &patch[0]));
11436 assert_eq!(out, b"a\nB\nc\nd\ne\nf\nG\nh\n");
11437 }
11438
11439 #[test]
11440 fn reject_on_context_mismatch() {
11441 let base = b"alpha\nDIFFERENT\ngamma\n";
11442 let patch = parse_unified_patch(
11443 b"--- a/x\n+++ b/x\n@@ -1,3 +1,3 @@\n alpha\n-beta\n+BETA\n gamma\n",
11444 )
11445 .expect("test operation should succeed");
11446 assert_eq!(apply_file_patch(base, &patch[0]), ApplyOutcome::Rejected);
11447 }
11448
11449 #[test]
11450 fn reject_when_match_end_required_but_not_at_eof() {
11451 let base = b"one\ntwo\nanchor\nalready\nappended\n";
11458 let patch =
11462 parse_unified_patch(b"--- a/x\n+++ b/x\n@@ -3,1 +3,3 @@\n anchor\n+added1\n+added2\n")
11463 .expect("test operation should succeed");
11464 assert_eq!(apply_file_patch(base, &patch[0]), ApplyOutcome::Rejected);
11465 }
11466
11467 #[test]
11468 fn append_at_eof_matches_when_context_reaches_end() {
11469 let base = b"one\ntwo\nanchor\n";
11473 let patch =
11474 parse_unified_patch(b"--- a/x\n+++ b/x\n@@ -3,1 +3,3 @@\n anchor\n+added1\n+added2\n")
11475 .expect("test operation should succeed");
11476 let out = applied(apply_file_patch(base, &patch[0]));
11477 assert_eq!(out, b"one\ntwo\nanchor\nadded1\nadded2\n");
11478 }
11479
11480 #[test]
11481 fn reject_when_match_beginning_required_but_not_at_start() {
11482 let base = b"junk\nalpha\nbeta\ngamma\n";
11486 let patch =
11487 parse_unified_patch(b"--- a/x\n+++ b/x\n@@ -1,2 +1,3 @@\n alpha\n+INSERT\n beta\n")
11488 .expect("test operation should succeed");
11489 assert_eq!(apply_file_patch(base, &patch[0]), ApplyOutcome::Rejected);
11490 }
11491
11492 #[test]
11493 fn no_default_fuzz_rejects_on_trailing_context_mismatch() {
11494 let base = b"alpha\nbeta\nDIVERGED\n";
11500 let patch = parse_unified_patch(
11501 b"--- a/x\n+++ b/x\n@@ -1,3 +1,3 @@\n alpha\n-beta\n+BETA\n gamma\n",
11502 )
11503 .expect("test operation should succeed");
11504 assert_eq!(apply_file_patch(base, &patch[0]), ApplyOutcome::Rejected);
11505 }
11506
11507 #[test]
11508 fn parse_and_apply_new_file() {
11509 let patch = parse_unified_patch(
11510 b"\
11511diff --git a/new.txt b/new.txt
11512new file mode 100644
11513index 0000000..1111111
11514--- /dev/null
11515+++ b/new.txt
11516@@ -0,0 +1,2 @@
11517+hello
11518+world
11519",
11520 )
11521 .expect("test operation should succeed");
11522 assert!(patches_first_is_new(&patch));
11523 assert_eq!(patch[0].old_path, None);
11524 assert_eq!(patch[0].new_path.as_deref(), Some(b"new.txt".as_slice()));
11525 assert_eq!(patch[0].new_mode, Some(0o100644));
11526 let out = applied(apply_file_patch(b"garbage that is ignored", &patch[0]));
11528 assert_eq!(out, b"hello\nworld\n");
11529 }
11530
11531 fn patches_first_is_new(patches: &[FilePatch]) -> bool {
11532 patches.first().map(|p| p.is_new).unwrap_or(false)
11533 }
11534
11535 #[test]
11536 fn parse_and_apply_delete_file() {
11537 let patch = parse_unified_patch(
11538 b"\
11539diff --git a/gone.txt b/gone.txt
11540deleted file mode 100644
11541index 1111111..0000000
11542--- a/gone.txt
11543+++ /dev/null
11544@@ -1,2 +0,0 @@
11545-hello
11546-world
11547",
11548 )
11549 .expect("test operation should succeed");
11550 assert!(patch[0].is_delete);
11551 assert_eq!(patch[0].old_path.as_deref(), Some(b"gone.txt".as_slice()));
11552 assert_eq!(patch[0].new_path, None);
11553 assert_eq!(patch[0].old_mode, Some(0o100644));
11554 let out = applied(apply_file_patch(b"hello\nworld\n", &patch[0]));
11555 assert_eq!(out, b"");
11556 }
11557
11558 #[test]
11559 fn parse_rename_headers() {
11560 let patch = parse_unified_patch(
11561 b"\
11562diff --git a/old/name.txt b/new/name.txt
11563similarity index 100%
11564rename from old/name.txt
11565rename to new/name.txt
11566",
11567 )
11568 .expect("test operation should succeed");
11569 assert!(patch[0].is_rename);
11570 assert_eq!(
11571 patch[0].old_path.as_deref(),
11572 Some(b"old/name.txt".as_slice())
11573 );
11574 assert_eq!(
11575 patch[0].new_path.as_deref(),
11576 Some(b"new/name.txt".as_slice())
11577 );
11578 assert!(patch[0].hunks.is_empty());
11579 }
11580
11581 #[test]
11582 fn parse_mode_change_headers() {
11583 let patch = parse_unified_patch(
11584 b"\
11585diff --git a/script.sh b/script.sh
11586old mode 100644
11587new mode 100755
11588",
11589 )
11590 .expect("test operation should succeed");
11591 assert_eq!(patch[0].old_mode, Some(0o100644));
11592 assert_eq!(patch[0].new_mode, Some(0o100755));
11593 assert!(!patch[0].is_new);
11594 assert!(!patch[0].is_delete);
11595 }
11596
11597 #[test]
11598 fn no_final_newline_base_preserved_when_untouched() {
11599 let base = b"alpha\nbeta\nnotail"; let patch = parse_unified_patch(
11609 b"--- a/x\n+++ b/x\n@@ -1,3 +1,3 @@\n-alpha\n+ALPHA\n beta\n notail\n\\ No newline at end of file\n",
11610 )
11611 .expect("test operation should succeed");
11612 let out = applied(apply_file_patch(base, &patch[0]));
11613 assert_eq!(out, b"ALPHA\nbeta\nnotail");
11614 }
11615
11616 #[test]
11617 fn no_final_newline_added_by_patch() {
11618 let base = b"alpha\nbeta\n";
11621 let patch = parse_unified_patch(
11622 b"--- a/x\n+++ b/x\n@@ -2,1 +2,1 @@\n-beta\n+beta-notail\n\\ No newline at end of file\n",
11623 )
11624 .expect("test operation should succeed");
11625 assert!(patch[0].hunks[0].new_no_newline);
11626 assert!(!patch[0].hunks[0].old_no_newline);
11627 let out = applied(apply_file_patch(base, &patch[0]));
11628 assert_eq!(out, b"alpha\nbeta-notail");
11629 }
11630
11631 #[test]
11632 fn no_final_newline_in_base_matched_and_kept() {
11633 let base = b"alpha\nbeta"; let patch = parse_unified_patch(
11637 b"--- a/x\n+++ b/x\n@@ -1,2 +1,2 @@\n-alpha\n+ALPHA\n beta\n\\ No newline at end of file\n",
11638 )
11639 .expect("test operation should succeed");
11640 assert!(patch[0].hunks[0].old_no_newline);
11641 assert!(patch[0].hunks[0].new_no_newline);
11642 let out = applied(apply_file_patch(base, &patch[0]));
11643 assert_eq!(out, b"ALPHA\nbeta");
11644 }
11645
11646 #[test]
11647 fn no_final_newline_mismatch_rejected() {
11648 let base = b"alpha\nbeta\n"; let patch = parse_unified_patch(
11652 b"--- a/x\n+++ b/x\n@@ -2,1 +2,1 @@\n-beta\n\\ No newline at end of file\n+beta2\n",
11653 )
11654 .expect("test operation should succeed");
11655 assert!(patch[0].hunks[0].old_no_newline);
11656 assert_eq!(apply_file_patch(base, &patch[0]), ApplyOutcome::Rejected);
11657 }
11658
11659 #[test]
11660 fn delete_with_no_final_newline() {
11661 let base = b"only line no newline";
11663 let patch = parse_unified_patch(
11664 b"--- a/x\n+++ /dev/null\n@@ -1,1 +0,0 @@\n-only line no newline\n\\ No newline at end of file\n",
11665 )
11666 .expect("test operation should succeed");
11667 assert!(patch[0].is_delete);
11668 let out = applied(apply_file_patch(base, &patch[0]));
11669 assert_eq!(out, b"");
11670 }
11671
11672 #[test]
11673 fn apply_pure_insertion_hunk() {
11674 let base = b"first\nsecond\n";
11675 let patch =
11676 parse_unified_patch(b"--- a/x\n+++ b/x\n@@ -1,2 +1,3 @@\n first\n+middle\n second\n")
11677 .expect("test operation should succeed");
11678 let out = applied(apply_file_patch(base, &patch[0]));
11679 assert_eq!(out, b"first\nmiddle\nsecond\n");
11680 }
11681
11682 #[test]
11683 fn apply_pure_deletion_hunk() {
11684 let base = b"first\nmiddle\nsecond\n";
11685 let patch =
11686 parse_unified_patch(b"--- a/x\n+++ b/x\n@@ -1,3 +1,2 @@\n first\n-middle\n second\n")
11687 .expect("test operation should succeed");
11688 let out = applied(apply_file_patch(base, &patch[0]));
11689 assert_eq!(out, b"first\nsecond\n");
11690 }
11691
11692 #[test]
11693 fn apply_then_reparse_round_trip() {
11694 let base = b"l1\nl2\nl3\nl4\nl5\n";
11698 let text = b"--- a/f\n+++ b/f\n@@ -2,3 +2,4 @@\n l2\n-l3\n+L3\n+L3b\n l4\n";
11699 let p1 = parse_unified_patch(text).expect("test operation should succeed");
11700 let p2 = parse_unified_patch(text).expect("test operation should succeed");
11701 assert_eq!(p1, p2);
11702 let out = applied(apply_file_patch(base, &p1[0]));
11703 assert_eq!(out, b"l1\nl2\nL3\nL3b\nl4\nl5\n");
11704 }
11705
11706 #[test]
11707 fn empty_context_line_without_trailing_space() {
11708 let base = b"a\n\nb\n";
11711 let patch = parse_unified_patch(b"--- a/x\n+++ b/x\n@@ -1,3 +1,3 @@\n a\n\n-b\n+B\n")
11712 .expect("test operation should succeed");
11713 assert_eq!(patch[0].hunks[0].lines[1], HunkLine::Context(Vec::new()));
11714 let out = applied(apply_file_patch(base, &patch[0]));
11715 assert_eq!(out, b"a\n\nB\n");
11716 }
11717
11718 #[test]
11719 fn split_blob_lines_handles_edge_cases() {
11720 assert!(split_blob_lines(b"").is_empty());
11721 let single = split_blob_lines(b"abc");
11722 assert_eq!(single.len(), 1);
11723 assert!(single[0].no_newline);
11724 let terminated = split_blob_lines(b"abc\n");
11725 assert_eq!(terminated.len(), 1);
11726 assert!(!terminated[0].no_newline);
11727 let blank_then_eof = split_blob_lines(b"x\n");
11728 assert_eq!(blank_then_eof.len(), 1);
11729 }
11730
11731 #[test]
11734 fn similarity_identical_and_empty_conventions() {
11735 assert_eq!(blob_similarity(b"hello\nworld\n", b"hello\nworld\n"), 100);
11737 assert_eq!(blob_similarity(b"", b""), 100);
11739 assert_eq!(blob_similarity(b"", b"hello\n"), 0);
11741 assert_eq!(blob_similarity(b"hello\n", b""), 0);
11742 }
11743
11744 #[test]
11745 fn similarity_one_changed_line_is_75_and_symmetric() {
11746 let a = b"one\ntwo\nthree\nfour\nfive\n";
11752 let b = b"one\ntwo\nTHREE\nfour\nfive\n";
11753 assert_eq!(blob_similarity(a, b), 75);
11754 assert_eq!(blob_similarity(b, a), 75);
11756 }
11757
11758 #[test]
11759 fn similarity_one_edited_line_of_three_is_66_not_67() {
11760 assert_eq!(blob_similarity(b"a\nb\nc\n", b"a\nB\nc\n"), 66);
11766 assert_eq!(blob_similarity(b"a\nB\nc\n", b"a\nb\nc\n"), 66);
11767 }
11768
11769 #[test]
11770 fn similarity_small_append_is_88() {
11771 let a = b"alpha\nbeta\ngamma\ndelta\nepsilon\nzeta\neta\ntheta\n";
11775 let b = b"alpha\nbeta\ngamma\ndelta\nepsilon\nzeta\neta\ntheta\nADDED\n";
11776 assert_eq!(blob_similarity(a, b), 88);
11777 }
11778
11779 #[test]
11780 fn similarity_half_rewrite_is_50() {
11781 let a = b"l1\nl2\nl3\nl4\nl5\nl6\n";
11784 let b = b"l1\nl2\nl3\nX4\nX5\nX6\n";
11785 assert_eq!(blob_similarity(a, b), 50);
11786 }
11787
11788 fn write_blob(db: &mut FileObjectDatabase, bytes: &[u8]) -> ObjectId {
11792 db.write_object(EncodedObject::new(ObjectType::Blob, bytes.to_vec()))
11793 .expect("test operation should succeed")
11794 }
11795
11796 fn write_tree(db: &mut FileObjectDatabase, entries: &[(&[u8], u32, ObjectId)]) -> ObjectId {
11799 let mut tree_entries: Vec<TreeEntry> = entries
11800 .iter()
11801 .map(|(name, mode, oid)| TreeEntry {
11802 mode: *mode,
11803 name: BString::from(*name),
11804 oid: *oid,
11805 })
11806 .collect();
11807 tree_entries.sort_by(|a, b| a.name.cmp(&b.name));
11808 let tree = Tree {
11809 entries: tree_entries,
11810 };
11811 db.write_object(EncodedObject::new(ObjectType::Tree, tree.write()))
11812 .expect("test operation should succeed")
11813 }
11814
11815 fn write_tree_in_order(
11816 db: &mut FileObjectDatabase,
11817 entries: &[(&[u8], u32, ObjectId)],
11818 ) -> ObjectId {
11819 let tree_entries: Vec<TreeEntry> = entries
11820 .iter()
11821 .map(|(name, mode, oid)| TreeEntry {
11822 mode: *mode,
11823 name: BString::from(*name),
11824 oid: *oid,
11825 })
11826 .collect();
11827 let tree = Tree {
11828 entries: tree_entries,
11829 };
11830 db.write_object(EncodedObject::new(ObjectType::Tree, tree.write()))
11831 .expect("test operation should succeed")
11832 }
11833
11834 fn skip_worktree_entry(path: &[u8], oid: ObjectId) -> sley_index::IndexEntry {
11835 let mut entry = sley_index::IndexEntry {
11836 ctime_seconds: 0,
11837 ctime_nanoseconds: 0,
11838 mtime_seconds: 0,
11839 mtime_nanoseconds: 0,
11840 dev: 0,
11841 ino: 0,
11842 mode: 0o100644,
11843 uid: 0,
11844 gid: 0,
11845 size: 0,
11846 oid,
11847 flags: path.len() as u16,
11848 flags_extended: 0,
11849 path: BString::from(path),
11850 };
11851 entry.set_skip_worktree(true);
11852 entry
11853 }
11854
11855 fn write_index(git_dir: &Path, mut entries: Vec<sley_index::IndexEntry>) {
11856 entries.sort_by(|left, right| left.path.as_bytes().cmp(right.path.as_bytes()));
11857 let index = Index {
11858 version: 3,
11859 entries,
11860 extensions: Vec::new(),
11861 checksum: None,
11862 };
11863 fs::write(
11864 git_dir.join("index"),
11865 index.write_sha1().expect("test operation should succeed"),
11866 )
11867 .expect("test operation should succeed");
11868 }
11869
11870 #[test]
11871 fn inexact_rename_detected_with_plausible_score() {
11872 let root = temp_root();
11875 let layout = RepositoryLayout::init_at(&root, ObjectFormat::Sha1, false)
11876 .expect("test operation should succeed");
11877 let mut db = FileObjectDatabase::from_git_dir(&layout.git_dir, ObjectFormat::Sha1);
11878
11879 let old = write_blob(&mut db, b"one\ntwo\nthree\nfour\nfive\n");
11880 let new = write_blob(&mut db, b"one\ntwo\nTHREE\nfour\nfive\n");
11881 let left = write_tree(&mut db, &[(b"a.txt", 0o100644, old)]);
11882 let right = write_tree(&mut db, &[(b"b.txt", 0o100644, new)]);
11883
11884 let opts = RenameDetectionOptions {
11885 base: DiffNameStatusOptions {
11886 detect_renames: true,
11887 detect_copies: false,
11888 find_copies_harder: false,
11889 rename_empty: true,
11890 },
11891 detect_inexact: true,
11892 rename_threshold: DEFAULT_RENAME_THRESHOLD,
11893 copy_threshold: DEFAULT_RENAME_THRESHOLD,
11894 rename_limit: 0,
11895 };
11896 let entries = diff_name_status_trees_with_rename_options(
11897 &db,
11898 ObjectFormat::Sha1,
11899 &left,
11900 &right,
11901 opts,
11902 )
11903 .expect("test operation should succeed");
11904
11905 assert_eq!(
11906 entries.len(),
11907 1,
11908 "expected a single rename entry: {entries:?}"
11909 );
11910 assert_eq!(entries[0].status, NameStatus::Renamed(75));
11911 assert_eq!(
11912 entries[0].old_path.as_ref().map(|p| p.as_bytes()),
11913 Some(b"a.txt".as_slice())
11914 );
11915 assert_eq!(entries[0].path, b"b.txt");
11916 assert_eq!(entries[0].line(), "R075\ta.txt\tb.txt");
11917 fs::remove_dir_all(root).expect("test operation should succeed");
11918 }
11919
11920 #[test]
11921 fn tree_diff_preserves_duplicate_entry_multiplicity() {
11922 let root = temp_root();
11923 let layout = RepositoryLayout::init_at(&root, ObjectFormat::Sha1, false)
11924 .expect("test operation should succeed");
11925 let mut db = FileObjectDatabase::from_git_dir(&layout.git_dir, ObjectFormat::Sha1);
11926
11927 let blob_one = write_blob(&mut db, b"one\n");
11928 let blob_two = write_blob(&mut db, b"two\n");
11929 let inner_one_a = write_tree_in_order(&mut db, &[(b"inner", 0o100644, blob_one)]);
11930 let inner_one_b = write_tree_in_order(
11931 &mut db,
11932 &[
11933 (b"inner", 0o100644, blob_two),
11934 (b"inner", 0o100644, blob_two),
11935 (b"inner", 0o100644, blob_two),
11936 ],
11937 );
11938 let outer_one = write_tree_in_order(
11939 &mut db,
11940 &[
11941 (b"outer", TREE_ENTRY_MODE, inner_one_a),
11942 (b"outer", TREE_ENTRY_MODE, inner_one_b),
11943 ],
11944 );
11945 let inner_two = write_tree_in_order(
11946 &mut db,
11947 &[
11948 (b"inner", 0o100644, blob_one),
11949 (b"inner", 0o100644, blob_two),
11950 (b"inner", 0o100644, blob_two),
11951 (b"inner", 0o100644, blob_two),
11952 ],
11953 );
11954 let outer_two = write_tree_in_order(&mut db, &[(b"outer", TREE_ENTRY_MODE, inner_two)]);
11955 let outer_three = write_tree_in_order(&mut db, &[(b"renamed", 0o100644, blob_one)]);
11956
11957 let raw_options = DiffNameStatusOptions {
11958 detect_renames: false,
11959 detect_copies: false,
11960 find_copies_harder: false,
11961 rename_empty: true,
11962 };
11963 let raw = diff_name_status_trees_with_options(
11964 &db,
11965 ObjectFormat::Sha1,
11966 &outer_one,
11967 &outer_two,
11968 raw_options,
11969 )
11970 .expect("test operation should succeed");
11971 assert_eq!(
11972 status_lines(&raw),
11973 vec![
11974 "A\touter/inner",
11975 "A\touter/inner",
11976 "A\touter/inner",
11977 "D\touter/inner",
11978 "D\touter/inner",
11979 "D\touter/inner",
11980 ]
11981 );
11982
11983 let rename_options = RenameDetectionOptions {
11984 base: DiffNameStatusOptions {
11985 detect_renames: true,
11986 detect_copies: false,
11987 find_copies_harder: false,
11988 rename_empty: true,
11989 },
11990 detect_inexact: true,
11991 rename_threshold: DEFAULT_RENAME_THRESHOLD,
11992 copy_threshold: DEFAULT_RENAME_THRESHOLD,
11993 rename_limit: 0,
11994 };
11995 let no_op_renames = diff_name_status_trees_with_rename_options(
11996 &db,
11997 ObjectFormat::Sha1,
11998 &outer_one,
11999 &outer_two,
12000 rename_options,
12001 )
12002 .expect("test operation should succeed");
12003 assert!(no_op_renames.is_empty());
12004
12005 let renamed = diff_name_status_trees_with_rename_options(
12006 &db,
12007 ObjectFormat::Sha1,
12008 &outer_one,
12009 &outer_three,
12010 rename_options,
12011 )
12012 .expect("test operation should succeed");
12013 assert_eq!(
12014 status_lines(&renamed),
12015 vec![
12016 "D\touter/inner",
12017 "D\touter/inner",
12018 "D\touter/inner",
12019 "R100\touter/inner\trenamed",
12020 ]
12021 );
12022
12023 fs::remove_dir_all(root).expect("test operation should succeed");
12024 }
12025
12026 #[test]
12027 fn inexact_rename_below_threshold_not_detected() {
12028 let root = temp_root();
12031 let layout = RepositoryLayout::init_at(&root, ObjectFormat::Sha1, false)
12032 .expect("test operation should succeed");
12033 let mut db = FileObjectDatabase::from_git_dir(&layout.git_dir, ObjectFormat::Sha1);
12034
12035 let old = write_blob(&mut db, b"l1\nl2\nl3\nl4\nl5\nl6\n");
12036 let new = write_blob(&mut db, b"l1\nl2\nl3\nX4\nX5\nX6\n");
12037 let left = write_tree(&mut db, &[(b"a.txt", 0o100644, old)]);
12038 let right = write_tree(&mut db, &[(b"b.txt", 0o100644, new)]);
12039
12040 let opts = RenameDetectionOptions {
12041 base: DiffNameStatusOptions {
12042 detect_renames: true,
12043 detect_copies: false,
12044 find_copies_harder: false,
12045 rename_empty: true,
12046 },
12047 detect_inexact: true,
12048 rename_threshold: 60,
12049 copy_threshold: 60,
12050 rename_limit: 0,
12051 };
12052 let entries = diff_name_status_trees_with_rename_options(
12053 &db,
12054 ObjectFormat::Sha1,
12055 &left,
12056 &right,
12057 opts,
12058 )
12059 .expect("test operation should succeed");
12060
12061 let statuses: Vec<_> = entries.iter().map(|e| e.status).collect();
12062 assert!(
12063 statuses.contains(&NameStatus::Added) && statuses.contains(&NameStatus::Deleted),
12064 "expected separate add/delete below threshold, got {entries:?}"
12065 );
12066 assert!(
12067 !statuses.iter().any(|s| matches!(s, NameStatus::Renamed(_))),
12068 "no rename should be reported below threshold: {entries:?}"
12069 );
12070
12071 let opts_low = RenameDetectionOptions {
12074 rename_threshold: 50,
12075 ..opts
12076 };
12077 let entries_low = diff_name_status_trees_with_rename_options(
12078 &db,
12079 ObjectFormat::Sha1,
12080 &left,
12081 &right,
12082 opts_low,
12083 )
12084 .expect("test operation should succeed");
12085 assert_eq!(entries_low.len(), 1);
12086 assert_eq!(entries_low[0].status, NameStatus::Renamed(50));
12087 fs::remove_dir_all(root).expect("test operation should succeed");
12088 }
12089
12090 #[test]
12091 fn exact_rename_scores_100_and_takes_priority() {
12092 let root = temp_root();
12095 let layout = RepositoryLayout::init_at(&root, ObjectFormat::Sha1, false)
12096 .expect("test operation should succeed");
12097 let mut db = FileObjectDatabase::from_git_dir(&layout.git_dir, ObjectFormat::Sha1);
12098
12099 let oid = write_blob(&mut db, b"identical\ncontent\nhere\n");
12100 let left = write_tree(&mut db, &[(b"old.txt", 0o100644, oid)]);
12101 let right = write_tree(&mut db, &[(b"new.txt", 0o100644, oid)]);
12102
12103 for inexact in [false, true] {
12104 let opts = RenameDetectionOptions {
12105 base: DiffNameStatusOptions {
12106 detect_renames: true,
12107 detect_copies: false,
12108 find_copies_harder: false,
12109 rename_empty: true,
12110 },
12111 detect_inexact: inexact,
12112 rename_threshold: DEFAULT_RENAME_THRESHOLD,
12113 copy_threshold: DEFAULT_RENAME_THRESHOLD,
12114 rename_limit: 0,
12115 };
12116 let entries = diff_name_status_trees_with_rename_options(
12117 &db,
12118 ObjectFormat::Sha1,
12119 &left,
12120 &right,
12121 opts,
12122 )
12123 .expect("test operation should succeed");
12124 assert_eq!(entries.len(), 1, "inexact={inexact}: {entries:?}");
12125 assert_eq!(entries[0].status, NameStatus::Renamed(100));
12126 assert_eq!(
12127 entries[0].old_path.as_ref().map(|p| p.as_bytes()),
12128 Some(b"old.txt".as_slice())
12129 );
12130 assert_eq!(entries[0].path, b"new.txt");
12131 }
12132 fs::remove_dir_all(root).expect("test operation should succeed");
12133 }
12134
12135 #[test]
12136 fn inexact_copy_detected_with_score() {
12137 let root = temp_root();
12142 let layout = RepositoryLayout::init_at(&root, ObjectFormat::Sha1, false)
12143 .expect("test operation should succeed");
12144 let mut db = FileObjectDatabase::from_git_dir(&layout.git_dir, ObjectFormat::Sha1);
12145
12146 let orig = write_blob(&mut db, b"aaa\nbbb\nccc\nddd\neee\n");
12147 let copy = write_blob(&mut db, b"aaa\nbbb\nccc\nddd\nEEE\n");
12148 let left = write_tree(&mut db, &[(b"orig.txt", 0o100644, orig.clone())]);
12149 let right = write_tree(
12150 &mut db,
12151 &[(b"orig.txt", 0o100644, orig), (b"copy.txt", 0o100644, copy)],
12152 );
12153
12154 let opts = RenameDetectionOptions {
12155 base: DiffNameStatusOptions {
12156 detect_renames: true,
12157 detect_copies: true,
12158 find_copies_harder: true,
12159 rename_empty: true,
12160 },
12161 detect_inexact: true,
12162 rename_threshold: DEFAULT_RENAME_THRESHOLD,
12163 copy_threshold: DEFAULT_RENAME_THRESHOLD,
12164 rename_limit: 0,
12165 };
12166 let entries = diff_name_status_trees_with_rename_options(
12167 &db,
12168 ObjectFormat::Sha1,
12169 &left,
12170 &right,
12171 opts,
12172 )
12173 .expect("test operation should succeed");
12174
12175 let copy_entry = entries
12176 .iter()
12177 .find(|e| e.path == b"copy.txt")
12178 .unwrap_or_else(|| panic!("no copy.txt entry: {entries:?}"));
12179 assert_eq!(copy_entry.status, NameStatus::Copied(80));
12180 assert_eq!(
12181 copy_entry.old_path.as_ref().map(|p| p.as_bytes()),
12182 Some(b"orig.txt".as_slice())
12183 );
12184 assert!(
12186 entries.iter().all(|e| e.status != NameStatus::Deleted),
12187 "copy must not delete the source: {entries:?}"
12188 );
12189 fs::remove_dir_all(root).expect("test operation should succeed");
12190 }
12191
12192 #[test]
12193 fn inexact_copy_skipped_over_rename_limit() {
12194 let root = temp_root();
12202 let layout = RepositoryLayout::init_at(&root, ObjectFormat::Sha1, false)
12203 .expect("test operation should succeed");
12204 let mut db = FileObjectDatabase::from_git_dir(&layout.git_dir, ObjectFormat::Sha1);
12205
12206 let orig = write_blob(&mut db, b"aaa\nbbb\nccc\nddd\neee\n");
12207 let extra = write_blob(&mut db, b"111\n222\n333\n444\n555\n");
12208 let copy = write_blob(&mut db, b"aaa\nbbb\nccc\nddd\nEEE\n");
12209 let left = write_tree(
12212 &mut db,
12213 &[
12214 (b"orig.txt", 0o100644, orig.clone()),
12215 (b"extra.txt", 0o100644, extra.clone()),
12216 ],
12217 );
12218 let right = write_tree(
12219 &mut db,
12220 &[
12221 (b"orig.txt", 0o100644, orig),
12222 (b"extra.txt", 0o100644, extra),
12223 (b"copy.txt", 0o100644, copy),
12224 ],
12225 );
12226
12227 let opts_for = |rename_limit| RenameDetectionOptions {
12228 base: DiffNameStatusOptions {
12229 detect_renames: true,
12230 detect_copies: true,
12231 find_copies_harder: true,
12232 rename_empty: true,
12233 },
12234 detect_inexact: true,
12235 rename_threshold: DEFAULT_RENAME_THRESHOLD,
12236 copy_threshold: DEFAULT_RENAME_THRESHOLD,
12237 rename_limit,
12238 };
12239
12240 let over = diff_name_status_trees_with_rename_options(
12242 &db,
12243 ObjectFormat::Sha1,
12244 &left,
12245 &right,
12246 opts_for(1),
12247 )
12248 .expect("test operation should succeed");
12249 let copy_over = over
12250 .iter()
12251 .find(|e| e.path == b"copy.txt")
12252 .unwrap_or_else(|| panic!("no copy.txt entry: {over:?}"));
12253 assert_eq!(
12254 copy_over.status,
12255 NameStatus::Added,
12256 "over rename_limit, copy must degrade to a plain Add: {over:?}"
12257 );
12258
12259 let under = diff_name_status_trees_with_rename_options(
12261 &db,
12262 ObjectFormat::Sha1,
12263 &left,
12264 &right,
12265 opts_for(4),
12266 )
12267 .expect("test operation should succeed");
12268 let copy_under = under
12269 .iter()
12270 .find(|e| e.path == b"copy.txt")
12271 .unwrap_or_else(|| panic!("no copy.txt entry: {under:?}"));
12272 assert_eq!(
12273 copy_under.status,
12274 NameStatus::Copied(80),
12275 "below rename_limit, copy detection is unaffected: {under:?}"
12276 );
12277
12278 fs::remove_dir_all(root).expect("test operation should succeed");
12279 }
12280
12281 #[test]
12282 fn inexact_rename_with_small_edit_scores_88() {
12283 let root = temp_root();
12286 let layout = RepositoryLayout::init_at(&root, ObjectFormat::Sha1, false)
12287 .expect("test operation should succeed");
12288 let mut db = FileObjectDatabase::from_git_dir(&layout.git_dir, ObjectFormat::Sha1);
12289
12290 let old = write_blob(
12291 &mut db,
12292 b"alpha\nbeta\ngamma\ndelta\nepsilon\nzeta\neta\ntheta\n",
12293 );
12294 let new = write_blob(
12295 &mut db,
12296 b"alpha\nbeta\ngamma\ndelta\nepsilon\nzeta\neta\ntheta\nADDED\n",
12297 );
12298 let left = write_tree(&mut db, &[(b"src.txt", 0o100644, old)]);
12299 let right = write_tree(&mut db, &[(b"dst.txt", 0o100644, new)]);
12300
12301 let opts = RenameDetectionOptions::inexact(DiffNameStatusOptions {
12302 detect_renames: true,
12303 detect_copies: false,
12304 find_copies_harder: false,
12305 rename_empty: true,
12306 });
12307 let entries = diff_name_status_trees_with_rename_options(
12308 &db,
12309 ObjectFormat::Sha1,
12310 &left,
12311 &right,
12312 opts,
12313 )
12314 .expect("test operation should succeed");
12315
12316 assert_eq!(entries.len(), 1, "{entries:?}");
12317 assert_eq!(entries[0].status, NameStatus::Renamed(88));
12318 assert_eq!(
12319 entries[0].old_path.as_ref().map(|p| p.as_bytes()),
12320 Some(b"src.txt".as_slice())
12321 );
12322 assert_eq!(entries[0].path, b"dst.txt");
12323 fs::remove_dir_all(root).expect("test operation should succeed");
12324 }
12325
12326 #[test]
12327 fn inexact_disabled_default_preserves_exact_only_behavior() {
12328 assert!(!RenameDetectionOptions::default().detect_inexact);
12332 assert_eq!(
12333 RenameDetectionOptions::default().rename_threshold,
12334 DEFAULT_RENAME_THRESHOLD
12335 );
12336
12337 let root = temp_root();
12338 let layout = RepositoryLayout::init_at(&root, ObjectFormat::Sha1, false)
12339 .expect("test operation should succeed");
12340 let mut db = FileObjectDatabase::from_git_dir(&layout.git_dir, ObjectFormat::Sha1);
12341
12342 let old = write_blob(&mut db, b"one\ntwo\nthree\nfour\nfive\n");
12343 let new = write_blob(&mut db, b"one\ntwo\nTHREE\nfour\nfive\n");
12344 let left = write_tree(&mut db, &[(b"a.txt", 0o100644, old)]);
12345 let right = write_tree(&mut db, &[(b"b.txt", 0o100644, new)]);
12346
12347 let entries = diff_name_status_trees_with_rename_options(
12348 &db,
12349 ObjectFormat::Sha1,
12350 &left,
12351 &right,
12352 RenameDetectionOptions::default(),
12353 )
12354 .expect("test operation should succeed");
12355 let statuses: Vec<_> = entries.iter().map(|e| e.status).collect();
12356 assert!(statuses.contains(&NameStatus::Added));
12357 assert!(statuses.contains(&NameStatus::Deleted));
12358 assert!(!statuses.iter().any(|s| matches!(s, NameStatus::Renamed(_))));
12359 fs::remove_dir_all(root).expect("test operation should succeed");
12360 }
12361
12362 fn apply_ops(old: &[DiffLine<'_>], new: &[DiffLine<'_>], ops: &[DiffOp]) -> Vec<u8> {
12370 let mut oi = 0usize;
12371 let mut ni = 0usize;
12372 let mut rebuilt: Vec<u8> = Vec::new();
12373 for op in ops {
12374 match *op {
12375 DiffOp::Equal(n) => {
12376 for _ in 0..n {
12377 assert_eq!(old[oi], new[ni], "Equal op covered unequal lines");
12379 rebuilt.extend_from_slice(old[oi].content);
12380 oi += 1;
12381 ni += 1;
12382 }
12383 }
12384 DiffOp::Delete(n) => oi += n,
12385 DiffOp::Insert(n) => {
12386 for _ in 0..n {
12387 rebuilt.extend_from_slice(new[ni].content);
12388 ni += 1;
12389 }
12390 }
12391 }
12392 }
12393 assert_eq!(oi, old.len(), "script did not consume all of old");
12395 assert_eq!(ni, new.len(), "script did not consume all of new");
12396 rebuilt
12397 }
12398
12399 fn assert_valid_script(old_bytes: &[u8], new_bytes: &[u8], ops: &[DiffOp]) {
12402 let old = split_lines(old_bytes);
12403 let new = split_lines(new_bytes);
12404 let rebuilt = apply_ops(&old, &new, ops);
12405 assert_eq!(rebuilt, new_bytes, "script did not rebuild new");
12406 for pair in ops.windows(2) {
12407 let same_kind = matches!(
12408 (pair[0], pair[1]),
12409 (DiffOp::Equal(_), DiffOp::Equal(_))
12410 | (DiffOp::Delete(_), DiffOp::Delete(_))
12411 | (DiffOp::Insert(_), DiffOp::Insert(_))
12412 );
12413 assert!(!same_kind, "ops not coalesced: {:?}", ops);
12414 }
12415 }
12416
12417 fn check_all_algorithms(old_bytes: &[u8], new_bytes: &[u8]) {
12420 let old = split_lines(old_bytes);
12421 let new = split_lines(new_bytes);
12422 for algo in [
12423 DiffAlgorithm::Myers,
12424 DiffAlgorithm::Minimal,
12425 DiffAlgorithm::Patience,
12426 DiffAlgorithm::Histogram,
12427 ] {
12428 let ops = diff_lines_with_algorithm(&old, &new, algo);
12429 assert_valid_script(old_bytes, new_bytes, &ops);
12430 }
12431 }
12432
12433 #[test]
12434 fn patience_and_histogram_match_myers_on_simple_cases() {
12435 let cases: &[(&[u8], &[u8], Vec<DiffOp>)] = &[
12438 (
12439 b"a\nb\nc\n",
12440 b"a\nx\nc\n",
12441 vec![
12442 DiffOp::Equal(1),
12443 DiffOp::Delete(1),
12444 DiffOp::Insert(1),
12445 DiffOp::Equal(1),
12446 ],
12447 ),
12448 (b"a\nb\nc\n", b"a\nb\nc\n", vec![DiffOp::Equal(3)]),
12449 (b"", b"a\nb\n", vec![DiffOp::Insert(2)]),
12450 (b"a\nb\n", b"", vec![DiffOp::Delete(2)]),
12451 (
12452 b"a\nb\nc\nd\n",
12453 b"a\nc\nd\n",
12454 vec![DiffOp::Equal(1), DiffOp::Delete(1), DiffOp::Equal(2)],
12455 ),
12456 ];
12457 for (old_bytes, new_bytes, expected) in cases {
12458 let old = split_lines(old_bytes);
12459 let new = split_lines(new_bytes);
12460 assert_eq!(&patience_diff_lines(&old, &new), expected);
12461 assert_eq!(&histogram_diff_lines(&old, &new), expected);
12462 assert_eq!(&myers_diff_lines(&old, &new), expected);
12463 }
12464 }
12465
12466 #[test]
12467 fn patience_handles_both_empty() {
12468 let empty = split_lines(b"");
12469 assert!(patience_diff_lines(&empty, &empty).is_empty());
12470 assert!(histogram_diff_lines(&empty, &empty).is_empty());
12471 }
12472
12473 #[test]
12474 fn patience_aligns_unique_anchors_across_moved_block() {
12475 check_all_algorithms(
12480 b"alpha\nbeta\ngamma\ndelta\n",
12481 b"gamma\ndelta\nalpha\nbeta\n",
12482 );
12483 }
12484
12485 #[test]
12486 fn histogram_differs_from_myers_keeping_block_contiguous() {
12487 let old = b"b\na\n";
12493 let new = b"a\nb\nb\na\nb\n";
12494 let old_l = split_lines(old);
12495 let new_l = split_lines(new);
12496
12497 let myers = myers_diff_lines(&old_l, &new_l);
12498 let histogram = histogram_diff_lines(&old_l, &new_l);
12499
12500 assert_valid_script(old, new, &myers);
12502 assert_valid_script(old, new, &histogram);
12503
12504 assert_eq!(
12507 myers,
12508 vec![
12509 DiffOp::Insert(1),
12510 DiffOp::Equal(1),
12511 DiffOp::Insert(1),
12512 DiffOp::Equal(1),
12513 DiffOp::Insert(1),
12514 ]
12515 );
12516 assert_eq!(
12517 histogram,
12518 vec![DiffOp::Insert(2), DiffOp::Equal(2), DiffOp::Insert(1)]
12519 );
12520 assert_ne!(myers, histogram);
12522 }
12523
12524 #[test]
12525 fn patience_differs_from_myers_on_repeated_lines() {
12526 let old = b"b\na\n";
12532 let new = b"a\na\nb\n";
12533 let old_l = split_lines(old);
12534 let new_l = split_lines(new);
12535
12536 let myers = myers_diff_lines(&old_l, &new_l);
12537 let patience = patience_diff_lines(&old_l, &new_l);
12538
12539 assert_valid_script(old, new, &myers);
12540 assert_valid_script(old, new, &patience);
12541
12542 assert_eq!(
12543 myers,
12544 vec![DiffOp::Delete(1), DiffOp::Equal(1), DiffOp::Insert(2)]
12545 );
12546 assert_eq!(
12547 patience,
12548 vec![DiffOp::Insert(2), DiffOp::Equal(1), DiffOp::Delete(1)]
12549 );
12550 assert_ne!(myers, patience);
12551 }
12552
12553 #[test]
12554 fn realistic_function_insertion_all_valid() {
12555 let old = b"int f() {\n return 1;\n}\n";
12560 let new = b"int g() {\n return 2;\n}\n\nint f() {\n return 1;\n}\n";
12561 check_all_algorithms(old, new);
12562 }
12563
12564 #[test]
12565 fn histogram_anchors_on_rare_line_when_no_unique_line_exists() {
12566 check_all_algorithms(b"x\nx\nmid\nx\nx\n", b"x\nmid\nx\nx\nx\n");
12571 check_all_algorithms(
12572 b"dup\ndup\nrare\ndup\ndup\n",
12573 b"dup\nrare\ndup\ndup\ndup\ndup\n",
12574 );
12575 }
12576
12577 #[test]
12578 fn all_algorithms_treat_missing_final_newline_as_change() {
12579 let old = split_lines(b"a\nb");
12581 let new = split_lines(b"a\nb\n");
12582 for algo in [
12583 DiffAlgorithm::Myers,
12584 DiffAlgorithm::Minimal,
12585 DiffAlgorithm::Patience,
12586 DiffAlgorithm::Histogram,
12587 ] {
12588 let ops = diff_lines_with_algorithm(&old, &new, algo);
12589 assert_eq!(
12590 ops,
12591 vec![DiffOp::Equal(1), DiffOp::Delete(1), DiffOp::Insert(1)],
12592 "algorithm {:?} mishandled missing final newline",
12593 algo
12594 );
12595 }
12596 }
12597
12598 #[test]
12599 fn dispatcher_routes_each_variant() {
12600 let old = split_lines(b"a\nb\nc\n");
12601 let new = split_lines(b"a\nx\nc\n");
12602 assert_eq!(
12603 diff_lines_with_algorithm(&old, &new, DiffAlgorithm::Myers),
12604 myers_diff_lines(&old, &new)
12605 );
12606 assert_eq!(
12608 diff_lines_with_algorithm(&old, &new, DiffAlgorithm::Minimal),
12609 myers_diff_lines(&old, &new)
12610 );
12611 assert_eq!(
12612 diff_lines_with_algorithm(&old, &new, DiffAlgorithm::Patience),
12613 patience_diff_lines(&old, &new)
12614 );
12615 assert_eq!(
12616 diff_lines_with_algorithm(&old, &new, DiffAlgorithm::Histogram),
12617 histogram_diff_lines(&old, &new)
12618 );
12619 }
12620
12621 #[test]
12622 fn patience_recurses_into_gaps_between_anchors() {
12623 let old = b"head\nmid1\nmid2\ntail\n";
12626 let new = b"head\nMID\nmid2\ntail\n";
12627 let old_l = split_lines(old);
12628 let new_l = split_lines(new);
12629 let ops = patience_diff_lines(&old_l, &new_l);
12630 assert_eq!(
12631 ops,
12632 vec![
12633 DiffOp::Equal(1),
12634 DiffOp::Delete(1),
12635 DiffOp::Insert(1),
12636 DiffOp::Equal(2),
12637 ]
12638 );
12639 assert_valid_script(old, new, &ops);
12640 }
12641
12642 #[test]
12643 fn patience_falls_back_to_myers_with_no_unique_lines() {
12644 let old = b"a\na\nb\nb\n";
12648 let new = b"a\na\na\nb\n";
12649 let old_l = split_lines(old);
12650 let new_l = split_lines(new);
12651 let ops = patience_diff_lines(&old_l, &new_l);
12652 assert_valid_script(old, new, &ops);
12657 }
12658
12659 #[test]
12660 fn algorithms_agree_with_myers_when_all_lines_distinct() {
12661 let cases: &[(&[u8], &[u8])] = &[
12664 (b"a\nb\nc\nd\ne\n", b"a\nc\nd\nf\ne\n"),
12665 (b"1\n2\n3\n4\n5\n6\n", b"1\n3\n2\n4\n6\n5\n"),
12666 (b"q\nw\ne\nr\nt\ny\n", b"q\nw\nx\nr\nt\nz\n"),
12667 ];
12668 for (old_bytes, new_bytes) in cases {
12669 let old = split_lines(old_bytes);
12670 let new = split_lines(new_bytes);
12671 let myers = myers_diff_lines(&old, &new);
12672 assert_eq!(
12673 patience_diff_lines(&old, &new),
12674 myers,
12675 "patience must equal Myers when all lines are distinct: {:?}",
12676 old_bytes
12677 );
12678 assert_eq!(
12679 histogram_diff_lines(&old, &new),
12680 myers,
12681 "histogram must equal Myers when all lines are distinct: {:?}",
12682 old_bytes
12683 );
12684 }
12685 }
12686
12687 #[test]
12688 fn fuzz_all_algorithms_reconstruct_new() {
12689 let mut state: u64 = 0x9E37_79B9_7F4A_7C15;
12693 let mut next = || {
12694 state = state
12695 .wrapping_mul(6364136223846793005)
12696 .wrapping_add(1442695040888963407);
12697 (state >> 33) as u32
12698 };
12699 let alphabet = [b"a\n", b"b\n", b"c\n", b"d\n"];
12700 let build = |rng: &mut dyn FnMut() -> u32| -> Vec<u8> {
12701 let len = (rng() % 9) as usize; let mut buf = Vec::new();
12703 for _ in 0..len {
12704 let pick = (rng() % alphabet.len() as u32) as usize;
12705 buf.extend_from_slice(alphabet[pick]);
12706 }
12707 if !buf.is_empty() && rng().is_multiple_of(4) {
12709 buf.pop();
12710 }
12711 buf
12712 };
12713 for _ in 0..400 {
12714 let old_bytes = build(&mut next);
12715 let new_bytes = build(&mut next);
12716 check_all_algorithms(&old_bytes, &new_bytes);
12717 }
12718 }
12719
12720 #[test]
12721 fn exhaustive_small_inputs_all_algorithms_reconstruct() {
12722 let syms = [b"a\n".to_vec(), b"b\n".to_vec(), b"c\n".to_vec()];
12727 let make = |n: usize, mut code: usize| -> Vec<u8> {
12728 let mut v = Vec::new();
12729 for _ in 0..n {
12730 v.extend_from_slice(&syms[code % 3]);
12731 code /= 3;
12732 }
12733 v
12734 };
12735 for la in 0..=5usize {
12736 for lb in 0..=5usize {
12737 for ca in 0..3usize.pow(la as u32) {
12738 for cb in 0..3usize.pow(lb as u32) {
12739 let ob = make(la, ca);
12740 let nb = make(lb, cb);
12741 let ol = split_lines(&ob);
12742 let nl = split_lines(&nb);
12743 assert_eq!(apply_ops(&ol, &nl, &myers_diff_lines(&ol, &nl)), nb);
12744 assert_eq!(apply_ops(&ol, &nl, &patience_diff_lines(&ol, &nl)), nb);
12745 assert_eq!(apply_ops(&ol, &nl, &histogram_diff_lines(&ol, &nl)), nb);
12746 }
12747 }
12748 }
12749 }
12750 }
12751
12752 #[test]
12753 fn fuzz_distinct_lines_patience_histogram_equal_myers() {
12754 let mut state: u64 = 0x1234_5678_9ABC_DEF0;
12758 let mut next = || {
12759 state = state
12760 .wrapping_mul(6364136223846793005)
12761 .wrapping_add(1442695040888963407);
12762 (state >> 33) as u32
12763 };
12764 for _ in 0..200 {
12765 let pick_subseq = |rng: &mut dyn FnMut() -> u32| -> Vec<u8> {
12768 let mut buf = Vec::new();
12769 for t in 0..10u32 {
12770 if rng().is_multiple_of(2) {
12771 buf.extend_from_slice(format!("{t}\n").as_bytes());
12772 }
12773 }
12774 buf
12775 };
12776 let old_bytes = pick_subseq(&mut next);
12777 let new_bytes = pick_subseq(&mut next);
12778 let old = split_lines(&old_bytes);
12779 let new = split_lines(&new_bytes);
12780 let myers = myers_diff_lines(&old, &new);
12781 assert_eq!(patience_diff_lines(&old, &new), myers);
12782 assert_eq!(histogram_diff_lines(&old, &new), myers);
12783 }
12784 }
12785
12786 fn status_lines(entries: &[NameStatusEntry]) -> Vec<String> {
12795 entries.iter().map(|entry| entry.line()).collect()
12796 }
12797
12798 fn assert_tree_diff_matches_full(
12801 db: &FileObjectDatabase,
12802 left: &ObjectId,
12803 right: &ObjectId,
12804 options: DiffNameStatusOptions,
12805 ) {
12806 let (full_left, full_right) = collect_full_tree_pair(db, ObjectFormat::Sha1, left, right)
12808 .expect("test operation should succeed");
12809 let reference = diff_name_status_maps(
12810 &full_left,
12811 &full_right,
12812 full_left.keys().chain(full_right.keys()),
12813 options,
12814 )
12815 .expect("test operation should succeed");
12816
12817 let (pruned_left, pruned_right) = changed_tree_entries(db, ObjectFormat::Sha1, left, right)
12819 .expect("test operation should succeed");
12820 let pruned = diff_name_status_maps(
12821 &pruned_left,
12822 &pruned_right,
12823 pruned_left.keys().chain(pruned_right.keys()),
12824 options,
12825 )
12826 .expect("test operation should succeed");
12827
12828 assert_eq!(
12829 status_lines(&reference),
12830 status_lines(&pruned),
12831 "pruned map diff diverged from full map diff for {options:?}"
12832 );
12833
12834 let public =
12837 diff_name_status_trees_with_options(db, ObjectFormat::Sha1, left, right, options)
12838 .expect("test operation should succeed");
12839 assert_eq!(
12840 status_lines(&reference),
12841 status_lines(&public),
12842 "public tree diff diverged from full map diff for {options:?}"
12843 );
12844
12845 for (path, tracked) in &pruned_left {
12849 assert_eq!(
12850 full_left.get(path),
12851 Some(tracked),
12852 "pruned left entry not present (or differs) in full left map: {:?}",
12853 String::from_utf8_lossy(path)
12854 );
12855 }
12856 for (path, tracked) in &pruned_right {
12857 assert_eq!(
12858 full_right.get(path),
12859 Some(tracked),
12860 "pruned right entry not present (or differs) in full right map: {:?}",
12861 String::from_utf8_lossy(path)
12862 );
12863 }
12864 for entry in &reference {
12867 let path = entry.path.as_bytes();
12868 match entry.status {
12869 NameStatus::Added => assert!(
12870 pruned_right.contains_key(path),
12871 "added path dropped by pruning: {:?}",
12872 String::from_utf8_lossy(path)
12873 ),
12874 NameStatus::Deleted => assert!(
12875 pruned_left.contains_key(path),
12876 "deleted path dropped by pruning: {:?}",
12877 String::from_utf8_lossy(path)
12878 ),
12879 NameStatus::Modified => {
12880 assert!(
12881 pruned_left.contains_key(path) && pruned_right.contains_key(path),
12882 "modified path dropped by pruning: {:?}",
12883 String::from_utf8_lossy(path)
12884 );
12885 }
12886 _ => {}
12887 }
12888 }
12889 }
12890
12891 fn assert_tree_diff_matches_full_all_modes(
12895 db: &FileObjectDatabase,
12896 left: &ObjectId,
12897 right: &ObjectId,
12898 ) {
12899 for detect_renames in [false, true] {
12900 for detect_copies in [false, true] {
12901 let options = DiffNameStatusOptions {
12902 detect_renames,
12903 detect_copies,
12904 find_copies_harder: false,
12905 rename_empty: true,
12906 };
12907 assert_tree_diff_matches_full(db, left, right, options);
12908 }
12909 }
12910 }
12911
12912 fn structural_db() -> (PathBuf, FileObjectDatabase) {
12914 let root = temp_root();
12915 let layout = RepositoryLayout::init_at(&root, ObjectFormat::Sha1, false)
12916 .expect("test operation should succeed");
12917 let db = FileObjectDatabase::from_git_dir(&layout.git_dir, ObjectFormat::Sha1);
12918 (root, db)
12919 }
12920
12921 #[test]
12922 fn pruned_walk_skips_identical_subtree_and_matches_full() {
12923 let (root, mut db) = structural_db();
12927
12928 let s1 = write_blob(&mut db, b"shared one\n");
12930 let s2 = write_blob(&mut db, b"shared two\n");
12931 let s3 = write_blob(&mut db, b"deep nested\n");
12932 let shared_inner = write_tree(&mut db, &[(b"c.txt", 0o100644, s3.clone())]);
12933 let shared = write_tree(
12934 &mut db,
12935 &[
12936 (b"a.txt", 0o100644, s1.clone()),
12937 (b"b.txt", 0o100644, s2.clone()),
12938 (b"inner", 0o040000, shared_inner.clone()),
12939 ],
12940 );
12941
12942 let app_old = write_blob(&mut db, b"version 1\n");
12944 let app_new = write_blob(&mut db, b"version 2\n");
12945 let app_left = write_tree(&mut db, &[(b"main.rs", 0o100644, app_old)]);
12946 let app_right = write_tree(&mut db, &[(b"main.rs", 0o100644, app_new)]);
12947
12948 let left = write_tree(
12949 &mut db,
12950 &[
12951 (b"app", 0o040000, app_left),
12952 (b"shared", 0o040000, shared.clone()),
12953 ],
12954 );
12955 let right = write_tree(
12956 &mut db,
12957 &[(b"app", 0o040000, app_right), (b"shared", 0o040000, shared)],
12958 );
12959
12960 let (pruned_left, pruned_right) =
12962 changed_tree_entries(&db, ObjectFormat::Sha1, &left, &right)
12963 .expect("test operation should succeed");
12964 assert_eq!(
12965 pruned_left.keys().collect::<Vec<_>>(),
12966 vec![&b"app/main.rs".to_vec()],
12967 "pruning should leave only the changed path on the left"
12968 );
12969 assert_eq!(
12970 pruned_right.keys().collect::<Vec<_>>(),
12971 vec![&b"app/main.rs".to_vec()],
12972 "pruning should leave only the changed path on the right"
12973 );
12974 assert!(
12975 !pruned_left.contains_key(b"shared/a.txt".as_slice()),
12976 "identical shared subtree must not appear in pruned maps"
12977 );
12978
12979 assert_tree_diff_matches_full_all_modes(&db, &left, &right);
12980 fs::remove_dir_all(root).expect("test operation should succeed");
12981 }
12982
12983 #[test]
12984 fn pruned_walk_matches_full_for_add_delete_modify_nested() {
12985 let (root, mut db) = structural_db();
12988
12989 let keep = write_blob(&mut db, b"unchanged\n");
12990 let untouched_dir = write_tree(&mut db, &[(b"keep.txt", 0o100644, keep.clone())]);
12991
12992 let nested_old = write_blob(&mut db, b"nested old\n");
12993 let nested_new = write_blob(&mut db, b"nested new\n");
12994 let dir_left = write_tree(
12995 &mut db,
12996 &[
12997 (b"changed.txt", 0o100644, nested_old),
12998 (b"stable.txt", 0o100644, keep.clone()),
12999 ],
13000 );
13001 let dir_right = write_tree(
13002 &mut db,
13003 &[
13004 (b"changed.txt", 0o100644, nested_new),
13005 (b"stable.txt", 0o100644, keep.clone()),
13006 ],
13007 );
13008
13009 let only_left = write_blob(&mut db, b"will be deleted\n");
13010 let only_right = write_blob(&mut db, b"freshly added\n");
13011
13012 let left = write_tree(
13013 &mut db,
13014 &[
13015 (b"dir", 0o040000, dir_left),
13016 (b"gone.txt", 0o100644, only_left),
13017 (b"untouched", 0o040000, untouched_dir.clone()),
13018 ],
13019 );
13020 let right = write_tree(
13021 &mut db,
13022 &[
13023 (b"dir", 0o040000, dir_right),
13024 (b"new.txt", 0o100644, only_right),
13025 (b"untouched", 0o040000, untouched_dir),
13026 ],
13027 );
13028
13029 let entries = diff_name_status_trees_with_options(
13030 &db,
13031 ObjectFormat::Sha1,
13032 &left,
13033 &right,
13034 DiffNameStatusOptions {
13035 detect_renames: false,
13036 detect_copies: false,
13037 find_copies_harder: false,
13038 rename_empty: true,
13039 },
13040 )
13041 .expect("test operation should succeed");
13042 assert_eq!(
13043 status_lines(&entries),
13044 vec![
13045 "M\tdir/changed.txt".to_string(),
13046 "D\tgone.txt".to_string(),
13047 "A\tnew.txt".to_string(),
13048 ],
13049 "unexpected raw status for mixed nested diff"
13050 );
13051
13052 assert_tree_diff_matches_full_all_modes(&db, &left, &right);
13053 fs::remove_dir_all(root).expect("test operation should succeed");
13054 }
13055
13056 #[test]
13057 fn pruned_walk_matches_full_for_rename_across_dirs() {
13058 let (root, mut db) = structural_db();
13062
13063 let moved = write_blob(&mut db, b"i get moved across directories\n");
13064 let companion = write_blob(&mut db, b"i stay put\n");
13065 let stable_dir = write_tree(&mut db, &[(b"keep.txt", 0o100644, companion.clone())]);
13066
13067 let src_dir = write_tree(&mut db, &[(b"file.txt", 0o100644, moved.clone())]);
13068 let dst_dir = write_tree(&mut db, &[(b"renamed.txt", 0o100644, moved.clone())]);
13069
13070 let left = write_tree(
13071 &mut db,
13072 &[
13073 (b"src", 0o040000, src_dir),
13074 (b"stable", 0o040000, stable_dir.clone()),
13075 ],
13076 );
13077 let right = write_tree(
13078 &mut db,
13079 &[
13080 (b"dst", 0o040000, dst_dir),
13081 (b"stable", 0o040000, stable_dir),
13082 ],
13083 );
13084
13085 let entries = diff_name_status_trees_with_options(
13086 &db,
13087 ObjectFormat::Sha1,
13088 &left,
13089 &right,
13090 DiffNameStatusOptions {
13091 detect_renames: true,
13092 detect_copies: false,
13093 find_copies_harder: false,
13094 rename_empty: true,
13095 },
13096 )
13097 .expect("test operation should succeed");
13098 assert_eq!(
13099 status_lines(&entries),
13100 vec!["R100\tsrc/file.txt\tdst/renamed.txt".to_string()],
13101 "rename across dirs should be detected on pruned set"
13102 );
13103
13104 assert_tree_diff_matches_full_all_modes(&db, &left, &right);
13105 fs::remove_dir_all(root).expect("test operation should succeed");
13106 }
13107
13108 #[test]
13109 fn pruned_walk_matches_full_for_binary_and_mode_change() {
13110 let (root, mut db) = structural_db();
13114
13115 let bin_old = write_blob(&mut db, &[0u8, 159, 146, 150, 0, 255, 1, 2, 3]);
13116 let bin_new = write_blob(&mut db, &[0u8, 159, 146, 150, 0, 254, 9, 8, 7]);
13117 let script = write_blob(&mut db, b"#!/bin/sh\necho hi\n");
13118
13119 let left = write_tree(
13120 &mut db,
13121 &[
13122 (b"image.bin", 0o100644, bin_old),
13123 (b"run.sh", 0o100644, script.clone()),
13124 ],
13125 );
13126 let right = write_tree(
13127 &mut db,
13128 &[
13129 (b"image.bin", 0o100644, bin_new),
13130 (b"run.sh", 0o100755, script),
13132 ],
13133 );
13134
13135 let entries = diff_name_status_trees_with_options(
13136 &db,
13137 ObjectFormat::Sha1,
13138 &left,
13139 &right,
13140 DiffNameStatusOptions {
13141 detect_renames: false,
13142 detect_copies: false,
13143 find_copies_harder: false,
13144 rename_empty: true,
13145 },
13146 )
13147 .expect("test operation should succeed");
13148 assert_eq!(
13149 status_lines(&entries),
13150 vec!["M\timage.bin".to_string(), "M\trun.sh".to_string()],
13151 "binary edit and mode-only change should both be Modify"
13152 );
13153
13154 assert_tree_diff_matches_full_all_modes(&db, &left, &right);
13155 fs::remove_dir_all(root).expect("test operation should succeed");
13156 }
13157
13158 #[test]
13159 fn pruned_walk_matches_full_for_dir_replaced_by_file() {
13160 let (root, mut db) = structural_db();
13165
13166 let inner_a = write_blob(&mut db, b"inner a\n");
13167 let inner_b = write_blob(&mut db, b"inner b\n");
13168 let thing_dir = write_tree(
13169 &mut db,
13170 &[(b"a.txt", 0o100644, inner_a), (b"b.txt", 0o100644, inner_b)],
13171 );
13172 let thing_file = write_blob(&mut db, b"now i am a file\n");
13173
13174 let other_file = write_blob(&mut db, b"i was a file\n");
13176 let other_inner = write_blob(&mut db, b"now nested\n");
13177 let other_dir = write_tree(&mut db, &[(b"x.txt", 0o100644, other_inner)]);
13178
13179 let left = write_tree(
13180 &mut db,
13181 &[
13182 (b"other", 0o100644, other_file),
13183 (b"thing", 0o040000, thing_dir),
13184 ],
13185 );
13186 let right = write_tree(
13187 &mut db,
13188 &[
13189 (b"other", 0o040000, other_dir),
13190 (b"thing", 0o100644, thing_file),
13191 ],
13192 );
13193
13194 let entries = diff_name_status_trees_with_options(
13195 &db,
13196 ObjectFormat::Sha1,
13197 &left,
13198 &right,
13199 DiffNameStatusOptions {
13200 detect_renames: false,
13201 detect_copies: false,
13202 find_copies_harder: false,
13203 rename_empty: true,
13204 },
13205 )
13206 .expect("test operation should succeed");
13207 assert_eq!(
13208 status_lines(&entries),
13209 vec![
13210 "D\tother".to_string(),
13211 "A\tother/x.txt".to_string(),
13212 "A\tthing".to_string(),
13213 "D\tthing/a.txt".to_string(),
13214 "D\tthing/b.txt".to_string(),
13215 ],
13216 "dir<->file swap should flatten to independent adds/deletes"
13217 );
13218
13219 assert_tree_diff_matches_full_all_modes(&db, &left, &right);
13220 fs::remove_dir_all(root).expect("test operation should succeed");
13221 }
13222
13223 #[test]
13224 fn pruned_walk_matches_full_for_identical_trees() {
13225 let (root, mut db) = structural_db();
13228
13229 let blob = write_blob(&mut db, b"same\n");
13230 let sub = write_tree(&mut db, &[(b"f.txt", 0o100644, blob.clone())]);
13231 let tree = write_tree(
13232 &mut db,
13233 &[(b"sub", 0o040000, sub), (b"top.txt", 0o100644, blob)],
13234 );
13235
13236 let (pruned_left, pruned_right) =
13237 changed_tree_entries(&db, ObjectFormat::Sha1, &tree, &tree)
13238 .expect("test operation should succeed");
13239 assert!(
13240 pruned_left.is_empty() && pruned_right.is_empty(),
13241 "identical trees must produce no changed entries"
13242 );
13243
13244 let entries = diff_name_status_trees_with_options(
13245 &db,
13246 ObjectFormat::Sha1,
13247 &tree,
13248 &tree,
13249 DiffNameStatusOptions::default(),
13250 )
13251 .expect("test operation should succeed");
13252 assert!(entries.is_empty(), "identical trees must produce no diff");
13253
13254 assert_tree_diff_matches_full_all_modes(&db, &tree, &tree);
13255 fs::remove_dir_all(root).expect("test operation should succeed");
13256 }
13257
13258 #[test]
13259 fn find_copies_harder_uses_full_left_map_and_finds_unchanged_source() {
13260 let (root, mut db) = structural_db();
13265
13266 let template = write_blob(&mut db, b"reusable boilerplate content\n");
13269 let lib_dir = write_tree(&mut db, &[(b"template.txt", 0o100644, template.clone())]);
13270
13271 let trigger_old = write_blob(&mut db, b"trigger old\n");
13272 let trigger_new = write_blob(&mut db, b"trigger new\n");
13273
13274 let left = write_tree(
13275 &mut db,
13276 &[
13277 (b"lib", 0o040000, lib_dir.clone()),
13278 (b"trigger.txt", 0o100644, trigger_old),
13279 ],
13280 );
13281 let right = write_tree(
13282 &mut db,
13283 &[
13284 (b"copy.txt", 0o100644, template.clone()),
13285 (b"lib", 0o040000, lib_dir),
13286 (b"trigger.txt", 0o100644, trigger_new),
13287 ],
13288 );
13289
13290 let options = DiffNameStatusOptions {
13291 detect_renames: true,
13292 detect_copies: true,
13293 find_copies_harder: true,
13294 rename_empty: true,
13295 };
13296
13297 let (full_left, full_right) =
13299 collect_full_tree_pair(&db, ObjectFormat::Sha1, &left, &right)
13300 .expect("test operation should succeed");
13301 let reference = diff_name_status_maps(
13302 &full_left,
13303 &full_right,
13304 full_left.keys().chain(full_right.keys()),
13305 options,
13306 )
13307 .expect("test operation should succeed");
13308
13309 let public =
13310 diff_name_status_trees_with_options(&db, ObjectFormat::Sha1, &left, &right, options)
13311 .expect("test operation should succeed");
13312 assert_eq!(
13313 status_lines(&reference),
13314 status_lines(&public),
13315 "find-copies-harder public diff must match full-map reference"
13316 );
13317 assert!(
13319 public
13320 .iter()
13321 .any(|entry| matches!(entry.status, NameStatus::Copied(_))
13322 && entry.old_path.as_ref().map(|p| p.as_bytes())
13323 == Some(b"lib/template.txt".as_slice())
13324 && entry.path == b"copy.txt"),
13325 "copy from unchanged source must be found with find_copies_harder: {public:?}"
13326 );
13327 fs::remove_dir_all(root).expect("test operation should succeed");
13328 }
13329
13330 #[test]
13331 fn pruned_walk_matches_full_with_inexact_rename_options() {
13332 let (root, mut db) = structural_db();
13335
13336 let untouched = write_blob(&mut db, b"untouched file\n");
13337 let untouched_dir = write_tree(&mut db, &[(b"u.txt", 0o100644, untouched.clone())]);
13338
13339 let old = write_blob(&mut db, b"one\ntwo\nthree\nfour\nfive\n");
13341 let new = write_blob(&mut db, b"one\ntwo\nTHREE\nfour\nfive\n");
13342
13343 let left = write_tree(
13344 &mut db,
13345 &[
13346 (b"a.txt", 0o100644, old),
13347 (b"keep", 0o040000, untouched_dir.clone()),
13348 ],
13349 );
13350 let right = write_tree(
13351 &mut db,
13352 &[
13353 (b"b.txt", 0o100644, new),
13354 (b"keep", 0o040000, untouched_dir),
13355 ],
13356 );
13357
13358 let options = RenameDetectionOptions {
13359 base: DiffNameStatusOptions {
13360 detect_renames: true,
13361 detect_copies: false,
13362 find_copies_harder: false,
13363 rename_empty: true,
13364 },
13365 detect_inexact: true,
13366 rename_threshold: DEFAULT_RENAME_THRESHOLD,
13367 copy_threshold: DEFAULT_RENAME_THRESHOLD,
13368 rename_limit: 0,
13369 };
13370
13371 let (full_left, full_right) =
13373 collect_full_tree_pair(&db, ObjectFormat::Sha1, &left, &right)
13374 .expect("test operation should succeed");
13375 let reference = diff_name_status_maps_with_renames(
13376 &full_left,
13377 &full_right,
13378 full_left.keys().chain(full_right.keys()),
13379 options,
13380 |oid| read_blob_bytes(&db, oid),
13381 )
13382 .expect("test operation should succeed");
13383
13384 let public = diff_name_status_trees_with_rename_options(
13385 &db,
13386 ObjectFormat::Sha1,
13387 &left,
13388 &right,
13389 options,
13390 )
13391 .expect("test operation should succeed");
13392
13393 assert_eq!(
13394 status_lines(&reference),
13395 status_lines(&public),
13396 "inexact rename via pruned walk must match full-map reference"
13397 );
13398 assert_eq!(
13399 status_lines(&public),
13400 vec!["R075\ta.txt\tb.txt".to_string()],
13401 "expected a 75% inexact rename"
13402 );
13403 fs::remove_dir_all(root).expect("test operation should succeed");
13404 }
13405}