1use std::fs;
24#[cfg(unix)]
25use std::os::unix::fs::MetadataExt;
26use std::path::{Path, PathBuf};
27
28use crate::config::ConfigSet;
29use crate::diff_indent_heuristic;
30use crate::error::{Error, Result};
31use crate::index::{Index, IndexEntry};
32use crate::objects::{parse_commit, parse_tree, CommitData, ObjectId, ObjectKind, TreeEntry};
33use crate::odb::Odb;
34use crate::userdiff::FuncnameMatcher;
35
36fn imara_unified_hunk_slices(body: &str) -> Vec<&str> {
38 let mut starts: Vec<usize> = Vec::new();
39 if body.starts_with("@@") {
40 starts.push(0);
41 }
42 for (idx, _) in body.match_indices("\n@@ ") {
43 starts.push(idx + 1);
44 }
45 starts.push(body.len());
46 starts.windows(2).map(|w| &body[w[0]..w[1]]).collect()
47}
48
49fn histogram_unified_body_raw(
50 old_content: &str,
51 new_content: &str,
52 context_lines: usize,
53 inter_hunk_context: usize,
54) -> String {
55 use imara_diff::{Algorithm, Diff, Hunk, InternedInput};
56 use std::fmt::Write as _;
57
58 let input = InternedInput::new(old_content, new_content);
59 let mut diff = Diff::compute(Algorithm::Histogram, &input);
60 diff.postprocess_lines(&input);
61
62 let hunks: Vec<Hunk> = diff.hunks().collect();
68 if hunks.is_empty() {
69 return String::new();
70 }
71
72 let ctx = context_lines.min(u32::MAX as usize) as u32;
73 let max_gap = (2usize.saturating_mul(context_lines))
74 .saturating_add(inter_hunk_context)
75 .min(u32::MAX as usize) as u32;
76 let before_len = input.before.len() as u32;
77 let after_len = input.after.len() as u32;
78
79 let mut groups: Vec<&[Hunk]> = Vec::new();
81 let mut group_start = 0usize;
82 for i in 1..hunks.len() {
83 if hunks[i].before.start - hunks[i - 1].before.end > max_gap {
84 groups.push(&hunks[group_start..i]);
85 group_start = i;
86 }
87 }
88 groups.push(&hunks[group_start..]);
89
90 fn push_line(out: &mut String, prefix: char, text: &str) {
91 out.push(prefix);
92 out.push_str(text);
93 if !text.ends_with('\n') {
94 out.push('\n');
95 }
96 }
97
98 fn fmt_side(start: u32, count: u32) -> String {
101 let shown_start = if count == 0 { start } else { start + 1 };
102 if count == 1 {
103 format!("{shown_start}")
104 } else {
105 format!("{shown_start},{count}")
106 }
107 }
108
109 let mut out = String::new();
110 for group in groups {
111 let first = &group[0];
112 let last = &group[group.len() - 1];
113 let b_start = first.before.start.saturating_sub(ctx);
114 let a_start = first.after.start.saturating_sub(ctx);
115 let b_end = (last.before.end.saturating_add(ctx)).min(before_len);
116 let a_end = (last.after.end.saturating_add(ctx)).min(after_len);
117
118 let _ = writeln!(
119 out,
120 "@@ -{} +{} @@",
121 fmt_side(b_start, b_end - b_start),
122 fmt_side(a_start, a_end - a_start)
123 );
124
125 let mut pos = b_start;
126 for hunk in group {
127 for &token in &input.before[pos as usize..hunk.before.start as usize] {
128 push_line(&mut out, ' ', input.interner[token]);
129 }
130 for &token in &input.before[hunk.before.start as usize..hunk.before.end as usize] {
131 push_line(&mut out, '-', input.interner[token]);
132 }
133 for &token in &input.after[hunk.after.start as usize..hunk.after.end as usize] {
134 push_line(&mut out, '+', input.interner[token]);
135 }
136 pos = hunk.before.end;
137 }
138 for &token in &input.before[pos as usize..b_end as usize] {
139 push_line(&mut out, ' ', input.interner[token]);
140 }
141 }
142
143 out
144}
145
146#[allow(clippy::too_many_arguments)]
162pub(crate) fn histogram_unified_body_ignore_fc<F>(
163 old_content: &str,
164 new_content: &str,
165 context_lines: usize,
166 inter_hunk_context: usize,
167 function_context: bool,
168 funcname_matcher: Option<&FuncnameMatcher>,
169 is_ignorable_change: F,
170) -> String
171where
172 F: Fn(&[&str], &[&str]) -> bool,
173{
174 use imara_diff::{Algorithm, Diff, Hunk, InternedInput};
175 use std::fmt::Write as _;
176
177 let input = InternedInput::new(old_content, new_content);
178 let mut diff = Diff::compute(Algorithm::Histogram, &input);
179 diff.postprocess_lines(&input);
180
181 let raw_hunks: Vec<Hunk> = diff.hunks().collect();
182 if raw_hunks.is_empty() {
183 return String::new();
184 }
185
186 let before_len = input.before.len() as i64;
187 let after_len = input.after.len() as i64;
188 let ctxlen = context_lines as i64;
189 let interhunk = inter_hunk_context as i64;
190 let max_common = ctxlen.saturating_add(ctxlen).saturating_add(interhunk);
192 let max_ignorable = ctxlen;
193
194 struct Change {
196 i1: i64,
197 chg1: i64,
198 i2: i64,
199 chg2: i64,
200 ignore: bool,
201 }
202
203 let mut changes: Vec<Change> = Vec::with_capacity(raw_hunks.len());
204 for h in &raw_hunks {
205 let i1 = h.before.start as i64;
206 let chg1 = (h.before.end - h.before.start) as i64;
207 let i2 = h.after.start as i64;
208 let chg2 = (h.after.end - h.after.start) as i64;
209 let removed: Vec<&str> = input.before[h.before.start as usize..h.before.end as usize]
210 .iter()
211 .map(|&t| input.interner[t])
212 .collect();
213 let added: Vec<&str> = input.after[h.after.start as usize..h.after.end as usize]
214 .iter()
215 .map(|&t| input.interner[t])
216 .collect();
217 let ignore = is_ignorable_change(&removed, &added);
218 changes.push(Change {
219 i1,
220 chg1,
221 i2,
222 chg2,
223 ignore,
224 });
225 }
226
227 let before_line = |idx: i64| -> &str { input.interner[input.before[idx as usize]] };
229 let after_line = |idx: i64| -> &str { input.interner[input.after[idx as usize]] };
230
231 let get_hunk = |start: usize| -> Option<(usize, usize)> {
237 let mut scr = start;
243 let mut xchp = start;
244 while xchp < changes.len() && changes[xchp].ignore {
245 let next = xchp + 1;
246 if next >= changes.len()
247 || changes[next].i1 - (changes[xchp].i1 + changes[xchp].chg1) >= max_ignorable
248 {
249 scr = next;
250 }
251 xchp = next;
252 }
253 if scr >= changes.len() {
254 return None;
255 }
256
257 let mut lxch = scr;
258 let mut ignored: i64 = 0;
259 let mut xchp = scr;
260 let mut idx = scr + 1;
261 while idx < changes.len() {
262 let xch = &changes[idx];
263 let prev = &changes[xchp];
264 let distance = xch.i1 - (prev.i1 + prev.chg1);
265 if distance > max_common {
266 break;
267 }
268 if distance < max_ignorable && (!xch.ignore || lxch == xchp) {
269 lxch = idx;
270 ignored = 0;
271 } else if distance < max_ignorable && xch.ignore {
272 ignored += xch.chg2;
273 } else if lxch != xchp
274 && xch.i1 + ignored - (changes[lxch].i1 + changes[lxch].chg1) > max_common
275 {
276 break;
277 } else if !xch.ignore {
278 lxch = idx;
279 ignored = 0;
280 } else {
281 ignored += xch.chg2;
282 }
283 xchp = idx;
284 idx += 1;
285 }
286 Some((scr, lxch))
287 };
288
289 fn push_line(out: &mut String, prefix: char, text: &str) {
290 out.push(prefix);
291 out.push_str(text);
292 if !text.ends_with('\n') {
293 out.push('\n');
294 }
295 }
296
297 fn fmt_side(start: i64, count: i64) -> String {
299 if count == 1 {
300 format!("{start}")
301 } else {
302 format!("{start},{count}")
303 }
304 }
305
306 let mut out = String::new();
307 let mut cursor = 0usize;
308 while cursor < changes.len() {
309 let Some((xch_idx, xche_idx)) = get_hunk(cursor) else {
310 break;
311 };
312 let xch = &changes[xch_idx];
313 let xche = &changes[xche_idx];
314
315 let mut s1 = (xch.i1 - ctxlen).max(0);
317 let mut s2 = (xch.i2 - ctxlen).max(0);
318 let mut lctx = ctxlen;
320 lctx = lctx.min(before_len - (xche.i1 + xche.chg1));
321 lctx = lctx.min(after_len - (xche.i2 + xche.chg2));
322 let mut e1 = xche.i1 + xche.chg1 + lctx;
323 let mut e2 = xche.i2 + xche.chg2 + lctx;
324
325 if function_context {
330 let mut fs1 = xch.i1;
333 while fs1 >= 0 && !is_func_line(before_line(fs1), funcname_matcher) {
334 fs1 -= 1;
335 }
336 while fs1 > 0
337 && before_line(fs1 - 1).trim().is_empty() == false
338 && !is_func_line(before_line(fs1 - 1), funcname_matcher)
339 {
340 fs1 -= 1;
341 }
342 if fs1 < 0 {
343 fs1 = 0;
344 }
345 if fs1 < s1 {
346 s2 = (s2 - (s1 - fs1)).max(0);
347 s1 = fs1;
348 }
349 let end1 = xche.i1 + xche.chg1;
352 let mut fe1 = end1;
353 while fe1 < before_len && !is_func_line(before_line(fe1), funcname_matcher) {
354 fe1 += 1;
355 }
356 while fe1 > 0 && before_line(fe1 - 1).trim().is_empty() {
357 fe1 -= 1;
358 }
359 if fe1 > before_len {
360 fe1 = before_len;
361 }
362 if fe1 > e1 {
363 e2 = (e2 + (fe1 - e1)).min(after_len);
364 e1 = fe1;
365 }
366 }
367
368 let _ = writeln!(
369 out,
370 "@@ -{} +{} @@",
371 fmt_side(s1 + 1, e1 - s1),
372 fmt_side(s2 + 1, e2 - s2)
373 );
374
375 let mut s2c = s2;
377 while s2c < xch.i2 {
378 push_line(&mut out, ' ', after_line(s2c));
379 s2c += 1;
380 }
381
382 let mut s1c = xch.i1;
384 let mut s2c = xch.i2;
385 let mut k = xch_idx;
386 loop {
387 let c = &changes[k];
388 while s1c < c.i1 && s2c < c.i2 {
390 push_line(&mut out, ' ', after_line(s2c));
391 s1c += 1;
392 s2c += 1;
393 }
394 let mut r = c.i1;
396 while r < c.i1 + c.chg1 {
397 push_line(&mut out, '-', before_line(r));
398 r += 1;
399 }
400 let mut a = c.i2;
402 while a < c.i2 + c.chg2 {
403 push_line(&mut out, '+', after_line(a));
404 a += 1;
405 }
406 if k == xche_idx {
407 break;
408 }
409 s1c = c.i1 + c.chg1;
410 s2c = c.i2 + c.chg2;
411 k += 1;
412 }
413
414 let mut s2p = xche.i2 + xche.chg2;
416 while s2p < e2 {
417 push_line(&mut out, ' ', after_line(s2p));
418 s2p += 1;
419 }
420
421 cursor = xche_idx + 1;
422 }
423
424 out
425}
426
427#[must_use]
431pub fn unified_diff_histogram_hunks_only(
432 old_content: &str,
433 new_content: &str,
434 context_lines: usize,
435 inter_hunk_context: usize,
436) -> String {
437 histogram_unified_body_raw(old_content, new_content, context_lines, inter_hunk_context)
438}
439
440#[must_use]
442pub fn unified_diff_histogram_with_prefix_and_funcname(
443 old_content: &str,
444 new_content: &str,
445 old_path: &str,
446 new_path: &str,
447 context_lines: usize,
448 inter_hunk_context: usize,
449 src_prefix: &str,
450 dst_prefix: &str,
451 funcname_matcher: Option<&FuncnameMatcher>,
452 quote_path_fully: bool,
453) -> String {
454 use crate::quote_path::format_diff_path_with_prefix;
455
456 let body =
457 histogram_unified_body_raw(old_content, new_content, context_lines, inter_hunk_context);
458
459 let mut output = String::new();
460 if old_path == "/dev/null" {
461 output.push_str("--- /dev/null\n");
462 } else if src_prefix.is_empty() {
463 output.push_str(&format!("--- {old_path}\n"));
464 } else {
465 output.push_str("--- ");
466 output.push_str(&format_diff_path_with_prefix(
467 src_prefix,
468 old_path,
469 quote_path_fully,
470 ));
471 output.push('\n');
472 }
473 if new_path == "/dev/null" {
474 output.push_str("+++ /dev/null\n");
475 } else if dst_prefix.is_empty() {
476 output.push_str(&format!("+++ {new_path}\n"));
477 } else {
478 output.push_str("+++ ");
479 output.push_str(&format_diff_path_with_prefix(
480 dst_prefix,
481 new_path,
482 quote_path_fully,
483 ));
484 output.push('\n');
485 }
486
487 let old_lines: Vec<&str> = old_content.lines().collect();
488 for hunk_str in imara_unified_hunk_slices(&body) {
489 if hunk_str.is_empty() {
490 continue;
491 }
492 if let Some(first_newline) = hunk_str.find('\n') {
493 let header_line = &hunk_str[..first_newline];
494 let rest = &hunk_str[first_newline..];
495 if let Some(func_ctx) =
496 extract_function_context(header_line, &old_lines, funcname_matcher)
497 {
498 output.push_str(header_line);
499 output.push(' ');
500 output.push_str(&func_ctx);
501 output.push_str(rest);
502 } else {
503 output.push_str(hunk_str);
504 }
505 } else {
506 output.push_str(hunk_str);
507 }
508 }
509
510 output
511}
512
513#[allow(clippy::too_many_arguments)]
523pub fn unified_diff_histogram_ignore_with_prefix_and_funcname<F>(
524 old_content: &str,
525 new_content: &str,
526 old_path: &str,
527 new_path: &str,
528 context_lines: usize,
529 inter_hunk_context: usize,
530 src_prefix: &str,
531 dst_prefix: &str,
532 funcname_matcher: Option<&FuncnameMatcher>,
533 quote_path_fully: bool,
534 function_context: bool,
535 is_ignorable_change: F,
536) -> String
537where
538 F: Fn(&[&str], &[&str]) -> bool,
539{
540 use crate::quote_path::format_diff_path_with_prefix;
541
542 let body = histogram_unified_body_ignore_fc(
543 old_content,
544 new_content,
545 context_lines,
546 inter_hunk_context,
547 function_context,
548 funcname_matcher,
549 is_ignorable_change,
550 );
551 if body.is_empty() {
552 return String::new();
553 }
554
555 let mut output = String::new();
556 if old_path == "/dev/null" {
557 output.push_str("--- /dev/null\n");
558 } else if src_prefix.is_empty() {
559 output.push_str(&format!("--- {old_path}\n"));
560 } else {
561 output.push_str("--- ");
562 output.push_str(&format_diff_path_with_prefix(
563 src_prefix,
564 old_path,
565 quote_path_fully,
566 ));
567 output.push('\n');
568 }
569 if new_path == "/dev/null" {
570 output.push_str("+++ /dev/null\n");
571 } else if dst_prefix.is_empty() {
572 output.push_str(&format!("+++ {new_path}\n"));
573 } else {
574 output.push_str("+++ ");
575 output.push_str(&format_diff_path_with_prefix(
576 dst_prefix,
577 new_path,
578 quote_path_fully,
579 ));
580 output.push('\n');
581 }
582
583 let old_lines: Vec<&str> = old_content.lines().collect();
584 for hunk_str in imara_unified_hunk_slices(&body) {
585 if hunk_str.is_empty() {
586 continue;
587 }
588 if let Some(first_newline) = hunk_str.find('\n') {
589 let header_line = &hunk_str[..first_newline];
590 let rest = &hunk_str[first_newline..];
591 if let Some(func_ctx) =
592 extract_function_context(header_line, &old_lines, funcname_matcher)
593 {
594 output.push_str(header_line);
595 output.push(' ');
596 output.push_str(&func_ctx);
597 output.push_str(rest);
598 } else {
599 output.push_str(hunk_str);
600 }
601 } else {
602 output.push_str(hunk_str);
603 }
604 }
605
606 output
607}
608
609#[must_use]
611pub fn indent_heuristic_from_config(config: &ConfigSet) -> bool {
612 match config.get_bool("diff.indentHeuristic") {
613 Some(Ok(b)) => b,
614 Some(Err(_)) | None => true,
615 }
616}
617
618#[must_use]
620pub fn resolve_indent_heuristic(
621 config: &ConfigSet,
622 cli_indent_heuristic: bool,
623 cli_no_indent_heuristic: bool,
624) -> bool {
625 if cli_no_indent_heuristic {
626 false
627 } else if cli_indent_heuristic {
628 true
629 } else {
630 indent_heuristic_from_config(config)
631 }
632}
633
634#[must_use]
636pub fn parse_indent_heuristic_cli_flags(argv: &[String]) -> (bool, bool) {
637 let mut indent_heuristic = false;
638 let mut no_indent_heuristic = false;
639 for a in argv {
640 match a.as_str() {
641 "--indent-heuristic" => {
642 indent_heuristic = true;
643 no_indent_heuristic = false;
644 }
645 "--no-indent-heuristic" => {
646 no_indent_heuristic = true;
647 indent_heuristic = false;
648 }
649 _ => {}
650 }
651 }
652 (indent_heuristic, no_indent_heuristic)
653}
654
655mod git_xdiff {
665 const XDL_MAX_COST_MIN: i64 = 256;
666 const XDL_HEUR_MIN_COST: i64 = 256;
667 const XDL_SNAKE_CNT: i64 = 20;
668 const XDL_K_HEUR: i64 = 4;
669 const XDL_LINE_MAX: i64 = i64::MAX;
670 const XDL_KPDIS_RUN: i64 = 4;
671 const XDL_MAX_EQLIMIT: i64 = 1024;
672 const XDL_SIMSCAN_WINDOW: i64 = 100;
673
674 const DISCARD: u8 = 0;
676 const KEEP: u8 = 1;
677 const INVESTIGATE: u8 = 2;
678
679 fn bogosqrt(mut n: i64) -> i64 {
681 let mut i: i64 = 1;
682 while n > 0 {
683 i <<= 1;
684 n >>= 2;
685 }
686 i
687 }
688
689 struct XdAlgoEnv {
690 mxcost: i64,
691 snake_cnt: i64,
692 heur_min: i64,
693 }
694
695 struct XdpSplit {
696 i1: i64,
697 i2: i64,
698 min_lo: bool,
699 min_hi: bool,
700 }
701
702 struct XdFile<'a> {
705 ids: &'a [u32],
706 changed: Vec<bool>,
707 reference_index: Vec<usize>,
708 dstart: i64,
709 dend: i64,
710 nreff: i64,
711 }
712
713 #[inline]
715 fn get_id(xdf: &XdFile<'_>, idx: i64) -> u32 {
716 xdf.ids[xdf.reference_index[idx as usize]]
717 }
718
719 #[allow(clippy::too_many_arguments)]
722 fn xdl_split(
723 xdf1: &XdFile<'_>,
724 off1: i64,
725 lim1: i64,
726 xdf2: &XdFile<'_>,
727 off2: i64,
728 lim2: i64,
729 kvdf: &mut [i64],
730 kvdb: &mut [i64],
731 koff: i64,
732 need_min: bool,
733 spl: &mut XdpSplit,
734 xenv: &XdAlgoEnv,
735 ) -> i64 {
736 let dmin = off1 - lim2;
737 let dmax = lim1 - off2;
738 let fmid = off1 - off2;
739 let bmid = lim1 - lim2;
740 let odd = ((fmid - bmid) & 1) != 0;
741 let mut fmin = fmid;
742 let mut fmax = fmid;
743 let mut bmin = bmid;
744 let mut bmax = bmid;
745
746 let kf = |k: i64| -> usize { (k + koff) as usize };
747
748 kvdf[kf(fmid)] = off1;
749 kvdb[kf(bmid)] = lim1;
750
751 let mut ec: i64 = 1;
752 loop {
753 let mut got_snake = false;
754
755 if fmin > dmin {
756 fmin -= 1;
757 kvdf[kf(fmin - 1)] = -1;
758 } else {
759 fmin += 1;
760 }
761 if fmax < dmax {
762 fmax += 1;
763 kvdf[kf(fmax + 1)] = -1;
764 } else {
765 fmax -= 1;
766 }
767
768 let mut d = fmax;
769 while d >= fmin {
770 let mut i1 = if kvdf[kf(d - 1)] >= kvdf[kf(d + 1)] {
771 kvdf[kf(d - 1)] + 1
772 } else {
773 kvdf[kf(d + 1)]
774 };
775 let prev1 = i1;
776 let mut i2 = i1 - d;
777 while i1 < lim1 && i2 < lim2 && get_id(xdf1, i1) == get_id(xdf2, i2) {
778 i1 += 1;
779 i2 += 1;
780 }
781 if i1 - prev1 > xenv.snake_cnt {
782 got_snake = true;
783 }
784 kvdf[kf(d)] = i1;
785 if odd && bmin <= d && d <= bmax && kvdb[kf(d)] <= i1 {
786 spl.i1 = i1;
787 spl.i2 = i2;
788 spl.min_lo = true;
789 spl.min_hi = true;
790 return ec;
791 }
792 d -= 2;
793 }
794
795 if bmin > dmin {
796 bmin -= 1;
797 kvdb[kf(bmin - 1)] = XDL_LINE_MAX;
798 } else {
799 bmin += 1;
800 }
801 if bmax < dmax {
802 bmax += 1;
803 kvdb[kf(bmax + 1)] = XDL_LINE_MAX;
804 } else {
805 bmax -= 1;
806 }
807
808 let mut d = bmax;
809 while d >= bmin {
810 let mut i1 = if kvdb[kf(d - 1)] < kvdb[kf(d + 1)] {
811 kvdb[kf(d - 1)]
812 } else {
813 kvdb[kf(d + 1)] - 1
814 };
815 let prev1 = i1;
816 let mut i2 = i1 - d;
817 while i1 > off1 && i2 > off2 && get_id(xdf1, i1 - 1) == get_id(xdf2, i2 - 1) {
818 i1 -= 1;
819 i2 -= 1;
820 }
821 if prev1 - i1 > xenv.snake_cnt {
822 got_snake = true;
823 }
824 kvdb[kf(d)] = i1;
825 if !odd && fmin <= d && d <= fmax && i1 <= kvdf[kf(d)] {
826 spl.i1 = i1;
827 spl.i2 = i2;
828 spl.min_lo = true;
829 spl.min_hi = true;
830 return ec;
831 }
832 d -= 2;
833 }
834
835 if need_min {
836 ec += 1;
837 continue;
838 }
839
840 if got_snake && ec > xenv.heur_min {
841 let mut best = 0i64;
842 let mut d = fmax;
843 while d >= fmin {
844 let dd = if d > fmid { d - fmid } else { fmid - d };
845 let i1 = kvdf[kf(d)];
846 let i2 = i1 - d;
847 let v = (i1 - off1) + (i2 - off2) - dd;
848 if v > XDL_K_HEUR * ec
849 && v > best
850 && off1 + xenv.snake_cnt <= i1
851 && i1 < lim1
852 && off2 + xenv.snake_cnt <= i2
853 && i2 < lim2
854 {
855 let mut k = 1i64;
856 while get_id(xdf1, i1 - k) == get_id(xdf2, i2 - k) {
857 if k == xenv.snake_cnt {
858 best = v;
859 spl.i1 = i1;
860 spl.i2 = i2;
861 break;
862 }
863 k += 1;
864 }
865 }
866 d -= 2;
867 }
868 if best > 0 {
869 spl.min_lo = true;
870 spl.min_hi = false;
871 return ec;
872 }
873
874 let mut best = 0i64;
875 let mut d = bmax;
876 while d >= bmin {
877 let dd = if d > bmid { d - bmid } else { bmid - d };
878 let i1 = kvdb[kf(d)];
879 let i2 = i1 - d;
880 let v = (lim1 - i1) + (lim2 - i2) - dd;
881 if v > XDL_K_HEUR * ec
882 && v > best
883 && off1 < i1
884 && i1 <= lim1 - xenv.snake_cnt
885 && off2 < i2
886 && i2 <= lim2 - xenv.snake_cnt
887 {
888 let mut k = 0i64;
889 while get_id(xdf1, i1 + k) == get_id(xdf2, i2 + k) {
890 if k == xenv.snake_cnt - 1 {
891 best = v;
892 spl.i1 = i1;
893 spl.i2 = i2;
894 break;
895 }
896 k += 1;
897 }
898 }
899 d -= 2;
900 }
901 if best > 0 {
902 spl.min_lo = false;
903 spl.min_hi = true;
904 return ec;
905 }
906 }
907
908 if ec >= xenv.mxcost {
909 let mut fbest = -1i64;
910 let mut fbest1 = -1i64;
911 let mut d = fmax;
912 while d >= fmin {
913 let mut i1 = kvdf[kf(d)].min(lim1);
914 let mut i2 = i1 - d;
915 if lim2 < i2 {
916 i1 = lim2 + d;
917 i2 = lim2;
918 }
919 if fbest < i1 + i2 {
920 fbest = i1 + i2;
921 fbest1 = i1;
922 }
923 d -= 2;
924 }
925
926 let mut bbest = XDL_LINE_MAX;
927 let mut bbest1 = XDL_LINE_MAX;
928 let mut d = bmax;
929 while d >= bmin {
930 let mut i1 = off1.max(kvdb[kf(d)]);
931 let mut i2 = i1 - d;
932 if i2 < off2 {
933 i1 = off2 + d;
934 i2 = off2;
935 }
936 if i1 + i2 < bbest {
937 bbest = i1 + i2;
938 bbest1 = i1;
939 }
940 d -= 2;
941 }
942
943 if (lim1 + lim2) - bbest < fbest - (off1 + off2) {
944 spl.i1 = fbest1;
945 spl.i2 = fbest - fbest1;
946 spl.min_lo = true;
947 spl.min_hi = false;
948 } else {
949 spl.i1 = bbest1;
950 spl.i2 = bbest - bbest1;
951 spl.min_lo = false;
952 spl.min_hi = true;
953 }
954 return ec;
955 }
956
957 ec += 1;
958 }
959 }
960
961 #[allow(clippy::too_many_arguments)]
963 fn xdl_recs_cmp(
964 xdf1: &mut XdFile<'_>,
965 mut off1: i64,
966 mut lim1: i64,
967 xdf2: &mut XdFile<'_>,
968 mut off2: i64,
969 mut lim2: i64,
970 kvdf: &mut [i64],
971 kvdb: &mut [i64],
972 koff: i64,
973 need_min: bool,
974 xenv: &XdAlgoEnv,
975 ) {
976 while off1 < lim1 && off2 < lim2 && get_id(xdf1, off1) == get_id(xdf2, off2) {
977 off1 += 1;
978 off2 += 1;
979 }
980 while off1 < lim1 && off2 < lim2 && get_id(xdf1, lim1 - 1) == get_id(xdf2, lim2 - 1) {
981 lim1 -= 1;
982 lim2 -= 1;
983 }
984
985 if off1 == lim1 {
986 while off2 < lim2 {
987 let r = xdf2.reference_index[off2 as usize];
988 xdf2.changed[r] = true;
989 off2 += 1;
990 }
991 } else if off2 == lim2 {
992 while off1 < lim1 {
993 let r = xdf1.reference_index[off1 as usize];
994 xdf1.changed[r] = true;
995 off1 += 1;
996 }
997 } else {
998 let mut spl = XdpSplit {
999 i1: 0,
1000 i2: 0,
1001 min_lo: false,
1002 min_hi: false,
1003 };
1004 xdl_split(
1005 xdf1, off1, lim1, xdf2, off2, lim2, kvdf, kvdb, koff, need_min, &mut spl, xenv,
1006 );
1007 xdl_recs_cmp(
1008 xdf1, off1, spl.i1, xdf2, off2, spl.i2, kvdf, kvdb, koff, spl.min_lo, xenv,
1009 );
1010 xdl_recs_cmp(
1011 xdf1, spl.i1, lim1, xdf2, spl.i2, lim2, kvdf, kvdb, koff, spl.min_hi, xenv,
1012 );
1013 }
1014 }
1015
1016 fn clean_mmatch(action: &[u8], i: i64, mut s: i64, mut e: i64) -> bool {
1018 if i - s > XDL_SIMSCAN_WINDOW {
1019 s = i - XDL_SIMSCAN_WINDOW;
1020 }
1021 if e - i > XDL_SIMSCAN_WINDOW {
1022 e = i + XDL_SIMSCAN_WINDOW;
1023 }
1024
1025 let mut rdis0 = 0i64;
1026 let mut rpdis0 = 1i64;
1027 let mut r = 1i64;
1028 while i - r >= s {
1029 match action[(i - r) as usize] {
1030 DISCARD => rdis0 += 1,
1031 INVESTIGATE => rpdis0 += 1,
1032 _ => break, }
1034 r += 1;
1035 }
1036 if rdis0 == 0 {
1037 return false;
1038 }
1039 let mut rdis1 = 0i64;
1040 let mut rpdis1 = 1i64;
1041 let mut r = 1i64;
1042 while i + r <= e {
1043 match action[(i + r) as usize] {
1044 DISCARD => rdis1 += 1,
1045 INVESTIGATE => rpdis1 += 1,
1046 _ => break, }
1048 r += 1;
1049 }
1050 if rdis1 == 0 {
1051 return false;
1052 }
1053 rdis1 += rdis0;
1054 rpdis1 += rpdis0;
1055 rpdis1 * XDL_KPDIS_RUN < (rpdis1 + rdis1)
1056 }
1057
1058 pub fn changed_flags(ids1: &[u32], ids2: &[u32]) -> (Vec<bool>, Vec<bool>) {
1062 let nrec1 = ids1.len();
1063 let nrec2 = ids2.len();
1064
1065 use std::collections::HashMap;
1067 let mut count1: HashMap<u32, i64> = HashMap::new();
1068 let mut count2: HashMap<u32, i64> = HashMap::new();
1069 for &id in ids1 {
1070 *count1.entry(id).or_insert(0) += 1;
1071 }
1072 for &id in ids2 {
1073 *count2.entry(id).or_insert(0) += 1;
1074 }
1075
1076 let mut xdf1 = XdFile {
1077 ids: ids1,
1078 changed: vec![false; nrec1],
1079 reference_index: Vec::new(),
1080 dstart: 0,
1081 dend: nrec1 as i64 - 1,
1082 nreff: 0,
1083 };
1084 let mut xdf2 = XdFile {
1085 ids: ids2,
1086 changed: vec![false; nrec2],
1087 reference_index: Vec::new(),
1088 dstart: 0,
1089 dend: nrec2 as i64 - 1,
1090 nreff: 0,
1091 };
1092
1093 let lim = nrec1.min(nrec2) as i64;
1095 let mut i = 0i64;
1096 while i < lim && ids1[i as usize] == ids2[i as usize] {
1097 i += 1;
1098 }
1099 xdf1.dstart = i;
1100 xdf2.dstart = i;
1101 let mut j = 0i64;
1102 let rem = lim - i;
1103 while j < rem && ids1[nrec1 - 1 - j as usize] == ids2[nrec2 - 1 - j as usize] {
1104 j += 1;
1105 }
1106 xdf1.dend = nrec1 as i64 - j - 1;
1107 xdf2.dend = nrec2 as i64 - j - 1;
1108
1109 let mut action1 = vec![0u8; nrec1 + 1];
1111 let mut action2 = vec![0u8; nrec2 + 1];
1112
1113 let mut mlim = bogosqrt(nrec1 as i64);
1114 if mlim > XDL_MAX_EQLIMIT {
1115 mlim = XDL_MAX_EQLIMIT;
1116 }
1117 let mut idx = xdf1.dstart;
1118 while idx <= xdf1.dend {
1119 let id = ids1[idx as usize];
1120 let nm = *count2.get(&id).unwrap_or(&0);
1121 action1[idx as usize] = if nm == 0 {
1122 DISCARD
1123 } else if nm >= mlim {
1124 INVESTIGATE
1125 } else {
1126 KEEP
1127 };
1128 idx += 1;
1129 }
1130
1131 let mut mlim = bogosqrt(nrec2 as i64);
1132 if mlim > XDL_MAX_EQLIMIT {
1133 mlim = XDL_MAX_EQLIMIT;
1134 }
1135 let mut idx = xdf2.dstart;
1136 while idx <= xdf2.dend {
1137 let id = ids2[idx as usize];
1138 let nm = *count1.get(&id).unwrap_or(&0);
1139 action2[idx as usize] = if nm == 0 {
1140 DISCARD
1141 } else if nm >= mlim {
1142 INVESTIGATE
1143 } else {
1144 KEEP
1145 };
1146 idx += 1;
1147 }
1148
1149 let mut idx = xdf1.dstart;
1150 while idx <= xdf1.dend {
1151 let a = action1[idx as usize];
1152 if a == KEEP
1153 || (a == INVESTIGATE && !clean_mmatch(&action1, idx, xdf1.dstart, xdf1.dend))
1154 {
1155 xdf1.reference_index.push(idx as usize);
1156 } else {
1157 xdf1.changed[idx as usize] = true;
1158 }
1159 idx += 1;
1160 }
1161 xdf1.nreff = xdf1.reference_index.len() as i64;
1162
1163 let mut idx = xdf2.dstart;
1164 while idx <= xdf2.dend {
1165 let a = action2[idx as usize];
1166 if a == KEEP
1167 || (a == INVESTIGATE && !clean_mmatch(&action2, idx, xdf2.dstart, xdf2.dend))
1168 {
1169 xdf2.reference_index.push(idx as usize);
1170 } else {
1171 xdf2.changed[idx as usize] = true;
1172 }
1173 idx += 1;
1174 }
1175 xdf2.nreff = xdf2.reference_index.len() as i64;
1176
1177 let ndiags = xdf1.nreff + xdf2.nreff + 3;
1179 let kvd_len = (2 * ndiags + 2) as usize;
1180 let mut kvd = vec![0i64; kvd_len];
1181 let koff = xdf2.nreff + 1;
1183 let (kvdf_slice, kvdb_slice) = kvd.split_at_mut(ndiags as usize);
1184 let xenv = XdAlgoEnv {
1187 mxcost: bogosqrt(ndiags).max(XDL_MAX_COST_MIN),
1188 snake_cnt: XDL_SNAKE_CNT,
1189 heur_min: XDL_HEUR_MIN_COST,
1190 };
1191
1192 let nreff1 = xdf1.nreff;
1193 let nreff2 = xdf2.nreff;
1194 xdl_recs_cmp(
1195 &mut xdf1, 0, nreff1, &mut xdf2, 0, nreff2, kvdf_slice, kvdb_slice, koff, false, &xenv,
1196 );
1197
1198 (xdf1.changed, xdf2.changed)
1199 }
1200}
1201
1202fn changed_flags_to_ops(
1204 changed1: &[bool],
1205 changed2: &[bool],
1206 old_len: usize,
1207 new_len: usize,
1208) -> Vec<similar::DiffOp> {
1209 use similar::DiffOp;
1210 let mut ops: Vec<DiffOp> = Vec::new();
1211 let mut i = 0usize;
1212 let mut j = 0usize;
1213 while i < old_len || j < new_len {
1214 let del = i < old_len && changed1[i];
1215 let ins = j < new_len && changed2[j];
1216 if !del && !ins {
1217 let start_i = i;
1219 let start_j = j;
1220 while i < old_len && j < new_len && !changed1[i] && !changed2[j] {
1221 i += 1;
1222 j += 1;
1223 }
1224 ops.push(DiffOp::Equal {
1225 old_index: start_i,
1226 new_index: start_j,
1227 len: i - start_i,
1228 });
1229 } else {
1230 let start_i = i;
1232 let start_j = j;
1233 while i < old_len && changed1[i] {
1234 i += 1;
1235 }
1236 while j < new_len && changed2[j] {
1237 j += 1;
1238 }
1239 let dlen = i - start_i;
1240 let ilen = j - start_j;
1241 if dlen > 0 && ilen > 0 {
1242 ops.push(DiffOp::Replace {
1243 old_index: start_i,
1244 old_len: dlen,
1245 new_index: start_j,
1246 new_len: ilen,
1247 });
1248 } else if dlen > 0 {
1249 ops.push(DiffOp::Delete {
1250 old_index: start_i,
1251 old_len: dlen,
1252 new_index: start_j,
1253 });
1254 } else if ilen > 0 {
1255 ops.push(DiffOp::Insert {
1256 old_index: start_i,
1257 new_index: start_j,
1258 new_len: ilen,
1259 });
1260 }
1261 }
1262 }
1263 ops
1264}
1265
1266#[must_use]
1272pub fn word_diff_ops_imara(old_words: &[&str], new_words: &[&str]) -> Vec<similar::DiffOp> {
1273 use std::collections::HashMap;
1274
1275 let mut interner: HashMap<&str, u32> = HashMap::new();
1277 let mut ids1: Vec<u32> = Vec::with_capacity(old_words.len());
1278 for &w in old_words {
1279 let next = interner.len() as u32;
1280 let id = *interner.entry(w).or_insert(next);
1281 ids1.push(id);
1282 }
1283 let mut ids2: Vec<u32> = Vec::with_capacity(new_words.len());
1284 for &w in new_words {
1285 let next = interner.len() as u32;
1286 let id = *interner.entry(w).or_insert(next);
1287 ids2.push(id);
1288 }
1289
1290 let (changed1, changed2) = git_xdiff::changed_flags(&ids1, &ids2);
1291 let ops = changed_flags_to_ops(&changed1, &changed2, old_words.len(), new_words.len());
1292
1293 diff_indent_heuristic::apply_change_compact_to_ops(&ops, old_words, new_words, false)
1296}
1297
1298#[must_use]
1300pub fn diff_slice_ops_compacted(
1301 old_lines: &[&str],
1302 new_lines: &[&str],
1303 algorithm: similar::Algorithm,
1304 indent_heuristic: bool,
1305) -> Vec<similar::DiffOp> {
1306 diff_indent_heuristic::diff_slice_ops_compacted(
1307 old_lines,
1308 new_lines,
1309 algorithm,
1310 indent_heuristic,
1311 )
1312}
1313
1314#[must_use]
1316pub fn map_new_to_old_lines_compacted(
1317 old_joined: &str,
1318 new_joined: &str,
1319 algorithm: similar::Algorithm,
1320 indent_heuristic: bool,
1321 new_line_count: usize,
1322) -> Vec<Option<usize>> {
1323 let ops = diff_indent_heuristic::diff_lines_ops_compacted(
1324 old_joined,
1325 new_joined,
1326 algorithm,
1327 indent_heuristic,
1328 );
1329 diff_indent_heuristic::map_new_to_old_from_ops(&ops, new_line_count)
1330}
1331
1332#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1334pub enum DiffStatus {
1335 Added,
1337 Deleted,
1339 Modified,
1341 Renamed,
1343 Copied,
1345 TypeChanged,
1347 Unmerged,
1349}
1350
1351impl DiffStatus {
1352 #[must_use]
1354 pub fn letter(&self) -> char {
1355 match self {
1356 Self::Added => 'A',
1357 Self::Deleted => 'D',
1358 Self::Modified => 'M',
1359 Self::Renamed => 'R',
1360 Self::Copied => 'C',
1361 Self::TypeChanged => 'T',
1362 Self::Unmerged => 'U',
1363 }
1364 }
1365}
1366
1367#[derive(Debug, Clone, PartialEq, Eq)]
1369pub struct DiffEntry {
1370 pub status: DiffStatus,
1372 pub old_path: Option<String>,
1374 pub new_path: Option<String>,
1376 pub old_mode: String,
1378 pub new_mode: String,
1380 pub old_oid: ObjectId,
1382 pub new_oid: ObjectId,
1384 pub score: Option<u32>,
1386}
1387
1388impl DiffEntry {
1389 #[must_use]
1391 pub fn path(&self) -> &str {
1392 self.new_path
1393 .as_deref()
1394 .or(self.old_path.as_deref())
1395 .unwrap_or("")
1396 }
1397
1398 #[must_use]
1403 pub fn display_path(&self) -> String {
1404 match self.status {
1405 DiffStatus::Renamed | DiffStatus::Copied => {
1406 let old = self.old_path.as_deref().unwrap_or("");
1407 let new = self.new_path.as_deref().unwrap_or("");
1408 if old.is_empty() || new.is_empty() {
1409 self.path().to_owned()
1410 } else {
1411 format!("{old} -> {new}")
1412 }
1413 }
1414 _ => self.path().to_owned(),
1415 }
1416 }
1417}
1418
1419pub const ZERO_OID: &str = "0000000000000000000000000000000000000000";
1421
1422#[must_use]
1424pub fn zero_oid() -> ObjectId {
1425 ObjectId::from_bytes(&[0u8; 20]).unwrap_or_else(|_| {
1426 panic!("internal error: failed to create zero OID");
1428 })
1429}
1430
1431#[must_use]
1433pub fn empty_blob_oid() -> ObjectId {
1434 ObjectId::from_hex("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391").unwrap_or_else(|_| {
1435 panic!("internal error: failed to create empty blob OID");
1437 })
1438}
1439
1440pub fn diff_trees(
1455 odb: &Odb,
1456 old_tree_oid: Option<&ObjectId>,
1457 new_tree_oid: Option<&ObjectId>,
1458 prefix: &str,
1459) -> Result<Vec<DiffEntry>> {
1460 diff_trees_opts(odb, old_tree_oid, new_tree_oid, prefix, false)
1461}
1462
1463pub fn diff_trees_show_tree_entries(
1467 odb: &Odb,
1468 old_tree_oid: Option<&ObjectId>,
1469 new_tree_oid: Option<&ObjectId>,
1470 prefix: &str,
1471) -> Result<Vec<DiffEntry>> {
1472 diff_trees_opts(odb, old_tree_oid, new_tree_oid, prefix, true)
1473}
1474
1475fn diff_trees_opts(
1476 odb: &Odb,
1477 old_tree_oid: Option<&ObjectId>,
1478 new_tree_oid: Option<&ObjectId>,
1479 prefix: &str,
1480 show_trees: bool,
1481) -> Result<Vec<DiffEntry>> {
1482 let old_entries = match old_tree_oid {
1483 Some(oid) => read_tree(odb, oid)?,
1484 None => Vec::new(),
1485 };
1486 let new_entries = match new_tree_oid {
1487 Some(oid) => read_tree(odb, oid)?,
1488 None => Vec::new(),
1489 };
1490
1491 let mut result = Vec::new();
1492 diff_tree_entries_opts(
1493 odb,
1494 &old_entries,
1495 &new_entries,
1496 prefix,
1497 show_trees,
1498 &mut result,
1499 )?;
1500 Ok(result)
1501}
1502
1503fn read_tree(odb: &Odb, oid: &ObjectId) -> Result<Vec<TreeEntry>> {
1505 let obj = odb.read(oid)?;
1506 if obj.kind != ObjectKind::Tree {
1507 return Err(Error::CorruptObject(format!(
1508 "expected tree, got {}",
1509 obj.kind.as_str()
1510 )));
1511 }
1512 parse_tree(&obj.data)
1513}
1514
1515fn diff_tree_entries_opts(
1517 odb: &Odb,
1518 old: &[TreeEntry],
1519 new: &[TreeEntry],
1520 prefix: &str,
1521 show_trees: bool,
1522 result: &mut Vec<DiffEntry>,
1523) -> Result<()> {
1524 let mut oi = 0;
1525 let mut ni = 0;
1526
1527 while oi < old.len() || ni < new.len() {
1528 match (old.get(oi), new.get(ni)) {
1529 (Some(o), Some(n)) => {
1530 let cmp = crate::objects::tree_entry_cmp(
1531 &o.name,
1532 is_tree_mode(o.mode),
1533 &n.name,
1534 is_tree_mode(n.mode),
1535 );
1536 match cmp {
1537 std::cmp::Ordering::Less => {
1538 emit_deleted_opts(odb, o, prefix, show_trees, result)?;
1540 oi += 1;
1541 }
1542 std::cmp::Ordering::Greater => {
1543 emit_added_opts(odb, n, prefix, show_trees, result)?;
1545 ni += 1;
1546 }
1547 std::cmp::Ordering::Equal => {
1548 if o.oid != n.oid || o.mode != n.mode {
1550 let name_str = String::from_utf8_lossy(&o.name);
1551 let path = format_path(prefix, &name_str);
1552 if is_tree_mode(o.mode) && is_tree_mode(n.mode) {
1553 if show_trees {
1555 result.push(DiffEntry {
1556 status: DiffStatus::Modified,
1557 old_path: Some(path.clone()),
1558 new_path: Some(path.clone()),
1559 old_mode: format_mode(o.mode),
1560 new_mode: format_mode(n.mode),
1561 old_oid: o.oid,
1562 new_oid: n.oid,
1563 score: None,
1564 });
1565 }
1566 let nested = diff_trees_opts(
1568 odb,
1569 Some(&o.oid),
1570 Some(&n.oid),
1571 &path,
1572 show_trees,
1573 )?;
1574 result.extend(nested);
1575 } else if is_tree_mode(o.mode) && !is_tree_mode(n.mode) {
1576 emit_deleted_opts(odb, o, prefix, show_trees, result)?;
1578 emit_added_opts(odb, n, prefix, show_trees, result)?;
1579 } else if !is_tree_mode(o.mode) && is_tree_mode(n.mode) {
1580 emit_deleted_opts(odb, o, prefix, show_trees, result)?;
1582 emit_added_opts(odb, n, prefix, show_trees, result)?;
1583 } else {
1584 let old_type = o.mode & 0o170000;
1588 let new_type = n.mode & 0o170000;
1589 result.push(DiffEntry {
1590 status: if old_type != new_type {
1591 DiffStatus::TypeChanged
1592 } else {
1593 DiffStatus::Modified
1594 },
1595 old_path: Some(path.clone()),
1596 new_path: Some(path),
1597 old_mode: format_mode(o.mode),
1598 new_mode: format_mode(n.mode),
1599 old_oid: o.oid,
1600 new_oid: n.oid,
1601 score: None,
1602 });
1603 }
1604 }
1605 oi += 1;
1606 ni += 1;
1607 }
1608 }
1609 }
1610 (Some(o), None) => {
1611 emit_deleted_opts(odb, o, prefix, show_trees, result)?;
1612 oi += 1;
1613 }
1614 (None, Some(n)) => {
1615 emit_added_opts(odb, n, prefix, show_trees, result)?;
1616 ni += 1;
1617 }
1618 (None, None) => break,
1619 }
1620 }
1621
1622 Ok(())
1623}
1624
1625fn emit_deleted_opts(
1626 odb: &Odb,
1627 entry: &TreeEntry,
1628 prefix: &str,
1629 show_trees: bool,
1630 result: &mut Vec<DiffEntry>,
1631) -> Result<()> {
1632 let name_str = String::from_utf8_lossy(&entry.name);
1633 let path = format_path(prefix, &name_str);
1634 if is_tree_mode(entry.mode) {
1635 if show_trees {
1636 result.push(DiffEntry {
1637 status: DiffStatus::Deleted,
1638 old_path: Some(path.clone()),
1639 new_path: None,
1640 old_mode: format_mode(entry.mode),
1641 new_mode: "000000".to_owned(),
1642 old_oid: entry.oid,
1643 new_oid: zero_oid(),
1644 score: None,
1645 });
1646 }
1647 let nested = diff_trees_opts(odb, Some(&entry.oid), None, &path, show_trees)?;
1649 result.extend(nested);
1650 } else {
1651 result.push(DiffEntry {
1652 status: DiffStatus::Deleted,
1653 old_path: Some(path.clone()),
1654 new_path: None,
1655 old_mode: format_mode(entry.mode),
1656 new_mode: "000000".to_owned(),
1657 old_oid: entry.oid,
1658 new_oid: zero_oid(),
1659 score: None,
1660 });
1661 }
1662 Ok(())
1663}
1664
1665fn emit_added_opts(
1666 odb: &Odb,
1667 entry: &TreeEntry,
1668 prefix: &str,
1669 show_trees: bool,
1670 result: &mut Vec<DiffEntry>,
1671) -> Result<()> {
1672 let name_str = String::from_utf8_lossy(&entry.name);
1673 let path = format_path(prefix, &name_str);
1674 if is_tree_mode(entry.mode) {
1675 if show_trees {
1676 result.push(DiffEntry {
1677 status: DiffStatus::Added,
1678 old_path: None,
1679 new_path: Some(path.clone()),
1680 old_mode: "000000".to_owned(),
1681 new_mode: format_mode(entry.mode),
1682 old_oid: zero_oid(),
1683 new_oid: entry.oid,
1684 score: None,
1685 });
1686 }
1687 let nested = diff_trees_opts(odb, None, Some(&entry.oid), &path, show_trees)?;
1689 result.extend(nested);
1690 } else {
1691 result.push(DiffEntry {
1692 status: DiffStatus::Added,
1693 old_path: None,
1694 new_path: Some(path),
1695 old_mode: "000000".to_owned(),
1696 new_mode: format_mode(entry.mode),
1697 old_oid: zero_oid(),
1698 new_oid: entry.oid,
1699 score: None,
1700 });
1701 }
1702 Ok(())
1703}
1704
1705pub fn diff_index_to_tree(
1725 odb: &Odb,
1726 index: &Index,
1727 tree_oid: Option<&ObjectId>,
1728 ignore_submodules: bool,
1729) -> Result<Vec<DiffEntry>> {
1730 let tree_entries = match tree_oid {
1732 Some(oid) => flatten_tree(odb, oid, "")?,
1733 None => Vec::new(),
1734 };
1735
1736 let mut tree_map: std::collections::BTreeMap<&str, &FlatEntry> =
1738 std::collections::BTreeMap::new();
1739 for entry in &tree_entries {
1740 tree_map.insert(&entry.path, entry);
1741 }
1742
1743 let mut result = Vec::new();
1744 let mut stage0_paths = std::collections::BTreeSet::new();
1745 let mut unmerged_modes: std::collections::BTreeMap<String, (u8, u32)> =
1746 std::collections::BTreeMap::new();
1747
1748 for ie in &index.entries {
1750 let path = String::from_utf8_lossy(&ie.path).to_string();
1751 if ie.stage() == 0 && ie.intent_to_add() {
1752 continue;
1755 }
1756 if ie.stage() != 0 {
1757 let rank = match ie.stage() {
1758 2 => 0u8,
1759 3 => 1u8,
1760 1 => 2u8,
1761 _ => 3u8,
1762 };
1763 match unmerged_modes.get(&path) {
1764 Some((existing_rank, _)) if *existing_rank <= rank => {}
1765 _ => {
1766 unmerged_modes.insert(path, (rank, ie.mode));
1767 }
1768 }
1769 continue;
1770 }
1771 if ignore_submodules && ie.mode == 0o160000 {
1772 let _ = tree_map.remove(path.as_str());
1773 stage0_paths.insert(path.clone());
1774 continue;
1775 }
1776 stage0_paths.insert(path.clone());
1777 match tree_map.remove(path.as_str()) {
1778 Some(te) => {
1779 if te.oid != ie.oid || te.mode != ie.mode {
1781 result.push(DiffEntry {
1782 status: DiffStatus::Modified,
1783 old_path: Some(path.clone()),
1784 new_path: Some(path),
1785 old_mode: format_mode(te.mode),
1786 new_mode: format_mode(ie.mode),
1787 old_oid: te.oid,
1788 new_oid: ie.oid,
1789 score: None,
1790 });
1791 }
1792 }
1793 None => {
1794 result.push(DiffEntry {
1796 status: DiffStatus::Added,
1797 old_path: None,
1798 new_path: Some(path),
1799 old_mode: "000000".to_owned(),
1800 new_mode: format_mode(ie.mode),
1801 old_oid: zero_oid(),
1802 new_oid: ie.oid,
1803 score: None,
1804 });
1805 }
1806 }
1807 }
1808
1809 for (path, (_, mode)) in &unmerged_modes {
1810 if stage0_paths.contains(path) {
1811 continue;
1812 }
1813 tree_map.remove(path.as_str());
1814 result.push(DiffEntry {
1815 status: DiffStatus::Unmerged,
1816 old_path: Some(path.clone()),
1817 new_path: Some(path.clone()),
1818 old_mode: "000000".to_owned(),
1819 new_mode: format_mode(*mode),
1820 old_oid: zero_oid(),
1821 new_oid: zero_oid(),
1822 score: None,
1823 });
1824 }
1825
1826 for (path, te) in tree_map {
1828 if ignore_submodules && te.mode == 0o160000 {
1829 continue;
1830 }
1831 result.push(DiffEntry {
1832 status: DiffStatus::Deleted,
1833 old_path: Some(path.to_owned()),
1834 new_path: None,
1835 old_mode: format_mode(te.mode),
1836 new_mode: "000000".to_owned(),
1837 old_oid: te.oid,
1838 new_oid: zero_oid(),
1839 score: None,
1840 });
1841 }
1842
1843 result.sort_by(|a, b| a.path().cmp(b.path()));
1844 Ok(result)
1845}
1846
1847pub fn diff_index_to_worktree(
1872 odb: &Odb,
1873 index: &Index,
1874 work_tree: &Path,
1875 ignore_submodule_untracked: bool,
1876 simplify_gitlinks: bool,
1877) -> Result<Vec<DiffEntry>> {
1878 diff_index_to_worktree_with_options(
1879 odb,
1880 index,
1881 work_tree,
1882 DiffIndexToWorktreeOptions {
1883 ignore_submodule_untracked,
1884 simplify_gitlinks,
1885 ..DiffIndexToWorktreeOptions::default()
1886 },
1887 )
1888}
1889
1890#[derive(Debug, Clone, Copy, Default)]
1892pub struct DiffIndexToWorktreeOptions {
1893 pub index_mtime: Option<(u32, u32)>,
1899 pub ignore_submodule_untracked: bool,
1901 pub simplify_gitlinks: bool,
1903 pub error_on_broken_gitlinks: bool,
1906}
1907
1908pub fn diff_index_to_worktree_with_options(
1925 odb: &Odb,
1926 index: &Index,
1927 work_tree: &Path,
1928 options: DiffIndexToWorktreeOptions,
1929) -> Result<Vec<DiffEntry>> {
1930 use crate::config::ConfigSet;
1931 use crate::crlf;
1932
1933 let ignore_submodule_untracked = options.ignore_submodule_untracked;
1934 let simplify_gitlinks = options.simplify_gitlinks;
1935
1936 let git_dir = work_tree.join(".git");
1937 let config = ConfigSet::load(Some(&git_dir), true).unwrap_or_else(|_| ConfigSet::new());
1938 let conv = crlf::ConversionConfig::from_config(&config);
1939 let attrs = crlf::load_gitattributes(work_tree);
1940
1941 let mut result = Vec::new();
1942 let mut unmerged_base: std::collections::BTreeMap<String, (u8, &IndexEntry)> =
1943 std::collections::BTreeMap::new();
1944
1945 for ie in &index.entries {
1946 if ie.stage() != 0 {
1947 let path = String::from_utf8_lossy(&ie.path).to_string();
1948 let rank = match ie.stage() {
1949 2 => 0u8,
1950 3 => 1u8,
1951 1 => 2u8,
1952 _ => 3u8,
1953 };
1954 match unmerged_base.get(&path) {
1955 Some((existing_rank, _)) if *existing_rank <= rank => {}
1956 _ => {
1957 unmerged_base.insert(path, (rank, ie));
1958 }
1959 }
1960 continue;
1961 }
1962 if ie.skip_worktree() || ie.assume_unchanged() {
1965 continue;
1966 }
1967 let path_str_ref = std::str::from_utf8(&ie.path).unwrap_or("");
1970 let is_intent_to_add = ie.intent_to_add();
1971
1972 if ie.mode == 0o160000 {
1977 let sub_dir = work_tree.join(path_str_ref);
1978 let sub_head_oid = read_submodule_head_oid(&sub_dir);
1979 if !simplify_gitlinks && sub_head_oid.is_none() && !sub_dir.exists() {
1986 let path_owned = path_str_ref.to_owned();
1987 result.push(DiffEntry {
1988 status: DiffStatus::Deleted,
1989 old_path: Some(path_owned.clone()),
1990 new_path: Some(path_owned),
1991 old_mode: format_mode(ie.mode),
1992 new_mode: "000000".to_owned(),
1993 old_oid: ie.oid,
1994 new_oid: zero_oid(),
1995 score: None,
1996 });
1997 continue;
1998 }
1999 let ref_matches = if let Some(oid) = sub_head_oid {
2000 oid == ie.oid
2001 } else {
2002 let is_placeholder = submodule_worktree_is_unpopulated_placeholder(&sub_dir);
2003 if options.error_on_broken_gitlinks
2004 && !is_placeholder
2005 && submodule_embedded_git_dir(&sub_dir).is_some()
2006 {
2007 return Err(Error::ConfigError(format!(
2008 "could not read submodule HEAD for '{path_str_ref}'"
2009 )));
2010 }
2011 is_placeholder
2012 };
2013 if simplify_gitlinks {
2014 if !ref_matches {
2015 let path_owned = path_str_ref.to_owned();
2016 let new_oid = sub_head_oid.unwrap_or_else(zero_oid);
2017 result.push(DiffEntry {
2018 status: DiffStatus::Modified,
2019 old_path: Some(path_owned.clone()),
2020 new_path: Some(path_owned),
2021 old_mode: format_mode(ie.mode),
2022 new_mode: format_mode(ie.mode),
2023 old_oid: ie.oid,
2024 new_oid,
2025 score: None,
2026 });
2027 }
2028 continue;
2029 }
2030 if sub_head_oid.is_some() && submodule_head_object_broken(&sub_dir) {
2035 return Err(Error::ConfigError(format!(
2036 "'git status --porcelain=2' failed in submodule {path_str_ref}"
2037 )));
2038 }
2039 let mut flags = submodule_porcelain_flags(work_tree, path_str_ref, ie.oid);
2040 if ignore_submodule_untracked {
2041 flags.untracked = false;
2042 }
2043 let inner_dirty = flags.modified || flags.untracked;
2044 if !ref_matches || inner_dirty {
2045 let path_owned = path_str_ref.to_owned();
2046 let new_oid = if !ref_matches {
2047 sub_head_oid.unwrap_or_else(zero_oid)
2048 } else {
2049 zero_oid()
2050 };
2051 result.push(DiffEntry {
2052 status: DiffStatus::Modified,
2053 old_path: Some(path_owned.clone()),
2054 new_path: Some(path_owned),
2055 old_mode: format_mode(ie.mode),
2056 new_mode: format_mode(ie.mode),
2057 old_oid: ie.oid,
2058 new_oid,
2059 score: None,
2060 });
2061 }
2062 continue;
2063 }
2064
2065 let file_path = work_tree.join(path_str_ref);
2066
2067 if is_intent_to_add {
2068 match fs::symlink_metadata(&file_path) {
2069 Ok(meta) => {
2070 let file_attrs = crlf::get_file_attrs(&attrs, path_str_ref, false, &config);
2071 let worktree_oid = hash_worktree_file(
2072 odb,
2073 &file_path,
2074 &meta,
2075 &conv,
2076 &file_attrs,
2077 path_str_ref,
2078 None,
2079 )?;
2080 let worktree_mode = mode_from_metadata(&meta);
2081 result.push(DiffEntry {
2082 status: DiffStatus::Added,
2083 old_path: None,
2084 new_path: Some(path_str_ref.to_owned()),
2085 old_mode: "000000".to_owned(),
2086 new_mode: format_mode(worktree_mode),
2087 old_oid: zero_oid(),
2090 new_oid: worktree_oid,
2091 score: None,
2092 });
2093 }
2094 Err(e)
2095 if e.kind() == std::io::ErrorKind::NotFound
2096 || e.raw_os_error() == Some(20) =>
2097 {
2098 result.push(DiffEntry {
2099 status: DiffStatus::Deleted,
2100 old_path: Some(path_str_ref.to_owned()),
2101 new_path: None,
2102 old_mode: format_mode(ie.mode),
2103 new_mode: "000000".to_owned(),
2104 old_oid: ie.oid,
2105 new_oid: zero_oid(),
2106 score: None,
2107 });
2108 }
2109 Err(e) => return Err(Error::Io(e)),
2110 }
2111 continue;
2112 }
2113
2114 if has_symlink_in_path(work_tree, path_str_ref) {
2117 result.push(DiffEntry {
2118 status: DiffStatus::Deleted,
2119 old_path: Some(path_str_ref.to_owned()),
2120 new_path: None,
2121 old_mode: format_mode(ie.mode),
2122 new_mode: "000000".to_owned(),
2123 old_oid: ie.oid,
2124 new_oid: zero_oid(),
2125 score: None,
2126 });
2127 continue;
2128 }
2129
2130 match fs::symlink_metadata(&file_path) {
2131 Ok(meta) if meta.is_dir() => {
2132 if file_path.join(".git").exists() {
2137 let head = read_submodule_head_oid(&file_path).unwrap_or_else(zero_oid);
2138 let path_owned = path_str_ref.to_owned();
2139 result.push(DiffEntry {
2140 status: DiffStatus::TypeChanged,
2141 old_path: Some(path_owned.clone()),
2142 new_path: Some(path_owned),
2143 old_mode: format_mode(ie.mode),
2144 new_mode: format_mode(0o160000),
2145 old_oid: ie.oid,
2146 new_oid: head,
2147 score: None,
2148 });
2149 continue;
2150 }
2151 result.push(DiffEntry {
2152 status: DiffStatus::Deleted,
2153 old_path: Some(path_str_ref.to_owned()),
2154 new_path: None,
2155 old_mode: format_mode(ie.mode),
2156 new_mode: String::new(),
2157 old_oid: ie.oid,
2158 new_oid: zero_oid(),
2159 score: None,
2160 });
2161 }
2162 Ok(meta) => {
2163 let worktree_mode = mode_from_metadata(&meta);
2164 let stat_same = stat_matches(ie, &meta);
2165 if stat_same && worktree_mode != ie.mode {
2167 let path_owned = path_str_ref.to_owned();
2168 result.push(DiffEntry {
2169 status: DiffStatus::Modified,
2170 old_path: Some(path_owned.clone()),
2171 new_path: Some(path_owned),
2172 old_mode: format_mode(ie.mode),
2173 new_mode: format_mode(worktree_mode),
2174 old_oid: ie.oid,
2175 new_oid: ie.oid,
2176 score: None,
2177 });
2178 continue;
2179 }
2180
2181 if stat_same && worktree_mode == ie.mode && !entry_is_racy(ie, options.index_mtime) {
2184 continue;
2185 }
2186
2187 let file_attrs = crlf::get_file_attrs(&attrs, path_str_ref, false, &config);
2189 let worktree_oid = hash_worktree_file(
2190 odb,
2191 &file_path,
2192 &meta,
2193 &conv,
2194 &file_attrs,
2195 path_str_ref,
2196 Some(ie),
2197 )?;
2198
2199 let mut eff_oid = worktree_oid;
2203 if eff_oid != ie.oid {
2204 if let Ok(raw) = fs::read(&file_path) {
2205 let raw_oid = Odb::hash_object_data(ObjectKind::Blob, &raw);
2206 if raw_oid == ie.oid {
2207 eff_oid = ie.oid;
2208 }
2209 }
2210 }
2211
2212 if eff_oid != ie.oid || worktree_mode != ie.mode {
2213 let path_owned = path_str_ref.to_owned();
2214 result.push(DiffEntry {
2215 status: DiffStatus::Modified,
2216 old_path: Some(path_owned.clone()),
2217 new_path: Some(path_owned),
2218 old_mode: format_mode(ie.mode),
2219 new_mode: format_mode(worktree_mode),
2220 old_oid: ie.oid,
2221 new_oid: eff_oid,
2222 score: None,
2223 });
2224 }
2225 }
2226 Err(e) if e.kind() == std::io::ErrorKind::NotFound
2227 || e.raw_os_error() == Some(20) => {
2228 result.push(DiffEntry {
2230 status: DiffStatus::Deleted,
2231 old_path: Some(path_str_ref.to_owned()),
2232 new_path: None,
2233 old_mode: format_mode(ie.mode),
2234 new_mode: "000000".to_owned(),
2235 old_oid: ie.oid,
2236 new_oid: zero_oid(),
2237 score: None,
2238 });
2239 }
2240 Err(e) => return Err(Error::Io(e)),
2241 }
2242 }
2243
2244 for (path, (_, base_entry)) in unmerged_base {
2245 let file_path = work_tree.join(&path);
2246 let wt_meta = match fs::symlink_metadata(&file_path) {
2247 Ok(meta) => Some(meta),
2248 Err(e)
2249 if e.kind() == std::io::ErrorKind::NotFound
2250 || e.raw_os_error() == Some(20) =>
2251 {
2252 None
2253 }
2254 Err(e) => return Err(Error::Io(e)),
2255 };
2256
2257 let new_mode = wt_meta.as_ref().map_or_else(
2258 || "000000".to_owned(),
2259 |meta| format_mode(mode_from_metadata(meta)),
2260 );
2261 result.push(DiffEntry {
2262 status: DiffStatus::Unmerged,
2263 old_path: Some(path.clone()),
2264 new_path: Some(path.clone()),
2265 old_mode: "000000".to_owned(),
2266 new_mode,
2267 old_oid: zero_oid(),
2268 new_oid: zero_oid(),
2269 score: None,
2270 });
2271
2272 if let Some(meta) = wt_meta {
2273 let file_attrs = crlf::get_file_attrs(&attrs, &path, false, &config);
2274 let wt_oid = hash_worktree_file(
2275 odb,
2276 &file_path,
2277 &meta,
2278 &conv,
2279 &file_attrs,
2280 &path,
2281 Some(base_entry),
2282 )?;
2283 let wt_mode = mode_from_metadata(&meta);
2284 if wt_oid != base_entry.oid || wt_mode != base_entry.mode {
2285 result.push(DiffEntry {
2286 status: DiffStatus::Modified,
2287 old_path: Some(path.clone()),
2288 new_path: Some(path),
2289 old_mode: format_mode(base_entry.mode),
2290 new_mode: format_mode(wt_mode),
2291 old_oid: base_entry.oid,
2292 new_oid: wt_oid,
2293 score: None,
2294 });
2295 }
2296 }
2297 }
2298
2299 Ok(result)
2300}
2301
2302fn entry_is_racy(ie: &IndexEntry, index_mtime: Option<(u32, u32)>) -> bool {
2303 let Some((index_mtime_sec, index_mtime_nsec)) = index_mtime else {
2304 return false;
2305 };
2306 if index_mtime_sec == 0 {
2307 return false;
2308 }
2309 index_mtime_sec < ie.mtime_sec
2310 || (index_mtime_sec == ie.mtime_sec && index_mtime_nsec <= ie.mtime_nsec)
2311}
2312
2313pub fn worktree_differs_from_index_entry(
2321 odb: &Odb,
2322 work_tree: &Path,
2323 ie: &IndexEntry,
2324 ignore_submodule_untracked: bool,
2325) -> Result<bool> {
2326 use crate::config::ConfigSet;
2327 use crate::crlf;
2328
2329 let path_str_ref = std::str::from_utf8(&ie.path).unwrap_or("");
2330 let file_path = work_tree.join(path_str_ref);
2331
2332 if ie.mode == 0o160000 {
2333 let sub_head_oid = read_submodule_head(&file_path);
2334 let ref_matches = match sub_head_oid {
2335 Some(oid) => oid == ie.oid,
2336 None => submodule_worktree_is_unpopulated_placeholder(&file_path),
2337 };
2338 let mut flags = submodule_porcelain_flags(work_tree, path_str_ref, ie.oid);
2339 if ignore_submodule_untracked {
2340 flags.untracked = false;
2341 }
2342 return Ok(!ref_matches || flags.modified || flags.untracked);
2343 }
2344
2345 let meta = match fs::symlink_metadata(&file_path) {
2346 Ok(m) => m,
2347 Err(e)
2348 if e.kind() == std::io::ErrorKind::NotFound
2349 || e.raw_os_error() == Some(20) =>
2350 {
2351 return Ok(true);
2352 }
2353 Err(e) => return Err(Error::Io(e)),
2354 };
2355
2356 if meta.is_dir() {
2357 return Ok(true);
2358 }
2359
2360 let worktree_mode = mode_from_metadata(&meta);
2361 if worktree_mode != ie.mode {
2362 return Ok(true);
2363 }
2364
2365 let git_dir = work_tree.join(".git");
2366 let config = ConfigSet::load(Some(&git_dir), true).unwrap_or_else(|_| ConfigSet::new());
2367 let conv = crlf::ConversionConfig::from_config(&config);
2368 let attrs = crlf::load_gitattributes(work_tree);
2369 let file_attrs = crlf::get_file_attrs(&attrs, path_str_ref, false, &config);
2370 let worktree_oid = hash_worktree_file(
2371 odb,
2372 &file_path,
2373 &meta,
2374 &conv,
2375 &file_attrs,
2376 path_str_ref,
2377 Some(ie),
2378 )?;
2379
2380 let mut eff_oid = worktree_oid;
2381 if eff_oid != ie.oid {
2382 if let Ok(raw) = fs::read(&file_path) {
2383 let raw_oid = Odb::hash_object_data(ObjectKind::Blob, &raw);
2384 if raw_oid == ie.oid {
2385 eff_oid = ie.oid;
2386 }
2387 }
2388 }
2389
2390 Ok(eff_oid != ie.oid)
2391}
2392
2393pub fn stat_matches(ie: &IndexEntry, meta: &fs::Metadata) -> bool {
2394 if meta.len() as u32 != ie.size {
2396 return false;
2397 }
2398 #[cfg(unix)]
2399 {
2400 use std::os::unix::fs::MetadataExt;
2401 if meta.mtime() as u32 != ie.mtime_sec {
2403 return false;
2404 }
2405 if meta.mtime_nsec() as u32 != ie.mtime_nsec {
2406 return false;
2407 }
2408 if meta.ctime() as u32 != ie.ctime_sec {
2410 return false;
2411 }
2412 if meta.ctime_nsec() as u32 != ie.ctime_nsec {
2413 return false;
2414 }
2415 if meta.ino() as u32 != ie.ino {
2417 return false;
2418 }
2419 if meta.dev() as u32 != ie.dev {
2420 return false;
2421 }
2422 }
2423 #[cfg(not(unix))]
2424 {
2425 use std::time::UNIX_EPOCH;
2426 if let Ok(mtime) = meta.modified() {
2427 if let Ok(dur) = mtime.duration_since(UNIX_EPOCH) {
2428 if dur.as_secs() as u32 != ie.mtime_sec {
2429 return false;
2430 }
2431 if dur.subsec_nanos() != ie.mtime_nsec {
2432 return false;
2433 }
2434 }
2435 }
2436 }
2437 true
2438}
2439
2440pub fn refresh_index_stat_content_verified(
2460 index: &mut Index,
2461 work_tree: &Path,
2462 index_mtime: Option<(u32, u32)>,
2463) -> bool {
2464 use crate::index::{MODE_EXECUTABLE, MODE_REGULAR, MODE_SYMLINK};
2465 let mut changed = false;
2466 for ie in &mut index.entries {
2467 if ie.stage() != 0 || ie.skip_worktree() || ie.assume_unchanged() || ie.intent_to_add() {
2468 continue;
2469 }
2470 if ie.mode != MODE_REGULAR && ie.mode != MODE_EXECUTABLE && ie.mode != MODE_SYMLINK {
2471 continue;
2472 }
2473 let Ok(path) = std::str::from_utf8(&ie.path) else {
2474 continue;
2475 };
2476 let abs = work_tree.join(path);
2477 let Ok(meta) = fs::symlink_metadata(&abs) else {
2478 continue;
2479 };
2480 if stat_matches(ie, &meta) {
2481 if entry_is_racy(ie, index_mtime)
2487 && !worktree_content_matches_index_oid(ie, &abs, &meta)
2488 {
2489 invalidate_index_stat_cache(ie);
2490 changed = true;
2491 }
2492 continue;
2493 }
2494 if !worktree_content_matches_index_oid(ie, &abs, &meta) {
2495 continue;
2496 }
2497 let refreshed = crate::index::entry_from_metadata(&meta, &ie.path, ie.oid, ie.mode);
2498 ie.ctime_sec = refreshed.ctime_sec;
2499 ie.ctime_nsec = refreshed.ctime_nsec;
2500 ie.mtime_sec = refreshed.mtime_sec;
2501 ie.mtime_nsec = refreshed.mtime_nsec;
2502 ie.dev = refreshed.dev;
2503 ie.ino = refreshed.ino;
2504 ie.uid = refreshed.uid;
2505 ie.gid = refreshed.gid;
2506 ie.size = refreshed.size;
2507 changed = true;
2508 }
2509 changed
2510}
2511
2512fn symlink_target_bytes(target: &Path) -> Vec<u8> {
2518 #[cfg(unix)]
2519 {
2520 use std::os::unix::ffi::OsStrExt as _;
2521 target.as_os_str().as_bytes().to_vec()
2522 }
2523 #[cfg(not(unix))]
2524 {
2525 target.as_os_str().to_string_lossy().as_bytes().to_vec()
2526 }
2527}
2528
2529fn worktree_content_matches_index_oid(ie: &IndexEntry, abs: &Path, meta: &fs::Metadata) -> bool {
2531 use crate::index::{MODE_EXECUTABLE, MODE_REGULAR, MODE_SYMLINK};
2532 if ie.mode == MODE_SYMLINK {
2533 if !meta.file_type().is_symlink() {
2534 return false;
2535 }
2536 fs::read_link(abs)
2537 .map(|t| Odb::hash_object_data(ObjectKind::Blob, &symlink_target_bytes(&t)) == ie.oid)
2538 .unwrap_or(false)
2539 } else if ie.mode == MODE_REGULAR || ie.mode == MODE_EXECUTABLE {
2540 if !meta.file_type().is_file() {
2541 return false;
2542 }
2543 fs::read(abs)
2544 .map(|bytes| Odb::hash_object_data(ObjectKind::Blob, &bytes) == ie.oid)
2545 .unwrap_or(false)
2546 } else {
2547 false
2548 }
2549}
2550
2551fn invalidate_index_stat_cache(ie: &mut IndexEntry) {
2553 ie.ctime_sec = 0;
2554 ie.ctime_nsec = 0;
2555 ie.mtime_sec = 0;
2556 ie.mtime_nsec = 0;
2557 ie.dev = 0;
2558 ie.ino = 0;
2559 ie.size = 0;
2560}
2561
2562fn has_symlink_in_path(work_tree: &Path, rel_path: &str) -> bool {
2565 let mut check = work_tree.to_path_buf();
2566 let components: Vec<&str> = rel_path.split('/').collect();
2567 for component in &components[..components.len().saturating_sub(1)] {
2569 check.push(component);
2570 match fs::symlink_metadata(&check) {
2571 Ok(meta) if meta.file_type().is_symlink() => return true,
2572 _ => {}
2573 }
2574 }
2575 false
2576}
2577
2578pub fn hash_worktree_file(
2579 odb: &Odb,
2580 path: &Path,
2581 meta: &fs::Metadata,
2582 conv: &crate::crlf::ConversionConfig,
2583 file_attrs: &crate::crlf::FileAttrs,
2584 rel_path: &str,
2585 index_entry: Option<&IndexEntry>,
2586) -> Result<ObjectId> {
2587 let prior_blob: Option<Vec<u8>> = index_entry
2588 .filter(|e| e.oid != zero_oid())
2589 .and_then(|e| odb.read(&e.oid).ok().map(|o| o.data));
2590 let data = if meta.file_type().is_symlink() {
2591 let target = fs::read_link(path)?;
2593 target.to_string_lossy().into_owned().into_bytes()
2594 } else if meta.is_dir() {
2595 Vec::new()
2598 } else {
2599 let raw = fs::read(path)?;
2600 let opts = crate::crlf::ConvertToGitOpts {
2603 index_blob: prior_blob.as_deref(),
2604 renormalize: false,
2605 check_safecrlf: false,
2606 };
2607 crate::crlf::convert_to_git_with_opts(&raw, rel_path, conv, file_attrs, opts).unwrap_or(raw)
2608 };
2609
2610 Ok(Odb::hash_object_data(ObjectKind::Blob, &data))
2611}
2612
2613pub fn mode_from_metadata(meta: &fs::Metadata) -> u32 {
2615 if meta.file_type().is_symlink() {
2616 0o120000
2617 } else {
2618 #[cfg(unix)]
2619 {
2620 if meta.mode() & 0o111 != 0 {
2621 return 0o100755;
2622 }
2623 }
2624 0o100644
2625 }
2626}
2627
2628pub fn diff_tree_to_worktree(
2645 odb: &Odb,
2646 tree_oid: Option<&ObjectId>,
2647 work_tree: &Path,
2648 index: &Index,
2649) -> Result<Vec<DiffEntry>> {
2650 use crate::config::ConfigSet;
2651 use crate::crlf;
2652
2653 let git_dir = work_tree.join(".git");
2654 let config = ConfigSet::load(Some(&git_dir), true).unwrap_or_else(|_| ConfigSet::new());
2655 let conv = crlf::ConversionConfig::from_config(&config);
2656 let attrs = crlf::load_gitattributes(work_tree);
2657
2658 let tree_flat = match tree_oid {
2660 Some(oid) => flatten_tree(odb, oid, "")?,
2661 None => Vec::new(),
2662 };
2663 let tree_map: std::collections::BTreeMap<String, &FlatEntry> =
2664 tree_flat.iter().map(|e| (e.path.clone(), e)).collect();
2665
2666 let mut index_entries: std::collections::BTreeMap<&[u8], &IndexEntry> =
2668 std::collections::BTreeMap::new();
2669 let mut index_paths: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
2670 let mut stage0_paths: std::collections::BTreeSet<Vec<u8>> = std::collections::BTreeSet::new();
2671 for ie in &index.entries {
2672 if ie.stage() != 0 {
2673 continue;
2674 }
2675 let path = String::from_utf8_lossy(&ie.path).to_string();
2676 index_entries.insert(&ie.path, ie);
2677 index_paths.insert(path);
2678 stage0_paths.insert(ie.path.clone());
2679 }
2680
2681 let mut unmerged_only_paths: std::collections::BTreeSet<String> =
2684 std::collections::BTreeSet::new();
2685 for ie in &index.entries {
2686 if !(1..=3).contains(&ie.stage()) {
2687 continue;
2688 }
2689 if stage0_paths.contains(&ie.path) {
2690 continue;
2691 }
2692 unmerged_only_paths.insert(String::from_utf8_lossy(&ie.path).into_owned());
2693 }
2694
2695 let mut all_paths: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
2697 all_paths.extend(tree_map.keys().cloned());
2698 all_paths.extend(index_paths.iter().cloned());
2699 all_paths.extend(unmerged_only_paths.iter().cloned());
2700
2701 let mut result = Vec::new();
2702
2703 for path in &all_paths {
2704 if index_entries
2705 .get(path.as_bytes())
2706 .is_some_and(|ie| ie.skip_worktree())
2707 {
2708 continue;
2711 }
2712
2713 let tree_entry = tree_map.get(path.as_str());
2714
2715 let is_gitlink = tree_entry.is_some_and(|te| te.mode == 0o160000)
2717 || index_entries
2718 .get(path.as_bytes())
2719 .is_some_and(|ie| ie.mode == 0o160000);
2720 if is_gitlink {
2721 let sub_dir = work_tree.join(path);
2722 let index_gitlink_oid = index_entries
2723 .get(path.as_bytes())
2724 .filter(|ie| ie.mode == 0o160000)
2725 .map(|ie| ie.oid);
2726 match (tree_entry, index_gitlink_oid) {
2727 (Some(te), _) => {
2728 let sub_head = read_submodule_head_oid(&sub_dir);
2729 if sub_head.is_none() && !sub_dir.exists() {
2733 result.push(DiffEntry {
2734 status: DiffStatus::Deleted,
2735 old_path: Some(path.clone()),
2736 new_path: Some(path.clone()),
2737 old_mode: format_mode(te.mode),
2738 new_mode: "000000".to_string(),
2739 old_oid: te.oid,
2740 new_oid: zero_oid(),
2741 score: None,
2742 });
2743 continue;
2744 }
2745 let index_matches_tree = index_gitlink_oid.is_some_and(|oid| oid == te.oid);
2746 let head_differs = sub_head.as_ref() != Some(&te.oid);
2747 let dirty_while_aligned = index_matches_tree
2748 && !head_differs
2749 && submodule_has_dirty_worktree_for_super_diff(work_tree, path, &te.oid);
2750 if head_differs || dirty_while_aligned {
2751 let new_oid = if head_differs { zero_oid() } else { te.oid };
2755 result.push(DiffEntry {
2756 status: DiffStatus::Modified,
2757 old_path: Some(path.clone()),
2758 new_path: Some(path.clone()),
2759 old_mode: format_mode(te.mode),
2760 new_mode: format_mode(te.mode),
2761 old_oid: te.oid,
2762 new_oid,
2763 score: None,
2764 });
2765 }
2766 }
2767 (None, Some(idx_oid)) => {
2768 let new_oid = read_submodule_head_oid(&sub_dir).unwrap_or(idx_oid);
2773 result.push(DiffEntry {
2774 status: DiffStatus::Added,
2775 old_path: Some(path.clone()),
2776 new_path: Some(path.clone()),
2777 old_mode: "000000".to_string(),
2778 new_mode: format_mode(0o160000),
2779 old_oid: zero_oid(),
2780 new_oid,
2781 score: None,
2782 });
2783 }
2784 (None, None) => {}
2785 }
2786 continue;
2787 }
2788
2789 let file_path = work_tree.join(path);
2790
2791 let wt_meta = match fs::symlink_metadata(&file_path) {
2792 Ok(m) => Some(m),
2793 Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
2794 Err(e) => return Err(Error::Io(e)),
2795 };
2796
2797 if unmerged_only_paths.contains(path) {
2798 if let (Some(te), Some(meta)) = (tree_entry, wt_meta.as_ref()) {
2799 let file_attrs = crlf::get_file_attrs(&attrs, path, false, &config);
2800 let wt_oid =
2801 hash_worktree_file(odb, &file_path, meta, &conv, &file_attrs, path, None)?;
2802 let wt_mode = mode_from_metadata(meta);
2803 if wt_oid != te.oid || wt_mode != te.mode {
2804 result.push(DiffEntry {
2805 status: DiffStatus::Modified,
2806 old_path: Some(path.clone()),
2807 new_path: Some(path.clone()),
2808 old_mode: format_mode(te.mode),
2809 new_mode: format_mode(wt_mode),
2810 old_oid: te.oid,
2811 new_oid: wt_oid,
2812 score: None,
2813 });
2814 }
2815 }
2816 continue;
2817 }
2818
2819 match (tree_entry, wt_meta) {
2820 (Some(te), Some(ref meta)) => {
2821 let wt_mode = mode_from_metadata(meta);
2822 let Some(ie) = index_entries.get(path.as_bytes()) else {
2823 continue;
2824 };
2825
2826 let index_matches_tree = ie.oid == te.oid && ie.mode == te.mode;
2827
2828 if index_matches_tree && wt_mode == te.mode && stat_matches(ie, meta) {
2830 continue;
2831 }
2832
2833 let file_attrs = crlf::get_file_attrs(&attrs, path, false, &config);
2834 let idx_ent = index_entries.get(path.as_bytes()).copied();
2835
2836 if ie.oid == te.oid && ie.mode != te.mode {
2838 result.push(DiffEntry {
2839 status: DiffStatus::Modified,
2840 old_path: Some(path.clone()),
2841 new_path: Some(path.clone()),
2842 old_mode: format_mode(te.mode),
2843 new_mode: format_mode(ie.mode),
2844 old_oid: te.oid,
2845 new_oid: te.oid,
2846 score: None,
2847 });
2848 continue;
2849 }
2850
2851 if index_matches_tree {
2854 let wt_oid = hash_worktree_file(
2855 odb,
2856 &file_path,
2857 meta,
2858 &conv,
2859 &file_attrs,
2860 path,
2861 idx_ent,
2862 )?;
2863 let mut eff_oid = wt_oid;
2864 if eff_oid != te.oid {
2865 if let Ok(raw) = fs::read(&file_path) {
2866 let raw_oid = Odb::hash_object_data(ObjectKind::Blob, &raw);
2867 if raw_oid == te.oid {
2868 eff_oid = te.oid;
2869 }
2870 }
2871 }
2872 if eff_oid != te.oid {
2873 result.push(DiffEntry {
2874 status: DiffStatus::Modified,
2875 old_path: Some(path.clone()),
2876 new_path: Some(path.clone()),
2877 old_mode: format_mode(te.mode),
2878 new_mode: format_mode(wt_mode),
2879 old_oid: te.oid,
2880 new_oid: eff_oid,
2881 score: None,
2882 });
2883 } else if wt_mode != te.mode {
2884 result.push(DiffEntry {
2885 status: DiffStatus::Modified,
2886 old_path: Some(path.clone()),
2887 new_path: Some(path.clone()),
2888 old_mode: format_mode(te.mode),
2889 new_mode: format_mode(wt_mode),
2890 old_oid: te.oid,
2891 new_oid: te.oid,
2892 score: None,
2893 });
2894 }
2895 continue;
2896 }
2897
2898 let wt_oid =
2900 hash_worktree_file(odb, &file_path, meta, &conv, &file_attrs, path, idx_ent)?;
2901 let mut eff_oid = wt_oid;
2902 if eff_oid != te.oid {
2903 if let Ok(raw) = fs::read(&file_path) {
2904 let raw_oid = Odb::hash_object_data(ObjectKind::Blob, &raw);
2905 if raw_oid == te.oid {
2906 eff_oid = te.oid;
2907 }
2908 }
2909 }
2910 if eff_oid != te.oid || wt_mode != te.mode {
2911 result.push(DiffEntry {
2912 status: DiffStatus::Modified,
2913 old_path: Some(path.clone()),
2914 new_path: Some(path.clone()),
2915 old_mode: format_mode(te.mode),
2916 new_mode: format_mode(wt_mode),
2917 old_oid: te.oid,
2918 new_oid: eff_oid,
2919 score: None,
2920 });
2921 }
2922 }
2923 (Some(te), None) => {
2924 result.push(DiffEntry {
2926 status: DiffStatus::Deleted,
2927 old_path: Some(path.clone()),
2928 new_path: None,
2929 old_mode: format_mode(te.mode),
2930 new_mode: "000000".to_owned(),
2931 old_oid: te.oid,
2932 new_oid: zero_oid(),
2933 score: None,
2934 });
2935 }
2936 (None, Some(ref meta)) => {
2937 let file_attrs = crlf::get_file_attrs(&attrs, path, false, &config);
2939 let wt_oid = hash_worktree_file(
2940 odb,
2941 &file_path,
2942 meta,
2943 &conv,
2944 &file_attrs,
2945 path,
2946 index_entries.get(path.as_bytes()).copied(),
2947 )?;
2948 let wt_mode = mode_from_metadata(meta);
2949 result.push(DiffEntry {
2950 status: DiffStatus::Added,
2951 old_path: None,
2952 new_path: Some(path.clone()),
2953 old_mode: "000000".to_owned(),
2954 new_mode: format_mode(wt_mode),
2955 old_oid: zero_oid(),
2956 new_oid: wt_oid,
2957 score: None,
2958 });
2959 }
2960 (None, None) => {
2961 }
2963 }
2964 }
2965
2966 result.sort_by(|a, b| a.path().cmp(b.path()));
2967 Ok(result)
2968}
2969
2970fn read_added_entry_bytes(
2973 odb: &Odb,
2974 entry: &DiffEntry,
2975 work_root: Option<&Path>,
2976) -> Option<Vec<u8>> {
2977 if entry.new_oid != zero_oid() {
2978 return odb.read(&entry.new_oid).ok().map(|obj| obj.data);
2979 }
2980 let path = entry.new_path.as_deref()?;
2981 let root = work_root?;
2982 fs::read(root.join(path)).ok()
2983}
2984
2985fn modified_as_copy_from_sources(
2986 odb: &Odb,
2987 work_root: Option<&Path>,
2988 e: &DiffEntry,
2989 threshold: u32,
2990 sources: &[(String, ObjectId, bool)],
2991 source_contents: &[Option<Vec<u8>>],
2992 source_tree_entries: &[(String, String, ObjectId)],
2993) -> Option<DiffEntry> {
2994 fn regular_file_mode(mode: &str) -> bool {
2995 mode == "100644" || mode == "100755"
2996 }
2997
2998 if e.status != DiffStatus::Modified || !regular_file_mode(&e.new_mode) {
2999 return None;
3000 }
3001 let new_data = read_added_entry_bytes(odb, e, work_root)?;
3002 let new_oid_eff = if e.new_oid != zero_oid() {
3003 e.new_oid
3004 } else {
3005 Odb::hash_object_data(ObjectKind::Blob, &new_data)
3006 };
3007
3008 let mut best: Option<(usize, u32)> = None;
3009 for (si, (src_path, src_oid, is_deleted)) in sources.iter().enumerate() {
3010 if *is_deleted {
3011 continue;
3012 }
3013 if e.new_path.as_deref() == Some(src_path.as_str()) {
3014 continue;
3015 }
3016 let src_mode_str = source_tree_entries
3017 .iter()
3018 .find(|(p, _, _)| p == src_path)
3019 .map(|(_, m, _)| m.as_str())
3020 .unwrap_or("100644");
3021 if !regular_file_mode(src_mode_str) {
3022 continue;
3023 }
3024
3025 let score = if *src_oid == new_oid_eff {
3026 100
3027 } else {
3028 match (&source_contents[si], Some(new_data.as_slice())) {
3029 (Some(old_data), Some(nd)) => compute_similarity(old_data, nd),
3030 _ => 0,
3031 }
3032 };
3033 if score >= threshold {
3034 let replace = match best {
3035 None => true,
3036 Some((_, s)) => score > s,
3037 };
3038 if replace {
3039 best = Some((si, score));
3040 }
3041 }
3042 }
3043
3044 let (si, score) = best?;
3045 let (src_path, src_oid, _) = &sources[si];
3046 let src_mode = source_tree_entries
3047 .iter()
3048 .find(|(p, _, _)| p == src_path)
3049 .map(|(_, m, _)| m.clone())
3050 .unwrap_or_else(|| e.old_mode.clone());
3051
3052 Some(DiffEntry {
3053 status: DiffStatus::Copied,
3054 old_path: Some(src_path.clone()),
3055 new_path: e.new_path.clone(),
3056 old_mode: src_mode,
3057 new_mode: e.new_mode.clone(),
3058 old_oid: *src_oid,
3059 new_oid: e.new_oid,
3060 score: Some(score),
3061 })
3062}
3063
3064pub fn detect_renames(
3076 odb: &Odb,
3077 work_root: Option<&Path>,
3078 entries: Vec<DiffEntry>,
3079 threshold: u32,
3080) -> Vec<DiffEntry> {
3081 let mut deleted: Vec<DiffEntry> = Vec::new();
3083 let mut added: Vec<DiffEntry> = Vec::new();
3084 let mut others: Vec<DiffEntry> = Vec::new();
3085
3086 for entry in entries {
3087 match entry.status {
3088 DiffStatus::Deleted => deleted.push(entry),
3089 DiffStatus::Added => added.push(entry),
3090 _ => others.push(entry),
3091 }
3092 }
3093
3094 if deleted.is_empty() || added.is_empty() {
3095 let mut result = others;
3097 result.extend(deleted);
3098 result.extend(added);
3099 result.sort_by(|a, b| a.path().cmp(b.path()));
3100 return result;
3101 }
3102
3103 let deleted_contents: Vec<Option<Vec<u8>>> = deleted
3105 .iter()
3106 .map(|d| odb.read(&d.old_oid).ok().map(|obj| obj.data))
3107 .collect();
3108
3109 let added_contents: Vec<Option<Vec<u8>>> = added
3111 .iter()
3112 .map(|a| read_added_entry_bytes(odb, a, work_root))
3113 .collect();
3114
3115 let mut scores: Vec<(u32, usize, usize)> = Vec::new();
3118
3119 fn is_regularish_mode(mode: &str) -> bool {
3120 mode == "100644" || mode == "100755"
3121 }
3122
3123 fn same_path_same_blob(del: &DiffEntry, add: &DiffEntry) -> bool {
3124 del.old_path == add.new_path && del.old_oid == add.new_oid && del.old_mode == add.new_mode
3125 }
3126
3127 for (di, del) in deleted.iter().enumerate() {
3128 for (ai, add) in added.iter().enumerate() {
3129 if del.old_oid == add.new_oid {
3131 scores.push((100, di, ai));
3132 continue;
3133 }
3134
3135 if !is_regularish_mode(&del.old_mode) || !is_regularish_mode(&add.new_mode) {
3138 continue;
3139 }
3140
3141 let score = match (&deleted_contents[di], &added_contents[ai]) {
3142 (Some(old_data), Some(new_data)) => compute_similarity(old_data, new_data),
3143 _ => 0,
3144 };
3145
3146 if score >= threshold {
3147 scores.push((score, di, ai));
3148 }
3149 }
3150 }
3151
3152 scores.sort_by(|a, b| {
3156 let a_noop = same_path_same_blob(&deleted[a.1], &added[a.2]);
3157 let b_noop = same_path_same_blob(&deleted[b.1], &added[b.2]);
3158 let a_same = same_basename(&deleted[a.1], &added[a.2]);
3159 let b_same = same_basename(&deleted[b.1], &added[b.2]);
3160 a_noop
3161 .cmp(&b_noop)
3162 .then_with(|| b_same.cmp(&a_same))
3163 .then_with(|| b.0.cmp(&a.0))
3164 });
3165
3166 let mut used_deleted = vec![false; deleted.len()];
3167 let mut used_added = vec![false; added.len()];
3168 let mut renames: Vec<DiffEntry> = Vec::new();
3169
3170 for (score, di, ai) in &scores {
3171 if used_deleted[*di] || used_added[*ai] {
3172 continue;
3173 }
3174 used_deleted[*di] = true;
3175 used_added[*ai] = true;
3176
3177 let del = &deleted[*di];
3178 let add = &added[*ai];
3179
3180 if same_path_same_blob(del, add) {
3185 continue;
3186 }
3187
3188 renames.push(DiffEntry {
3189 status: DiffStatus::Renamed,
3190 old_path: del.old_path.clone(),
3191 new_path: add.new_path.clone(),
3192 old_mode: del.old_mode.clone(),
3193 new_mode: add.new_mode.clone(),
3194 old_oid: del.old_oid,
3195 new_oid: add.new_oid,
3196 score: Some(*score),
3197 });
3198 }
3199
3200 let mut result = others;
3202 result.extend(renames);
3203 for (i, entry) in deleted.into_iter().enumerate() {
3204 if !used_deleted[i] {
3205 result.push(entry);
3206 }
3207 }
3208 for (i, entry) in added.into_iter().enumerate() {
3209 if !used_added[i] {
3210 result.push(entry);
3211 }
3212 }
3213
3214 result.sort_by(|a, b| a.path().cmp(b.path()));
3215 result
3216}
3217
3218pub fn detect_copies(
3229 odb: &Odb,
3230 work_root: Option<&Path>,
3231 entries: Vec<DiffEntry>,
3232 threshold: u32,
3233 find_copies_harder: bool,
3234 source_tree_entries: &[(String, String, ObjectId)],
3235) -> Vec<DiffEntry> {
3236 use std::collections::{HashMap, HashSet};
3237
3238 let mut deleted: Vec<DiffEntry> = Vec::new();
3240 let mut added: Vec<DiffEntry> = Vec::new();
3241 let mut others: Vec<DiffEntry> = Vec::new();
3242
3243 for entry in entries {
3244 match entry.status {
3245 DiffStatus::Deleted => deleted.push(entry),
3246 DiffStatus::Added => added.push(entry),
3247 _ => others.push(entry),
3248 }
3249 }
3250
3251 let mut sources: Vec<(String, ObjectId, bool)> = Vec::new(); let mut deleted_source_idx: HashMap<String, usize> = HashMap::new();
3255
3256 for entry in &deleted {
3257 if let Some(ref path) = entry.old_path {
3258 deleted_source_idx.insert(path.clone(), sources.len());
3259 sources.push((path.clone(), entry.old_oid, true));
3260 }
3261 }
3262
3263 for entry in &others {
3266 if matches!(entry.status, DiffStatus::Modified | DiffStatus::TypeChanged) {
3267 if let Some(ref old_path) = entry.old_path {
3268 if !sources.iter().any(|(p, _, _)| p == old_path) {
3269 sources.push((old_path.clone(), entry.old_oid, false));
3270 }
3271 }
3272 }
3273 }
3274
3275 if find_copies_harder {
3277 for (path, _mode, oid) in source_tree_entries {
3278 if !sources.iter().any(|(p, _, _)| p == path) {
3279 sources.push((path.clone(), *oid, false));
3280 }
3281 }
3282 }
3283
3284 if sources.is_empty() {
3285 let mut result = others;
3286 result.extend(deleted);
3287 result.extend(added);
3288 result.sort_by(|a, b| a.path().cmp(b.path()));
3289 return result;
3290 }
3291
3292 let source_contents: Vec<Option<Vec<u8>>> = sources
3294 .iter()
3295 .map(|(_, oid, _)| odb.read(oid).ok().map(|obj| obj.data))
3296 .collect();
3297
3298 let mut result_entries: Vec<DiffEntry> = Vec::new();
3299 let mut renamed_deleted: HashSet<usize> = HashSet::new();
3300 let mut used_added2 = vec![false; added.len()];
3301
3302 if !added.is_empty() {
3303 let added_contents: Vec<Option<Vec<u8>>> = added
3305 .iter()
3306 .map(|a| read_added_entry_bytes(odb, a, work_root))
3307 .collect();
3308
3309 let mut scores: Vec<(u32, usize, usize)> = Vec::new();
3311 for (si, (src_path, src_oid, _)) in sources.iter().enumerate() {
3312 for (ai, add) in added.iter().enumerate() {
3313 if add.new_path.as_deref() == Some(src_path.as_str()) {
3316 continue;
3317 }
3318 let add_oid = if add.new_oid != zero_oid() {
3319 add.new_oid
3320 } else if let Some(ref data) = added_contents[ai] {
3321 Odb::hash_object_data(ObjectKind::Blob, data)
3322 } else {
3323 zero_oid()
3324 };
3325 if *src_oid == add_oid {
3326 scores.push((100, si, ai));
3327 continue;
3328 }
3329 let score = match (&source_contents[si], &added_contents[ai]) {
3330 (Some(old_data), Some(new_data)) => compute_similarity(old_data, new_data),
3331 _ => 0,
3332 };
3333 if score >= threshold {
3334 scores.push((score, si, ai));
3335 }
3336 }
3337 }
3338
3339 scores.sort_by(|a, b| b.0.cmp(&a.0));
3341
3342 let mut used_added = vec![false; added.len()];
3344 let mut source_to_added: HashMap<usize, Vec<(usize, u32)>> = HashMap::new();
3345 for &(score, si, ai) in &scores {
3346 if used_added[ai] {
3347 continue;
3348 }
3349 used_added[ai] = true;
3350 source_to_added.entry(si).or_default().push((ai, score));
3351 }
3352
3353 for (&si, assignments_for_src) in &source_to_added {
3355 let (_, _, is_deleted) = &sources[si];
3356 if *is_deleted && !assignments_for_src.is_empty() {
3357 let rename_ai = assignments_for_src
3360 .iter()
3361 .max_by_key(|(ai, _score)| added[*ai].path().to_string())
3362 .map(|(ai, _)| *ai);
3363
3364 for &(ai, score) in assignments_for_src {
3365 let (ref src_path, _, _) = sources[si];
3366 let add = &added[ai];
3367 let src_mode = source_tree_entries
3368 .iter()
3369 .find(|(p, _, _)| p == src_path)
3370 .map(|(_, m, _)| m.clone())
3371 .unwrap_or_else(|| add.old_mode.clone());
3372
3373 let is_rename = Some(ai) == rename_ai;
3374 result_entries.push(DiffEntry {
3375 status: if is_rename {
3376 DiffStatus::Renamed
3377 } else {
3378 DiffStatus::Copied
3379 },
3380 old_path: Some(src_path.clone()),
3381 new_path: add.new_path.clone(),
3382 old_mode: src_mode,
3383 new_mode: add.new_mode.clone(),
3384 old_oid: sources[si].1,
3385 new_oid: add.new_oid,
3386 score: Some(score),
3387 });
3388 used_added2[ai] = true;
3389 }
3390 renamed_deleted.insert(si);
3391 } else {
3392 for &(ai, score) in assignments_for_src {
3394 let (ref src_path, _, _) = sources[si];
3395 let add = &added[ai];
3396 let src_mode = source_tree_entries
3397 .iter()
3398 .find(|(p, _, _)| p == src_path)
3399 .map(|(_, m, _)| m.clone())
3400 .unwrap_or_else(|| add.old_mode.clone());
3401
3402 result_entries.push(DiffEntry {
3403 status: DiffStatus::Copied,
3404 old_path: Some(src_path.clone()),
3405 new_path: add.new_path.clone(),
3406 old_mode: src_mode,
3407 new_mode: add.new_mode.clone(),
3408 old_oid: sources[si].1,
3409 new_oid: add.new_oid,
3410 score: Some(score),
3411 });
3412 used_added2[ai] = true;
3413 }
3414 }
3415 }
3416 }
3417
3418 for entry in deleted.into_iter() {
3420 if let Some(ref path) = entry.old_path {
3421 if let Some(&si) = deleted_source_idx.get(path) {
3422 if renamed_deleted.contains(&si) {
3423 continue;
3425 }
3426 }
3427 }
3428 result_entries.push(entry);
3429 }
3430
3431 let mut result = others;
3432 result.extend(result_entries);
3433 for (i, entry) in added.into_iter().enumerate() {
3435 if !used_added2[i] {
3436 result.push(entry);
3437 }
3438 }
3439
3440 let mut final_result = Vec::with_capacity(result.len());
3441 for e in result {
3442 if let Some(c) = modified_as_copy_from_sources(
3443 odb,
3444 work_root,
3445 &e,
3446 threshold,
3447 &sources,
3448 &source_contents,
3449 source_tree_entries,
3450 ) {
3451 final_result.push(c);
3452 } else {
3453 final_result.push(e);
3454 }
3455 }
3456
3457 final_result.sort_by(|a, b| a.path().cmp(b.path()));
3458 final_result
3459}
3460
3461pub fn status_apply_rename_copy_detection(
3471 odb: &Odb,
3472 unstaged_raw: Vec<DiffEntry>,
3473 threshold: u32,
3474 copies: bool,
3475 head_tree: Option<&ObjectId>,
3476) -> Result<Vec<DiffEntry>> {
3477 if !copies {
3478 return Ok(detect_renames(odb, None, unstaged_raw, threshold));
3479 }
3480 let source_tree_entries: Vec<(String, String, ObjectId)> = match head_tree {
3481 Some(oid) => flatten_tree(odb, oid, "")?
3482 .into_iter()
3483 .map(|e| (e.path, format_mode(e.mode), e.oid))
3484 .collect(),
3485 None => Vec::new(),
3486 };
3487 Ok(detect_copies(
3488 odb,
3489 None,
3490 unstaged_raw,
3491 threshold,
3492 false,
3493 &source_tree_entries,
3494 ))
3495}
3496
3497pub fn format_rename_path(old: &str, new: &str) -> String {
3505 let ob = old.as_bytes();
3506 let nb = new.as_bytes();
3507
3508 let pfx = {
3510 let mut last_sep = 0usize;
3511 let min_len = ob.len().min(nb.len());
3512 for i in 0..min_len {
3513 if ob[i] != nb[i] {
3514 break;
3515 }
3516 if ob[i] == b'/' {
3517 last_sep = i + 1;
3518 }
3519 }
3520 last_sep
3521 };
3522
3523 let mut sfx = {
3525 let mut last_sep = 0usize;
3526 let min_len = ob.len().min(nb.len());
3527 for i in 0..min_len {
3528 let oi = ob.len() - 1 - i;
3529 let ni = nb.len() - 1 - i;
3530 if ob[oi] != nb[ni] {
3531 break;
3532 }
3533 if ob[oi] == b'/' {
3534 last_sep = i + 1;
3535 }
3536 }
3537 last_sep
3538 };
3539
3540 let mut sfx_at_old = ob.len() - sfx;
3542 let mut sfx_at_new = nb.len() - sfx;
3543
3544 while pfx > sfx_at_old && pfx > sfx_at_new && sfx > 0 {
3547 let suffix_bytes = &ob[sfx_at_old..];
3549 let mut new_sfx = 0;
3550 for (i, &b) in suffix_bytes.iter().enumerate().skip(1) {
3552 if b == b'/' {
3553 new_sfx = sfx - i;
3554 break;
3555 }
3556 }
3557 if new_sfx == 0 || new_sfx >= sfx {
3558 sfx_at_old = ob.len();
3559 sfx_at_new = nb.len();
3560 break;
3561 }
3562 sfx = new_sfx;
3563 sfx_at_old = ob.len() - sfx;
3564 sfx_at_new = nb.len() - sfx;
3565 }
3566
3567 let prefix = &old[..pfx];
3574 let suffix = &old[sfx_at_old..];
3575 let old_mid = if pfx <= sfx_at_old {
3576 &old[pfx..sfx_at_old]
3577 } else {
3578 ""
3579 };
3580 let new_mid = if pfx <= sfx_at_new {
3581 &new[pfx..sfx_at_new]
3582 } else {
3583 ""
3584 };
3585
3586 if prefix.is_empty() && suffix.is_empty() {
3587 return format!("{old} => {new}");
3588 }
3589
3590 format!("{prefix}{{{old_mid} => {new_mid}}}{suffix}")
3591}
3592
3593fn same_basename(del: &DiffEntry, add: &DiffEntry) -> bool {
3595 let old = del.old_path.as_deref().unwrap_or("");
3596 let new = add.new_path.as_deref().unwrap_or("");
3597 let old_base = old.rsplit('/').next().unwrap_or(old);
3598 let new_base = new.rsplit('/').next().unwrap_or(new);
3599 old_base == new_base && !old_base.is_empty()
3600}
3601
3602fn compute_similarity(old: &[u8], new: &[u8]) -> u32 {
3607 let old_norm = crate::crlf::crlf_to_lf(old);
3610 let new_norm = crate::crlf::crlf_to_lf(new);
3611
3612 let src_size = old_norm.len();
3613 let dst_size = new_norm.len();
3614
3615 if src_size == 0 && dst_size == 0 {
3616 return 100;
3617 }
3618 let total = src_size + dst_size;
3619 if total == 0 {
3620 return 100;
3621 }
3622
3623 use similar::{ChangeTag, TextDiff};
3625 let old_str = String::from_utf8_lossy(&old_norm);
3626 let new_str = String::from_utf8_lossy(&new_norm);
3627 let diff = TextDiff::from_lines(&old_str as &str, &new_str as &str);
3628
3629 let mut shared_bytes = 0usize;
3630 for change in diff.iter_all_changes() {
3631 if change.tag() == ChangeTag::Equal {
3632 shared_bytes += change.value().len();
3634 }
3635 }
3636
3637 let max_size = src_size.max(dst_size);
3640
3641 ((shared_bytes * 100) / max_size).min(100) as u32
3642}
3643
3644#[must_use]
3648pub fn rename_similarity_score(old: &[u8], new: &[u8]) -> u32 {
3649 compute_similarity(old, new)
3650}
3651
3652pub fn format_raw(entry: &DiffEntry) -> String {
3658 let path = match entry.status {
3659 DiffStatus::Renamed | DiffStatus::Copied => {
3660 format!(
3661 "{}\t{}",
3662 entry.old_path.as_deref().unwrap_or(""),
3663 entry.new_path.as_deref().unwrap_or("")
3664 )
3665 }
3666 _ => entry.path().to_owned(),
3667 };
3668
3669 let status_str = match (entry.status, entry.score) {
3670 (DiffStatus::Renamed, Some(s)) => format!("R{:03}", s),
3671 (DiffStatus::Copied, Some(s)) => format!("C{:03}", s),
3672 _ => entry.status.letter().to_string(),
3673 };
3674
3675 let (old_hex, new_hex) = raw_oid_hex_pair(&entry.old_oid, &entry.new_oid);
3676 format!(
3677 ":{} {} {} {} {}\t{}",
3678 entry.old_mode, entry.new_mode, old_hex, new_hex, status_str, path
3679 )
3680}
3681
3682fn raw_oid_hex_pair(old: &ObjectId, new: &ObjectId) -> (String, String) {
3690 let width = if !old.is_zero() {
3691 old.algo().hex_len()
3692 } else if !new.is_zero() {
3693 new.algo().hex_len()
3694 } else {
3695 old.algo().hex_len()
3696 };
3697 let render = |oid: &ObjectId| {
3698 if oid.is_zero() {
3699 "0".repeat(width)
3700 } else {
3701 oid.to_hex()
3702 }
3703 };
3704 (render(old), render(new))
3705}
3706
3707pub fn format_raw_abbrev(entry: &DiffEntry, abbrev_len: usize) -> String {
3709 let ellipsis = if std::env::var("GIT_PRINT_SHA1_ELLIPSIS").ok().as_deref() == Some("yes") {
3710 "..."
3711 } else {
3712 ""
3713 };
3714 let old_hex = format!("{}", entry.old_oid);
3715 let new_hex = format!("{}", entry.new_oid);
3716 let old_abbrev = &old_hex[..abbrev_len.min(old_hex.len())];
3717 let new_abbrev = &new_hex[..abbrev_len.min(new_hex.len())];
3718
3719 let path = match entry.status {
3721 DiffStatus::Renamed | DiffStatus::Copied => format!(
3722 "{}\t{}",
3723 entry.old_path.as_deref().unwrap_or(""),
3724 entry.new_path.as_deref().unwrap_or("")
3725 ),
3726 _ => entry.path().to_owned(),
3727 };
3728 let status_str = match (entry.status, entry.score) {
3729 (DiffStatus::Renamed, Some(s)) => format!("R{s:03}"),
3730 (DiffStatus::Copied, Some(s)) => format!("C{s:03}"),
3731 _ => entry.status.letter().to_string(),
3732 };
3733
3734 format!(
3735 ":{} {} {}{} {}{} {}\t{}",
3736 entry.old_mode,
3737 entry.new_mode,
3738 old_abbrev,
3739 ellipsis,
3740 new_abbrev,
3741 ellipsis,
3742 status_str,
3743 path
3744 )
3745}
3746
3747pub fn unified_diff(
3762 old_content: &str,
3763 new_content: &str,
3764 old_path: &str,
3765 new_path: &str,
3766 context_lines: usize,
3767 indent_heuristic: bool,
3768 quote_path_fully: bool,
3769) -> String {
3770 unified_diff_with_prefix(
3771 old_content,
3772 new_content,
3773 old_path,
3774 new_path,
3775 context_lines,
3776 0,
3777 "a/",
3778 "b/",
3779 indent_heuristic,
3780 quote_path_fully,
3781 )
3782}
3783
3784#[allow(clippy::too_many_arguments)] pub fn unified_diff_with_prefix(
3790 old_content: &str,
3791 new_content: &str,
3792 old_path: &str,
3793 new_path: &str,
3794 context_lines: usize,
3795 inter_hunk_context: usize,
3796 src_prefix: &str,
3797 dst_prefix: &str,
3798 indent_heuristic: bool,
3799 quote_path_fully: bool,
3800) -> String {
3801 unified_diff_with_prefix_and_funcname(
3802 old_content,
3803 new_content,
3804 old_path,
3805 new_path,
3806 context_lines,
3807 inter_hunk_context,
3808 src_prefix,
3809 dst_prefix,
3810 None,
3811 indent_heuristic,
3812 quote_path_fully,
3813 )
3814}
3815
3816#[allow(clippy::too_many_arguments)]
3819pub fn unified_diff_with_prefix_and_funcname(
3820 old_content: &str,
3821 new_content: &str,
3822 old_path: &str,
3823 new_path: &str,
3824 context_lines: usize,
3825 inter_hunk_context: usize,
3826 src_prefix: &str,
3827 dst_prefix: &str,
3828 funcname_matcher: Option<&FuncnameMatcher>,
3829 indent_heuristic: bool,
3830 quote_path_fully: bool,
3831) -> String {
3832 unified_diff_with_prefix_and_funcname_and_algorithm(
3833 old_content,
3834 new_content,
3835 old_path,
3836 new_path,
3837 context_lines,
3838 inter_hunk_context,
3839 src_prefix,
3840 dst_prefix,
3841 funcname_matcher,
3842 similar::Algorithm::Myers,
3843 false,
3844 false,
3845 indent_heuristic,
3846 quote_path_fully,
3847 )
3848}
3849
3850#[allow(clippy::too_many_arguments)]
3856pub fn unified_diff_with_prefix_and_funcname_and_algorithm(
3857 old_content: &str,
3858 new_content: &str,
3859 old_path: &str,
3860 new_path: &str,
3861 context_lines: usize,
3862 inter_hunk_context: usize,
3863 src_prefix: &str,
3864 dst_prefix: &str,
3865 funcname_matcher: Option<&FuncnameMatcher>,
3866 algorithm: similar::Algorithm,
3867 function_context: bool,
3868 use_git_histogram: bool,
3869 indent_heuristic: bool,
3870 quote_path_fully: bool,
3871) -> String {
3872 if function_context {
3876 return unified_diff_with_function_context(
3877 old_content,
3878 new_content,
3879 old_path,
3880 new_path,
3881 context_lines,
3882 inter_hunk_context,
3883 src_prefix,
3884 dst_prefix,
3885 funcname_matcher,
3886 algorithm,
3887 indent_heuristic,
3888 quote_path_fully,
3889 );
3890 }
3891
3892 if use_git_histogram {
3893 return unified_diff_histogram_with_prefix_and_funcname(
3894 old_content,
3895 new_content,
3896 old_path,
3897 new_path,
3898 context_lines,
3899 inter_hunk_context,
3900 src_prefix,
3901 dst_prefix,
3902 funcname_matcher,
3903 quote_path_fully,
3904 );
3905 }
3906
3907 use crate::quote_path::format_diff_path_with_prefix;
3908 use similar::{udiff::UnifiedDiffHunk, TextDiff};
3909
3910 let diff = TextDiff::configure()
3911 .algorithm(algorithm)
3912 .diff_lines(old_content, new_content);
3913 let compacted_ops = diff_indent_heuristic::diff_lines_ops_compacted(
3914 old_content,
3915 new_content,
3916 algorithm,
3917 indent_heuristic,
3918 );
3919
3920 let mut output = String::new();
3921 if old_path == "/dev/null" {
3922 output.push_str("--- /dev/null\n");
3923 } else if src_prefix.is_empty() {
3924 output.push_str(&format!("--- {old_path}\n"));
3927 } else {
3928 output.push_str("--- ");
3929 output.push_str(&format_diff_path_with_prefix(
3930 src_prefix,
3931 old_path,
3932 quote_path_fully,
3933 ));
3934 output.push('\n');
3935 }
3936 if new_path == "/dev/null" {
3937 output.push_str("+++ /dev/null\n");
3938 } else if dst_prefix.is_empty() {
3939 output.push_str(&format!("+++ {new_path}\n"));
3940 } else {
3941 output.push_str("+++ ");
3942 output.push_str(&format_diff_path_with_prefix(
3943 dst_prefix,
3944 new_path,
3945 quote_path_fully,
3946 ));
3947 output.push('\n');
3948 }
3949
3950 let old_lines: Vec<&str> = old_content.lines().collect();
3951
3952 let max_common_gap = context_lines
3958 .saturating_mul(2)
3959 .saturating_add(inter_hunk_context);
3960 let op_groups = group_diff_ops_gap(compacted_ops, context_lines, max_common_gap);
3961
3962 for ops in op_groups {
3963 if ops.is_empty() {
3964 continue;
3965 }
3966 let hunk = UnifiedDiffHunk::new(ops, &diff, true);
3967 let hunk_str = format!("{hunk}");
3968 if let Some(first_newline) = hunk_str.find('\n') {
3972 let header_line = &hunk_str[..first_newline];
3973 let rest = &hunk_str[first_newline..];
3974
3975 if let Some(func_ctx) =
3977 extract_function_context(header_line, &old_lines, funcname_matcher)
3978 {
3979 output.push_str(header_line);
3980 output.push(' ');
3981 output.push_str(&func_ctx);
3982 output.push_str(rest);
3983 } else {
3984 output.push_str(&hunk_str);
3985 }
3986 } else {
3987 output.push_str(&hunk_str);
3988 }
3989 }
3990
3991 output
3992}
3993
3994fn group_diff_ops_gap(
4000 mut ops: Vec<similar::DiffOp>,
4001 context: usize,
4002 max_common_gap: usize,
4003) -> Vec<Vec<similar::DiffOp>> {
4004 use similar::DiffOp;
4005 if ops.is_empty() {
4006 return vec![];
4007 }
4008
4009 let mut pending_group = Vec::new();
4010 let mut rv = Vec::new();
4011
4012 if let Some(DiffOp::Equal {
4013 old_index,
4014 new_index,
4015 len,
4016 }) = ops.first_mut()
4017 {
4018 let offset = (*len).saturating_sub(context);
4019 *old_index += offset;
4020 *new_index += offset;
4021 *len -= offset;
4022 }
4023
4024 if let Some(DiffOp::Equal { len, .. }) = ops.last_mut() {
4025 *len -= (*len).saturating_sub(context);
4026 }
4027
4028 for op in ops.into_iter() {
4029 if let DiffOp::Equal {
4030 old_index,
4031 new_index,
4032 len,
4033 } = op
4034 {
4035 if len > max_common_gap {
4038 pending_group.push(DiffOp::Equal {
4039 old_index,
4040 new_index,
4041 len: context,
4042 });
4043 rv.push(pending_group);
4044 let offset = len.saturating_sub(context);
4045 pending_group = vec![DiffOp::Equal {
4046 old_index: old_index + offset,
4047 new_index: new_index + offset,
4048 len: len - offset,
4049 }];
4050 continue;
4051 }
4052 }
4053 pending_group.push(op);
4054 }
4055
4056 match &pending_group[..] {
4057 &[] | &[similar::DiffOp::Equal { .. }] => {}
4058 _ => rv.push(pending_group),
4059 }
4060
4061 rv
4062}
4063
4064fn unified_diff_with_function_context(
4066 old_content: &str,
4067 new_content: &str,
4068 old_path: &str,
4069 new_path: &str,
4070 context_lines: usize,
4071 inter_hunk_context: usize,
4072 src_prefix: &str,
4073 dst_prefix: &str,
4074 funcname_matcher: Option<&FuncnameMatcher>,
4075 algorithm: similar::Algorithm,
4076 indent_heuristic: bool,
4077 quote_path_fully: bool,
4078) -> String {
4079 use crate::quote_path::format_diff_path_with_prefix;
4080 use similar::{udiff::UnifiedDiffHunk, TextDiff};
4081
4082 let diff = TextDiff::configure()
4083 .algorithm(algorithm)
4084 .diff_lines(old_content, new_content);
4085
4086 let old_lines: Vec<&str> = old_content.lines().collect();
4087 let new_lines: Vec<&str> = new_content.lines().collect();
4088 let n_old = old_lines.len();
4089 let n_new = new_lines.len();
4090
4091 let max_common_gap = context_lines
4098 .saturating_mul(2)
4099 .saturating_add(inter_hunk_context);
4100 let all_ops = diff.ops().to_vec();
4101 let op_groups = group_diff_ops_gap(all_ops.clone(), context_lines, max_common_gap);
4102
4103 let mut ranges: Vec<(usize, usize, usize, usize)> = Vec::new();
4104
4105 for ops in op_groups {
4106 if ops.is_empty() {
4107 continue;
4108 }
4109 let i1_anchor = func_context_old_anchor(&ops, n_old);
4110 let i1_end = hunk_old_change_end_exclusive(&ops);
4111 let skip_preimage_pull =
4112 append_with_whole_function_added(&ops, n_old, n_new, &new_lines, funcname_matcher);
4113 let hunk = UnifiedDiffHunk::new(ops, &diff, true);
4114 let hunk_str = format!("{hunk}");
4115 let header_line = hunk_str
4116 .lines()
4117 .next()
4118 .unwrap_or("")
4119 .trim_end_matches(['\r', '\n']);
4120 let Some((base_s1, _base_e1, _base_s2, _base_e2)) =
4121 parse_unified_hunk_header_ranges(header_line)
4122 else {
4123 continue;
4124 };
4125
4126 let ctx = context_lines;
4127 let (s1, e1, s2, e2) = if skip_preimage_pull {
4128 let s = n_old.saturating_sub(ctx);
4129 let s2 = map_old_line_to_new(&all_ops, s, n_new).min(n_new);
4130 (s, n_old, s2, n_new)
4131 } else {
4132 let mut s1 = base_s1.saturating_sub(ctx);
4133 let mut s2 = map_old_line_to_new(&all_ops, s1, n_new).min(n_new);
4134
4135 let base_pre_s1 = i1_anchor.saturating_sub(ctx);
4136 if base_pre_s1 < s1 {
4137 s1 = base_pre_s1;
4138 s2 = map_old_line_to_new(&all_ops, s1, n_new).min(n_new);
4139 }
4140
4141 let fs1 = expand_func_pre_start(s1, i1_anchor, n_old, &old_lines, funcname_matcher);
4142 if fs1 < s1 {
4143 s1 = fs1;
4144 s2 = map_old_line_to_new(&all_ops, s1, n_new).min(n_new);
4145 }
4146
4147 let mut e1 = (i1_end + ctx).min(n_old);
4153 let mut e2 = map_old_line_to_new(&all_ops, e1, n_new).min(n_new);
4154 let fe1 = expand_func_post_end(e1, i1_end, n_old, &old_lines, funcname_matcher);
4155 if fe1 > e1 {
4156 e1 = fe1;
4157 e2 = map_old_line_to_new(&all_ops, e1, n_new).min(n_new);
4158 }
4159 (s1, e1, s2, e2)
4160 };
4161
4162 ranges.push((s1, e1, s2, e2));
4163 }
4164
4165 ranges.sort_by_key(|r| (r.0, r.2));
4168 let mut merged: Vec<(usize, usize, usize, usize)> = Vec::with_capacity(ranges.len());
4169 for (s1, e1, s2, e2) in ranges {
4170 if let Some(last) = merged.last_mut() {
4171 if s1 < last.1 {
4172 last.1 = last.1.max(e1);
4173 last.3 = last.3.max(e2);
4174 continue;
4175 }
4176 }
4177 merged.push((s1, e1, s2, e2));
4178 }
4179 let ranges = merged;
4180
4181 let mut output = String::new();
4182 if old_path == "/dev/null" {
4183 output.push_str("--- /dev/null\n");
4184 } else if src_prefix.is_empty() {
4185 output.push_str(&format!("--- {old_path}\n"));
4186 } else {
4187 output.push_str("--- ");
4188 output.push_str(&format_diff_path_with_prefix(
4189 src_prefix,
4190 old_path,
4191 quote_path_fully,
4192 ));
4193 output.push('\n');
4194 }
4195 if new_path == "/dev/null" {
4196 output.push_str("+++ /dev/null\n");
4197 } else if dst_prefix.is_empty() {
4198 output.push_str(&format!("+++ {new_path}\n"));
4199 } else {
4200 output.push_str("+++ ");
4201 output.push_str(&format_diff_path_with_prefix(
4202 dst_prefix,
4203 new_path,
4204 quote_path_fully,
4205 ));
4206 output.push('\n');
4207 }
4208
4209 for (s1, e1, s2, e2) in ranges {
4210 if s1 >= e1 && s2 >= e2 {
4211 continue;
4212 }
4213 let old_seg =
4214 line_slice_for_diff_with_eof_nl(&old_lines, s1, e1, old_content.ends_with('\n'));
4215 let new_seg =
4216 line_slice_for_diff_with_eof_nl(&new_lines, s2, e2, new_content.ends_with('\n'));
4217 let inner_ctx = old_seg.lines().count().max(new_seg.lines().count()).max(1);
4218 let piece = unified_diff_with_prefix_and_funcname_and_algorithm(
4219 &old_seg,
4220 &new_seg,
4221 old_path,
4222 new_path,
4223 inner_ctx,
4224 0,
4225 src_prefix,
4226 dst_prefix,
4227 funcname_matcher,
4228 algorithm,
4229 false,
4230 false,
4231 indent_heuristic,
4232 quote_path_fully,
4233 );
4234 let shifted = shift_unified_hunk_headers_to_full_file(&piece, s1, s2);
4235 let with_func =
4236 enrich_unified_hunk_headers_funcname(&shifted, &old_lines, funcname_matcher);
4237 for line in with_func.lines() {
4238 if line.starts_with("--- ") || line.starts_with("+++ ") {
4239 continue;
4240 }
4241 output.push_str(line);
4242 output.push('\n');
4243 }
4244 }
4245
4246 output
4247}
4248
4249fn shift_unified_hunk_headers_to_full_file(
4254 patch: &str,
4255 delta_old: usize,
4256 delta_new: usize,
4257) -> String {
4258 if delta_old == 0 && delta_new == 0 {
4259 return patch.to_owned();
4260 }
4261 let mut out = String::with_capacity(patch.len());
4262 for line in patch.lines() {
4263 if let Some(shifted) = shift_one_unified_hunk_header(line, delta_old, delta_new) {
4264 out.push_str(&shifted);
4265 } else {
4266 out.push_str(line);
4267 }
4268 out.push('\n');
4269 }
4270 out
4271}
4272
4273fn shift_one_unified_hunk_header(line: &str, delta_old: usize, delta_new: usize) -> Option<String> {
4274 let rest = line.strip_prefix("@@ ")?;
4275 let (old_chunk, after_plus) = rest.split_once(" +")?;
4276 let old_spec = old_chunk.strip_prefix('-')?;
4277 let (new_spec, suffix) = after_plus.split_once(" @@")?;
4278 let shifted_old = shift_unified_range_spec(old_spec, delta_old)?;
4279 let shifted_new = shift_unified_range_spec(new_spec, delta_new)?;
4280 Some(format!("@@ -{shifted_old} +{shifted_new} @@{suffix}"))
4281}
4282
4283fn shift_unified_range_spec(spec: &str, delta: usize) -> Option<String> {
4284 let spec = spec.trim();
4285 if let Some((start_s, count_s)) = spec.split_once(',') {
4286 let start: usize = start_s.parse().ok()?;
4287 let count: usize = count_s.parse().ok()?;
4288 Some(format!("{},{}", start.saturating_add(delta), count))
4289 } else {
4290 let start: usize = spec.parse().ok()?;
4291 Some(format!("{}", start.saturating_add(delta)))
4292 }
4293}
4294
4295fn enrich_unified_hunk_headers_funcname(
4297 patch: &str,
4298 full_old_lines: &[&str],
4299 funcname_matcher: Option<&FuncnameMatcher>,
4300) -> String {
4301 let mut out = String::with_capacity(patch.len());
4302 for line in patch.lines() {
4303 if let Some(fixed) = enrich_one_hunk_header_funcname(line, full_old_lines, funcname_matcher)
4304 {
4305 out.push_str(&fixed);
4306 } else {
4307 out.push_str(line);
4308 }
4309 out.push('\n');
4310 }
4311 out
4312}
4313
4314fn enrich_one_hunk_header_funcname(
4315 line: &str,
4316 full_old_lines: &[&str],
4317 funcname_matcher: Option<&FuncnameMatcher>,
4318) -> Option<String> {
4319 let after_at = line.strip_prefix("@@ ")?;
4320 let idx = after_at.find(" @@")?;
4321 let mid = after_at[..idx].trim();
4322 let tail = after_at[idx + 3..].trim_start();
4323 let header_for_parse = format!("@@ {mid} @@");
4324 let func = extract_function_context(&header_for_parse, full_old_lines, funcname_matcher);
4325 Some(if let Some(f) = func {
4326 format!("@@ {mid} @@ {f}")
4327 } else if !tail.is_empty() {
4328 format!("@@ {mid} @@ {tail}")
4329 } else {
4330 format!("@@ {mid} @@")
4331 })
4332}
4333
4334fn line_slice_for_diff_with_eof_nl(
4335 lines: &[&str],
4336 start: usize,
4337 end: usize,
4338 full_file_ends_with_newline: bool,
4339) -> String {
4340 if start >= end {
4341 return String::new();
4342 }
4343 let mut s = lines[start..end].join("\n");
4344 let slice_is_suffix_of_file = end == lines.len();
4345 let need_trailing_nl = if slice_is_suffix_of_file {
4346 full_file_ends_with_newline
4347 } else {
4348 true
4349 };
4350 if need_trailing_nl && !s.ends_with('\n') {
4351 s.push('\n');
4352 }
4353 s
4354}
4355
4356fn map_old_line_to_new(ops: &[similar::DiffOp], old_line: usize, n_new: usize) -> usize {
4359 use similar::DiffOp;
4360 let mut n = 0usize;
4361 for op in ops {
4362 match *op {
4363 DiffOp::Equal {
4364 old_index,
4365 new_index,
4366 len,
4367 } => {
4368 if old_index + len <= old_line {
4369 n = new_index + len;
4370 continue;
4371 }
4372 if old_index < old_line {
4373 let take = old_line - old_index;
4374 return (new_index + take).min(n_new);
4375 }
4376 return new_index.min(n_new);
4377 }
4378 DiffOp::Delete {
4379 old_index,
4380 old_len,
4381 new_index,
4382 } => {
4383 if old_index + old_len <= old_line {
4384 n = new_index;
4385 continue;
4386 }
4387 if old_index < old_line {
4388 return new_index.min(n_new);
4389 }
4390 }
4391 DiffOp::Insert {
4392 old_index,
4393 new_index,
4394 new_len,
4395 } => {
4396 if old_index < old_line {
4397 n = new_index + new_len;
4398 continue;
4399 }
4400 if old_index == old_line {
4401 return (new_index + new_len).min(n_new);
4404 }
4405 return new_index.min(n_new);
4406 }
4407 DiffOp::Replace {
4408 old_index,
4409 old_len,
4410 new_index,
4411 new_len,
4412 } => {
4413 if old_index + old_len <= old_line {
4414 n = new_index + new_len;
4415 continue;
4416 }
4417 if old_index < old_line {
4418 let into_old = old_line - old_index;
4419 let mapped = new_index + into_old.min(new_len);
4420 return mapped.min(n_new);
4421 }
4422 return new_index.min(n_new);
4423 }
4424 }
4425 }
4426 n.min(n_new)
4427}
4428
4429fn parse_unified_hunk_header_ranges(header: &str) -> Option<(usize, usize, usize, usize)> {
4431 let rest = header.strip_prefix("@@ ")?;
4432 let (old_tok, rest2) = rest.split_once(" +")?;
4433 let old_tok = old_tok.strip_prefix('-')?;
4434 let new_tok = rest2.split_once(" @@").map(|(a, _)| a)?;
4435
4436 fn parse_side(spec: &str) -> Option<(usize, usize)> {
4437 let spec = spec.trim();
4438 let (start_one_based, count) = if let Some((a, b)) = spec.split_once(',') {
4439 (a.parse::<usize>().ok()?, b.parse::<usize>().ok()?)
4440 } else {
4441 let s = spec.parse::<usize>().ok()?;
4442 (s, 1usize)
4443 };
4444 let s0 = start_one_based.saturating_sub(1);
4445 let e0 = s0.saturating_add(count);
4446 Some((s0, e0))
4447 }
4448
4449 let (os, oe) = parse_side(old_tok)?;
4450 let (ns, ne) = parse_side(new_tok)?;
4451 Some((os, oe, ns, ne))
4452}
4453
4454fn append_with_whole_function_added(
4457 ops: &[similar::DiffOp],
4458 n_old: usize,
4459 n_new: usize,
4460 new_lines: &[&str],
4461 matcher: Option<&FuncnameMatcher>,
4462) -> bool {
4463 use similar::DiffOp;
4464 if n_old == 0 {
4465 return false;
4466 }
4467 let mut only_ins_or_eq = true;
4468 let mut min_new_ins = usize::MAX;
4469 for op in ops {
4470 match *op {
4471 DiffOp::Equal { .. } => {}
4472 DiffOp::Insert {
4473 new_index, new_len, ..
4474 } => {
4475 min_new_ins = min_new_ins.min(new_index);
4476 if new_len == 0 {
4477 only_ins_or_eq = false;
4478 }
4479 }
4480 DiffOp::Delete { .. } | DiffOp::Replace { .. } => {
4481 only_ins_or_eq = false;
4482 }
4483 }
4484 }
4485 let mut insert_at_eof = false;
4486 for op in ops {
4487 if let DiffOp::Insert { old_index, .. } = *op {
4488 if old_index == n_old {
4489 insert_at_eof = true;
4490 break;
4491 }
4492 }
4493 }
4494 let append_at_eof = min_new_ins == n_old || insert_at_eof;
4495 if !only_ins_or_eq || !append_at_eof || min_new_ins == usize::MAX {
4496 return false;
4497 }
4498 let mut j = min_new_ins;
4503 while j < n_new {
4504 let line = new_lines[j];
4505 if line.trim().is_empty() {
4506 j += 1;
4507 continue;
4508 }
4509 if let Some(m) = matcher {
4510 if m.match_line(line).is_some() {
4511 return true;
4512 }
4513 } else if inserted_block_starts_with_c_like_function_definition(line) {
4514 return true;
4515 }
4516 j += 1;
4517 }
4518 false
4519}
4520
4521fn inserted_block_starts_with_c_like_function_definition(line: &str) -> bool {
4522 let t = line.trim_start();
4523 let Some(open_paren) = t.find('(') else {
4524 return false;
4525 };
4526 let head = &t[..open_paren];
4527 let tokens: Vec<&str> = head.split_whitespace().collect();
4528 if tokens.len() < 2 {
4529 return false;
4531 }
4532 let nameish = tokens.last().copied().unwrap_or("");
4533 let name = nameish.trim_end_matches(['*', '&']);
4534 if name.is_empty() || !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
4535 return false;
4536 }
4537 let type_or_modifier = |tok: &str| {
4538 matches!(
4539 tok,
4540 "static"
4541 | "extern"
4542 | "inline"
4543 | "void"
4544 | "int"
4545 | "char"
4546 | "short"
4547 | "long"
4548 | "float"
4549 | "double"
4550 | "unsigned"
4551 | "signed"
4552 | "struct"
4553 | "enum"
4554 | "union"
4555 | "const"
4556 | "volatile"
4557 | "typedef"
4558 )
4559 };
4560 tokens[..tokens.len() - 1]
4561 .iter()
4562 .any(|tok| type_or_modifier(tok))
4563}
4564
4565fn hunk_old_change_end_exclusive(ops: &[similar::DiffOp]) -> usize {
4566 use similar::DiffOp;
4567 let mut max_o = 0usize;
4568 for op in ops {
4569 match *op {
4570 DiffOp::Delete {
4571 old_index, old_len, ..
4572 } => {
4573 max_o = max_o.max(old_index + old_len);
4574 }
4575 DiffOp::Replace {
4576 old_index, old_len, ..
4577 } => {
4578 max_o = max_o.max(old_index + old_len);
4579 }
4580 DiffOp::Insert { old_index, .. } => {
4581 max_o = max_o.max(old_index);
4584 }
4585 DiffOp::Equal { .. } => {}
4586 }
4587 }
4588 max_o
4589}
4590
4591fn func_context_old_anchor(ops: &[similar::DiffOp], n_old: usize) -> usize {
4592 use similar::DiffOp;
4593 let mut has_delete_or_replace = false;
4594 let mut min_del = usize::MAX;
4595 let mut min_ins_old = usize::MAX;
4596
4597 for op in ops {
4598 match *op {
4599 DiffOp::Delete {
4600 old_index, old_len, ..
4601 } => {
4602 has_delete_or_replace = true;
4603 min_del = min_del.min(old_index);
4604 min_del = min_del.min(old_index + old_len.saturating_sub(1));
4605 }
4606 DiffOp::Replace {
4607 old_index, old_len, ..
4608 } => {
4609 has_delete_or_replace = true;
4610 min_del = min_del.min(old_index);
4611 min_del = min_del.min(old_index + old_len.saturating_sub(1));
4612 }
4613 DiffOp::Insert { old_index, .. } => {
4614 min_ins_old = min_ins_old.min(old_index);
4615 }
4616 DiffOp::Equal { .. } => {}
4617 }
4618 }
4619
4620 let mut i1 = if has_delete_or_replace {
4621 min_del
4622 } else if min_ins_old != usize::MAX {
4623 min_ins_old
4624 } else {
4625 0
4626 };
4627
4628 let pure_insert = ops
4629 .iter()
4630 .all(|op| matches!(op, DiffOp::Insert { .. } | DiffOp::Equal { .. }))
4631 && ops.iter().any(|op| matches!(op, DiffOp::Insert { .. }));
4632
4633 if pure_insert && i1 >= n_old && n_old > 0 {
4634 i1 = n_old - 1;
4635 }
4636
4637 i1.min(n_old.saturating_sub(1))
4638}
4639
4640fn expand_func_pre_start(
4641 s1: usize,
4642 i1: usize,
4643 n_old: usize,
4644 old_lines: &[&str],
4645 matcher: Option<&FuncnameMatcher>,
4646) -> usize {
4647 if n_old == 0 {
4648 return s1;
4649 }
4650 let i1 = i1.min(n_old.saturating_sub(1));
4651 let mut fs1 = get_func_line_backward(old_lines, i1, matcher).unwrap_or(i1);
4652 while fs1 > 0
4653 && !is_line_empty_for_func_context(old_lines[fs1 - 1])
4654 && !is_func_line(old_lines[fs1 - 1], matcher)
4655 {
4656 fs1 -= 1;
4657 }
4658 s1.min(fs1)
4659}
4660
4661fn expand_func_post_end(
4662 e1: usize,
4663 i1_end: usize,
4664 n_old: usize,
4665 old_lines: &[&str],
4666 matcher: Option<&FuncnameMatcher>,
4667) -> usize {
4668 let from = i1_end.min(n_old);
4669 let fe1 = get_func_line_forward(old_lines, from, matcher).unwrap_or(n_old);
4670 let mut fe1_adj = fe1;
4671 while fe1_adj > 0 && is_line_empty_for_func_context(old_lines[fe1_adj - 1]) {
4672 fe1_adj -= 1;
4673 }
4674 e1.max(fe1_adj).min(n_old)
4675}
4676
4677fn is_line_empty_for_func_context(line: &str) -> bool {
4678 line.chars().all(|c| c.is_whitespace())
4679}
4680
4681fn is_func_line(line: &str, matcher: Option<&FuncnameMatcher>) -> bool {
4682 if let Some(m) = matcher {
4683 return m.match_line(line).is_some();
4684 }
4685 let t = line.trim_end_matches(['\n', '\r']);
4686 if t.is_empty() {
4687 return false;
4688 }
4689 let b = t.as_bytes()[0];
4690 b.is_ascii_alphabetic() || b == b'_' || b == b'$'
4691}
4692
4693fn get_func_line_backward(
4694 old_lines: &[&str],
4695 start: usize,
4696 matcher: Option<&FuncnameMatcher>,
4697) -> Option<usize> {
4698 let mut l = start.min(old_lines.len().saturating_sub(1));
4699 if old_lines.is_empty() {
4700 return None;
4701 }
4702 loop {
4703 if is_func_line(old_lines[l], matcher) {
4704 return Some(l);
4705 }
4706 if l == 0 {
4707 break;
4708 }
4709 l -= 1;
4710 }
4711 None
4712}
4713
4714fn get_func_line_forward(
4715 old_lines: &[&str],
4716 start: usize,
4717 matcher: Option<&FuncnameMatcher>,
4718) -> Option<usize> {
4719 let mut l = start;
4720 while l < old_lines.len() {
4721 if is_func_line(old_lines[l], matcher) {
4722 return Some(l);
4723 }
4724 l += 1;
4725 }
4726 None
4727}
4728
4729pub fn anchored_unified_diff(
4739 old_content: &str,
4740 new_content: &str,
4741 old_path: &str,
4742 new_path: &str,
4743 context_lines: usize,
4744 anchors: &[String],
4745 algorithm: similar::Algorithm,
4746 use_git_histogram: bool,
4747 indent_heuristic: bool,
4748 quote_path_fully: bool,
4749) -> String {
4750 use crate::quote_path::format_diff_path_with_prefix;
4751 use similar::TextDiff;
4752
4753 let old_lines: Vec<&str> = old_content.lines().collect();
4754 let new_lines: Vec<&str> = new_content.lines().collect();
4755
4756 let mut anchor_pairs: Vec<(usize, usize)> = Vec::new(); for anchor in anchors {
4760 let anchor_str = anchor.as_str();
4761
4762 let old_positions: Vec<usize> = old_lines
4764 .iter()
4765 .enumerate()
4766 .filter(|(_, l)| l.trim_end() == anchor_str)
4767 .map(|(i, _)| i)
4768 .collect();
4769
4770 let new_positions: Vec<usize> = new_lines
4772 .iter()
4773 .enumerate()
4774 .filter(|(_, l)| l.trim_end() == anchor_str)
4775 .map(|(i, _)| i)
4776 .collect();
4777
4778 if old_positions.len() == 1 && new_positions.len() == 1 {
4780 anchor_pairs.push((old_positions[0], new_positions[0]));
4781 }
4782 }
4783
4784 if anchor_pairs.is_empty() {
4786 return unified_diff_with_prefix_and_funcname_and_algorithm(
4787 old_content,
4788 new_content,
4789 old_path,
4790 new_path,
4791 context_lines,
4792 0,
4793 "a/",
4794 "b/",
4795 None,
4796 algorithm,
4797 false,
4798 use_git_histogram,
4799 indent_heuristic,
4800 quote_path_fully,
4801 );
4802 }
4803
4804 anchor_pairs.sort_by_key(|&(old_idx, _)| old_idx);
4806
4807 let mut filtered: Vec<(usize, usize)> = Vec::new();
4810 for &pair in &anchor_pairs {
4811 if filtered.is_empty() || filtered.last().is_some_and(|last| pair.1 > last.1) {
4812 filtered.push(pair);
4813 }
4814 }
4815 let anchor_pairs = filtered;
4816
4817 struct LineDiffOp {
4826 tag: char, line: String,
4828 }
4829
4830 let append_segment_diff =
4831 |ops: &mut Vec<LineDiffOp>, old_seg_input: &str, new_seg_input: &str| {
4832 use similar::ChangeTag;
4833 let old_ls: Vec<&str> = old_seg_input.lines().collect();
4834 let new_ls: Vec<&str> = new_seg_input.lines().collect();
4835 if old_ls.is_empty() && new_ls.is_empty() {
4836 return;
4837 }
4838 let seg_diff = TextDiff::configure()
4839 .algorithm(algorithm)
4840 .diff_slices(&old_ls, &new_ls);
4841 let raw = seg_diff.ops().to_vec();
4842 let compacted = diff_indent_heuristic::apply_change_compact_to_ops(
4843 &raw,
4844 &old_ls,
4845 &new_ls,
4846 indent_heuristic,
4847 );
4848 for op in &compacted {
4849 for ch in op.iter_changes(&old_ls, &new_ls) {
4850 let t = match ch.tag() {
4851 ChangeTag::Equal => ' ',
4852 ChangeTag::Delete => '-',
4853 ChangeTag::Insert => '+',
4854 };
4855 ops.push(LineDiffOp {
4856 tag: t,
4857 line: ch.value().to_string(),
4858 });
4859 }
4860 }
4861 };
4862
4863 let mut ops: Vec<LineDiffOp> = Vec::new();
4864 let mut old_pos = 0usize;
4865 let mut new_pos = 0usize;
4866
4867 for &(old_anchor, new_anchor) in &anchor_pairs {
4868 let old_segment: Vec<&str> = old_lines[old_pos..old_anchor].to_vec();
4870 let new_segment: Vec<&str> = new_lines[new_pos..new_anchor].to_vec();
4871
4872 let old_seg_text = old_segment.join("\n");
4873 let new_seg_text = new_segment.join("\n");
4874
4875 if !old_seg_text.is_empty() || !new_seg_text.is_empty() {
4876 let old_seg_input = if old_seg_text.is_empty() {
4877 String::new()
4878 } else {
4879 format!("{}\n", old_seg_text)
4880 };
4881 let new_seg_input = if new_seg_text.is_empty() {
4882 String::new()
4883 } else {
4884 format!("{}\n", new_seg_text)
4885 };
4886 append_segment_diff(&mut ops, &old_seg_input, &new_seg_input);
4887 }
4888
4889 ops.push(LineDiffOp {
4891 tag: ' ',
4892 line: old_lines[old_anchor].to_string(),
4893 });
4894
4895 old_pos = old_anchor + 1;
4896 new_pos = new_anchor + 1;
4897 }
4898
4899 let old_segment: Vec<&str> = old_lines[old_pos..].to_vec();
4901 let new_segment: Vec<&str> = new_lines[new_pos..].to_vec();
4902 let old_seg_text = old_segment.join("\n");
4903 let new_seg_text = new_segment.join("\n");
4904
4905 if !old_seg_text.is_empty() || !new_seg_text.is_empty() {
4906 let old_seg_input = if old_seg_text.is_empty() {
4907 String::new()
4908 } else {
4909 format!("{}\n", old_seg_text)
4910 };
4911 let new_seg_input = if new_seg_text.is_empty() {
4912 String::new()
4913 } else {
4914 format!("{}\n", new_seg_text)
4915 };
4916 append_segment_diff(&mut ops, &old_seg_input, &new_seg_input);
4917 }
4918
4919 let mut output = String::new();
4921 if old_path == "/dev/null" {
4922 output.push_str("--- /dev/null\n");
4923 } else {
4924 output.push_str("--- ");
4925 output.push_str(&format_diff_path_with_prefix(
4926 "a/",
4927 old_path,
4928 quote_path_fully,
4929 ));
4930 output.push('\n');
4931 }
4932 if new_path == "/dev/null" {
4933 output.push_str("+++ /dev/null\n");
4934 } else {
4935 output.push_str("+++ ");
4936 output.push_str(&format_diff_path_with_prefix(
4937 "b/",
4938 new_path,
4939 quote_path_fully,
4940 ));
4941 output.push('\n');
4942 }
4943
4944 let total_ops = ops.len();
4946 if total_ops == 0 {
4947 return output;
4948 }
4949
4950 let mut hunks: Vec<(usize, usize)> = Vec::new(); let mut i = 0;
4953 while i < total_ops {
4954 if ops[i].tag != ' ' {
4955 let start = i.saturating_sub(context_lines);
4956 let mut end = i;
4957 while end < total_ops {
4959 if ops[end].tag != ' ' {
4960 end += 1;
4961 continue;
4962 }
4963 let mut next_change = end;
4965 while next_change < total_ops && ops[next_change].tag == ' ' {
4966 next_change += 1;
4967 }
4968 if next_change < total_ops && next_change - end <= context_lines * 2 {
4969 end = next_change + 1;
4970 } else {
4971 end = (end + context_lines).min(total_ops);
4972 break;
4973 }
4974 }
4975 if let Some(last) = hunks.last_mut() {
4977 if start <= last.1 {
4978 last.1 = end;
4979 } else {
4980 hunks.push((start, end));
4981 }
4982 } else {
4983 hunks.push((start, end));
4984 }
4985 i = end;
4986 } else {
4987 i += 1;
4988 }
4989 }
4990
4991 for (start, end) in hunks {
4993 let mut old_start = 1usize;
4995 let mut new_start = 1usize;
4996 for op in &ops[..start] {
4998 match op.tag {
4999 ' ' => {
5000 old_start += 1;
5001 new_start += 1;
5002 }
5003 '-' => {
5004 old_start += 1;
5005 }
5006 '+' => {
5007 new_start += 1;
5008 }
5009 _ => {}
5010 }
5011 }
5012 let mut old_count = 0usize;
5013 let mut new_count = 0usize;
5014 for op in &ops[start..end] {
5015 match op.tag {
5016 ' ' => {
5017 old_count += 1;
5018 new_count += 1;
5019 }
5020 '-' => {
5021 old_count += 1;
5022 }
5023 '+' => {
5024 new_count += 1;
5025 }
5026 _ => {}
5027 }
5028 }
5029
5030 output.push_str(&format!(
5031 "@@ -{},{} +{},{} @@\n",
5032 old_start, old_count, new_start, new_count
5033 ));
5034 for op in &ops[start..end] {
5035 output.push(op.tag);
5036 output.push_str(&op.line);
5037 output.push('\n');
5038 }
5039 }
5040
5041 output
5042}
5043
5044fn extract_function_context(
5050 header: &str,
5051 old_lines: &[&str],
5052 funcname_matcher: Option<&FuncnameMatcher>,
5053) -> Option<String> {
5054 let at_pos = header.find("-")?;
5056 let rest = &header[at_pos + 1..];
5057 let comma_or_space = rest.find([',', ' '])?;
5058 let start_str = &rest[..comma_or_space];
5059 let start_line: usize = start_str.parse().ok()?;
5060
5061 let old_token_end = rest.find([' ', '\t']).unwrap_or(rest.len());
5065 let old_token = &rest[..old_token_end];
5066 let old_count: usize = if let Some(comma) = old_token.find(',') {
5067 old_token[comma + 1..].parse().unwrap_or(1)
5068 } else {
5069 1
5070 };
5071
5072 if start_line == 0 {
5073 return None;
5074 }
5075
5076 let search_end = if old_count == 0 {
5083 start_line.min(old_lines.len())
5084 } else {
5085 if start_line <= 1 {
5086 return None;
5087 }
5088 (start_line - 1).min(old_lines.len())
5089 };
5090 let truncate = |text: &str| {
5091 if text.len() > 80 {
5092 let mut end = 80;
5093 while end > 0 && !text.is_char_boundary(end) {
5094 end -= 1;
5095 }
5096 text[..end].to_owned()
5097 } else {
5098 text.to_owned()
5099 }
5100 };
5101
5102 for i in (0..search_end).rev() {
5103 let line = old_lines[i];
5104 if line.is_empty() {
5105 continue;
5106 }
5107 if let Some(matcher) = funcname_matcher {
5108 if let Some(matched) = matcher.match_line(line) {
5109 return Some(truncate(&matched));
5110 }
5111 continue;
5112 }
5113
5114 let first = line.as_bytes()[0];
5115 if first.is_ascii_alphabetic() || first == b'_' || first == b'$' {
5116 return Some(truncate(line.trim_end_matches(char::is_whitespace)));
5117 }
5118 }
5119 None
5120}
5121
5122pub fn format_stat_line(
5126 path: &str,
5127 insertions: usize,
5128 deletions: usize,
5129 max_path_len: usize,
5130) -> String {
5131 format_stat_line_width(path, insertions, deletions, max_path_len, 0)
5132}
5133
5134pub fn format_stat_line_width(
5135 path: &str,
5136 insertions: usize,
5137 deletions: usize,
5138 max_path_len: usize,
5139 count_width: usize,
5140) -> String {
5141 let total = insertions + deletions;
5142 let plus = "+".repeat(insertions.min(50));
5143 let minus = "-".repeat(deletions.min(50));
5144 let cw = if count_width > 0 {
5145 count_width
5146 } else {
5147 format!("{}", total).len()
5148 };
5149 let bar = format!("{}{}", plus, minus);
5150 if bar.is_empty() {
5151 format!(
5152 " {:<width$} | {:>cw$}",
5153 path,
5154 total,
5155 width = max_path_len,
5156 cw = cw
5157 )
5158 } else {
5159 format!(
5160 " {:<width$} | {:>cw$} {}",
5161 path,
5162 total,
5163 bar,
5164 width = max_path_len,
5165 cw = cw
5166 )
5167 }
5168}
5169
5170#[must_use]
5172pub fn normalize_ignore_space_change_line(line: &str) -> String {
5173 let mut result = String::with_capacity(line.len());
5174 let mut in_space = false;
5175 for c in line.chars() {
5176 if c.is_whitespace() {
5177 if !in_space {
5178 result.push(' ');
5179 in_space = true;
5180 }
5181 } else {
5182 result.push(c);
5183 in_space = false;
5184 }
5185 }
5186 while result.ends_with(' ') {
5187 result.pop();
5188 }
5189 result
5190}
5191
5192#[must_use]
5198pub fn normalize_ignore_space_change(content: &str) -> String {
5199 content
5200 .lines()
5201 .map(normalize_ignore_space_change_line)
5202 .collect::<Vec<_>>()
5203 .join("\n")
5204}
5205
5206pub fn count_changes(old_content: &str, new_content: &str) -> (usize, usize) {
5210 count_changes_with_algorithm(old_content, new_content, similar::Algorithm::Myers, false)
5211}
5212
5213#[must_use]
5218pub fn count_changes_with_algorithm(
5219 old_content: &str,
5220 new_content: &str,
5221 algorithm: similar::Algorithm,
5222 use_git_histogram: bool,
5223) -> (usize, usize) {
5224 if use_git_histogram {
5225 use imara_diff::{Algorithm, Diff, InternedInput};
5226 let input = InternedInput::new(old_content, new_content);
5227 let mut d = Diff::compute(Algorithm::Histogram, &input);
5228 d.postprocess_lines(&input);
5229 return (d.count_additions() as usize, d.count_removals() as usize);
5230 }
5231
5232 use similar::{ChangeTag, TextDiff};
5233
5234 let diff = TextDiff::configure()
5235 .algorithm(algorithm)
5236 .diff_lines(old_content, new_content);
5237 let mut ins = 0;
5238 let mut del = 0;
5239
5240 for change in diff.iter_all_changes() {
5241 match change.tag() {
5242 ChangeTag::Insert => ins += 1,
5243 ChangeTag::Delete => del += 1,
5244 ChangeTag::Equal => {}
5245 }
5246 }
5247
5248 (ins, del)
5249}
5250
5251#[must_use]
5256pub fn count_git_lines(data: &[u8]) -> usize {
5257 if data.is_empty() {
5258 return 0;
5259 }
5260 let mut count = 0usize;
5261 let mut nl_just_seen = false;
5262 for &ch in data {
5263 if ch == b'\n' {
5264 count += 1;
5265 nl_just_seen = true;
5266 } else {
5267 nl_just_seen = false;
5268 }
5269 }
5270 if !nl_just_seen {
5271 count += 1;
5272 }
5273 count
5274}
5275
5276pub const GIT_DIFF_MAX_SCORE: u64 = 60_000;
5278const DIFF_MAX_SCORE: u64 = GIT_DIFF_MAX_SCORE;
5279const DIFF_MINIMUM_BREAK_SIZE: usize = 400;
5280const DIFF_DEFAULT_BREAK_SCORE: u64 = 30_000;
5281pub const GIT_DIFF_DEFAULT_BREAK_SCORE: u64 = DIFF_DEFAULT_BREAK_SCORE;
5283pub const GIT_DIFF_DEFAULT_MERGE_SCORE_AFTER_BREAK: u64 = 36_000;
5286const DIFF_HASHBASE: u32 = 107_927;
5287
5288#[derive(Clone, Copy, Default)]
5289struct SpanSlot {
5290 hashval: u32,
5291 cnt: u32,
5292}
5293
5294struct SpanHashTop {
5295 alloc_log2: u8,
5296 free_slots: i32,
5297 data: Vec<SpanSlot>,
5298}
5299
5300impl SpanHashTop {
5301 fn new(initial_log2: u8) -> Self {
5302 let cap = 1usize << initial_log2;
5303 Self {
5304 alloc_log2: initial_log2,
5305 free_slots: initial_free(initial_log2),
5306 data: vec![SpanSlot::default(); cap],
5307 }
5308 }
5309
5310 fn len(&self) -> usize {
5311 1usize << self.alloc_log2
5312 }
5313
5314 fn add_span(&mut self, hashval: u32, cnt: u32) {
5315 loop {
5316 let lim = self.len();
5317 let mut bucket = (hashval as usize) & (lim - 1);
5318 loop {
5319 let h = &mut self.data[bucket];
5320 if h.cnt == 0 {
5321 h.hashval = hashval;
5322 h.cnt = cnt;
5323 self.free_slots -= 1;
5324 if self.free_slots < 0 {
5325 self.rehash();
5326 break;
5327 }
5328 return;
5329 }
5330 if h.hashval == hashval {
5331 h.cnt = h.cnt.saturating_add(cnt);
5332 return;
5333 }
5334 bucket += 1;
5335 if bucket >= lim {
5336 bucket = 0;
5337 }
5338 }
5339 }
5340 }
5341
5342 fn rehash(&mut self) {
5343 let old = std::mem::take(&mut self.data);
5344 let old_log = self.alloc_log2;
5345 self.alloc_log2 = old_log.saturating_add(1);
5346 let new_len = 1usize << self.alloc_log2;
5347 self.free_slots = initial_free(self.alloc_log2);
5348 self.data = vec![SpanSlot::default(); new_len];
5349 let old_sz = 1usize << old_log;
5350 for o in old.iter().take(old_sz) {
5351 let o = *o;
5352 if o.cnt == 0 {
5353 continue;
5354 }
5355 self.add_span_after_rehash(o.hashval, o.cnt);
5356 }
5357 }
5358
5359 fn add_span_after_rehash(&mut self, hashval: u32, cnt: u32) {
5360 loop {
5361 let lim = self.len();
5362 let mut bucket = (hashval as usize) & (lim - 1);
5363 loop {
5364 let h = &mut self.data[bucket];
5365 if h.cnt == 0 {
5366 h.hashval = hashval;
5367 h.cnt = cnt;
5368 self.free_slots -= 1;
5369 if self.free_slots < 0 {
5370 self.rehash();
5371 break;
5372 }
5373 return;
5374 }
5375 if h.hashval == hashval {
5376 h.cnt = h.cnt.saturating_add(cnt);
5377 return;
5378 }
5379 bucket += 1;
5380 if bucket >= lim {
5381 bucket = 0;
5382 }
5383 }
5384 }
5385 }
5386
5387 fn sort_by_hashval(&mut self) {
5388 let sz = self.len();
5389 self.data[..sz].sort_by(|a, b| {
5390 if a.cnt == 0 {
5391 return std::cmp::Ordering::Greater;
5392 }
5393 if b.cnt == 0 {
5394 return std::cmp::Ordering::Less;
5395 }
5396 a.hashval.cmp(&b.hashval)
5397 });
5398 }
5399}
5400
5401fn initial_free(sz_log2: u8) -> i32 {
5402 let sz = sz_log2 as i32;
5403 ((1i32 << sz_log2) * (sz - 3) / sz).max(0)
5404}
5405
5406fn hash_blob_spans(buf: &[u8], is_text: bool) -> SpanHashTop {
5407 let mut hash = SpanHashTop::new(9);
5408 let mut n = 0u32;
5409 let mut accum1: u32 = 0;
5410 let mut accum2: u32 = 0;
5411 let mut i = 0usize;
5412 while i < buf.len() {
5413 let c = buf[i] as u32;
5414 let old_1 = accum1;
5415 i += 1;
5416
5417 if is_text && c == b'\r' as u32 && i < buf.len() && buf[i] == b'\n' {
5418 continue;
5419 }
5420
5421 accum1 = accum1.wrapping_shl(7) ^ accum2.wrapping_shr(25);
5422 accum2 = accum2.wrapping_shl(7) ^ old_1.wrapping_shr(25);
5423 accum1 = accum1.wrapping_add(c);
5424 n += 1;
5425 if n < 64 && c != b'\n' as u32 {
5426 continue;
5427 }
5428 let hashval = (accum1.wrapping_add(accum2.wrapping_mul(0x61))) % DIFF_HASHBASE;
5429 hash.add_span(hashval, n);
5430 n = 0;
5431 accum1 = 0;
5432 accum2 = 0;
5433 }
5434 if n > 0 {
5435 let hashval = (accum1.wrapping_add(accum2.wrapping_mul(0x61))) % DIFF_HASHBASE;
5436 hash.add_span(hashval, n);
5437 }
5438 hash.sort_by_hashval();
5439 hash
5440}
5441
5442#[must_use]
5447pub fn diffcore_count_changes(old: &[u8], new: &[u8]) -> (u64, u64) {
5448 let src_is_text = !crate::merge_file::is_binary(old);
5449 let dst_is_text = !crate::merge_file::is_binary(new);
5450 let src_count = hash_blob_spans(old, src_is_text);
5451 let dst_count = hash_blob_spans(new, dst_is_text);
5452 let mut sc: u64 = 0;
5453 let mut la: u64 = 0;
5454 let mut si = 0usize;
5455 let mut di = 0usize;
5456 let src_len = src_count.len();
5457 let dst_len = dst_count.len();
5458 loop {
5459 if si >= src_len || src_count.data[si].cnt == 0 {
5460 break;
5461 }
5462 let s_hash = src_count.data[si].hashval;
5463 let s_cnt = u64::from(src_count.data[si].cnt);
5464 while di < dst_len && dst_count.data[di].cnt != 0 && dst_count.data[di].hashval < s_hash {
5465 la += u64::from(dst_count.data[di].cnt);
5466 di += 1;
5467 }
5468 let mut dst_cnt = 0u64;
5469 if di < dst_len && dst_count.data[di].cnt != 0 && dst_count.data[di].hashval == s_hash {
5470 dst_cnt = u64::from(dst_count.data[di].cnt);
5471 di += 1;
5472 }
5473 if s_cnt < dst_cnt {
5474 la += dst_cnt - s_cnt;
5475 sc += s_cnt;
5476 } else {
5477 sc += dst_cnt;
5478 }
5479 si += 1;
5480 }
5481 while di < dst_len && dst_count.data[di].cnt != 0 {
5482 la += u64::from(dst_count.data[di].cnt);
5483 di += 1;
5484 }
5485 (sc, la)
5486}
5487
5488#[must_use]
5491pub fn should_break_rewrite_for_stat(old: &[u8], new: &[u8]) -> bool {
5492 should_break_rewrite_inner(old, new, DIFF_DEFAULT_BREAK_SCORE)
5493}
5494
5495#[must_use]
5499pub fn should_break_rewrite_pair(old: &[u8], new: &[u8], break_score: u64) -> bool {
5500 should_break_rewrite_inner(old, new, break_score)
5501}
5502
5503pub fn parse_diff_rename_score_token(arg: &str) -> Option<u64> {
5506 let mut num: u64 = 0;
5507 let mut scale: u64 = 1;
5508 let mut dot = false;
5509 let mut saw_digit = false;
5510 for ch in arg.chars() {
5511 if !dot && ch == '.' {
5512 scale = 1;
5513 dot = true;
5514 continue;
5515 }
5516 if ch == '%' {
5517 scale = if dot { scale.saturating_mul(100) } else { 100 };
5518 break;
5519 }
5520 if ch.is_ascii_digit() {
5521 saw_digit = true;
5522 if scale < 100_000 {
5523 scale = scale.saturating_mul(10);
5524 num = num.saturating_mul(10) + u64::from(ch as u8 - b'0');
5525 }
5526 } else {
5527 break;
5528 }
5529 }
5530 if !saw_digit {
5531 return None;
5532 }
5533 Some(if num >= scale {
5534 GIT_DIFF_MAX_SCORE
5535 } else {
5536 GIT_DIFF_MAX_SCORE * num / scale
5537 })
5538}
5539
5540#[must_use]
5543pub fn rewrite_merge_score(old: &[u8], new: &[u8]) -> Option<u64> {
5544 if old.is_empty() {
5545 return None;
5546 }
5547 let max_size = old.len().max(new.len());
5548 if max_size < DIFF_MINIMUM_BREAK_SIZE {
5549 return None;
5550 }
5551 let (src_copied, _) = diffcore_count_changes(old, new);
5552 let src_copied = src_copied.min(old.len() as u64);
5553 let src_removed = (old.len() as u64).saturating_sub(src_copied);
5554 Some(src_removed * DIFF_MAX_SCORE / old.len() as u64)
5555}
5556
5557#[must_use]
5559pub fn rewrite_dissimilarity_index_percent(old: &[u8], new: &[u8]) -> Option<u32> {
5560 let score = rewrite_merge_score(old, new)?;
5561 Some((score * 100 / DIFF_MAX_SCORE).min(100) as u32)
5562}
5563
5564fn should_break_rewrite_inner(src: &[u8], dst: &[u8], break_score: u64) -> bool {
5565 if src.is_empty() {
5566 return false;
5567 }
5568 let max_size = src.len().max(dst.len());
5569 if max_size < DIFF_MINIMUM_BREAK_SIZE {
5570 return false;
5571 }
5572 let (src_copied, literal_added) = diffcore_count_changes(src, dst);
5573 let src_copied = src_copied.min(src.len() as u64);
5574 let mut literal_added = literal_added;
5575 let dst_len = dst.len() as u64;
5576 if src_copied < dst_len && literal_added + src_copied > dst_len {
5577 literal_added = dst_len.saturating_sub(src_copied);
5578 }
5579 let src_removed = (src.len() as u64).saturating_sub(src_copied);
5580 let merge_score = src_removed * DIFF_MAX_SCORE / src.len() as u64;
5581 if merge_score > break_score {
5582 return true;
5583 }
5584 let delta_size = src_removed.saturating_add(literal_added);
5585 if delta_size * DIFF_MAX_SCORE / (max_size as u64) < break_score {
5586 return false;
5587 }
5588 let s = src.len() as u64;
5589 if (s * break_score < src_removed * DIFF_MAX_SCORE)
5590 && (literal_added * 20 < src_removed)
5591 && (literal_added * 20 < src_copied)
5592 {
5593 return false;
5594 }
5595 true
5596}
5597
5598struct FlatEntry {
5602 path: String,
5603 mode: u32,
5604 oid: ObjectId,
5605}
5606
5607fn flatten_tree(odb: &Odb, tree_oid: &ObjectId, prefix: &str) -> Result<Vec<FlatEntry>> {
5608 let entries = read_tree(odb, tree_oid)?;
5609 let mut result = Vec::new();
5610
5611 for entry in entries {
5612 let name_str = String::from_utf8_lossy(&entry.name);
5613 let path = format_path(prefix, &name_str);
5614 if is_tree_mode(entry.mode) {
5615 let nested = flatten_tree(odb, &entry.oid, &path)?;
5616 result.extend(nested);
5617 } else {
5618 result.push(FlatEntry {
5619 path,
5620 mode: entry.mode,
5621 oid: entry.oid,
5622 });
5623 }
5624 }
5625
5626 Ok(result)
5627}
5628
5629pub fn head_path_states(
5631 odb: &Odb,
5632 head_tree: Option<&ObjectId>,
5633) -> Result<std::collections::BTreeMap<String, (u32, ObjectId)>> {
5634 let mut m = std::collections::BTreeMap::new();
5635 let Some(t) = head_tree else {
5636 return Ok(m);
5637 };
5638 for fe in flatten_tree(odb, t, "")? {
5639 m.insert(fe.path, (fe.mode, fe.oid));
5640 }
5641 Ok(m)
5642}
5643
5644fn is_tree_mode(mode: u32) -> bool {
5646 mode == 0o040000
5647}
5648
5649fn format_path(prefix: &str, name: &str) -> String {
5651 if prefix.is_empty() {
5652 name.to_owned()
5653 } else {
5654 format!("{prefix}/{name}")
5655 }
5656}
5657
5658pub fn format_mode(mode: u32) -> String {
5660 format!("{mode:06o}")
5661}
5662
5663#[must_use]
5667pub fn read_submodule_head_for_checkout(sub_dir: &Path) -> Option<ObjectId> {
5668 read_submodule_head(sub_dir)
5669}
5670
5671#[must_use]
5676pub fn submodule_commit_subject_line(c: &CommitData) -> String {
5677 let enc = c.encoding.as_deref().unwrap_or("UTF-8");
5678 let is_latin1 = enc.eq_ignore_ascii_case("ISO8859-1")
5679 || enc.eq_ignore_ascii_case("ISO-8859-1")
5680 || enc.eq_ignore_ascii_case("LATIN1")
5681 || enc.eq_ignore_ascii_case("ISO-8859-15");
5682 if let Some(raw) = c.raw_message.as_deref() {
5683 let line = raw.split(|b| *b == b'\n').next().unwrap_or(raw);
5684 if is_latin1 {
5685 return line
5686 .iter()
5687 .map(|&b| b as char)
5688 .collect::<String>()
5689 .trim()
5690 .to_owned();
5691 }
5692 return String::from_utf8_lossy(line).trim().to_string();
5693 }
5694 c.message.lines().next().unwrap_or("").trim().to_owned()
5695}
5696
5697fn submodule_worktree_is_unpopulated_placeholder(sub_dir: &Path) -> bool {
5700 match fs::read_dir(sub_dir) {
5701 Ok(mut it) => it.next().is_none(),
5702 Err(e) if e.kind() == std::io::ErrorKind::NotFound => true,
5703 Err(_) => false,
5704 }
5705}
5706
5707fn read_submodule_head(sub_dir: &Path) -> Option<ObjectId> {
5708 read_submodule_head_oid(sub_dir)
5709}
5710
5711#[must_use]
5713pub fn submodule_embedded_git_dir(sub_dir: &Path) -> Option<PathBuf> {
5714 let gitfile = sub_dir.join(".git");
5715 if gitfile.is_file() {
5716 let content = fs::read_to_string(&gitfile).ok()?;
5717 let gitdir = content
5718 .lines()
5719 .find_map(|l| l.strip_prefix("gitdir: "))?
5720 .trim();
5721 Some(if Path::new(gitdir).is_absolute() {
5722 PathBuf::from(gitdir)
5723 } else {
5724 sub_dir.join(gitdir)
5725 })
5726 } else if gitfile.is_dir() {
5727 Some(gitfile)
5728 } else {
5729 None
5730 }
5731}
5732
5733fn find_superproject_git(sub_dir: &Path) -> Option<(PathBuf, PathBuf)> {
5735 let mut cur = sub_dir.parent()?;
5736 loop {
5737 let git_path = cur.join(".git");
5738 if git_path.exists() {
5739 let gd = if git_path.is_file() {
5740 let content = fs::read_to_string(&git_path).ok()?;
5741 let line = content
5742 .lines()
5743 .find_map(|l| l.strip_prefix("gitdir: "))?
5744 .trim();
5745 if Path::new(line).is_absolute() {
5746 PathBuf::from(line)
5747 } else {
5748 cur.join(line)
5749 }
5750 } else {
5751 git_path
5752 };
5753 return Some((cur.to_path_buf(), gd));
5754 }
5755 cur = cur.parent()?;
5756 }
5757}
5758
5759pub fn read_submodule_head_oid(sub_dir: &Path) -> Option<ObjectId> {
5765 let mut git_dir = submodule_embedded_git_dir(sub_dir)?;
5768 if let Some((super_wt, super_git_dir)) = find_superproject_git(sub_dir) {
5769 let rel = sub_dir.strip_prefix(&super_wt).ok()?;
5770 let rel_str = rel.to_string_lossy().replace('\\', "/");
5771 let local_mod = super_git_dir
5772 .join("modules")
5773 .join(rel_str.trim_start_matches('/'));
5774 if local_mod.join("HEAD").exists() {
5775 let sg = super_git_dir.canonicalize().unwrap_or(super_git_dir);
5776 let cur = git_dir.canonicalize().unwrap_or_else(|_| git_dir.clone());
5777 if !cur.starts_with(&sg) {
5778 git_dir = local_mod;
5779 }
5780 }
5781 }
5782 let head_content = fs::read_to_string(git_dir.join("HEAD")).ok()?;
5783 let head_trimmed = head_content.trim();
5784 if head_trimmed.starts_with("ref: ") {
5785 match crate::refs::resolve_ref(&git_dir, "HEAD") {
5789 Ok(oid) => Some(oid),
5790 Err(_) => {
5791 let mut found = None;
5792 for branch in ["main", "master"] {
5793 let p = git_dir.join("refs/heads").join(branch);
5794 if let Ok(s) = fs::read_to_string(&p) {
5795 if let Ok(o) = ObjectId::from_hex(s.trim()) {
5796 found = Some(o);
5797 break;
5798 }
5799 }
5800 }
5801 found
5802 }
5803 }
5804 } else {
5805 ObjectId::from_hex(head_trimmed).ok()
5806 }
5807}
5808
5809#[must_use]
5821pub fn submodule_head_object_broken(sub_dir: &Path) -> bool {
5822 let Some(sub_git_dir) = submodule_embedded_git_dir(sub_dir) else {
5823 return false;
5824 };
5825 let Some(head_oid) = read_submodule_head_oid(sub_dir) else {
5826 return false;
5827 };
5828 let odb = Odb::new(&sub_git_dir.join("objects"));
5829 match odb.read(&head_oid) {
5830 Ok(obj) => parse_commit(&obj.data).is_err(),
5833 Err(_) => true,
5834 }
5835}
5836
5837fn submodule_has_dirty_worktree_for_super_diff(
5840 super_worktree: &Path,
5841 rel_path: &str,
5842 recorded_oid: &ObjectId,
5843) -> bool {
5844 let flags = submodule_porcelain_flags(super_worktree, rel_path, *recorded_oid);
5845 flags.modified || flags.untracked
5846}
5847
5848#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
5850pub struct SubmodulePorcelainFlags {
5851 pub new_commits: bool,
5853 pub modified: bool,
5855 pub untracked: bool,
5857}
5858
5859pub fn submodule_porcelain_flags(
5865 super_worktree: &Path,
5866 rel_path: &str,
5867 recorded_oid: ObjectId,
5868) -> SubmodulePorcelainFlags {
5869 let sub_dir = super_worktree.join(rel_path);
5870 let Some(sub_git_dir) = submodule_embedded_git_dir(&sub_dir) else {
5871 return SubmodulePorcelainFlags::default();
5872 };
5873 let Some(sub_head) = read_submodule_head_oid(&sub_dir) else {
5874 return SubmodulePorcelainFlags::default();
5875 };
5876
5877 let new_commits = sub_head != recorded_oid;
5878
5879 let index_path = sub_git_dir.join("index");
5880 let sub_index = match crate::index::Index::load(&index_path) {
5881 Ok(ix) => ix,
5882 Err(_) => {
5883 return SubmodulePorcelainFlags {
5884 new_commits,
5885 ..Default::default()
5886 }
5887 }
5888 };
5889
5890 let tracked: std::collections::BTreeSet<String> = sub_index
5891 .entries
5892 .iter()
5893 .filter(|e| e.stage() == 0)
5894 .map(|e| String::from_utf8_lossy(&e.path).into_owned())
5895 .collect();
5896 let untracked = submodule_dir_has_untracked_inner(&sub_dir, &sub_dir, &tracked, &sub_index);
5897
5898 let objects_dir = sub_git_dir.join("objects");
5899 let odb = Odb::new(&objects_dir);
5900
5901 let sub_head_tree = (|| -> Option<ObjectId> {
5902 let h = fs::read_to_string(sub_git_dir.join("HEAD")).ok()?;
5903 let h_str = h.trim();
5904 let commit_oid = if let Some(r) = h_str.strip_prefix("ref: ") {
5905 let oid_hex = fs::read_to_string(sub_git_dir.join(r)).ok()?;
5906 ObjectId::from_hex(oid_hex.trim()).ok()?
5907 } else {
5908 ObjectId::from_hex(h_str).ok()?
5909 };
5910 let obj = odb.read(&commit_oid).ok()?;
5911 let commit = parse_commit(&obj.data).ok()?;
5912 Some(commit.tree)
5913 })();
5914
5915 let staged_dirty = sub_head_tree
5916 .as_ref()
5917 .map(|t| diff_index_to_tree(&odb, &sub_index, Some(t), false).map(|v| !v.is_empty()))
5918 .unwrap_or(Ok(false));
5919 let staged_dirty = staged_dirty.unwrap_or(false);
5920
5921 let unstaged_dirty = diff_index_to_worktree(&odb, &sub_index, &sub_dir, false, true)
5922 .map(|v| !v.is_empty())
5923 .unwrap_or(false);
5924
5925 let mut modified = staged_dirty || unstaged_dirty;
5926
5927 for e in &sub_index.entries {
5932 if e.stage() != 0 || e.mode != 0o160000 {
5933 continue;
5934 }
5935 let child = String::from_utf8_lossy(&e.path).into_owned();
5936 let full_rel = if rel_path.is_empty() {
5937 child
5938 } else {
5939 format!("{rel_path}/{child}")
5940 };
5941 let nested = submodule_porcelain_flags(super_worktree, &full_rel, e.oid);
5942 modified |= nested.modified;
5943 }
5944
5945 SubmodulePorcelainFlags {
5946 new_commits,
5947 modified,
5948 untracked,
5949 }
5950}
5951
5952fn submodule_dir_has_untracked_inner(
5953 dir: &Path,
5954 root: &Path,
5955 tracked: &std::collections::BTreeSet<String>,
5956 owning_index: &Index,
5957) -> bool {
5958 let entries = match fs::read_dir(dir) {
5959 Ok(e) => e,
5960 Err(_) => return false,
5961 };
5962 let mut sorted: Vec<_> = entries.filter_map(|e| e.ok()).collect();
5963 sorted.sort_by_key(|e| e.file_name());
5964
5965 for entry in sorted {
5966 let name = entry.file_name().to_string_lossy().to_string();
5967 if name == ".git" {
5968 continue;
5969 }
5970 let path = entry.path();
5971 let rel = path
5972 .strip_prefix(root)
5973 .map(|p| p.to_string_lossy().to_string())
5974 .unwrap_or_else(|_| name.clone());
5975
5976 let is_dir = entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false);
5977 if is_dir {
5978 let is_gitlink = owning_index
5979 .get(rel.as_bytes(), 0)
5980 .is_some_and(|e| e.mode == 0o160000);
5981 if is_gitlink {
5982 let Some(nested_git) = submodule_embedded_git_dir(&path) else {
5983 continue;
5984 };
5985 let nested_index_path = nested_git.join("index");
5986 let Ok(nested_ix) = crate::index::Index::load(&nested_index_path) else {
5987 continue;
5988 };
5989 let nested_tracked: std::collections::BTreeSet<String> = nested_ix
5990 .entries
5991 .iter()
5992 .filter(|e| e.stage() == 0)
5993 .map(|e| String::from_utf8_lossy(&e.path).into_owned())
5994 .collect();
5995 if submodule_dir_has_untracked_inner(&path, &path, &nested_tracked, &nested_ix) {
5996 return true;
5997 }
5998 } else if submodule_dir_has_untracked_inner(&path, root, tracked, owning_index) {
5999 return true;
6000 }
6001 } else if !tracked.contains(&rel) {
6002 return true;
6003 }
6004 }
6005 false
6006}
6007
6008pub fn apply_orderfile_entries(
6011 entries: Vec<DiffEntry>,
6012 order_path: &str,
6013 cwd: &Path,
6014) -> Result<Vec<DiffEntry>> {
6015 apply_orderfile(entries, order_path, cwd)
6016}
6017
6018fn apply_orderfile(
6019 mut entries: Vec<DiffEntry>,
6020 order_path: &str,
6021 cwd: &Path,
6022) -> Result<Vec<DiffEntry>> {
6023 let patterns = read_orderfile_patterns(order_path, cwd)?;
6024 let sort_key = |entry: &DiffEntry| -> usize {
6025 let path = entry
6026 .new_path
6027 .as_ref()
6028 .or(entry.old_path.as_ref())
6029 .cloned()
6030 .unwrap_or_default();
6031 for (i, pat) in patterns.iter().enumerate() {
6032 if orderfile_pattern_matches(pat, &path) {
6033 return i;
6034 }
6035 }
6036 patterns.len()
6037 };
6038 entries.sort_by_key(|e| sort_key(e));
6039 Ok(entries)
6040}
6041
6042pub fn read_orderfile_patterns(order_path: &str, cwd: &Path) -> Result<Vec<String>> {
6044 let path = Path::new(order_path);
6045 let resolved = if path.is_absolute() {
6046 path.to_path_buf()
6047 } else {
6048 cwd.join(path)
6049 };
6050 let _meta = std::fs::metadata(&resolved)
6051 .map_err(|e| Error::Message(format!("could not read orderfile {order_path}: {e}")))?;
6052 let mut f = std::fs::File::open(&resolved)
6053 .map_err(|e| Error::Message(format!("could not read orderfile {order_path}: {e}")))?;
6054 let mut content = String::new();
6055 std::io::Read::read_to_string(&mut f, &mut content)
6056 .map_err(|e| Error::Message(format!("could not read orderfile {order_path}: {e}")))?;
6057 Ok(content
6058 .lines()
6059 .map(|l| l.trim().to_string())
6060 .filter(|l| !l.is_empty() && !l.starts_with('#'))
6061 .collect())
6062}
6063
6064pub fn apply_rotate_skip_entries(
6066 mut entries: Vec<DiffEntry>,
6067 rotate_to: Option<&str>,
6068 skip_to: Option<&str>,
6069) -> Result<Vec<DiffEntry>> {
6070 let Some(needle) = rotate_to.or(skip_to) else {
6071 return Ok(entries);
6072 };
6073 let needle = needle.trim();
6074 if needle.is_empty() {
6075 return Ok(entries);
6076 }
6077 let idx = entries
6078 .iter()
6079 .position(|e| e.path() == needle)
6080 .ok_or_else(|| Error::Message(format!("fatal: No such path '{needle}' in the diff")))?;
6081 if rotate_to.is_some() {
6082 entries.rotate_left(idx);
6083 }
6084 if let Some(skip) = skip_to.filter(|s| !s.trim().is_empty()) {
6085 let pos = entries
6086 .iter()
6087 .position(|e| e.path() == skip)
6088 .ok_or_else(|| Error::Message(format!("fatal: No such path '{skip}' in the diff")))?;
6089 entries.drain(..pos);
6090 }
6091 Ok(entries)
6092}
6093
6094pub fn apply_rotate_skip_log_entries(
6097 odb: &Odb,
6098 commit_tree: &ObjectId,
6099 entries: Vec<DiffEntry>,
6100 rotate_to: Option<&str>,
6101 skip_to: Option<&str>,
6102) -> Result<Vec<DiffEntry>> {
6103 fn trimmed(s: Option<&str>) -> Option<&str> {
6108 s.map(str::trim).filter(|t| !t.is_empty())
6109 }
6110 if trimmed(rotate_to).is_none() && trimmed(skip_to).is_none() {
6111 return Ok(entries);
6112 }
6113 let tree_paths = crate::merge_diff::all_blob_paths_in_tree_order(odb, commit_tree);
6114 apply_rotate_skip_ordered_paths(&tree_paths, entries, rotate_to, skip_to)
6115}
6116
6117fn apply_rotate_skip_ordered_paths(
6118 tree_paths: &[String],
6119 entries: Vec<DiffEntry>,
6120 rotate_to: Option<&str>,
6121 skip_to: Option<&str>,
6122) -> Result<Vec<DiffEntry>> {
6123 let rotate = rotate_to.and_then(|s| {
6124 let t = s.trim();
6125 (!t.is_empty()).then_some(t)
6126 });
6127 let skip = skip_to.and_then(|s| {
6128 let t = s.trim();
6129 (!t.is_empty()).then_some(t)
6130 });
6131 if rotate.is_none() && skip.is_none() {
6132 return Ok(entries);
6133 }
6134
6135 use std::collections::HashMap;
6136 let mut by_path: HashMap<String, DiffEntry> = HashMap::new();
6137 for e in entries {
6138 by_path.insert(e.path().to_string(), e);
6139 }
6140
6141 if rotate.is_none() {
6144 let Some(skip_path) = skip else {
6145 return Ok(by_path.into_values().collect());
6146 };
6147 let idx = tree_paths
6148 .iter()
6149 .position(|p| p == skip_path)
6150 .ok_or_else(|| {
6151 Error::Message(format!("fatal: No such path '{skip_path}' in the diff"))
6152 })?;
6153 let mut out = Vec::new();
6154 for p in tree_paths.iter().skip(idx) {
6155 if let Some(e) = by_path.remove(p) {
6156 out.push(e);
6157 }
6158 }
6159 return Ok(out);
6160 }
6161
6162 let Some(needle) = rotate else {
6163 return Ok(by_path.into_values().collect());
6164 };
6165 let idx = tree_paths
6166 .iter()
6167 .position(|p| p == needle)
6168 .ok_or_else(|| Error::Message(format!("fatal: No such path '{needle}' in the diff")))?;
6169 let mut order: Vec<String> = tree_paths.to_vec();
6170 order.rotate_left(idx);
6171 if let Some(skip_path) = skip {
6172 let pos = order.iter().position(|p| p == skip_path).ok_or_else(|| {
6173 Error::Message(format!("fatal: No such path '{skip_path}' in the diff"))
6174 })?;
6175 order.drain(..pos);
6176 }
6177 let mut out = Vec::new();
6178 for p in order {
6179 if let Some(e) = by_path.remove(&p) {
6180 out.push(e);
6181 }
6182 }
6183 Ok(out)
6184}
6185
6186pub fn orderfile_pattern_matches(pattern: &str, path: &str) -> bool {
6189 let name = path.rsplit('/').next().unwrap_or(path);
6190 orderfile_glob_match(pattern, name) || orderfile_glob_match(pattern, path)
6191}
6192
6193fn orderfile_glob_match(pattern: &str, text: &str) -> bool {
6195 let mut pi = 0;
6196 let mut ti = 0;
6197 let pb = pattern.as_bytes();
6198 let tb = text.as_bytes();
6199 let mut star_pi = usize::MAX;
6200 let mut star_ti = 0;
6201
6202 while ti < tb.len() {
6203 if pi < pb.len() && (pb[pi] == b'?' || pb[pi] == tb[ti]) {
6204 pi += 1;
6205 ti += 1;
6206 } else if pi < pb.len() && pb[pi] == b'*' {
6207 star_pi = pi;
6208 star_ti = ti;
6209 pi += 1;
6210 } else if star_pi != usize::MAX {
6211 pi = star_pi + 1;
6212 star_ti += 1;
6213 ti = star_ti;
6214 } else {
6215 return false;
6216 }
6217 }
6218 while pi < pb.len() && pb[pi] == b'*' {
6219 pi += 1;
6220 }
6221 pi == pb.len()
6222}