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 let mut dir_symlinks = SymlinkDirCache::default();
1949
1950 for ie in &index.entries {
1951 if ie.stage() != 0 {
1952 let path = String::from_utf8_lossy(&ie.path).to_string();
1953 let rank = match ie.stage() {
1954 2 => 0u8,
1955 3 => 1u8,
1956 1 => 2u8,
1957 _ => 3u8,
1958 };
1959 match unmerged_base.get(&path) {
1960 Some((existing_rank, _)) if *existing_rank <= rank => {}
1961 _ => {
1962 unmerged_base.insert(path, (rank, ie));
1963 }
1964 }
1965 continue;
1966 }
1967 if ie.skip_worktree() || ie.assume_unchanged() {
1970 continue;
1971 }
1972 let path_str_ref = std::str::from_utf8(&ie.path).unwrap_or("");
1975 let is_intent_to_add = ie.intent_to_add();
1976
1977 if ie.mode == 0o160000 {
1982 let sub_dir = work_tree.join(path_str_ref);
1983 let sub_head_oid = read_submodule_head_oid(&sub_dir);
1984 if !simplify_gitlinks && sub_head_oid.is_none() && !sub_dir.exists() {
1991 let path_owned = path_str_ref.to_owned();
1992 result.push(DiffEntry {
1993 status: DiffStatus::Deleted,
1994 old_path: Some(path_owned.clone()),
1995 new_path: Some(path_owned),
1996 old_mode: format_mode(ie.mode),
1997 new_mode: "000000".to_owned(),
1998 old_oid: ie.oid,
1999 new_oid: zero_oid(),
2000 score: None,
2001 });
2002 continue;
2003 }
2004 let ref_matches = if let Some(oid) = sub_head_oid {
2005 oid == ie.oid
2006 } else {
2007 let is_placeholder = submodule_worktree_is_unpopulated_placeholder(&sub_dir);
2008 if options.error_on_broken_gitlinks
2009 && !is_placeholder
2010 && submodule_embedded_git_dir(&sub_dir).is_some()
2011 {
2012 return Err(Error::ConfigError(format!(
2013 "could not read submodule HEAD for '{path_str_ref}'"
2014 )));
2015 }
2016 is_placeholder
2017 };
2018 if simplify_gitlinks {
2019 if !ref_matches {
2020 let path_owned = path_str_ref.to_owned();
2021 let new_oid = sub_head_oid.unwrap_or_else(zero_oid);
2022 result.push(DiffEntry {
2023 status: DiffStatus::Modified,
2024 old_path: Some(path_owned.clone()),
2025 new_path: Some(path_owned),
2026 old_mode: format_mode(ie.mode),
2027 new_mode: format_mode(ie.mode),
2028 old_oid: ie.oid,
2029 new_oid,
2030 score: None,
2031 });
2032 }
2033 continue;
2034 }
2035 if sub_head_oid.is_some() && submodule_head_object_broken(&sub_dir) {
2040 return Err(Error::ConfigError(format!(
2041 "'git status --porcelain=2' failed in submodule {path_str_ref}"
2042 )));
2043 }
2044 let mut flags = submodule_porcelain_flags(work_tree, path_str_ref, ie.oid);
2045 if ignore_submodule_untracked {
2046 flags.untracked = false;
2047 }
2048 let inner_dirty = flags.modified || flags.untracked;
2049 if !ref_matches || inner_dirty {
2050 let path_owned = path_str_ref.to_owned();
2051 let new_oid = if !ref_matches {
2052 sub_head_oid.unwrap_or_else(zero_oid)
2053 } else {
2054 zero_oid()
2055 };
2056 result.push(DiffEntry {
2057 status: DiffStatus::Modified,
2058 old_path: Some(path_owned.clone()),
2059 new_path: Some(path_owned),
2060 old_mode: format_mode(ie.mode),
2061 new_mode: format_mode(ie.mode),
2062 old_oid: ie.oid,
2063 new_oid,
2064 score: None,
2065 });
2066 }
2067 continue;
2068 }
2069
2070 let file_path = work_tree.join(path_str_ref);
2071
2072 if is_intent_to_add {
2073 match fs::symlink_metadata(&file_path) {
2074 Ok(meta) => {
2075 let file_attrs = crlf::get_file_attrs(&attrs, path_str_ref, false, &config);
2076 let worktree_oid = hash_worktree_file(
2077 odb,
2078 &file_path,
2079 &meta,
2080 &conv,
2081 &file_attrs,
2082 path_str_ref,
2083 None,
2084 )?;
2085 let worktree_mode = mode_from_metadata(&meta);
2086 result.push(DiffEntry {
2087 status: DiffStatus::Added,
2088 old_path: None,
2089 new_path: Some(path_str_ref.to_owned()),
2090 old_mode: "000000".to_owned(),
2091 new_mode: format_mode(worktree_mode),
2092 old_oid: zero_oid(),
2095 new_oid: worktree_oid,
2096 score: None,
2097 });
2098 }
2099 Err(e)
2100 if e.kind() == std::io::ErrorKind::NotFound
2101 || e.raw_os_error() == Some(20) =>
2102 {
2103 result.push(DiffEntry {
2104 status: DiffStatus::Deleted,
2105 old_path: Some(path_str_ref.to_owned()),
2106 new_path: None,
2107 old_mode: format_mode(ie.mode),
2108 new_mode: "000000".to_owned(),
2109 old_oid: ie.oid,
2110 new_oid: zero_oid(),
2111 score: None,
2112 });
2113 }
2114 Err(e) => return Err(Error::Io(e)),
2115 }
2116 continue;
2117 }
2118
2119 if dir_symlinks.has_symlink_in_path(work_tree, path_str_ref) {
2122 result.push(DiffEntry {
2123 status: DiffStatus::Deleted,
2124 old_path: Some(path_str_ref.to_owned()),
2125 new_path: None,
2126 old_mode: format_mode(ie.mode),
2127 new_mode: "000000".to_owned(),
2128 old_oid: ie.oid,
2129 new_oid: zero_oid(),
2130 score: None,
2131 });
2132 continue;
2133 }
2134
2135 match fs::symlink_metadata(&file_path) {
2136 Ok(meta) if meta.is_dir() => {
2137 if file_path.join(".git").exists() {
2142 let head = read_submodule_head_oid(&file_path).unwrap_or_else(zero_oid);
2143 let path_owned = path_str_ref.to_owned();
2144 result.push(DiffEntry {
2145 status: DiffStatus::TypeChanged,
2146 old_path: Some(path_owned.clone()),
2147 new_path: Some(path_owned),
2148 old_mode: format_mode(ie.mode),
2149 new_mode: format_mode(0o160000),
2150 old_oid: ie.oid,
2151 new_oid: head,
2152 score: None,
2153 });
2154 continue;
2155 }
2156 result.push(DiffEntry {
2157 status: DiffStatus::Deleted,
2158 old_path: Some(path_str_ref.to_owned()),
2159 new_path: None,
2160 old_mode: format_mode(ie.mode),
2161 new_mode: String::new(),
2162 old_oid: ie.oid,
2163 new_oid: zero_oid(),
2164 score: None,
2165 });
2166 }
2167 Ok(meta) => {
2168 let worktree_mode = mode_from_metadata(&meta);
2169 let stat_same = stat_matches(ie, &meta);
2170 if stat_same && worktree_mode != ie.mode {
2172 let path_owned = path_str_ref.to_owned();
2173 result.push(DiffEntry {
2174 status: DiffStatus::Modified,
2175 old_path: Some(path_owned.clone()),
2176 new_path: Some(path_owned),
2177 old_mode: format_mode(ie.mode),
2178 new_mode: format_mode(worktree_mode),
2179 old_oid: ie.oid,
2180 new_oid: ie.oid,
2181 score: None,
2182 });
2183 continue;
2184 }
2185
2186 if stat_same && worktree_mode == ie.mode && !entry_is_racy(ie, options.index_mtime) {
2189 continue;
2190 }
2191
2192 let file_attrs = crlf::get_file_attrs(&attrs, path_str_ref, false, &config);
2194 let worktree_oid = hash_worktree_file(
2195 odb,
2196 &file_path,
2197 &meta,
2198 &conv,
2199 &file_attrs,
2200 path_str_ref,
2201 Some(ie),
2202 )?;
2203
2204 let mut eff_oid = worktree_oid;
2208 if eff_oid != ie.oid {
2209 if let Ok(raw) = fs::read(&file_path) {
2210 let raw_oid = Odb::hash_object_data(ObjectKind::Blob, &raw);
2211 if raw_oid == ie.oid {
2212 eff_oid = ie.oid;
2213 }
2214 }
2215 }
2216
2217 if eff_oid != ie.oid || worktree_mode != ie.mode {
2218 let path_owned = path_str_ref.to_owned();
2219 result.push(DiffEntry {
2220 status: DiffStatus::Modified,
2221 old_path: Some(path_owned.clone()),
2222 new_path: Some(path_owned),
2223 old_mode: format_mode(ie.mode),
2224 new_mode: format_mode(worktree_mode),
2225 old_oid: ie.oid,
2226 new_oid: eff_oid,
2227 score: None,
2228 });
2229 }
2230 }
2231 Err(e) if e.kind() == std::io::ErrorKind::NotFound
2232 || e.raw_os_error() == Some(20) => {
2233 result.push(DiffEntry {
2235 status: DiffStatus::Deleted,
2236 old_path: Some(path_str_ref.to_owned()),
2237 new_path: None,
2238 old_mode: format_mode(ie.mode),
2239 new_mode: "000000".to_owned(),
2240 old_oid: ie.oid,
2241 new_oid: zero_oid(),
2242 score: None,
2243 });
2244 }
2245 Err(e) => return Err(Error::Io(e)),
2246 }
2247 }
2248
2249 for (path, (_, base_entry)) in unmerged_base {
2250 let file_path = work_tree.join(&path);
2251 let wt_meta = match fs::symlink_metadata(&file_path) {
2252 Ok(meta) => Some(meta),
2253 Err(e)
2254 if e.kind() == std::io::ErrorKind::NotFound
2255 || e.raw_os_error() == Some(20) =>
2256 {
2257 None
2258 }
2259 Err(e) => return Err(Error::Io(e)),
2260 };
2261
2262 let new_mode = wt_meta.as_ref().map_or_else(
2263 || "000000".to_owned(),
2264 |meta| format_mode(mode_from_metadata(meta)),
2265 );
2266 result.push(DiffEntry {
2267 status: DiffStatus::Unmerged,
2268 old_path: Some(path.clone()),
2269 new_path: Some(path.clone()),
2270 old_mode: "000000".to_owned(),
2271 new_mode,
2272 old_oid: zero_oid(),
2273 new_oid: zero_oid(),
2274 score: None,
2275 });
2276
2277 if let Some(meta) = wt_meta {
2278 let file_attrs = crlf::get_file_attrs(&attrs, &path, false, &config);
2279 let wt_oid = hash_worktree_file(
2280 odb,
2281 &file_path,
2282 &meta,
2283 &conv,
2284 &file_attrs,
2285 &path,
2286 Some(base_entry),
2287 )?;
2288 let wt_mode = mode_from_metadata(&meta);
2289 if wt_oid != base_entry.oid || wt_mode != base_entry.mode {
2290 result.push(DiffEntry {
2291 status: DiffStatus::Modified,
2292 old_path: Some(path.clone()),
2293 new_path: Some(path),
2294 old_mode: format_mode(base_entry.mode),
2295 new_mode: format_mode(wt_mode),
2296 old_oid: base_entry.oid,
2297 new_oid: wt_oid,
2298 score: None,
2299 });
2300 }
2301 }
2302 }
2303
2304 Ok(result)
2305}
2306
2307#[derive(Default)]
2310struct SymlinkDirCache {
2311 symlink: std::collections::HashSet<String>,
2313 plain: std::collections::HashSet<String>,
2315}
2316
2317impl SymlinkDirCache {
2318 fn has_symlink_in_path(&mut self, work_tree: &Path, rel_path: &str) -> bool {
2320 let components: Vec<&str> = rel_path.split('/').collect();
2321 let mut prefix = String::new();
2322 for component in &components[..components.len().saturating_sub(1)] {
2324 if !prefix.is_empty() {
2325 prefix.push('/');
2326 }
2327 prefix.push_str(component);
2328 if self.symlink.contains(&prefix) {
2329 return true;
2330 }
2331 if self.plain.contains(&prefix) {
2332 continue;
2333 }
2334 match fs::symlink_metadata(work_tree.join(&prefix)) {
2335 Ok(meta) if meta.file_type().is_symlink() => {
2336 self.symlink.insert(prefix.clone());
2337 return true;
2338 }
2339 _ => {
2340 self.plain.insert(prefix.clone());
2341 }
2342 }
2343 }
2344 false
2345 }
2346}
2347
2348fn entry_is_racy(ie: &IndexEntry, index_mtime: Option<(u32, u32)>) -> bool {
2349 let Some((index_mtime_sec, index_mtime_nsec)) = index_mtime else {
2350 return false;
2351 };
2352 if index_mtime_sec == 0 {
2353 return false;
2354 }
2355 index_mtime_sec < ie.mtime_sec
2356 || (index_mtime_sec == ie.mtime_sec && index_mtime_nsec <= ie.mtime_nsec)
2357}
2358
2359pub fn worktree_differs_from_index_entry(
2367 odb: &Odb,
2368 work_tree: &Path,
2369 ie: &IndexEntry,
2370 ignore_submodule_untracked: bool,
2371) -> Result<bool> {
2372 use crate::config::ConfigSet;
2373 use crate::crlf;
2374
2375 let path_str_ref = std::str::from_utf8(&ie.path).unwrap_or("");
2376 let file_path = work_tree.join(path_str_ref);
2377
2378 if ie.mode == 0o160000 {
2379 let sub_head_oid = read_submodule_head(&file_path);
2380 let ref_matches = match sub_head_oid {
2381 Some(oid) => oid == ie.oid,
2382 None => submodule_worktree_is_unpopulated_placeholder(&file_path),
2383 };
2384 let mut flags = submodule_porcelain_flags(work_tree, path_str_ref, ie.oid);
2385 if ignore_submodule_untracked {
2386 flags.untracked = false;
2387 }
2388 return Ok(!ref_matches || flags.modified || flags.untracked);
2389 }
2390
2391 let meta = match fs::symlink_metadata(&file_path) {
2392 Ok(m) => m,
2393 Err(e)
2394 if e.kind() == std::io::ErrorKind::NotFound
2395 || e.raw_os_error() == Some(20) =>
2396 {
2397 return Ok(true);
2398 }
2399 Err(e) => return Err(Error::Io(e)),
2400 };
2401
2402 if meta.is_dir() {
2403 return Ok(true);
2404 }
2405
2406 let worktree_mode = mode_from_metadata(&meta);
2407 if worktree_mode != ie.mode {
2408 return Ok(true);
2409 }
2410
2411 let git_dir = work_tree.join(".git");
2412 let config = ConfigSet::load(Some(&git_dir), true).unwrap_or_else(|_| ConfigSet::new());
2413 let conv = crlf::ConversionConfig::from_config(&config);
2414 let attrs = crlf::load_gitattributes(work_tree);
2415 let file_attrs = crlf::get_file_attrs(&attrs, path_str_ref, false, &config);
2416 let worktree_oid = hash_worktree_file(
2417 odb,
2418 &file_path,
2419 &meta,
2420 &conv,
2421 &file_attrs,
2422 path_str_ref,
2423 Some(ie),
2424 )?;
2425
2426 let mut eff_oid = worktree_oid;
2427 if eff_oid != ie.oid {
2428 if let Ok(raw) = fs::read(&file_path) {
2429 let raw_oid = Odb::hash_object_data(ObjectKind::Blob, &raw);
2430 if raw_oid == ie.oid {
2431 eff_oid = ie.oid;
2432 }
2433 }
2434 }
2435
2436 Ok(eff_oid != ie.oid)
2437}
2438
2439pub fn stat_matches(ie: &IndexEntry, meta: &fs::Metadata) -> bool {
2440 if meta.len() as u32 != ie.size {
2442 return false;
2443 }
2444 #[cfg(unix)]
2445 {
2446 use std::os::unix::fs::MetadataExt;
2447 if meta.mtime() as u32 != ie.mtime_sec {
2449 return false;
2450 }
2451 if meta.mtime_nsec() as u32 != ie.mtime_nsec {
2452 return false;
2453 }
2454 if meta.ctime() as u32 != ie.ctime_sec {
2456 return false;
2457 }
2458 if meta.ctime_nsec() as u32 != ie.ctime_nsec {
2459 return false;
2460 }
2461 if meta.ino() as u32 != ie.ino {
2466 return false;
2467 }
2468 }
2469 #[cfg(not(unix))]
2470 {
2471 use std::time::UNIX_EPOCH;
2472 if let Ok(mtime) = meta.modified() {
2473 if let Ok(dur) = mtime.duration_since(UNIX_EPOCH) {
2474 if dur.as_secs() as u32 != ie.mtime_sec {
2475 return false;
2476 }
2477 if dur.subsec_nanos() != ie.mtime_nsec {
2478 return false;
2479 }
2480 }
2481 }
2482 }
2483 true
2484}
2485
2486pub fn refresh_index_stat_content_verified(
2506 index: &mut Index,
2507 work_tree: &Path,
2508 index_mtime: Option<(u32, u32)>,
2509) -> bool {
2510 use crate::index::{MODE_EXECUTABLE, MODE_REGULAR, MODE_SYMLINK};
2511 let mut changed = false;
2512 for ie in &mut index.entries {
2513 if ie.stage() != 0 || ie.skip_worktree() || ie.assume_unchanged() || ie.intent_to_add() {
2514 continue;
2515 }
2516 if ie.mode != MODE_REGULAR && ie.mode != MODE_EXECUTABLE && ie.mode != MODE_SYMLINK {
2517 continue;
2518 }
2519 let Ok(path) = std::str::from_utf8(&ie.path) else {
2520 continue;
2521 };
2522 let abs = work_tree.join(path);
2523 let Ok(meta) = fs::symlink_metadata(&abs) else {
2524 continue;
2525 };
2526 if stat_matches(ie, &meta) {
2527 if entry_is_racy(ie, index_mtime)
2533 && !worktree_content_matches_index_oid(ie, &abs, &meta)
2534 {
2535 invalidate_index_stat_cache(ie);
2536 changed = true;
2537 }
2538 continue;
2539 }
2540 if !worktree_content_matches_index_oid(ie, &abs, &meta) {
2541 continue;
2542 }
2543 let refreshed = crate::index::entry_from_metadata(&meta, &ie.path, ie.oid, ie.mode);
2544 ie.ctime_sec = refreshed.ctime_sec;
2545 ie.ctime_nsec = refreshed.ctime_nsec;
2546 ie.mtime_sec = refreshed.mtime_sec;
2547 ie.mtime_nsec = refreshed.mtime_nsec;
2548 ie.dev = refreshed.dev;
2549 ie.ino = refreshed.ino;
2550 ie.uid = refreshed.uid;
2551 ie.gid = refreshed.gid;
2552 ie.size = refreshed.size;
2553 changed = true;
2554 }
2555 changed
2556}
2557
2558fn symlink_target_bytes(target: &Path) -> Vec<u8> {
2564 #[cfg(unix)]
2565 {
2566 use std::os::unix::ffi::OsStrExt as _;
2567 target.as_os_str().as_bytes().to_vec()
2568 }
2569 #[cfg(not(unix))]
2570 {
2571 target.as_os_str().to_string_lossy().as_bytes().to_vec()
2572 }
2573}
2574
2575fn worktree_content_matches_index_oid(ie: &IndexEntry, abs: &Path, meta: &fs::Metadata) -> bool {
2577 use crate::index::{MODE_EXECUTABLE, MODE_REGULAR, MODE_SYMLINK};
2578 if ie.mode == MODE_SYMLINK {
2579 if !meta.file_type().is_symlink() {
2580 return false;
2581 }
2582 fs::read_link(abs)
2583 .map(|t| Odb::hash_object_data(ObjectKind::Blob, &symlink_target_bytes(&t)) == ie.oid)
2584 .unwrap_or(false)
2585 } else if ie.mode == MODE_REGULAR || ie.mode == MODE_EXECUTABLE {
2586 if !meta.file_type().is_file() {
2587 return false;
2588 }
2589 fs::read(abs)
2590 .map(|bytes| Odb::hash_object_data(ObjectKind::Blob, &bytes) == ie.oid)
2591 .unwrap_or(false)
2592 } else {
2593 false
2594 }
2595}
2596
2597fn invalidate_index_stat_cache(ie: &mut IndexEntry) {
2599 ie.ctime_sec = 0;
2600 ie.ctime_nsec = 0;
2601 ie.mtime_sec = 0;
2602 ie.mtime_nsec = 0;
2603 ie.dev = 0;
2604 ie.ino = 0;
2605 ie.size = 0;
2606}
2607
2608pub fn hash_worktree_file(
2609 odb: &Odb,
2610 path: &Path,
2611 meta: &fs::Metadata,
2612 conv: &crate::crlf::ConversionConfig,
2613 file_attrs: &crate::crlf::FileAttrs,
2614 rel_path: &str,
2615 index_entry: Option<&IndexEntry>,
2616) -> Result<ObjectId> {
2617 let prior_blob: Option<Vec<u8>> = index_entry
2618 .filter(|e| e.oid != zero_oid())
2619 .and_then(|e| odb.read(&e.oid).ok().map(|o| o.data));
2620 let data = if meta.file_type().is_symlink() {
2621 let target = fs::read_link(path)?;
2623 target.to_string_lossy().into_owned().into_bytes()
2624 } else if meta.is_dir() {
2625 Vec::new()
2628 } else {
2629 let raw = fs::read(path)?;
2630 let opts = crate::crlf::ConvertToGitOpts {
2633 index_blob: prior_blob.as_deref(),
2634 renormalize: false,
2635 check_safecrlf: false,
2636 };
2637 crate::crlf::convert_to_git_with_opts(&raw, rel_path, conv, file_attrs, opts).unwrap_or(raw)
2638 };
2639
2640 Ok(Odb::hash_object_data(ObjectKind::Blob, &data))
2641}
2642
2643pub fn mode_from_metadata(meta: &fs::Metadata) -> u32 {
2645 if meta.file_type().is_symlink() {
2646 0o120000
2647 } else {
2648 #[cfg(unix)]
2649 {
2650 if meta.mode() & 0o111 != 0 {
2651 return 0o100755;
2652 }
2653 }
2654 0o100644
2655 }
2656}
2657
2658pub fn diff_tree_to_worktree(
2675 odb: &Odb,
2676 tree_oid: Option<&ObjectId>,
2677 work_tree: &Path,
2678 index: &Index,
2679) -> Result<Vec<DiffEntry>> {
2680 use crate::config::ConfigSet;
2681 use crate::crlf;
2682
2683 let git_dir = work_tree.join(".git");
2684 let config = ConfigSet::load(Some(&git_dir), true).unwrap_or_else(|_| ConfigSet::new());
2685 let conv = crlf::ConversionConfig::from_config(&config);
2686 let attrs = crlf::load_gitattributes(work_tree);
2687
2688 let tree_flat = match tree_oid {
2690 Some(oid) => flatten_tree(odb, oid, "")?,
2691 None => Vec::new(),
2692 };
2693 let tree_map: std::collections::BTreeMap<String, &FlatEntry> =
2694 tree_flat.iter().map(|e| (e.path.clone(), e)).collect();
2695
2696 let mut index_entries: std::collections::BTreeMap<&[u8], &IndexEntry> =
2698 std::collections::BTreeMap::new();
2699 let mut index_paths: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
2700 let mut stage0_paths: std::collections::BTreeSet<Vec<u8>> = std::collections::BTreeSet::new();
2701 for ie in &index.entries {
2702 if ie.stage() != 0 {
2703 continue;
2704 }
2705 let path = String::from_utf8_lossy(&ie.path).to_string();
2706 index_entries.insert(&ie.path, ie);
2707 index_paths.insert(path);
2708 stage0_paths.insert(ie.path.clone());
2709 }
2710
2711 let mut unmerged_only_paths: std::collections::BTreeSet<String> =
2714 std::collections::BTreeSet::new();
2715 for ie in &index.entries {
2716 if !(1..=3).contains(&ie.stage()) {
2717 continue;
2718 }
2719 if stage0_paths.contains(&ie.path) {
2720 continue;
2721 }
2722 unmerged_only_paths.insert(String::from_utf8_lossy(&ie.path).into_owned());
2723 }
2724
2725 let mut all_paths: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
2727 all_paths.extend(tree_map.keys().cloned());
2728 all_paths.extend(index_paths.iter().cloned());
2729 all_paths.extend(unmerged_only_paths.iter().cloned());
2730
2731 let mut result = Vec::new();
2732
2733 for path in &all_paths {
2734 if index_entries
2735 .get(path.as_bytes())
2736 .is_some_and(|ie| ie.skip_worktree())
2737 {
2738 continue;
2741 }
2742
2743 let tree_entry = tree_map.get(path.as_str());
2744
2745 let is_gitlink = tree_entry.is_some_and(|te| te.mode == 0o160000)
2747 || index_entries
2748 .get(path.as_bytes())
2749 .is_some_and(|ie| ie.mode == 0o160000);
2750 if is_gitlink {
2751 let sub_dir = work_tree.join(path);
2752 let index_gitlink_oid = index_entries
2753 .get(path.as_bytes())
2754 .filter(|ie| ie.mode == 0o160000)
2755 .map(|ie| ie.oid);
2756 match (tree_entry, index_gitlink_oid) {
2757 (Some(te), _) => {
2758 let sub_head = read_submodule_head_oid(&sub_dir);
2759 if sub_head.is_none() && !sub_dir.exists() {
2763 result.push(DiffEntry {
2764 status: DiffStatus::Deleted,
2765 old_path: Some(path.clone()),
2766 new_path: Some(path.clone()),
2767 old_mode: format_mode(te.mode),
2768 new_mode: "000000".to_string(),
2769 old_oid: te.oid,
2770 new_oid: zero_oid(),
2771 score: None,
2772 });
2773 continue;
2774 }
2775 let index_matches_tree = index_gitlink_oid.is_some_and(|oid| oid == te.oid);
2776 let head_differs = sub_head.as_ref() != Some(&te.oid);
2777 let dirty_while_aligned = index_matches_tree
2778 && !head_differs
2779 && submodule_has_dirty_worktree_for_super_diff(work_tree, path, &te.oid);
2780 if head_differs || dirty_while_aligned {
2781 let new_oid = if head_differs { zero_oid() } else { te.oid };
2785 result.push(DiffEntry {
2786 status: DiffStatus::Modified,
2787 old_path: Some(path.clone()),
2788 new_path: Some(path.clone()),
2789 old_mode: format_mode(te.mode),
2790 new_mode: format_mode(te.mode),
2791 old_oid: te.oid,
2792 new_oid,
2793 score: None,
2794 });
2795 }
2796 }
2797 (None, Some(idx_oid)) => {
2798 let new_oid = read_submodule_head_oid(&sub_dir).unwrap_or(idx_oid);
2803 result.push(DiffEntry {
2804 status: DiffStatus::Added,
2805 old_path: Some(path.clone()),
2806 new_path: Some(path.clone()),
2807 old_mode: "000000".to_string(),
2808 new_mode: format_mode(0o160000),
2809 old_oid: zero_oid(),
2810 new_oid,
2811 score: None,
2812 });
2813 }
2814 (None, None) => {}
2815 }
2816 continue;
2817 }
2818
2819 let file_path = work_tree.join(path);
2820
2821 let wt_meta = match fs::symlink_metadata(&file_path) {
2822 Ok(m) => Some(m),
2823 Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
2824 Err(e) => return Err(Error::Io(e)),
2825 };
2826
2827 if unmerged_only_paths.contains(path) {
2828 if let (Some(te), Some(meta)) = (tree_entry, wt_meta.as_ref()) {
2829 let file_attrs = crlf::get_file_attrs(&attrs, path, false, &config);
2830 let wt_oid =
2831 hash_worktree_file(odb, &file_path, meta, &conv, &file_attrs, path, None)?;
2832 let wt_mode = mode_from_metadata(meta);
2833 if wt_oid != te.oid || wt_mode != te.mode {
2834 result.push(DiffEntry {
2835 status: DiffStatus::Modified,
2836 old_path: Some(path.clone()),
2837 new_path: Some(path.clone()),
2838 old_mode: format_mode(te.mode),
2839 new_mode: format_mode(wt_mode),
2840 old_oid: te.oid,
2841 new_oid: wt_oid,
2842 score: None,
2843 });
2844 }
2845 }
2846 continue;
2847 }
2848
2849 match (tree_entry, wt_meta) {
2850 (Some(te), Some(ref meta)) => {
2851 let wt_mode = mode_from_metadata(meta);
2852 let Some(ie) = index_entries.get(path.as_bytes()) else {
2853 continue;
2854 };
2855
2856 let index_matches_tree = ie.oid == te.oid && ie.mode == te.mode;
2857
2858 if index_matches_tree && wt_mode == te.mode && stat_matches(ie, meta) {
2860 continue;
2861 }
2862
2863 let file_attrs = crlf::get_file_attrs(&attrs, path, false, &config);
2864 let idx_ent = index_entries.get(path.as_bytes()).copied();
2865
2866 if ie.oid == te.oid && ie.mode != te.mode {
2868 result.push(DiffEntry {
2869 status: DiffStatus::Modified,
2870 old_path: Some(path.clone()),
2871 new_path: Some(path.clone()),
2872 old_mode: format_mode(te.mode),
2873 new_mode: format_mode(ie.mode),
2874 old_oid: te.oid,
2875 new_oid: te.oid,
2876 score: None,
2877 });
2878 continue;
2879 }
2880
2881 if index_matches_tree {
2884 let wt_oid = hash_worktree_file(
2885 odb,
2886 &file_path,
2887 meta,
2888 &conv,
2889 &file_attrs,
2890 path,
2891 idx_ent,
2892 )?;
2893 let mut eff_oid = wt_oid;
2894 if eff_oid != te.oid {
2895 if let Ok(raw) = fs::read(&file_path) {
2896 let raw_oid = Odb::hash_object_data(ObjectKind::Blob, &raw);
2897 if raw_oid == te.oid {
2898 eff_oid = te.oid;
2899 }
2900 }
2901 }
2902 if eff_oid != te.oid {
2903 result.push(DiffEntry {
2904 status: DiffStatus::Modified,
2905 old_path: Some(path.clone()),
2906 new_path: Some(path.clone()),
2907 old_mode: format_mode(te.mode),
2908 new_mode: format_mode(wt_mode),
2909 old_oid: te.oid,
2910 new_oid: eff_oid,
2911 score: None,
2912 });
2913 } else if wt_mode != te.mode {
2914 result.push(DiffEntry {
2915 status: DiffStatus::Modified,
2916 old_path: Some(path.clone()),
2917 new_path: Some(path.clone()),
2918 old_mode: format_mode(te.mode),
2919 new_mode: format_mode(wt_mode),
2920 old_oid: te.oid,
2921 new_oid: te.oid,
2922 score: None,
2923 });
2924 }
2925 continue;
2926 }
2927
2928 let wt_oid =
2930 hash_worktree_file(odb, &file_path, meta, &conv, &file_attrs, path, idx_ent)?;
2931 let mut eff_oid = wt_oid;
2932 if eff_oid != te.oid {
2933 if let Ok(raw) = fs::read(&file_path) {
2934 let raw_oid = Odb::hash_object_data(ObjectKind::Blob, &raw);
2935 if raw_oid == te.oid {
2936 eff_oid = te.oid;
2937 }
2938 }
2939 }
2940 if eff_oid != te.oid || wt_mode != te.mode {
2941 result.push(DiffEntry {
2942 status: DiffStatus::Modified,
2943 old_path: Some(path.clone()),
2944 new_path: Some(path.clone()),
2945 old_mode: format_mode(te.mode),
2946 new_mode: format_mode(wt_mode),
2947 old_oid: te.oid,
2948 new_oid: eff_oid,
2949 score: None,
2950 });
2951 }
2952 }
2953 (Some(te), None) => {
2954 result.push(DiffEntry {
2956 status: DiffStatus::Deleted,
2957 old_path: Some(path.clone()),
2958 new_path: None,
2959 old_mode: format_mode(te.mode),
2960 new_mode: "000000".to_owned(),
2961 old_oid: te.oid,
2962 new_oid: zero_oid(),
2963 score: None,
2964 });
2965 }
2966 (None, Some(ref meta)) => {
2967 let file_attrs = crlf::get_file_attrs(&attrs, path, false, &config);
2969 let wt_oid = hash_worktree_file(
2970 odb,
2971 &file_path,
2972 meta,
2973 &conv,
2974 &file_attrs,
2975 path,
2976 index_entries.get(path.as_bytes()).copied(),
2977 )?;
2978 let wt_mode = mode_from_metadata(meta);
2979 result.push(DiffEntry {
2980 status: DiffStatus::Added,
2981 old_path: None,
2982 new_path: Some(path.clone()),
2983 old_mode: "000000".to_owned(),
2984 new_mode: format_mode(wt_mode),
2985 old_oid: zero_oid(),
2986 new_oid: wt_oid,
2987 score: None,
2988 });
2989 }
2990 (None, None) => {
2991 }
2993 }
2994 }
2995
2996 result.sort_by(|a, b| a.path().cmp(b.path()));
2997 Ok(result)
2998}
2999
3000fn read_added_entry_bytes(
3003 odb: &Odb,
3004 entry: &DiffEntry,
3005 work_root: Option<&Path>,
3006) -> Option<Vec<u8>> {
3007 if entry.new_oid != zero_oid() {
3008 return odb.read(&entry.new_oid).ok().map(|obj| obj.data);
3009 }
3010 let path = entry.new_path.as_deref()?;
3011 let root = work_root?;
3012 fs::read(root.join(path)).ok()
3013}
3014
3015fn modified_as_copy_from_sources(
3016 odb: &Odb,
3017 work_root: Option<&Path>,
3018 e: &DiffEntry,
3019 threshold: u32,
3020 sources: &[(String, ObjectId, bool)],
3021 source_contents: &[Option<Vec<u8>>],
3022 source_tree_entries: &[(String, String, ObjectId)],
3023) -> Option<DiffEntry> {
3024 fn regular_file_mode(mode: &str) -> bool {
3025 mode == "100644" || mode == "100755"
3026 }
3027
3028 if e.status != DiffStatus::Modified || !regular_file_mode(&e.new_mode) {
3029 return None;
3030 }
3031 let new_data = read_added_entry_bytes(odb, e, work_root)?;
3032 let new_oid_eff = if e.new_oid != zero_oid() {
3033 e.new_oid
3034 } else {
3035 Odb::hash_object_data(ObjectKind::Blob, &new_data)
3036 };
3037
3038 let mut best: Option<(usize, u32)> = None;
3039 for (si, (src_path, src_oid, is_deleted)) in sources.iter().enumerate() {
3040 if *is_deleted {
3041 continue;
3042 }
3043 if e.new_path.as_deref() == Some(src_path.as_str()) {
3044 continue;
3045 }
3046 let src_mode_str = source_tree_entries
3047 .iter()
3048 .find(|(p, _, _)| p == src_path)
3049 .map(|(_, m, _)| m.as_str())
3050 .unwrap_or("100644");
3051 if !regular_file_mode(src_mode_str) {
3052 continue;
3053 }
3054
3055 let score = if *src_oid == new_oid_eff {
3056 100
3057 } else {
3058 match (&source_contents[si], Some(new_data.as_slice())) {
3059 (Some(old_data), Some(nd)) => compute_similarity(old_data, nd),
3060 _ => 0,
3061 }
3062 };
3063 if score >= threshold {
3064 let replace = match best {
3065 None => true,
3066 Some((_, s)) => score > s,
3067 };
3068 if replace {
3069 best = Some((si, score));
3070 }
3071 }
3072 }
3073
3074 let (si, score) = best?;
3075 let (src_path, src_oid, _) = &sources[si];
3076 let src_mode = source_tree_entries
3077 .iter()
3078 .find(|(p, _, _)| p == src_path)
3079 .map(|(_, m, _)| m.clone())
3080 .unwrap_or_else(|| e.old_mode.clone());
3081
3082 Some(DiffEntry {
3083 status: DiffStatus::Copied,
3084 old_path: Some(src_path.clone()),
3085 new_path: e.new_path.clone(),
3086 old_mode: src_mode,
3087 new_mode: e.new_mode.clone(),
3088 old_oid: *src_oid,
3089 new_oid: e.new_oid,
3090 score: Some(score),
3091 })
3092}
3093
3094pub fn detect_renames(
3106 odb: &Odb,
3107 work_root: Option<&Path>,
3108 entries: Vec<DiffEntry>,
3109 threshold: u32,
3110) -> Vec<DiffEntry> {
3111 let mut deleted: Vec<DiffEntry> = Vec::new();
3113 let mut added: Vec<DiffEntry> = Vec::new();
3114 let mut others: Vec<DiffEntry> = Vec::new();
3115
3116 for entry in entries {
3117 match entry.status {
3118 DiffStatus::Deleted => deleted.push(entry),
3119 DiffStatus::Added => added.push(entry),
3120 _ => others.push(entry),
3121 }
3122 }
3123
3124 if deleted.is_empty() || added.is_empty() {
3125 let mut result = others;
3127 result.extend(deleted);
3128 result.extend(added);
3129 result.sort_by(|a, b| a.path().cmp(b.path()));
3130 return result;
3131 }
3132
3133 let deleted_contents: Vec<Option<Vec<u8>>> = deleted
3135 .iter()
3136 .map(|d| odb.read(&d.old_oid).ok().map(|obj| obj.data))
3137 .collect();
3138
3139 let added_contents: Vec<Option<Vec<u8>>> = added
3141 .iter()
3142 .map(|a| read_added_entry_bytes(odb, a, work_root))
3143 .collect();
3144
3145 let mut scores: Vec<(u32, usize, usize)> = Vec::new();
3148
3149 fn is_regularish_mode(mode: &str) -> bool {
3150 mode == "100644" || mode == "100755"
3151 }
3152
3153 fn same_path_same_blob(del: &DiffEntry, add: &DiffEntry) -> bool {
3154 del.old_path == add.new_path && del.old_oid == add.new_oid && del.old_mode == add.new_mode
3155 }
3156
3157 for (di, del) in deleted.iter().enumerate() {
3158 for (ai, add) in added.iter().enumerate() {
3159 if del.old_oid == add.new_oid {
3161 scores.push((100, di, ai));
3162 continue;
3163 }
3164
3165 if !is_regularish_mode(&del.old_mode) || !is_regularish_mode(&add.new_mode) {
3168 continue;
3169 }
3170
3171 let score = match (&deleted_contents[di], &added_contents[ai]) {
3172 (Some(old_data), Some(new_data)) => compute_similarity(old_data, new_data),
3173 _ => 0,
3174 };
3175
3176 if score >= threshold {
3177 scores.push((score, di, ai));
3178 }
3179 }
3180 }
3181
3182 scores.sort_by(|a, b| {
3186 let a_noop = same_path_same_blob(&deleted[a.1], &added[a.2]);
3187 let b_noop = same_path_same_blob(&deleted[b.1], &added[b.2]);
3188 let a_same = same_basename(&deleted[a.1], &added[a.2]);
3189 let b_same = same_basename(&deleted[b.1], &added[b.2]);
3190 a_noop
3191 .cmp(&b_noop)
3192 .then_with(|| b_same.cmp(&a_same))
3193 .then_with(|| b.0.cmp(&a.0))
3194 });
3195
3196 let mut used_deleted = vec![false; deleted.len()];
3197 let mut used_added = vec![false; added.len()];
3198 let mut renames: Vec<DiffEntry> = Vec::new();
3199
3200 for (score, di, ai) in &scores {
3201 if used_deleted[*di] || used_added[*ai] {
3202 continue;
3203 }
3204 used_deleted[*di] = true;
3205 used_added[*ai] = true;
3206
3207 let del = &deleted[*di];
3208 let add = &added[*ai];
3209
3210 if same_path_same_blob(del, add) {
3215 continue;
3216 }
3217
3218 renames.push(DiffEntry {
3219 status: DiffStatus::Renamed,
3220 old_path: del.old_path.clone(),
3221 new_path: add.new_path.clone(),
3222 old_mode: del.old_mode.clone(),
3223 new_mode: add.new_mode.clone(),
3224 old_oid: del.old_oid,
3225 new_oid: add.new_oid,
3226 score: Some(*score),
3227 });
3228 }
3229
3230 let mut result = others;
3232 result.extend(renames);
3233 for (i, entry) in deleted.into_iter().enumerate() {
3234 if !used_deleted[i] {
3235 result.push(entry);
3236 }
3237 }
3238 for (i, entry) in added.into_iter().enumerate() {
3239 if !used_added[i] {
3240 result.push(entry);
3241 }
3242 }
3243
3244 result.sort_by(|a, b| a.path().cmp(b.path()));
3245 result
3246}
3247
3248pub fn detect_copies(
3259 odb: &Odb,
3260 work_root: Option<&Path>,
3261 entries: Vec<DiffEntry>,
3262 threshold: u32,
3263 find_copies_harder: bool,
3264 source_tree_entries: &[(String, String, ObjectId)],
3265) -> Vec<DiffEntry> {
3266 use std::collections::{HashMap, HashSet};
3267
3268 let mut deleted: Vec<DiffEntry> = Vec::new();
3270 let mut added: Vec<DiffEntry> = Vec::new();
3271 let mut others: Vec<DiffEntry> = Vec::new();
3272
3273 for entry in entries {
3274 match entry.status {
3275 DiffStatus::Deleted => deleted.push(entry),
3276 DiffStatus::Added => added.push(entry),
3277 _ => others.push(entry),
3278 }
3279 }
3280
3281 let mut sources: Vec<(String, ObjectId, bool)> = Vec::new(); let mut deleted_source_idx: HashMap<String, usize> = HashMap::new();
3285
3286 for entry in &deleted {
3287 if let Some(ref path) = entry.old_path {
3288 deleted_source_idx.insert(path.clone(), sources.len());
3289 sources.push((path.clone(), entry.old_oid, true));
3290 }
3291 }
3292
3293 for entry in &others {
3296 if matches!(entry.status, DiffStatus::Modified | DiffStatus::TypeChanged) {
3297 if let Some(ref old_path) = entry.old_path {
3298 if !sources.iter().any(|(p, _, _)| p == old_path) {
3299 sources.push((old_path.clone(), entry.old_oid, false));
3300 }
3301 }
3302 }
3303 }
3304
3305 if find_copies_harder {
3307 for (path, _mode, oid) in source_tree_entries {
3308 if !sources.iter().any(|(p, _, _)| p == path) {
3309 sources.push((path.clone(), *oid, false));
3310 }
3311 }
3312 }
3313
3314 if sources.is_empty() {
3315 let mut result = others;
3316 result.extend(deleted);
3317 result.extend(added);
3318 result.sort_by(|a, b| a.path().cmp(b.path()));
3319 return result;
3320 }
3321
3322 let source_contents: Vec<Option<Vec<u8>>> = sources
3324 .iter()
3325 .map(|(_, oid, _)| odb.read(oid).ok().map(|obj| obj.data))
3326 .collect();
3327
3328 let mut result_entries: Vec<DiffEntry> = Vec::new();
3329 let mut renamed_deleted: HashSet<usize> = HashSet::new();
3330 let mut used_added2 = vec![false; added.len()];
3331
3332 if !added.is_empty() {
3333 let added_contents: Vec<Option<Vec<u8>>> = added
3335 .iter()
3336 .map(|a| read_added_entry_bytes(odb, a, work_root))
3337 .collect();
3338
3339 let mut scores: Vec<(u32, usize, usize)> = Vec::new();
3341 for (si, (src_path, src_oid, _)) in sources.iter().enumerate() {
3342 for (ai, add) in added.iter().enumerate() {
3343 if add.new_path.as_deref() == Some(src_path.as_str()) {
3346 continue;
3347 }
3348 let add_oid = if add.new_oid != zero_oid() {
3349 add.new_oid
3350 } else if let Some(ref data) = added_contents[ai] {
3351 Odb::hash_object_data(ObjectKind::Blob, data)
3352 } else {
3353 zero_oid()
3354 };
3355 if *src_oid == add_oid {
3356 scores.push((100, si, ai));
3357 continue;
3358 }
3359 let score = match (&source_contents[si], &added_contents[ai]) {
3360 (Some(old_data), Some(new_data)) => compute_similarity(old_data, new_data),
3361 _ => 0,
3362 };
3363 if score >= threshold {
3364 scores.push((score, si, ai));
3365 }
3366 }
3367 }
3368
3369 scores.sort_by(|a, b| b.0.cmp(&a.0));
3371
3372 let mut used_added = vec![false; added.len()];
3374 let mut source_to_added: HashMap<usize, Vec<(usize, u32)>> = HashMap::new();
3375 for &(score, si, ai) in &scores {
3376 if used_added[ai] {
3377 continue;
3378 }
3379 used_added[ai] = true;
3380 source_to_added.entry(si).or_default().push((ai, score));
3381 }
3382
3383 for (&si, assignments_for_src) in &source_to_added {
3385 let (_, _, is_deleted) = &sources[si];
3386 if *is_deleted && !assignments_for_src.is_empty() {
3387 let rename_ai = assignments_for_src
3390 .iter()
3391 .max_by_key(|(ai, _score)| added[*ai].path().to_string())
3392 .map(|(ai, _)| *ai);
3393
3394 for &(ai, score) in assignments_for_src {
3395 let (ref src_path, _, _) = sources[si];
3396 let add = &added[ai];
3397 let src_mode = source_tree_entries
3398 .iter()
3399 .find(|(p, _, _)| p == src_path)
3400 .map(|(_, m, _)| m.clone())
3401 .unwrap_or_else(|| add.old_mode.clone());
3402
3403 let is_rename = Some(ai) == rename_ai;
3404 result_entries.push(DiffEntry {
3405 status: if is_rename {
3406 DiffStatus::Renamed
3407 } else {
3408 DiffStatus::Copied
3409 },
3410 old_path: Some(src_path.clone()),
3411 new_path: add.new_path.clone(),
3412 old_mode: src_mode,
3413 new_mode: add.new_mode.clone(),
3414 old_oid: sources[si].1,
3415 new_oid: add.new_oid,
3416 score: Some(score),
3417 });
3418 used_added2[ai] = true;
3419 }
3420 renamed_deleted.insert(si);
3421 } else {
3422 for &(ai, score) in assignments_for_src {
3424 let (ref src_path, _, _) = sources[si];
3425 let add = &added[ai];
3426 let src_mode = source_tree_entries
3427 .iter()
3428 .find(|(p, _, _)| p == src_path)
3429 .map(|(_, m, _)| m.clone())
3430 .unwrap_or_else(|| add.old_mode.clone());
3431
3432 result_entries.push(DiffEntry {
3433 status: DiffStatus::Copied,
3434 old_path: Some(src_path.clone()),
3435 new_path: add.new_path.clone(),
3436 old_mode: src_mode,
3437 new_mode: add.new_mode.clone(),
3438 old_oid: sources[si].1,
3439 new_oid: add.new_oid,
3440 score: Some(score),
3441 });
3442 used_added2[ai] = true;
3443 }
3444 }
3445 }
3446 }
3447
3448 for entry in deleted.into_iter() {
3450 if let Some(ref path) = entry.old_path {
3451 if let Some(&si) = deleted_source_idx.get(path) {
3452 if renamed_deleted.contains(&si) {
3453 continue;
3455 }
3456 }
3457 }
3458 result_entries.push(entry);
3459 }
3460
3461 let mut result = others;
3462 result.extend(result_entries);
3463 for (i, entry) in added.into_iter().enumerate() {
3465 if !used_added2[i] {
3466 result.push(entry);
3467 }
3468 }
3469
3470 let mut final_result = Vec::with_capacity(result.len());
3471 for e in result {
3472 if let Some(c) = modified_as_copy_from_sources(
3473 odb,
3474 work_root,
3475 &e,
3476 threshold,
3477 &sources,
3478 &source_contents,
3479 source_tree_entries,
3480 ) {
3481 final_result.push(c);
3482 } else {
3483 final_result.push(e);
3484 }
3485 }
3486
3487 final_result.sort_by(|a, b| a.path().cmp(b.path()));
3488 final_result
3489}
3490
3491pub fn status_apply_rename_copy_detection(
3501 odb: &Odb,
3502 unstaged_raw: Vec<DiffEntry>,
3503 threshold: u32,
3504 copies: bool,
3505 head_tree: Option<&ObjectId>,
3506) -> Result<Vec<DiffEntry>> {
3507 if !copies {
3508 return Ok(detect_renames(odb, None, unstaged_raw, threshold));
3509 }
3510 let source_tree_entries: Vec<(String, String, ObjectId)> = match head_tree {
3511 Some(oid) => flatten_tree(odb, oid, "")?
3512 .into_iter()
3513 .map(|e| (e.path, format_mode(e.mode), e.oid))
3514 .collect(),
3515 None => Vec::new(),
3516 };
3517 Ok(detect_copies(
3518 odb,
3519 None,
3520 unstaged_raw,
3521 threshold,
3522 false,
3523 &source_tree_entries,
3524 ))
3525}
3526
3527pub fn format_rename_path(old: &str, new: &str) -> String {
3535 let ob = old.as_bytes();
3536 let nb = new.as_bytes();
3537
3538 let pfx = {
3540 let mut last_sep = 0usize;
3541 let min_len = ob.len().min(nb.len());
3542 for i in 0..min_len {
3543 if ob[i] != nb[i] {
3544 break;
3545 }
3546 if ob[i] == b'/' {
3547 last_sep = i + 1;
3548 }
3549 }
3550 last_sep
3551 };
3552
3553 let mut sfx = {
3555 let mut last_sep = 0usize;
3556 let min_len = ob.len().min(nb.len());
3557 for i in 0..min_len {
3558 let oi = ob.len() - 1 - i;
3559 let ni = nb.len() - 1 - i;
3560 if ob[oi] != nb[ni] {
3561 break;
3562 }
3563 if ob[oi] == b'/' {
3564 last_sep = i + 1;
3565 }
3566 }
3567 last_sep
3568 };
3569
3570 let mut sfx_at_old = ob.len() - sfx;
3572 let mut sfx_at_new = nb.len() - sfx;
3573
3574 while pfx > sfx_at_old && pfx > sfx_at_new && sfx > 0 {
3577 let suffix_bytes = &ob[sfx_at_old..];
3579 let mut new_sfx = 0;
3580 for (i, &b) in suffix_bytes.iter().enumerate().skip(1) {
3582 if b == b'/' {
3583 new_sfx = sfx - i;
3584 break;
3585 }
3586 }
3587 if new_sfx == 0 || new_sfx >= sfx {
3588 sfx_at_old = ob.len();
3589 sfx_at_new = nb.len();
3590 break;
3591 }
3592 sfx = new_sfx;
3593 sfx_at_old = ob.len() - sfx;
3594 sfx_at_new = nb.len() - sfx;
3595 }
3596
3597 let prefix = &old[..pfx];
3604 let suffix = &old[sfx_at_old..];
3605 let old_mid = if pfx <= sfx_at_old {
3606 &old[pfx..sfx_at_old]
3607 } else {
3608 ""
3609 };
3610 let new_mid = if pfx <= sfx_at_new {
3611 &new[pfx..sfx_at_new]
3612 } else {
3613 ""
3614 };
3615
3616 if prefix.is_empty() && suffix.is_empty() {
3617 return format!("{old} => {new}");
3618 }
3619
3620 format!("{prefix}{{{old_mid} => {new_mid}}}{suffix}")
3621}
3622
3623fn same_basename(del: &DiffEntry, add: &DiffEntry) -> bool {
3625 let old = del.old_path.as_deref().unwrap_or("");
3626 let new = add.new_path.as_deref().unwrap_or("");
3627 let old_base = old.rsplit('/').next().unwrap_or(old);
3628 let new_base = new.rsplit('/').next().unwrap_or(new);
3629 old_base == new_base && !old_base.is_empty()
3630}
3631
3632fn compute_similarity(old: &[u8], new: &[u8]) -> u32 {
3637 let old_norm = crate::crlf::crlf_to_lf(old);
3640 let new_norm = crate::crlf::crlf_to_lf(new);
3641
3642 let src_size = old_norm.len();
3643 let dst_size = new_norm.len();
3644
3645 if src_size == 0 && dst_size == 0 {
3646 return 100;
3647 }
3648 let total = src_size + dst_size;
3649 if total == 0 {
3650 return 100;
3651 }
3652
3653 use similar::{ChangeTag, TextDiff};
3655 let old_str = String::from_utf8_lossy(&old_norm);
3656 let new_str = String::from_utf8_lossy(&new_norm);
3657 let diff = TextDiff::from_lines(&old_str as &str, &new_str as &str);
3658
3659 let mut shared_bytes = 0usize;
3660 for change in diff.iter_all_changes() {
3661 if change.tag() == ChangeTag::Equal {
3662 shared_bytes += change.value().len();
3664 }
3665 }
3666
3667 let max_size = src_size.max(dst_size);
3670
3671 ((shared_bytes * 100) / max_size).min(100) as u32
3672}
3673
3674#[must_use]
3678pub fn rename_similarity_score(old: &[u8], new: &[u8]) -> u32 {
3679 compute_similarity(old, new)
3680}
3681
3682pub fn format_raw(entry: &DiffEntry) -> String {
3688 let path = match entry.status {
3689 DiffStatus::Renamed | DiffStatus::Copied => {
3690 format!(
3691 "{}\t{}",
3692 entry.old_path.as_deref().unwrap_or(""),
3693 entry.new_path.as_deref().unwrap_or("")
3694 )
3695 }
3696 _ => entry.path().to_owned(),
3697 };
3698
3699 let status_str = match (entry.status, entry.score) {
3700 (DiffStatus::Renamed, Some(s)) => format!("R{:03}", s),
3701 (DiffStatus::Copied, Some(s)) => format!("C{:03}", s),
3702 _ => entry.status.letter().to_string(),
3703 };
3704
3705 let (old_hex, new_hex) = raw_oid_hex_pair(&entry.old_oid, &entry.new_oid);
3706 format!(
3707 ":{} {} {} {} {}\t{}",
3708 entry.old_mode, entry.new_mode, old_hex, new_hex, status_str, path
3709 )
3710}
3711
3712fn raw_oid_hex_pair(old: &ObjectId, new: &ObjectId) -> (String, String) {
3720 let width = if !old.is_zero() {
3721 old.algo().hex_len()
3722 } else if !new.is_zero() {
3723 new.algo().hex_len()
3724 } else {
3725 old.algo().hex_len()
3726 };
3727 let render = |oid: &ObjectId| {
3728 if oid.is_zero() {
3729 "0".repeat(width)
3730 } else {
3731 oid.to_hex()
3732 }
3733 };
3734 (render(old), render(new))
3735}
3736
3737pub fn format_raw_abbrev(entry: &DiffEntry, abbrev_len: usize) -> String {
3739 let ellipsis = if std::env::var("GIT_PRINT_SHA1_ELLIPSIS").ok().as_deref() == Some("yes") {
3740 "..."
3741 } else {
3742 ""
3743 };
3744 let old_hex = format!("{}", entry.old_oid);
3745 let new_hex = format!("{}", entry.new_oid);
3746 let old_abbrev = &old_hex[..abbrev_len.min(old_hex.len())];
3747 let new_abbrev = &new_hex[..abbrev_len.min(new_hex.len())];
3748
3749 let path = match entry.status {
3751 DiffStatus::Renamed | DiffStatus::Copied => format!(
3752 "{}\t{}",
3753 entry.old_path.as_deref().unwrap_or(""),
3754 entry.new_path.as_deref().unwrap_or("")
3755 ),
3756 _ => entry.path().to_owned(),
3757 };
3758 let status_str = match (entry.status, entry.score) {
3759 (DiffStatus::Renamed, Some(s)) => format!("R{s:03}"),
3760 (DiffStatus::Copied, Some(s)) => format!("C{s:03}"),
3761 _ => entry.status.letter().to_string(),
3762 };
3763
3764 format!(
3765 ":{} {} {}{} {}{} {}\t{}",
3766 entry.old_mode,
3767 entry.new_mode,
3768 old_abbrev,
3769 ellipsis,
3770 new_abbrev,
3771 ellipsis,
3772 status_str,
3773 path
3774 )
3775}
3776
3777pub fn unified_diff(
3792 old_content: &str,
3793 new_content: &str,
3794 old_path: &str,
3795 new_path: &str,
3796 context_lines: usize,
3797 indent_heuristic: bool,
3798 quote_path_fully: bool,
3799) -> String {
3800 unified_diff_with_prefix(
3801 old_content,
3802 new_content,
3803 old_path,
3804 new_path,
3805 context_lines,
3806 0,
3807 "a/",
3808 "b/",
3809 indent_heuristic,
3810 quote_path_fully,
3811 )
3812}
3813
3814#[allow(clippy::too_many_arguments)] pub fn unified_diff_with_prefix(
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 indent_heuristic: bool,
3829 quote_path_fully: bool,
3830) -> String {
3831 unified_diff_with_prefix_and_funcname(
3832 old_content,
3833 new_content,
3834 old_path,
3835 new_path,
3836 context_lines,
3837 inter_hunk_context,
3838 src_prefix,
3839 dst_prefix,
3840 None,
3841 indent_heuristic,
3842 quote_path_fully,
3843 )
3844}
3845
3846#[allow(clippy::too_many_arguments)]
3849pub fn unified_diff_with_prefix_and_funcname(
3850 old_content: &str,
3851 new_content: &str,
3852 old_path: &str,
3853 new_path: &str,
3854 context_lines: usize,
3855 inter_hunk_context: usize,
3856 src_prefix: &str,
3857 dst_prefix: &str,
3858 funcname_matcher: Option<&FuncnameMatcher>,
3859 indent_heuristic: bool,
3860 quote_path_fully: bool,
3861) -> String {
3862 unified_diff_with_prefix_and_funcname_and_algorithm(
3863 old_content,
3864 new_content,
3865 old_path,
3866 new_path,
3867 context_lines,
3868 inter_hunk_context,
3869 src_prefix,
3870 dst_prefix,
3871 funcname_matcher,
3872 similar::Algorithm::Myers,
3873 false,
3874 false,
3875 indent_heuristic,
3876 quote_path_fully,
3877 )
3878}
3879
3880#[allow(clippy::too_many_arguments)]
3886pub fn unified_diff_with_prefix_and_funcname_and_algorithm(
3887 old_content: &str,
3888 new_content: &str,
3889 old_path: &str,
3890 new_path: &str,
3891 context_lines: usize,
3892 inter_hunk_context: usize,
3893 src_prefix: &str,
3894 dst_prefix: &str,
3895 funcname_matcher: Option<&FuncnameMatcher>,
3896 algorithm: similar::Algorithm,
3897 function_context: bool,
3898 use_git_histogram: bool,
3899 indent_heuristic: bool,
3900 quote_path_fully: bool,
3901) -> String {
3902 if function_context {
3906 return unified_diff_with_function_context(
3907 old_content,
3908 new_content,
3909 old_path,
3910 new_path,
3911 context_lines,
3912 inter_hunk_context,
3913 src_prefix,
3914 dst_prefix,
3915 funcname_matcher,
3916 algorithm,
3917 indent_heuristic,
3918 quote_path_fully,
3919 );
3920 }
3921
3922 if use_git_histogram {
3923 return unified_diff_histogram_with_prefix_and_funcname(
3924 old_content,
3925 new_content,
3926 old_path,
3927 new_path,
3928 context_lines,
3929 inter_hunk_context,
3930 src_prefix,
3931 dst_prefix,
3932 funcname_matcher,
3933 quote_path_fully,
3934 );
3935 }
3936
3937 use crate::quote_path::format_diff_path_with_prefix;
3938 use similar::{udiff::UnifiedDiffHunk, TextDiff};
3939
3940 let diff = TextDiff::configure()
3941 .algorithm(algorithm)
3942 .diff_lines(old_content, new_content);
3943 let compacted_ops = diff_indent_heuristic::diff_lines_ops_compacted(
3944 old_content,
3945 new_content,
3946 algorithm,
3947 indent_heuristic,
3948 );
3949
3950 let mut output = String::new();
3951 if old_path == "/dev/null" {
3952 output.push_str("--- /dev/null\n");
3953 } else if src_prefix.is_empty() {
3954 output.push_str(&format!("--- {old_path}\n"));
3957 } else {
3958 output.push_str("--- ");
3959 output.push_str(&format_diff_path_with_prefix(
3960 src_prefix,
3961 old_path,
3962 quote_path_fully,
3963 ));
3964 output.push('\n');
3965 }
3966 if new_path == "/dev/null" {
3967 output.push_str("+++ /dev/null\n");
3968 } else if dst_prefix.is_empty() {
3969 output.push_str(&format!("+++ {new_path}\n"));
3970 } else {
3971 output.push_str("+++ ");
3972 output.push_str(&format_diff_path_with_prefix(
3973 dst_prefix,
3974 new_path,
3975 quote_path_fully,
3976 ));
3977 output.push('\n');
3978 }
3979
3980 let old_lines: Vec<&str> = old_content.lines().collect();
3981
3982 let max_common_gap = context_lines
3988 .saturating_mul(2)
3989 .saturating_add(inter_hunk_context);
3990 let op_groups = group_diff_ops_gap(compacted_ops, context_lines, max_common_gap);
3991
3992 for ops in op_groups {
3993 if ops.is_empty() {
3994 continue;
3995 }
3996 let hunk = UnifiedDiffHunk::new(ops, &diff, true);
3997 let hunk_str = format!("{hunk}");
3998 if let Some(first_newline) = hunk_str.find('\n') {
4002 let header_line = &hunk_str[..first_newline];
4003 let rest = &hunk_str[first_newline..];
4004
4005 if let Some(func_ctx) =
4007 extract_function_context(header_line, &old_lines, funcname_matcher)
4008 {
4009 output.push_str(header_line);
4010 output.push(' ');
4011 output.push_str(&func_ctx);
4012 output.push_str(rest);
4013 } else {
4014 output.push_str(&hunk_str);
4015 }
4016 } else {
4017 output.push_str(&hunk_str);
4018 }
4019 }
4020
4021 output
4022}
4023
4024fn group_diff_ops_gap(
4030 mut ops: Vec<similar::DiffOp>,
4031 context: usize,
4032 max_common_gap: usize,
4033) -> Vec<Vec<similar::DiffOp>> {
4034 use similar::DiffOp;
4035 if ops.is_empty() {
4036 return vec![];
4037 }
4038
4039 let mut pending_group = Vec::new();
4040 let mut rv = Vec::new();
4041
4042 if let Some(DiffOp::Equal {
4043 old_index,
4044 new_index,
4045 len,
4046 }) = ops.first_mut()
4047 {
4048 let offset = (*len).saturating_sub(context);
4049 *old_index += offset;
4050 *new_index += offset;
4051 *len -= offset;
4052 }
4053
4054 if let Some(DiffOp::Equal { len, .. }) = ops.last_mut() {
4055 *len -= (*len).saturating_sub(context);
4056 }
4057
4058 for op in ops.into_iter() {
4059 if let DiffOp::Equal {
4060 old_index,
4061 new_index,
4062 len,
4063 } = op
4064 {
4065 if len > max_common_gap {
4068 pending_group.push(DiffOp::Equal {
4069 old_index,
4070 new_index,
4071 len: context,
4072 });
4073 rv.push(pending_group);
4074 let offset = len.saturating_sub(context);
4075 pending_group = vec![DiffOp::Equal {
4076 old_index: old_index + offset,
4077 new_index: new_index + offset,
4078 len: len - offset,
4079 }];
4080 continue;
4081 }
4082 }
4083 pending_group.push(op);
4084 }
4085
4086 match &pending_group[..] {
4087 &[] | &[similar::DiffOp::Equal { .. }] => {}
4088 _ => rv.push(pending_group),
4089 }
4090
4091 rv
4092}
4093
4094fn unified_diff_with_function_context(
4096 old_content: &str,
4097 new_content: &str,
4098 old_path: &str,
4099 new_path: &str,
4100 context_lines: usize,
4101 inter_hunk_context: usize,
4102 src_prefix: &str,
4103 dst_prefix: &str,
4104 funcname_matcher: Option<&FuncnameMatcher>,
4105 algorithm: similar::Algorithm,
4106 indent_heuristic: bool,
4107 quote_path_fully: bool,
4108) -> String {
4109 use crate::quote_path::format_diff_path_with_prefix;
4110 use similar::{udiff::UnifiedDiffHunk, TextDiff};
4111
4112 let diff = TextDiff::configure()
4113 .algorithm(algorithm)
4114 .diff_lines(old_content, new_content);
4115
4116 let old_lines: Vec<&str> = old_content.lines().collect();
4117 let new_lines: Vec<&str> = new_content.lines().collect();
4118 let n_old = old_lines.len();
4119 let n_new = new_lines.len();
4120
4121 let max_common_gap = context_lines
4128 .saturating_mul(2)
4129 .saturating_add(inter_hunk_context);
4130 let all_ops = diff.ops().to_vec();
4131 let op_groups = group_diff_ops_gap(all_ops.clone(), context_lines, max_common_gap);
4132
4133 let mut ranges: Vec<(usize, usize, usize, usize)> = Vec::new();
4134
4135 for ops in op_groups {
4136 if ops.is_empty() {
4137 continue;
4138 }
4139 let i1_anchor = func_context_old_anchor(&ops, n_old);
4140 let i1_end = hunk_old_change_end_exclusive(&ops);
4141 let skip_preimage_pull =
4142 append_with_whole_function_added(&ops, n_old, n_new, &new_lines, funcname_matcher);
4143 let hunk = UnifiedDiffHunk::new(ops, &diff, true);
4144 let hunk_str = format!("{hunk}");
4145 let header_line = hunk_str
4146 .lines()
4147 .next()
4148 .unwrap_or("")
4149 .trim_end_matches(['\r', '\n']);
4150 let Some((base_s1, _base_e1, _base_s2, _base_e2)) =
4151 parse_unified_hunk_header_ranges(header_line)
4152 else {
4153 continue;
4154 };
4155
4156 let ctx = context_lines;
4157 let (s1, e1, s2, e2) = if skip_preimage_pull {
4158 let s = n_old.saturating_sub(ctx);
4159 let s2 = map_old_line_to_new(&all_ops, s, n_new).min(n_new);
4160 (s, n_old, s2, n_new)
4161 } else {
4162 let mut s1 = base_s1.saturating_sub(ctx);
4163 let mut s2 = map_old_line_to_new(&all_ops, s1, n_new).min(n_new);
4164
4165 let base_pre_s1 = i1_anchor.saturating_sub(ctx);
4166 if base_pre_s1 < s1 {
4167 s1 = base_pre_s1;
4168 s2 = map_old_line_to_new(&all_ops, s1, n_new).min(n_new);
4169 }
4170
4171 let fs1 = expand_func_pre_start(s1, i1_anchor, n_old, &old_lines, funcname_matcher);
4172 if fs1 < s1 {
4173 s1 = fs1;
4174 s2 = map_old_line_to_new(&all_ops, s1, n_new).min(n_new);
4175 }
4176
4177 let mut e1 = (i1_end + ctx).min(n_old);
4183 let mut e2 = map_old_line_to_new(&all_ops, e1, n_new).min(n_new);
4184 let fe1 = expand_func_post_end(e1, i1_end, n_old, &old_lines, funcname_matcher);
4185 if fe1 > e1 {
4186 e1 = fe1;
4187 e2 = map_old_line_to_new(&all_ops, e1, n_new).min(n_new);
4188 }
4189 (s1, e1, s2, e2)
4190 };
4191
4192 ranges.push((s1, e1, s2, e2));
4193 }
4194
4195 ranges.sort_by_key(|r| (r.0, r.2));
4198 let mut merged: Vec<(usize, usize, usize, usize)> = Vec::with_capacity(ranges.len());
4199 for (s1, e1, s2, e2) in ranges {
4200 if let Some(last) = merged.last_mut() {
4201 if s1 < last.1 {
4202 last.1 = last.1.max(e1);
4203 last.3 = last.3.max(e2);
4204 continue;
4205 }
4206 }
4207 merged.push((s1, e1, s2, e2));
4208 }
4209 let ranges = merged;
4210
4211 let mut output = String::new();
4212 if old_path == "/dev/null" {
4213 output.push_str("--- /dev/null\n");
4214 } else if src_prefix.is_empty() {
4215 output.push_str(&format!("--- {old_path}\n"));
4216 } else {
4217 output.push_str("--- ");
4218 output.push_str(&format_diff_path_with_prefix(
4219 src_prefix,
4220 old_path,
4221 quote_path_fully,
4222 ));
4223 output.push('\n');
4224 }
4225 if new_path == "/dev/null" {
4226 output.push_str("+++ /dev/null\n");
4227 } else if dst_prefix.is_empty() {
4228 output.push_str(&format!("+++ {new_path}\n"));
4229 } else {
4230 output.push_str("+++ ");
4231 output.push_str(&format_diff_path_with_prefix(
4232 dst_prefix,
4233 new_path,
4234 quote_path_fully,
4235 ));
4236 output.push('\n');
4237 }
4238
4239 for (s1, e1, s2, e2) in ranges {
4240 if s1 >= e1 && s2 >= e2 {
4241 continue;
4242 }
4243 let old_seg =
4244 line_slice_for_diff_with_eof_nl(&old_lines, s1, e1, old_content.ends_with('\n'));
4245 let new_seg =
4246 line_slice_for_diff_with_eof_nl(&new_lines, s2, e2, new_content.ends_with('\n'));
4247 let inner_ctx = old_seg.lines().count().max(new_seg.lines().count()).max(1);
4248 let piece = unified_diff_with_prefix_and_funcname_and_algorithm(
4249 &old_seg,
4250 &new_seg,
4251 old_path,
4252 new_path,
4253 inner_ctx,
4254 0,
4255 src_prefix,
4256 dst_prefix,
4257 funcname_matcher,
4258 algorithm,
4259 false,
4260 false,
4261 indent_heuristic,
4262 quote_path_fully,
4263 );
4264 let shifted = shift_unified_hunk_headers_to_full_file(&piece, s1, s2);
4265 let with_func =
4266 enrich_unified_hunk_headers_funcname(&shifted, &old_lines, funcname_matcher);
4267 for line in with_func.lines() {
4268 if line.starts_with("--- ") || line.starts_with("+++ ") {
4269 continue;
4270 }
4271 output.push_str(line);
4272 output.push('\n');
4273 }
4274 }
4275
4276 output
4277}
4278
4279fn shift_unified_hunk_headers_to_full_file(
4284 patch: &str,
4285 delta_old: usize,
4286 delta_new: usize,
4287) -> String {
4288 if delta_old == 0 && delta_new == 0 {
4289 return patch.to_owned();
4290 }
4291 let mut out = String::with_capacity(patch.len());
4292 for line in patch.lines() {
4293 if let Some(shifted) = shift_one_unified_hunk_header(line, delta_old, delta_new) {
4294 out.push_str(&shifted);
4295 } else {
4296 out.push_str(line);
4297 }
4298 out.push('\n');
4299 }
4300 out
4301}
4302
4303fn shift_one_unified_hunk_header(line: &str, delta_old: usize, delta_new: usize) -> Option<String> {
4304 let rest = line.strip_prefix("@@ ")?;
4305 let (old_chunk, after_plus) = rest.split_once(" +")?;
4306 let old_spec = old_chunk.strip_prefix('-')?;
4307 let (new_spec, suffix) = after_plus.split_once(" @@")?;
4308 let shifted_old = shift_unified_range_spec(old_spec, delta_old)?;
4309 let shifted_new = shift_unified_range_spec(new_spec, delta_new)?;
4310 Some(format!("@@ -{shifted_old} +{shifted_new} @@{suffix}"))
4311}
4312
4313fn shift_unified_range_spec(spec: &str, delta: usize) -> Option<String> {
4314 let spec = spec.trim();
4315 if let Some((start_s, count_s)) = spec.split_once(',') {
4316 let start: usize = start_s.parse().ok()?;
4317 let count: usize = count_s.parse().ok()?;
4318 Some(format!("{},{}", start.saturating_add(delta), count))
4319 } else {
4320 let start: usize = spec.parse().ok()?;
4321 Some(format!("{}", start.saturating_add(delta)))
4322 }
4323}
4324
4325fn enrich_unified_hunk_headers_funcname(
4327 patch: &str,
4328 full_old_lines: &[&str],
4329 funcname_matcher: Option<&FuncnameMatcher>,
4330) -> String {
4331 let mut out = String::with_capacity(patch.len());
4332 for line in patch.lines() {
4333 if let Some(fixed) = enrich_one_hunk_header_funcname(line, full_old_lines, funcname_matcher)
4334 {
4335 out.push_str(&fixed);
4336 } else {
4337 out.push_str(line);
4338 }
4339 out.push('\n');
4340 }
4341 out
4342}
4343
4344fn enrich_one_hunk_header_funcname(
4345 line: &str,
4346 full_old_lines: &[&str],
4347 funcname_matcher: Option<&FuncnameMatcher>,
4348) -> Option<String> {
4349 let after_at = line.strip_prefix("@@ ")?;
4350 let idx = after_at.find(" @@")?;
4351 let mid = after_at[..idx].trim();
4352 let tail = after_at[idx + 3..].trim_start();
4353 let header_for_parse = format!("@@ {mid} @@");
4354 let func = extract_function_context(&header_for_parse, full_old_lines, funcname_matcher);
4355 Some(if let Some(f) = func {
4356 format!("@@ {mid} @@ {f}")
4357 } else if !tail.is_empty() {
4358 format!("@@ {mid} @@ {tail}")
4359 } else {
4360 format!("@@ {mid} @@")
4361 })
4362}
4363
4364fn line_slice_for_diff_with_eof_nl(
4365 lines: &[&str],
4366 start: usize,
4367 end: usize,
4368 full_file_ends_with_newline: bool,
4369) -> String {
4370 if start >= end {
4371 return String::new();
4372 }
4373 let mut s = lines[start..end].join("\n");
4374 let slice_is_suffix_of_file = end == lines.len();
4375 let need_trailing_nl = if slice_is_suffix_of_file {
4376 full_file_ends_with_newline
4377 } else {
4378 true
4379 };
4380 if need_trailing_nl && !s.ends_with('\n') {
4381 s.push('\n');
4382 }
4383 s
4384}
4385
4386fn map_old_line_to_new(ops: &[similar::DiffOp], old_line: usize, n_new: usize) -> usize {
4389 use similar::DiffOp;
4390 let mut n = 0usize;
4391 for op in ops {
4392 match *op {
4393 DiffOp::Equal {
4394 old_index,
4395 new_index,
4396 len,
4397 } => {
4398 if old_index + len <= old_line {
4399 n = new_index + len;
4400 continue;
4401 }
4402 if old_index < old_line {
4403 let take = old_line - old_index;
4404 return (new_index + take).min(n_new);
4405 }
4406 return new_index.min(n_new);
4407 }
4408 DiffOp::Delete {
4409 old_index,
4410 old_len,
4411 new_index,
4412 } => {
4413 if old_index + old_len <= old_line {
4414 n = new_index;
4415 continue;
4416 }
4417 if old_index < old_line {
4418 return new_index.min(n_new);
4419 }
4420 }
4421 DiffOp::Insert {
4422 old_index,
4423 new_index,
4424 new_len,
4425 } => {
4426 if old_index < old_line {
4427 n = new_index + new_len;
4428 continue;
4429 }
4430 if old_index == old_line {
4431 return (new_index + new_len).min(n_new);
4434 }
4435 return new_index.min(n_new);
4436 }
4437 DiffOp::Replace {
4438 old_index,
4439 old_len,
4440 new_index,
4441 new_len,
4442 } => {
4443 if old_index + old_len <= old_line {
4444 n = new_index + new_len;
4445 continue;
4446 }
4447 if old_index < old_line {
4448 let into_old = old_line - old_index;
4449 let mapped = new_index + into_old.min(new_len);
4450 return mapped.min(n_new);
4451 }
4452 return new_index.min(n_new);
4453 }
4454 }
4455 }
4456 n.min(n_new)
4457}
4458
4459fn parse_unified_hunk_header_ranges(header: &str) -> Option<(usize, usize, usize, usize)> {
4461 let rest = header.strip_prefix("@@ ")?;
4462 let (old_tok, rest2) = rest.split_once(" +")?;
4463 let old_tok = old_tok.strip_prefix('-')?;
4464 let new_tok = rest2.split_once(" @@").map(|(a, _)| a)?;
4465
4466 fn parse_side(spec: &str) -> Option<(usize, usize)> {
4467 let spec = spec.trim();
4468 let (start_one_based, count) = if let Some((a, b)) = spec.split_once(',') {
4469 (a.parse::<usize>().ok()?, b.parse::<usize>().ok()?)
4470 } else {
4471 let s = spec.parse::<usize>().ok()?;
4472 (s, 1usize)
4473 };
4474 let s0 = start_one_based.saturating_sub(1);
4475 let e0 = s0.saturating_add(count);
4476 Some((s0, e0))
4477 }
4478
4479 let (os, oe) = parse_side(old_tok)?;
4480 let (ns, ne) = parse_side(new_tok)?;
4481 Some((os, oe, ns, ne))
4482}
4483
4484fn append_with_whole_function_added(
4487 ops: &[similar::DiffOp],
4488 n_old: usize,
4489 n_new: usize,
4490 new_lines: &[&str],
4491 matcher: Option<&FuncnameMatcher>,
4492) -> bool {
4493 use similar::DiffOp;
4494 if n_old == 0 {
4495 return false;
4496 }
4497 let mut only_ins_or_eq = true;
4498 let mut min_new_ins = usize::MAX;
4499 for op in ops {
4500 match *op {
4501 DiffOp::Equal { .. } => {}
4502 DiffOp::Insert {
4503 new_index, new_len, ..
4504 } => {
4505 min_new_ins = min_new_ins.min(new_index);
4506 if new_len == 0 {
4507 only_ins_or_eq = false;
4508 }
4509 }
4510 DiffOp::Delete { .. } | DiffOp::Replace { .. } => {
4511 only_ins_or_eq = false;
4512 }
4513 }
4514 }
4515 let mut insert_at_eof = false;
4516 for op in ops {
4517 if let DiffOp::Insert { old_index, .. } = *op {
4518 if old_index == n_old {
4519 insert_at_eof = true;
4520 break;
4521 }
4522 }
4523 }
4524 let append_at_eof = min_new_ins == n_old || insert_at_eof;
4525 if !only_ins_or_eq || !append_at_eof || min_new_ins == usize::MAX {
4526 return false;
4527 }
4528 let mut j = min_new_ins;
4533 while j < n_new {
4534 let line = new_lines[j];
4535 if line.trim().is_empty() {
4536 j += 1;
4537 continue;
4538 }
4539 if let Some(m) = matcher {
4540 if m.match_line(line).is_some() {
4541 return true;
4542 }
4543 } else if inserted_block_starts_with_c_like_function_definition(line) {
4544 return true;
4545 }
4546 j += 1;
4547 }
4548 false
4549}
4550
4551fn inserted_block_starts_with_c_like_function_definition(line: &str) -> bool {
4552 let t = line.trim_start();
4553 let Some(open_paren) = t.find('(') else {
4554 return false;
4555 };
4556 let head = &t[..open_paren];
4557 let tokens: Vec<&str> = head.split_whitespace().collect();
4558 if tokens.len() < 2 {
4559 return false;
4561 }
4562 let nameish = tokens.last().copied().unwrap_or("");
4563 let name = nameish.trim_end_matches(['*', '&']);
4564 if name.is_empty() || !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
4565 return false;
4566 }
4567 let type_or_modifier = |tok: &str| {
4568 matches!(
4569 tok,
4570 "static"
4571 | "extern"
4572 | "inline"
4573 | "void"
4574 | "int"
4575 | "char"
4576 | "short"
4577 | "long"
4578 | "float"
4579 | "double"
4580 | "unsigned"
4581 | "signed"
4582 | "struct"
4583 | "enum"
4584 | "union"
4585 | "const"
4586 | "volatile"
4587 | "typedef"
4588 )
4589 };
4590 tokens[..tokens.len() - 1]
4591 .iter()
4592 .any(|tok| type_or_modifier(tok))
4593}
4594
4595fn hunk_old_change_end_exclusive(ops: &[similar::DiffOp]) -> usize {
4596 use similar::DiffOp;
4597 let mut max_o = 0usize;
4598 for op in ops {
4599 match *op {
4600 DiffOp::Delete {
4601 old_index, old_len, ..
4602 } => {
4603 max_o = max_o.max(old_index + old_len);
4604 }
4605 DiffOp::Replace {
4606 old_index, old_len, ..
4607 } => {
4608 max_o = max_o.max(old_index + old_len);
4609 }
4610 DiffOp::Insert { old_index, .. } => {
4611 max_o = max_o.max(old_index);
4614 }
4615 DiffOp::Equal { .. } => {}
4616 }
4617 }
4618 max_o
4619}
4620
4621fn func_context_old_anchor(ops: &[similar::DiffOp], n_old: usize) -> usize {
4622 use similar::DiffOp;
4623 let mut has_delete_or_replace = false;
4624 let mut min_del = usize::MAX;
4625 let mut min_ins_old = usize::MAX;
4626
4627 for op in ops {
4628 match *op {
4629 DiffOp::Delete {
4630 old_index, old_len, ..
4631 } => {
4632 has_delete_or_replace = true;
4633 min_del = min_del.min(old_index);
4634 min_del = min_del.min(old_index + old_len.saturating_sub(1));
4635 }
4636 DiffOp::Replace {
4637 old_index, old_len, ..
4638 } => {
4639 has_delete_or_replace = true;
4640 min_del = min_del.min(old_index);
4641 min_del = min_del.min(old_index + old_len.saturating_sub(1));
4642 }
4643 DiffOp::Insert { old_index, .. } => {
4644 min_ins_old = min_ins_old.min(old_index);
4645 }
4646 DiffOp::Equal { .. } => {}
4647 }
4648 }
4649
4650 let mut i1 = if has_delete_or_replace {
4651 min_del
4652 } else if min_ins_old != usize::MAX {
4653 min_ins_old
4654 } else {
4655 0
4656 };
4657
4658 let pure_insert = ops
4659 .iter()
4660 .all(|op| matches!(op, DiffOp::Insert { .. } | DiffOp::Equal { .. }))
4661 && ops.iter().any(|op| matches!(op, DiffOp::Insert { .. }));
4662
4663 if pure_insert && i1 >= n_old && n_old > 0 {
4664 i1 = n_old - 1;
4665 }
4666
4667 i1.min(n_old.saturating_sub(1))
4668}
4669
4670fn expand_func_pre_start(
4671 s1: usize,
4672 i1: usize,
4673 n_old: usize,
4674 old_lines: &[&str],
4675 matcher: Option<&FuncnameMatcher>,
4676) -> usize {
4677 if n_old == 0 {
4678 return s1;
4679 }
4680 let i1 = i1.min(n_old.saturating_sub(1));
4681 let mut fs1 = get_func_line_backward(old_lines, i1, matcher).unwrap_or(i1);
4682 while fs1 > 0
4683 && !is_line_empty_for_func_context(old_lines[fs1 - 1])
4684 && !is_func_line(old_lines[fs1 - 1], matcher)
4685 {
4686 fs1 -= 1;
4687 }
4688 s1.min(fs1)
4689}
4690
4691fn expand_func_post_end(
4692 e1: usize,
4693 i1_end: usize,
4694 n_old: usize,
4695 old_lines: &[&str],
4696 matcher: Option<&FuncnameMatcher>,
4697) -> usize {
4698 let from = i1_end.min(n_old);
4699 let fe1 = get_func_line_forward(old_lines, from, matcher).unwrap_or(n_old);
4700 let mut fe1_adj = fe1;
4701 while fe1_adj > 0 && is_line_empty_for_func_context(old_lines[fe1_adj - 1]) {
4702 fe1_adj -= 1;
4703 }
4704 e1.max(fe1_adj).min(n_old)
4705}
4706
4707fn is_line_empty_for_func_context(line: &str) -> bool {
4708 line.chars().all(|c| c.is_whitespace())
4709}
4710
4711fn is_func_line(line: &str, matcher: Option<&FuncnameMatcher>) -> bool {
4712 if let Some(m) = matcher {
4713 return m.match_line(line).is_some();
4714 }
4715 let t = line.trim_end_matches(['\n', '\r']);
4716 if t.is_empty() {
4717 return false;
4718 }
4719 let b = t.as_bytes()[0];
4720 b.is_ascii_alphabetic() || b == b'_' || b == b'$'
4721}
4722
4723fn get_func_line_backward(
4724 old_lines: &[&str],
4725 start: usize,
4726 matcher: Option<&FuncnameMatcher>,
4727) -> Option<usize> {
4728 let mut l = start.min(old_lines.len().saturating_sub(1));
4729 if old_lines.is_empty() {
4730 return None;
4731 }
4732 loop {
4733 if is_func_line(old_lines[l], matcher) {
4734 return Some(l);
4735 }
4736 if l == 0 {
4737 break;
4738 }
4739 l -= 1;
4740 }
4741 None
4742}
4743
4744fn get_func_line_forward(
4745 old_lines: &[&str],
4746 start: usize,
4747 matcher: Option<&FuncnameMatcher>,
4748) -> Option<usize> {
4749 let mut l = start;
4750 while l < old_lines.len() {
4751 if is_func_line(old_lines[l], matcher) {
4752 return Some(l);
4753 }
4754 l += 1;
4755 }
4756 None
4757}
4758
4759pub fn anchored_unified_diff(
4769 old_content: &str,
4770 new_content: &str,
4771 old_path: &str,
4772 new_path: &str,
4773 context_lines: usize,
4774 anchors: &[String],
4775 algorithm: similar::Algorithm,
4776 use_git_histogram: bool,
4777 indent_heuristic: bool,
4778 quote_path_fully: bool,
4779) -> String {
4780 use crate::quote_path::format_diff_path_with_prefix;
4781 use similar::TextDiff;
4782
4783 let old_lines: Vec<&str> = old_content.lines().collect();
4784 let new_lines: Vec<&str> = new_content.lines().collect();
4785
4786 let mut anchor_pairs: Vec<(usize, usize)> = Vec::new(); for anchor in anchors {
4790 let anchor_str = anchor.as_str();
4791
4792 let old_positions: Vec<usize> = old_lines
4794 .iter()
4795 .enumerate()
4796 .filter(|(_, l)| l.trim_end() == anchor_str)
4797 .map(|(i, _)| i)
4798 .collect();
4799
4800 let new_positions: Vec<usize> = new_lines
4802 .iter()
4803 .enumerate()
4804 .filter(|(_, l)| l.trim_end() == anchor_str)
4805 .map(|(i, _)| i)
4806 .collect();
4807
4808 if old_positions.len() == 1 && new_positions.len() == 1 {
4810 anchor_pairs.push((old_positions[0], new_positions[0]));
4811 }
4812 }
4813
4814 if anchor_pairs.is_empty() {
4816 return unified_diff_with_prefix_and_funcname_and_algorithm(
4817 old_content,
4818 new_content,
4819 old_path,
4820 new_path,
4821 context_lines,
4822 0,
4823 "a/",
4824 "b/",
4825 None,
4826 algorithm,
4827 false,
4828 use_git_histogram,
4829 indent_heuristic,
4830 quote_path_fully,
4831 );
4832 }
4833
4834 anchor_pairs.sort_by_key(|&(old_idx, _)| old_idx);
4836
4837 let mut filtered: Vec<(usize, usize)> = Vec::new();
4840 for &pair in &anchor_pairs {
4841 if filtered.is_empty() || filtered.last().is_some_and(|last| pair.1 > last.1) {
4842 filtered.push(pair);
4843 }
4844 }
4845 let anchor_pairs = filtered;
4846
4847 struct LineDiffOp {
4856 tag: char, line: String,
4858 }
4859
4860 let append_segment_diff =
4861 |ops: &mut Vec<LineDiffOp>, old_seg_input: &str, new_seg_input: &str| {
4862 use similar::ChangeTag;
4863 let old_ls: Vec<&str> = old_seg_input.lines().collect();
4864 let new_ls: Vec<&str> = new_seg_input.lines().collect();
4865 if old_ls.is_empty() && new_ls.is_empty() {
4866 return;
4867 }
4868 let seg_diff = TextDiff::configure()
4869 .algorithm(algorithm)
4870 .diff_slices(&old_ls, &new_ls);
4871 let raw = seg_diff.ops().to_vec();
4872 let compacted = diff_indent_heuristic::apply_change_compact_to_ops(
4873 &raw,
4874 &old_ls,
4875 &new_ls,
4876 indent_heuristic,
4877 );
4878 for op in &compacted {
4879 for ch in op.iter_changes(&old_ls, &new_ls) {
4880 let t = match ch.tag() {
4881 ChangeTag::Equal => ' ',
4882 ChangeTag::Delete => '-',
4883 ChangeTag::Insert => '+',
4884 };
4885 ops.push(LineDiffOp {
4886 tag: t,
4887 line: ch.value().to_string(),
4888 });
4889 }
4890 }
4891 };
4892
4893 let mut ops: Vec<LineDiffOp> = Vec::new();
4894 let mut old_pos = 0usize;
4895 let mut new_pos = 0usize;
4896
4897 for &(old_anchor, new_anchor) in &anchor_pairs {
4898 let old_segment: Vec<&str> = old_lines[old_pos..old_anchor].to_vec();
4900 let new_segment: Vec<&str> = new_lines[new_pos..new_anchor].to_vec();
4901
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 ops.push(LineDiffOp {
4921 tag: ' ',
4922 line: old_lines[old_anchor].to_string(),
4923 });
4924
4925 old_pos = old_anchor + 1;
4926 new_pos = new_anchor + 1;
4927 }
4928
4929 let old_segment: Vec<&str> = old_lines[old_pos..].to_vec();
4931 let new_segment: Vec<&str> = new_lines[new_pos..].to_vec();
4932 let old_seg_text = old_segment.join("\n");
4933 let new_seg_text = new_segment.join("\n");
4934
4935 if !old_seg_text.is_empty() || !new_seg_text.is_empty() {
4936 let old_seg_input = if old_seg_text.is_empty() {
4937 String::new()
4938 } else {
4939 format!("{}\n", old_seg_text)
4940 };
4941 let new_seg_input = if new_seg_text.is_empty() {
4942 String::new()
4943 } else {
4944 format!("{}\n", new_seg_text)
4945 };
4946 append_segment_diff(&mut ops, &old_seg_input, &new_seg_input);
4947 }
4948
4949 let mut output = String::new();
4951 if old_path == "/dev/null" {
4952 output.push_str("--- /dev/null\n");
4953 } else {
4954 output.push_str("--- ");
4955 output.push_str(&format_diff_path_with_prefix(
4956 "a/",
4957 old_path,
4958 quote_path_fully,
4959 ));
4960 output.push('\n');
4961 }
4962 if new_path == "/dev/null" {
4963 output.push_str("+++ /dev/null\n");
4964 } else {
4965 output.push_str("+++ ");
4966 output.push_str(&format_diff_path_with_prefix(
4967 "b/",
4968 new_path,
4969 quote_path_fully,
4970 ));
4971 output.push('\n');
4972 }
4973
4974 let total_ops = ops.len();
4976 if total_ops == 0 {
4977 return output;
4978 }
4979
4980 let mut hunks: Vec<(usize, usize)> = Vec::new(); let mut i = 0;
4983 while i < total_ops {
4984 if ops[i].tag != ' ' {
4985 let start = i.saturating_sub(context_lines);
4986 let mut end = i;
4987 while end < total_ops {
4989 if ops[end].tag != ' ' {
4990 end += 1;
4991 continue;
4992 }
4993 let mut next_change = end;
4995 while next_change < total_ops && ops[next_change].tag == ' ' {
4996 next_change += 1;
4997 }
4998 if next_change < total_ops && next_change - end <= context_lines * 2 {
4999 end = next_change + 1;
5000 } else {
5001 end = (end + context_lines).min(total_ops);
5002 break;
5003 }
5004 }
5005 if let Some(last) = hunks.last_mut() {
5007 if start <= last.1 {
5008 last.1 = end;
5009 } else {
5010 hunks.push((start, end));
5011 }
5012 } else {
5013 hunks.push((start, end));
5014 }
5015 i = end;
5016 } else {
5017 i += 1;
5018 }
5019 }
5020
5021 for (start, end) in hunks {
5023 let mut old_start = 1usize;
5025 let mut new_start = 1usize;
5026 for op in &ops[..start] {
5028 match op.tag {
5029 ' ' => {
5030 old_start += 1;
5031 new_start += 1;
5032 }
5033 '-' => {
5034 old_start += 1;
5035 }
5036 '+' => {
5037 new_start += 1;
5038 }
5039 _ => {}
5040 }
5041 }
5042 let mut old_count = 0usize;
5043 let mut new_count = 0usize;
5044 for op in &ops[start..end] {
5045 match op.tag {
5046 ' ' => {
5047 old_count += 1;
5048 new_count += 1;
5049 }
5050 '-' => {
5051 old_count += 1;
5052 }
5053 '+' => {
5054 new_count += 1;
5055 }
5056 _ => {}
5057 }
5058 }
5059
5060 output.push_str(&format!(
5061 "@@ -{},{} +{},{} @@\n",
5062 old_start, old_count, new_start, new_count
5063 ));
5064 for op in &ops[start..end] {
5065 output.push(op.tag);
5066 output.push_str(&op.line);
5067 output.push('\n');
5068 }
5069 }
5070
5071 output
5072}
5073
5074fn extract_function_context(
5080 header: &str,
5081 old_lines: &[&str],
5082 funcname_matcher: Option<&FuncnameMatcher>,
5083) -> Option<String> {
5084 let at_pos = header.find("-")?;
5086 let rest = &header[at_pos + 1..];
5087 let comma_or_space = rest.find([',', ' '])?;
5088 let start_str = &rest[..comma_or_space];
5089 let start_line: usize = start_str.parse().ok()?;
5090
5091 let old_token_end = rest.find([' ', '\t']).unwrap_or(rest.len());
5095 let old_token = &rest[..old_token_end];
5096 let old_count: usize = if let Some(comma) = old_token.find(',') {
5097 old_token[comma + 1..].parse().unwrap_or(1)
5098 } else {
5099 1
5100 };
5101
5102 if start_line == 0 {
5103 return None;
5104 }
5105
5106 let search_end = if old_count == 0 {
5113 start_line.min(old_lines.len())
5114 } else {
5115 if start_line <= 1 {
5116 return None;
5117 }
5118 (start_line - 1).min(old_lines.len())
5119 };
5120 let truncate = |text: &str| {
5121 if text.len() > 80 {
5122 let mut end = 80;
5123 while end > 0 && !text.is_char_boundary(end) {
5124 end -= 1;
5125 }
5126 text[..end].to_owned()
5127 } else {
5128 text.to_owned()
5129 }
5130 };
5131
5132 for i in (0..search_end).rev() {
5133 let line = old_lines[i];
5134 if line.is_empty() {
5135 continue;
5136 }
5137 if let Some(matcher) = funcname_matcher {
5138 if let Some(matched) = matcher.match_line(line) {
5139 return Some(truncate(&matched));
5140 }
5141 continue;
5142 }
5143
5144 let first = line.as_bytes()[0];
5145 if first.is_ascii_alphabetic() || first == b'_' || first == b'$' {
5146 return Some(truncate(line.trim_end_matches(char::is_whitespace)));
5147 }
5148 }
5149 None
5150}
5151
5152pub fn format_stat_line(
5156 path: &str,
5157 insertions: usize,
5158 deletions: usize,
5159 max_path_len: usize,
5160) -> String {
5161 format_stat_line_width(path, insertions, deletions, max_path_len, 0)
5162}
5163
5164pub fn format_stat_line_width(
5165 path: &str,
5166 insertions: usize,
5167 deletions: usize,
5168 max_path_len: usize,
5169 count_width: usize,
5170) -> String {
5171 let total = insertions + deletions;
5172 let plus = "+".repeat(insertions.min(50));
5173 let minus = "-".repeat(deletions.min(50));
5174 let cw = if count_width > 0 {
5175 count_width
5176 } else {
5177 format!("{}", total).len()
5178 };
5179 let bar = format!("{}{}", plus, minus);
5180 if bar.is_empty() {
5181 format!(
5182 " {:<width$} | {:>cw$}",
5183 path,
5184 total,
5185 width = max_path_len,
5186 cw = cw
5187 )
5188 } else {
5189 format!(
5190 " {:<width$} | {:>cw$} {}",
5191 path,
5192 total,
5193 bar,
5194 width = max_path_len,
5195 cw = cw
5196 )
5197 }
5198}
5199
5200#[must_use]
5202pub fn normalize_ignore_space_change_line(line: &str) -> String {
5203 let mut result = String::with_capacity(line.len());
5204 let mut in_space = false;
5205 for c in line.chars() {
5206 if c.is_whitespace() {
5207 if !in_space {
5208 result.push(' ');
5209 in_space = true;
5210 }
5211 } else {
5212 result.push(c);
5213 in_space = false;
5214 }
5215 }
5216 while result.ends_with(' ') {
5217 result.pop();
5218 }
5219 result
5220}
5221
5222#[must_use]
5228pub fn normalize_ignore_space_change(content: &str) -> String {
5229 content
5230 .lines()
5231 .map(normalize_ignore_space_change_line)
5232 .collect::<Vec<_>>()
5233 .join("\n")
5234}
5235
5236pub fn count_changes(old_content: &str, new_content: &str) -> (usize, usize) {
5240 count_changes_with_algorithm(old_content, new_content, similar::Algorithm::Myers, false)
5241}
5242
5243#[must_use]
5248pub fn count_changes_with_algorithm(
5249 old_content: &str,
5250 new_content: &str,
5251 algorithm: similar::Algorithm,
5252 use_git_histogram: bool,
5253) -> (usize, usize) {
5254 if use_git_histogram {
5255 use imara_diff::{Algorithm, Diff, InternedInput};
5256 let input = InternedInput::new(old_content, new_content);
5257 let mut d = Diff::compute(Algorithm::Histogram, &input);
5258 d.postprocess_lines(&input);
5259 return (d.count_additions() as usize, d.count_removals() as usize);
5260 }
5261
5262 use similar::{ChangeTag, TextDiff};
5263
5264 let diff = TextDiff::configure()
5265 .algorithm(algorithm)
5266 .diff_lines(old_content, new_content);
5267 let mut ins = 0;
5268 let mut del = 0;
5269
5270 for change in diff.iter_all_changes() {
5271 match change.tag() {
5272 ChangeTag::Insert => ins += 1,
5273 ChangeTag::Delete => del += 1,
5274 ChangeTag::Equal => {}
5275 }
5276 }
5277
5278 (ins, del)
5279}
5280
5281#[must_use]
5286pub fn count_git_lines(data: &[u8]) -> usize {
5287 if data.is_empty() {
5288 return 0;
5289 }
5290 let mut count = 0usize;
5291 let mut nl_just_seen = false;
5292 for &ch in data {
5293 if ch == b'\n' {
5294 count += 1;
5295 nl_just_seen = true;
5296 } else {
5297 nl_just_seen = false;
5298 }
5299 }
5300 if !nl_just_seen {
5301 count += 1;
5302 }
5303 count
5304}
5305
5306pub const GIT_DIFF_MAX_SCORE: u64 = 60_000;
5308const DIFF_MAX_SCORE: u64 = GIT_DIFF_MAX_SCORE;
5309const DIFF_MINIMUM_BREAK_SIZE: usize = 400;
5310const DIFF_DEFAULT_BREAK_SCORE: u64 = 30_000;
5311pub const GIT_DIFF_DEFAULT_BREAK_SCORE: u64 = DIFF_DEFAULT_BREAK_SCORE;
5313pub const GIT_DIFF_DEFAULT_MERGE_SCORE_AFTER_BREAK: u64 = 36_000;
5316const DIFF_HASHBASE: u32 = 107_927;
5317
5318#[derive(Clone, Copy, Default)]
5319struct SpanSlot {
5320 hashval: u32,
5321 cnt: u32,
5322}
5323
5324struct SpanHashTop {
5325 alloc_log2: u8,
5326 free_slots: i32,
5327 data: Vec<SpanSlot>,
5328}
5329
5330impl SpanHashTop {
5331 fn new(initial_log2: u8) -> Self {
5332 let cap = 1usize << initial_log2;
5333 Self {
5334 alloc_log2: initial_log2,
5335 free_slots: initial_free(initial_log2),
5336 data: vec![SpanSlot::default(); cap],
5337 }
5338 }
5339
5340 fn len(&self) -> usize {
5341 1usize << self.alloc_log2
5342 }
5343
5344 fn add_span(&mut self, hashval: u32, cnt: u32) {
5345 loop {
5346 let lim = self.len();
5347 let mut bucket = (hashval as usize) & (lim - 1);
5348 loop {
5349 let h = &mut self.data[bucket];
5350 if h.cnt == 0 {
5351 h.hashval = hashval;
5352 h.cnt = cnt;
5353 self.free_slots -= 1;
5354 if self.free_slots < 0 {
5355 self.rehash();
5356 break;
5357 }
5358 return;
5359 }
5360 if h.hashval == hashval {
5361 h.cnt = h.cnt.saturating_add(cnt);
5362 return;
5363 }
5364 bucket += 1;
5365 if bucket >= lim {
5366 bucket = 0;
5367 }
5368 }
5369 }
5370 }
5371
5372 fn rehash(&mut self) {
5373 let old = std::mem::take(&mut self.data);
5374 let old_log = self.alloc_log2;
5375 self.alloc_log2 = old_log.saturating_add(1);
5376 let new_len = 1usize << self.alloc_log2;
5377 self.free_slots = initial_free(self.alloc_log2);
5378 self.data = vec![SpanSlot::default(); new_len];
5379 let old_sz = 1usize << old_log;
5380 for o in old.iter().take(old_sz) {
5381 let o = *o;
5382 if o.cnt == 0 {
5383 continue;
5384 }
5385 self.add_span_after_rehash(o.hashval, o.cnt);
5386 }
5387 }
5388
5389 fn add_span_after_rehash(&mut self, hashval: u32, cnt: u32) {
5390 loop {
5391 let lim = self.len();
5392 let mut bucket = (hashval as usize) & (lim - 1);
5393 loop {
5394 let h = &mut self.data[bucket];
5395 if h.cnt == 0 {
5396 h.hashval = hashval;
5397 h.cnt = cnt;
5398 self.free_slots -= 1;
5399 if self.free_slots < 0 {
5400 self.rehash();
5401 break;
5402 }
5403 return;
5404 }
5405 if h.hashval == hashval {
5406 h.cnt = h.cnt.saturating_add(cnt);
5407 return;
5408 }
5409 bucket += 1;
5410 if bucket >= lim {
5411 bucket = 0;
5412 }
5413 }
5414 }
5415 }
5416
5417 fn sort_by_hashval(&mut self) {
5418 let sz = self.len();
5419 self.data[..sz].sort_by(|a, b| {
5420 if a.cnt == 0 {
5421 return std::cmp::Ordering::Greater;
5422 }
5423 if b.cnt == 0 {
5424 return std::cmp::Ordering::Less;
5425 }
5426 a.hashval.cmp(&b.hashval)
5427 });
5428 }
5429}
5430
5431fn initial_free(sz_log2: u8) -> i32 {
5432 let sz = sz_log2 as i32;
5433 ((1i32 << sz_log2) * (sz - 3) / sz).max(0)
5434}
5435
5436fn hash_blob_spans(buf: &[u8], is_text: bool) -> SpanHashTop {
5437 let mut hash = SpanHashTop::new(9);
5438 let mut n = 0u32;
5439 let mut accum1: u32 = 0;
5440 let mut accum2: u32 = 0;
5441 let mut i = 0usize;
5442 while i < buf.len() {
5443 let c = buf[i] as u32;
5444 let old_1 = accum1;
5445 i += 1;
5446
5447 if is_text && c == b'\r' as u32 && i < buf.len() && buf[i] == b'\n' {
5448 continue;
5449 }
5450
5451 accum1 = accum1.wrapping_shl(7) ^ accum2.wrapping_shr(25);
5452 accum2 = accum2.wrapping_shl(7) ^ old_1.wrapping_shr(25);
5453 accum1 = accum1.wrapping_add(c);
5454 n += 1;
5455 if n < 64 && c != b'\n' as u32 {
5456 continue;
5457 }
5458 let hashval = (accum1.wrapping_add(accum2.wrapping_mul(0x61))) % DIFF_HASHBASE;
5459 hash.add_span(hashval, n);
5460 n = 0;
5461 accum1 = 0;
5462 accum2 = 0;
5463 }
5464 if n > 0 {
5465 let hashval = (accum1.wrapping_add(accum2.wrapping_mul(0x61))) % DIFF_HASHBASE;
5466 hash.add_span(hashval, n);
5467 }
5468 hash.sort_by_hashval();
5469 hash
5470}
5471
5472#[must_use]
5477pub fn diffcore_count_changes(old: &[u8], new: &[u8]) -> (u64, u64) {
5478 let src_is_text = !crate::merge_file::is_binary(old);
5479 let dst_is_text = !crate::merge_file::is_binary(new);
5480 let src_count = hash_blob_spans(old, src_is_text);
5481 let dst_count = hash_blob_spans(new, dst_is_text);
5482 let mut sc: u64 = 0;
5483 let mut la: u64 = 0;
5484 let mut si = 0usize;
5485 let mut di = 0usize;
5486 let src_len = src_count.len();
5487 let dst_len = dst_count.len();
5488 loop {
5489 if si >= src_len || src_count.data[si].cnt == 0 {
5490 break;
5491 }
5492 let s_hash = src_count.data[si].hashval;
5493 let s_cnt = u64::from(src_count.data[si].cnt);
5494 while di < dst_len && dst_count.data[di].cnt != 0 && dst_count.data[di].hashval < s_hash {
5495 la += u64::from(dst_count.data[di].cnt);
5496 di += 1;
5497 }
5498 let mut dst_cnt = 0u64;
5499 if di < dst_len && dst_count.data[di].cnt != 0 && dst_count.data[di].hashval == s_hash {
5500 dst_cnt = u64::from(dst_count.data[di].cnt);
5501 di += 1;
5502 }
5503 if s_cnt < dst_cnt {
5504 la += dst_cnt - s_cnt;
5505 sc += s_cnt;
5506 } else {
5507 sc += dst_cnt;
5508 }
5509 si += 1;
5510 }
5511 while di < dst_len && dst_count.data[di].cnt != 0 {
5512 la += u64::from(dst_count.data[di].cnt);
5513 di += 1;
5514 }
5515 (sc, la)
5516}
5517
5518#[must_use]
5521pub fn should_break_rewrite_for_stat(old: &[u8], new: &[u8]) -> bool {
5522 should_break_rewrite_inner(old, new, DIFF_DEFAULT_BREAK_SCORE)
5523}
5524
5525#[must_use]
5529pub fn should_break_rewrite_pair(old: &[u8], new: &[u8], break_score: u64) -> bool {
5530 should_break_rewrite_inner(old, new, break_score)
5531}
5532
5533pub fn parse_diff_rename_score_token(arg: &str) -> Option<u64> {
5536 let mut num: u64 = 0;
5537 let mut scale: u64 = 1;
5538 let mut dot = false;
5539 let mut saw_digit = false;
5540 for ch in arg.chars() {
5541 if !dot && ch == '.' {
5542 scale = 1;
5543 dot = true;
5544 continue;
5545 }
5546 if ch == '%' {
5547 scale = if dot { scale.saturating_mul(100) } else { 100 };
5548 break;
5549 }
5550 if ch.is_ascii_digit() {
5551 saw_digit = true;
5552 if scale < 100_000 {
5553 scale = scale.saturating_mul(10);
5554 num = num.saturating_mul(10) + u64::from(ch as u8 - b'0');
5555 }
5556 } else {
5557 break;
5558 }
5559 }
5560 if !saw_digit {
5561 return None;
5562 }
5563 Some(if num >= scale {
5564 GIT_DIFF_MAX_SCORE
5565 } else {
5566 GIT_DIFF_MAX_SCORE * num / scale
5567 })
5568}
5569
5570#[must_use]
5573pub fn rewrite_merge_score(old: &[u8], new: &[u8]) -> Option<u64> {
5574 if old.is_empty() {
5575 return None;
5576 }
5577 let max_size = old.len().max(new.len());
5578 if max_size < DIFF_MINIMUM_BREAK_SIZE {
5579 return None;
5580 }
5581 let (src_copied, _) = diffcore_count_changes(old, new);
5582 let src_copied = src_copied.min(old.len() as u64);
5583 let src_removed = (old.len() as u64).saturating_sub(src_copied);
5584 Some(src_removed * DIFF_MAX_SCORE / old.len() as u64)
5585}
5586
5587#[must_use]
5589pub fn rewrite_dissimilarity_index_percent(old: &[u8], new: &[u8]) -> Option<u32> {
5590 let score = rewrite_merge_score(old, new)?;
5591 Some((score * 100 / DIFF_MAX_SCORE).min(100) as u32)
5592}
5593
5594fn should_break_rewrite_inner(src: &[u8], dst: &[u8], break_score: u64) -> bool {
5595 if src.is_empty() {
5596 return false;
5597 }
5598 let max_size = src.len().max(dst.len());
5599 if max_size < DIFF_MINIMUM_BREAK_SIZE {
5600 return false;
5601 }
5602 let (src_copied, literal_added) = diffcore_count_changes(src, dst);
5603 let src_copied = src_copied.min(src.len() as u64);
5604 let mut literal_added = literal_added;
5605 let dst_len = dst.len() as u64;
5606 if src_copied < dst_len && literal_added + src_copied > dst_len {
5607 literal_added = dst_len.saturating_sub(src_copied);
5608 }
5609 let src_removed = (src.len() as u64).saturating_sub(src_copied);
5610 let merge_score = src_removed * DIFF_MAX_SCORE / src.len() as u64;
5611 if merge_score > break_score {
5612 return true;
5613 }
5614 let delta_size = src_removed.saturating_add(literal_added);
5615 if delta_size * DIFF_MAX_SCORE / (max_size as u64) < break_score {
5616 return false;
5617 }
5618 let s = src.len() as u64;
5619 if (s * break_score < src_removed * DIFF_MAX_SCORE)
5620 && (literal_added * 20 < src_removed)
5621 && (literal_added * 20 < src_copied)
5622 {
5623 return false;
5624 }
5625 true
5626}
5627
5628struct FlatEntry {
5632 path: String,
5633 mode: u32,
5634 oid: ObjectId,
5635}
5636
5637fn flatten_tree(odb: &Odb, tree_oid: &ObjectId, prefix: &str) -> Result<Vec<FlatEntry>> {
5638 let entries = read_tree(odb, tree_oid)?;
5639 let mut result = Vec::new();
5640
5641 for entry in entries {
5642 let name_str = String::from_utf8_lossy(&entry.name);
5643 let path = format_path(prefix, &name_str);
5644 if is_tree_mode(entry.mode) {
5645 let nested = flatten_tree(odb, &entry.oid, &path)?;
5646 result.extend(nested);
5647 } else {
5648 result.push(FlatEntry {
5649 path,
5650 mode: entry.mode,
5651 oid: entry.oid,
5652 });
5653 }
5654 }
5655
5656 Ok(result)
5657}
5658
5659pub fn head_path_states(
5661 odb: &Odb,
5662 head_tree: Option<&ObjectId>,
5663) -> Result<std::collections::BTreeMap<String, (u32, ObjectId)>> {
5664 let mut m = std::collections::BTreeMap::new();
5665 let Some(t) = head_tree else {
5666 return Ok(m);
5667 };
5668 for fe in flatten_tree(odb, t, "")? {
5669 m.insert(fe.path, (fe.mode, fe.oid));
5670 }
5671 Ok(m)
5672}
5673
5674fn is_tree_mode(mode: u32) -> bool {
5676 mode == 0o040000
5677}
5678
5679fn format_path(prefix: &str, name: &str) -> String {
5681 if prefix.is_empty() {
5682 name.to_owned()
5683 } else {
5684 format!("{prefix}/{name}")
5685 }
5686}
5687
5688pub fn format_mode(mode: u32) -> String {
5690 format!("{mode:06o}")
5691}
5692
5693#[must_use]
5697pub fn read_submodule_head_for_checkout(sub_dir: &Path) -> Option<ObjectId> {
5698 read_submodule_head(sub_dir)
5699}
5700
5701#[must_use]
5706pub fn submodule_commit_subject_line(c: &CommitData) -> String {
5707 let enc = c.encoding.as_deref().unwrap_or("UTF-8");
5708 let is_latin1 = enc.eq_ignore_ascii_case("ISO8859-1")
5709 || enc.eq_ignore_ascii_case("ISO-8859-1")
5710 || enc.eq_ignore_ascii_case("LATIN1")
5711 || enc.eq_ignore_ascii_case("ISO-8859-15");
5712 if let Some(raw) = c.raw_message.as_deref() {
5713 let line = raw.split(|b| *b == b'\n').next().unwrap_or(raw);
5714 if is_latin1 {
5715 return line
5716 .iter()
5717 .map(|&b| b as char)
5718 .collect::<String>()
5719 .trim()
5720 .to_owned();
5721 }
5722 return String::from_utf8_lossy(line).trim().to_string();
5723 }
5724 c.message.lines().next().unwrap_or("").trim().to_owned()
5725}
5726
5727fn submodule_worktree_is_unpopulated_placeholder(sub_dir: &Path) -> bool {
5730 match fs::read_dir(sub_dir) {
5731 Ok(mut it) => it.next().is_none(),
5732 Err(e) if e.kind() == std::io::ErrorKind::NotFound => true,
5733 Err(_) => false,
5734 }
5735}
5736
5737fn read_submodule_head(sub_dir: &Path) -> Option<ObjectId> {
5738 read_submodule_head_oid(sub_dir)
5739}
5740
5741#[must_use]
5743pub fn submodule_embedded_git_dir(sub_dir: &Path) -> Option<PathBuf> {
5744 let gitfile = sub_dir.join(".git");
5745 if gitfile.is_file() {
5746 let content = fs::read_to_string(&gitfile).ok()?;
5747 let gitdir = content
5748 .lines()
5749 .find_map(|l| l.strip_prefix("gitdir: "))?
5750 .trim();
5751 Some(if Path::new(gitdir).is_absolute() {
5752 PathBuf::from(gitdir)
5753 } else {
5754 sub_dir.join(gitdir)
5755 })
5756 } else if gitfile.is_dir() {
5757 Some(gitfile)
5758 } else {
5759 None
5760 }
5761}
5762
5763fn find_superproject_git(sub_dir: &Path) -> Option<(PathBuf, PathBuf)> {
5765 let mut cur = sub_dir.parent()?;
5766 loop {
5767 let git_path = cur.join(".git");
5768 if git_path.exists() {
5769 let gd = if git_path.is_file() {
5770 let content = fs::read_to_string(&git_path).ok()?;
5771 let line = content
5772 .lines()
5773 .find_map(|l| l.strip_prefix("gitdir: "))?
5774 .trim();
5775 if Path::new(line).is_absolute() {
5776 PathBuf::from(line)
5777 } else {
5778 cur.join(line)
5779 }
5780 } else {
5781 git_path
5782 };
5783 return Some((cur.to_path_buf(), gd));
5784 }
5785 cur = cur.parent()?;
5786 }
5787}
5788
5789pub fn read_submodule_head_oid(sub_dir: &Path) -> Option<ObjectId> {
5795 let mut git_dir = submodule_embedded_git_dir(sub_dir)?;
5798 if let Some((super_wt, super_git_dir)) = find_superproject_git(sub_dir) {
5799 let rel = sub_dir.strip_prefix(&super_wt).ok()?;
5800 let rel_str = rel.to_string_lossy().replace('\\', "/");
5801 let local_mod = super_git_dir
5802 .join("modules")
5803 .join(rel_str.trim_start_matches('/'));
5804 if local_mod.join("HEAD").exists() {
5805 let sg = super_git_dir.canonicalize().unwrap_or(super_git_dir);
5806 let cur = git_dir.canonicalize().unwrap_or_else(|_| git_dir.clone());
5807 if !cur.starts_with(&sg) {
5808 git_dir = local_mod;
5809 }
5810 }
5811 }
5812 let head_content = fs::read_to_string(git_dir.join("HEAD")).ok()?;
5813 let head_trimmed = head_content.trim();
5814 if head_trimmed.starts_with("ref: ") {
5815 match crate::refs::resolve_ref(&git_dir, "HEAD") {
5819 Ok(oid) => Some(oid),
5820 Err(_) => {
5821 let mut found = None;
5822 for branch in ["main", "master"] {
5823 let p = git_dir.join("refs/heads").join(branch);
5824 if let Ok(s) = fs::read_to_string(&p) {
5825 if let Ok(o) = ObjectId::from_hex(s.trim()) {
5826 found = Some(o);
5827 break;
5828 }
5829 }
5830 }
5831 found
5832 }
5833 }
5834 } else {
5835 ObjectId::from_hex(head_trimmed).ok()
5836 }
5837}
5838
5839#[must_use]
5851pub fn submodule_head_object_broken(sub_dir: &Path) -> bool {
5852 let Some(sub_git_dir) = submodule_embedded_git_dir(sub_dir) else {
5853 return false;
5854 };
5855 let Some(head_oid) = read_submodule_head_oid(sub_dir) else {
5856 return false;
5857 };
5858 let odb = Odb::new(&sub_git_dir.join("objects"));
5859 match odb.read(&head_oid) {
5860 Ok(obj) => parse_commit(&obj.data).is_err(),
5863 Err(_) => true,
5864 }
5865}
5866
5867fn submodule_has_dirty_worktree_for_super_diff(
5870 super_worktree: &Path,
5871 rel_path: &str,
5872 recorded_oid: &ObjectId,
5873) -> bool {
5874 let flags = submodule_porcelain_flags(super_worktree, rel_path, *recorded_oid);
5875 flags.modified || flags.untracked
5876}
5877
5878#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
5880pub struct SubmodulePorcelainFlags {
5881 pub new_commits: bool,
5883 pub modified: bool,
5885 pub untracked: bool,
5887}
5888
5889pub fn submodule_porcelain_flags(
5895 super_worktree: &Path,
5896 rel_path: &str,
5897 recorded_oid: ObjectId,
5898) -> SubmodulePorcelainFlags {
5899 let sub_dir = super_worktree.join(rel_path);
5900 let Some(sub_git_dir) = submodule_embedded_git_dir(&sub_dir) else {
5901 return SubmodulePorcelainFlags::default();
5902 };
5903 let Some(sub_head) = read_submodule_head_oid(&sub_dir) else {
5904 return SubmodulePorcelainFlags::default();
5905 };
5906
5907 let new_commits = sub_head != recorded_oid;
5908
5909 let index_path = sub_git_dir.join("index");
5910 let sub_index = match crate::index::Index::load(&index_path) {
5911 Ok(ix) => ix,
5912 Err(_) => {
5913 return SubmodulePorcelainFlags {
5914 new_commits,
5915 ..Default::default()
5916 }
5917 }
5918 };
5919
5920 let tracked: std::collections::BTreeSet<String> = sub_index
5921 .entries
5922 .iter()
5923 .filter(|e| e.stage() == 0)
5924 .map(|e| String::from_utf8_lossy(&e.path).into_owned())
5925 .collect();
5926 let untracked = submodule_dir_has_untracked_inner(&sub_dir, &sub_dir, &tracked, &sub_index);
5927
5928 let objects_dir = sub_git_dir.join("objects");
5929 let odb = Odb::new(&objects_dir);
5930
5931 let sub_head_tree = (|| -> Option<ObjectId> {
5932 let h = fs::read_to_string(sub_git_dir.join("HEAD")).ok()?;
5933 let h_str = h.trim();
5934 let commit_oid = if let Some(r) = h_str.strip_prefix("ref: ") {
5935 let oid_hex = fs::read_to_string(sub_git_dir.join(r)).ok()?;
5936 ObjectId::from_hex(oid_hex.trim()).ok()?
5937 } else {
5938 ObjectId::from_hex(h_str).ok()?
5939 };
5940 let obj = odb.read(&commit_oid).ok()?;
5941 let commit = parse_commit(&obj.data).ok()?;
5942 Some(commit.tree)
5943 })();
5944
5945 let staged_dirty = sub_head_tree
5946 .as_ref()
5947 .map(|t| diff_index_to_tree(&odb, &sub_index, Some(t), false).map(|v| !v.is_empty()))
5948 .unwrap_or(Ok(false));
5949 let staged_dirty = staged_dirty.unwrap_or(false);
5950
5951 let unstaged_dirty = diff_index_to_worktree(&odb, &sub_index, &sub_dir, false, true)
5952 .map(|v| !v.is_empty())
5953 .unwrap_or(false);
5954
5955 let mut modified = staged_dirty || unstaged_dirty;
5956
5957 for e in &sub_index.entries {
5962 if e.stage() != 0 || e.mode != 0o160000 {
5963 continue;
5964 }
5965 let child = String::from_utf8_lossy(&e.path).into_owned();
5966 let full_rel = if rel_path.is_empty() {
5967 child
5968 } else {
5969 format!("{rel_path}/{child}")
5970 };
5971 let nested = submodule_porcelain_flags(super_worktree, &full_rel, e.oid);
5972 modified |= nested.modified;
5973 }
5974
5975 SubmodulePorcelainFlags {
5976 new_commits,
5977 modified,
5978 untracked,
5979 }
5980}
5981
5982fn submodule_dir_has_untracked_inner(
5983 dir: &Path,
5984 root: &Path,
5985 tracked: &std::collections::BTreeSet<String>,
5986 owning_index: &Index,
5987) -> bool {
5988 let entries = match fs::read_dir(dir) {
5989 Ok(e) => e,
5990 Err(_) => return false,
5991 };
5992 let mut sorted: Vec<_> = entries.filter_map(|e| e.ok()).collect();
5993 sorted.sort_by_key(|e| e.file_name());
5994
5995 for entry in sorted {
5996 let name = entry.file_name().to_string_lossy().to_string();
5997 if name == ".git" {
5998 continue;
5999 }
6000 let path = entry.path();
6001 let rel = path
6002 .strip_prefix(root)
6003 .map(|p| p.to_string_lossy().to_string())
6004 .unwrap_or_else(|_| name.clone());
6005
6006 let is_dir = entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false);
6007 if is_dir {
6008 let is_gitlink = owning_index
6009 .get(rel.as_bytes(), 0)
6010 .is_some_and(|e| e.mode == 0o160000);
6011 if is_gitlink {
6012 let Some(nested_git) = submodule_embedded_git_dir(&path) else {
6013 continue;
6014 };
6015 let nested_index_path = nested_git.join("index");
6016 let Ok(nested_ix) = crate::index::Index::load(&nested_index_path) else {
6017 continue;
6018 };
6019 let nested_tracked: std::collections::BTreeSet<String> = nested_ix
6020 .entries
6021 .iter()
6022 .filter(|e| e.stage() == 0)
6023 .map(|e| String::from_utf8_lossy(&e.path).into_owned())
6024 .collect();
6025 if submodule_dir_has_untracked_inner(&path, &path, &nested_tracked, &nested_ix) {
6026 return true;
6027 }
6028 } else if submodule_dir_has_untracked_inner(&path, root, tracked, owning_index) {
6029 return true;
6030 }
6031 } else if !tracked.contains(&rel) {
6032 return true;
6033 }
6034 }
6035 false
6036}
6037
6038pub fn apply_orderfile_entries(
6041 entries: Vec<DiffEntry>,
6042 order_path: &str,
6043 cwd: &Path,
6044) -> Result<Vec<DiffEntry>> {
6045 apply_orderfile(entries, order_path, cwd)
6046}
6047
6048fn apply_orderfile(
6049 mut entries: Vec<DiffEntry>,
6050 order_path: &str,
6051 cwd: &Path,
6052) -> Result<Vec<DiffEntry>> {
6053 let patterns = read_orderfile_patterns(order_path, cwd)?;
6054 let sort_key = |entry: &DiffEntry| -> usize {
6055 let path = entry
6056 .new_path
6057 .as_ref()
6058 .or(entry.old_path.as_ref())
6059 .cloned()
6060 .unwrap_or_default();
6061 for (i, pat) in patterns.iter().enumerate() {
6062 if orderfile_pattern_matches(pat, &path) {
6063 return i;
6064 }
6065 }
6066 patterns.len()
6067 };
6068 entries.sort_by_key(|e| sort_key(e));
6069 Ok(entries)
6070}
6071
6072pub fn read_orderfile_patterns(order_path: &str, cwd: &Path) -> Result<Vec<String>> {
6074 let path = Path::new(order_path);
6075 let resolved = if path.is_absolute() {
6076 path.to_path_buf()
6077 } else {
6078 cwd.join(path)
6079 };
6080 let _meta = std::fs::metadata(&resolved)
6081 .map_err(|e| Error::Message(format!("could not read orderfile {order_path}: {e}")))?;
6082 let mut f = std::fs::File::open(&resolved)
6083 .map_err(|e| Error::Message(format!("could not read orderfile {order_path}: {e}")))?;
6084 let mut content = String::new();
6085 std::io::Read::read_to_string(&mut f, &mut content)
6086 .map_err(|e| Error::Message(format!("could not read orderfile {order_path}: {e}")))?;
6087 Ok(content
6088 .lines()
6089 .map(|l| l.trim().to_string())
6090 .filter(|l| !l.is_empty() && !l.starts_with('#'))
6091 .collect())
6092}
6093
6094pub fn apply_rotate_skip_entries(
6096 mut entries: Vec<DiffEntry>,
6097 rotate_to: Option<&str>,
6098 skip_to: Option<&str>,
6099) -> Result<Vec<DiffEntry>> {
6100 let Some(needle) = rotate_to.or(skip_to) else {
6101 return Ok(entries);
6102 };
6103 let needle = needle.trim();
6104 if needle.is_empty() {
6105 return Ok(entries);
6106 }
6107 let idx = entries
6108 .iter()
6109 .position(|e| e.path() == needle)
6110 .ok_or_else(|| Error::Message(format!("fatal: No such path '{needle}' in the diff")))?;
6111 if rotate_to.is_some() {
6112 entries.rotate_left(idx);
6113 }
6114 if let Some(skip) = skip_to.filter(|s| !s.trim().is_empty()) {
6115 let pos = entries
6116 .iter()
6117 .position(|e| e.path() == skip)
6118 .ok_or_else(|| Error::Message(format!("fatal: No such path '{skip}' in the diff")))?;
6119 entries.drain(..pos);
6120 }
6121 Ok(entries)
6122}
6123
6124pub fn apply_rotate_skip_log_entries(
6127 odb: &Odb,
6128 commit_tree: &ObjectId,
6129 entries: Vec<DiffEntry>,
6130 rotate_to: Option<&str>,
6131 skip_to: Option<&str>,
6132) -> Result<Vec<DiffEntry>> {
6133 fn trimmed(s: Option<&str>) -> Option<&str> {
6138 s.map(str::trim).filter(|t| !t.is_empty())
6139 }
6140 if trimmed(rotate_to).is_none() && trimmed(skip_to).is_none() {
6141 return Ok(entries);
6142 }
6143 let tree_paths = crate::merge_diff::all_blob_paths_in_tree_order(odb, commit_tree);
6144 apply_rotate_skip_ordered_paths(&tree_paths, entries, rotate_to, skip_to)
6145}
6146
6147fn apply_rotate_skip_ordered_paths(
6148 tree_paths: &[String],
6149 entries: Vec<DiffEntry>,
6150 rotate_to: Option<&str>,
6151 skip_to: Option<&str>,
6152) -> Result<Vec<DiffEntry>> {
6153 let rotate = rotate_to.and_then(|s| {
6154 let t = s.trim();
6155 (!t.is_empty()).then_some(t)
6156 });
6157 let skip = skip_to.and_then(|s| {
6158 let t = s.trim();
6159 (!t.is_empty()).then_some(t)
6160 });
6161 if rotate.is_none() && skip.is_none() {
6162 return Ok(entries);
6163 }
6164
6165 use std::collections::HashMap;
6166 let mut by_path: HashMap<String, DiffEntry> = HashMap::new();
6167 for e in entries {
6168 by_path.insert(e.path().to_string(), e);
6169 }
6170
6171 if rotate.is_none() {
6174 let Some(skip_path) = skip else {
6175 return Ok(by_path.into_values().collect());
6176 };
6177 let idx = tree_paths
6178 .iter()
6179 .position(|p| p == skip_path)
6180 .ok_or_else(|| {
6181 Error::Message(format!("fatal: No such path '{skip_path}' in the diff"))
6182 })?;
6183 let mut out = Vec::new();
6184 for p in tree_paths.iter().skip(idx) {
6185 if let Some(e) = by_path.remove(p) {
6186 out.push(e);
6187 }
6188 }
6189 return Ok(out);
6190 }
6191
6192 let Some(needle) = rotate else {
6193 return Ok(by_path.into_values().collect());
6194 };
6195 let idx = tree_paths
6196 .iter()
6197 .position(|p| p == needle)
6198 .ok_or_else(|| Error::Message(format!("fatal: No such path '{needle}' in the diff")))?;
6199 let mut order: Vec<String> = tree_paths.to_vec();
6200 order.rotate_left(idx);
6201 if let Some(skip_path) = skip {
6202 let pos = order.iter().position(|p| p == skip_path).ok_or_else(|| {
6203 Error::Message(format!("fatal: No such path '{skip_path}' in the diff"))
6204 })?;
6205 order.drain(..pos);
6206 }
6207 let mut out = Vec::new();
6208 for p in order {
6209 if let Some(e) = by_path.remove(&p) {
6210 out.push(e);
6211 }
6212 }
6213 Ok(out)
6214}
6215
6216pub fn orderfile_pattern_matches(pattern: &str, path: &str) -> bool {
6219 let name = path.rsplit('/').next().unwrap_or(path);
6220 orderfile_glob_match(pattern, name) || orderfile_glob_match(pattern, path)
6221}
6222
6223fn orderfile_glob_match(pattern: &str, text: &str) -> bool {
6225 let mut pi = 0;
6226 let mut ti = 0;
6227 let pb = pattern.as_bytes();
6228 let tb = text.as_bytes();
6229 let mut star_pi = usize::MAX;
6230 let mut star_ti = 0;
6231
6232 while ti < tb.len() {
6233 if pi < pb.len() && (pb[pi] == b'?' || pb[pi] == tb[ti]) {
6234 pi += 1;
6235 ti += 1;
6236 } else if pi < pb.len() && pb[pi] == b'*' {
6237 star_pi = pi;
6238 star_ti = ti;
6239 pi += 1;
6240 } else if star_pi != usize::MAX {
6241 pi = star_pi + 1;
6242 star_ti += 1;
6243 ti = star_ti;
6244 } else {
6245 return false;
6246 }
6247 }
6248 while pi < pb.len() && pb[pi] == b'*' {
6249 pi += 1;
6250 }
6251 pi == pb.len()
6252}