1use std::borrow::Cow;
24use std::collections::HashSet;
25use std::fs;
26#[cfg(unix)]
27use std::os::unix::fs::MetadataExt;
28use std::path::{Path, PathBuf};
29use std::sync::Arc;
30
31use crate::config::ConfigSet;
32use crate::diff_indent_heuristic;
33use crate::diff_rename_parallel::{
34 diff_rename_parallel_options, inexact_matrix_exceeds_limit, parallel_load_optional_blobs,
35 parallel_map_indices, parse_rename_limit,
36};
37use crate::error::{Error, Result};
38use crate::index::{CacheTreeNode, Index, IndexEntry, MODE_SYMLINK};
39use crate::objects::{parse_commit, CommitData, ObjectId, ObjectKind, TreeEntry, TreeRefIter};
40use crate::odb::Odb;
41use crate::parallel::ThreadPolicy;
42use crate::repo::Repository;
43use crate::userdiff::FuncnameMatcher;
44use crate::worktree_rules::WorktreeRules;
45
46use super::rename_lines::RenameLineTable;
47use super::unified_emit::{
48 emit_unified_body_from_edits, line_ending_only_edits, EditSpan, LineSide, UnifiedEmitOptions,
49};
50
51fn line_compare_keys<'a>(
57 text: &'a str,
58 side: &LineSide<'a>,
59 ws: super::WhitespaceMode,
60 normalized: &'a mut Vec<Cow<'a, str>>,
61) -> Vec<&'a str> {
62 if ws.normalizes_line_compare() {
63 normalized.extend(
65 side.lines
66 .iter()
67 .map(|line| ws.normalize_line_for_compare(line.body)),
68 );
69 return normalized.iter().map(Cow::as_ref).collect();
70 }
71 let mut keys = Vec::with_capacity(side.len());
72 let mut rest = text;
73 while !rest.is_empty() {
74 let end = memchr::memchr(b'\n', rest.as_bytes()).map_or(rest.len(), |nl| nl + 1);
75 let (line, tail) = rest.split_at(end);
76 keys.push(line);
77 rest = tail;
78 }
79 debug_assert_eq!(keys.len(), side.len());
80 keys
81}
82
83#[path = "../structured_diff.rs"]
84pub mod structured;
85
86pub(crate) fn imara_unified_hunk_slices(body: &str) -> Vec<&str> {
87 let mut starts: Vec<usize> = Vec::new();
88 if body.starts_with("@@") {
89 starts.push(0);
90 }
91 for (idx, _) in body.match_indices("\n@@ ") {
92 starts.push(idx + 1);
93 }
94 starts.push(body.len());
95 starts.windows(2).map(|w| &body[w[0]..w[1]]).collect()
96}
97
98pub(crate) fn histogram_compacted_line_ops(
100 old_content: &str,
101 new_content: &str,
102 indent_heuristic: bool,
103 ws: super::WhitespaceMode,
104) -> Vec<similar::DiffOp> {
105 let before = LineSide::from_text(old_content);
106 let after = LineSide::from_text(new_content);
107 let old_lines: Vec<&str> = before.lines.iter().map(|l| l.body).collect();
108 let new_lines: Vec<&str> = after.lines.iter().map(|l| l.body).collect();
109 let old_len = old_lines.len();
110 let new_len = new_lines.len();
111 let (mut old_normalized, mut new_normalized) = (Vec::new(), Vec::new());
112 let old_compare = line_compare_keys(old_content, &before, ws, &mut old_normalized);
113 let new_compare = line_compare_keys(new_content, &after, ws, &mut new_normalized);
114
115 let (changed1, changed2) = git_xdiff::histogram_changed_flags(&old_compare, &new_compare);
116
117 if !changed1.iter().any(|c| *c) && !changed2.iter().any(|c| *c) {
118 return Vec::new();
119 }
120 let raw = changed_flags_to_ops(&changed1, &changed2, old_len, new_len);
121 diff_indent_heuristic::apply_change_compact_to_ops_with_ws_histogram(
122 &raw,
123 &old_lines,
124 &new_lines,
125 indent_heuristic,
126 ws,
127 true,
128 )
129}
130
131pub(crate) fn histogram_unified_body_raw(
132 old_content: &str,
133 new_content: &str,
134 context_lines: usize,
135 inter_hunk_context: usize,
136 indent_heuristic: bool,
137 ws: super::WhitespaceMode,
138) -> String {
139 let before_side = LineSide::from_text(old_content);
140 let after_side = LineSide::from_text(new_content);
141 let ops = histogram_compacted_line_ops(old_content, new_content, indent_heuristic, ws);
142 let mut edits = diff_ops_to_edit_spans(&ops);
143 if edits.is_empty() {
144 if old_content != new_content && !ws.normalizes_line_compare() {
145 edits = line_ending_only_edits(&before_side, &after_side);
146 }
147 if edits.is_empty() {
148 return String::new();
149 }
150 }
151 emit_unified_body_from_edits(
152 &before_side,
153 &after_side,
154 &edits,
155 UnifiedEmitOptions {
156 context_lines,
157 inter_hunk_context,
158 function_context: false,
159 },
160 None,
161 )
162}
163
164#[allow(clippy::too_many_arguments)]
180fn diff_ops_to_ignorable_edit_spans<F>(
181 ops: &[similar::DiffOp],
182 old_lines: &[&str],
183 new_lines: &[&str],
184 is_ignorable_change: F,
185) -> Vec<EditSpan>
186where
187 F: Fn(&[&str], &[&str]) -> bool,
188{
189 use similar::DiffOp;
190 let mut out = Vec::new();
191 for op in ops {
192 match *op {
193 DiffOp::Equal { .. } => {}
194 DiffOp::Delete {
195 old_index,
196 old_len,
197 new_index,
198 } => {
199 let removed = &old_lines[old_index..old_index + old_len];
200 out.push(EditSpan {
201 before_start: old_index as i64,
202 before_len: old_len as i64,
203 after_start: new_index as i64,
204 after_len: 0,
205 skip_in_output: is_ignorable_change(removed, &[]),
206 });
207 }
208 DiffOp::Insert {
209 old_index,
210 new_index,
211 new_len,
212 } => {
213 let added = &new_lines[new_index..new_index + new_len];
214 out.push(EditSpan {
215 before_start: old_index as i64,
216 before_len: 0,
217 after_start: new_index as i64,
218 after_len: new_len as i64,
219 skip_in_output: is_ignorable_change(&[], added),
220 });
221 }
222 DiffOp::Replace {
223 old_index,
224 old_len,
225 new_index,
226 new_len,
227 } => {
228 let removed = &old_lines[old_index..old_index + old_len];
229 let added = &new_lines[new_index..new_index + new_len];
230 out.push(EditSpan {
231 before_start: old_index as i64,
232 before_len: old_len as i64,
233 after_start: new_index as i64,
234 after_len: new_len as i64,
235 skip_in_output: is_ignorable_change(removed, added),
236 });
237 }
238 }
239 }
240 out
241}
242
243#[allow(clippy::too_many_arguments)]
244pub(crate) fn histogram_unified_body_ignore_fc<F>(
245 old_content: &str,
246 new_content: &str,
247 context_lines: usize,
248 inter_hunk_context: usize,
249 function_context: bool,
250 funcname_matcher: Option<&FuncnameMatcher>,
251 indent_heuristic: bool,
252 ws: super::WhitespaceMode,
253 is_ignorable_change: F,
254) -> String
255where
256 F: Fn(&[&str], &[&str]) -> bool,
257{
258 let before = LineSide::from_text(old_content);
259 let after = LineSide::from_text(new_content);
260 let old_lines: Vec<&str> = before.lines.iter().map(|l| l.body).collect();
261 let new_lines: Vec<&str> = after.lines.iter().map(|l| l.body).collect();
262 let ops = histogram_compacted_line_ops(old_content, new_content, indent_heuristic, ws);
263 if ops.is_empty() {
264 return String::new();
265 }
266 let edits = diff_ops_to_ignorable_edit_spans(&ops, &old_lines, &new_lines, is_ignorable_change);
267 emit_unified_body_from_edits(
268 &before,
269 &after,
270 &edits,
271 UnifiedEmitOptions {
272 context_lines,
273 inter_hunk_context,
274 function_context,
275 },
276 funcname_matcher,
277 )
278}
279
280pub(crate) fn git_xdiff_compacted_line_ops(
281 old_compare: &[&str],
282 new_compare: &[&str],
283 old_lines: &[&str],
284 new_lines: &[&str],
285 indent_heuristic: bool,
286 need_min: bool,
287) -> Vec<similar::DiffOp> {
288 use crate::line_interner::LineInterner;
289
290 let mut interner = LineInterner::with_capacity(old_compare.len() + new_compare.len());
291 let ids1 = interner.intern_all(old_compare);
292 let ids2 = interner.intern_all(new_compare);
293 let (changed1, changed2) = git_xdiff::changed_flags(&ids1, &ids2, need_min);
294 diff_indent_heuristic::apply_change_compact_to_masks(
295 changed1,
296 changed2,
297 old_lines,
298 new_lines,
299 indent_heuristic,
300 )
301}
302
303fn diff_ops_to_edit_spans(ops: &[similar::DiffOp]) -> Vec<EditSpan> {
304 use similar::DiffOp;
305 let mut out = Vec::new();
306 for op in ops {
307 match *op {
308 DiffOp::Equal { .. } => {}
309 DiffOp::Delete {
310 old_index,
311 old_len,
312 new_index,
313 } => out.push(EditSpan {
314 before_start: old_index as i64,
315 before_len: old_len as i64,
316 after_start: new_index as i64,
317 after_len: 0,
318 skip_in_output: false,
319 }),
320 DiffOp::Insert {
321 old_index,
322 new_index,
323 new_len,
324 } => out.push(EditSpan {
325 before_start: old_index as i64,
326 before_len: 0,
327 after_start: new_index as i64,
328 after_len: new_len as i64,
329 skip_in_output: false,
330 }),
331 DiffOp::Replace {
332 old_index,
333 old_len,
334 new_index,
335 new_len,
336 } => out.push(EditSpan {
337 before_start: old_index as i64,
338 before_len: old_len as i64,
339 after_start: new_index as i64,
340 after_len: new_len as i64,
341 skip_in_output: false,
342 }),
343 }
344 }
345 out
346}
347
348fn git_xdiff_unified_body_raw(
350 old_content: &str,
351 new_content: &str,
352 context_lines: usize,
353 inter_hunk_context: usize,
354 indent_heuristic: bool,
355 need_min: bool,
356 ws: super::WhitespaceMode,
357) -> String {
358 git_xdiff_unified_body_raw_with_ignorable(
359 old_content,
360 new_content,
361 context_lines,
362 inter_hunk_context,
363 indent_heuristic,
364 need_min,
365 ws,
366 None::<fn(&[&str], &[&str]) -> bool>,
367 )
368}
369
370#[allow(clippy::too_many_arguments)]
371fn git_xdiff_unified_body_raw_with_ignorable<F>(
372 old_content: &str,
373 new_content: &str,
374 context_lines: usize,
375 inter_hunk_context: usize,
376 indent_heuristic: bool,
377 need_min: bool,
378 ws: super::WhitespaceMode,
379 is_ignorable_change: Option<F>,
380) -> String
381where
382 F: Fn(&[&str], &[&str]) -> bool,
383{
384 let before = LineSide::from_text(old_content);
385 let after = LineSide::from_text(new_content);
386 if before.is_empty() && after.is_empty() {
387 return String::new();
388 }
389 let old_refs: Vec<&str> = before.lines.iter().map(|l| l.body).collect();
390 let new_refs: Vec<&str> = after.lines.iter().map(|l| l.body).collect();
391 let (mut old_normalized, mut new_normalized) = (Vec::new(), Vec::new());
392 let old_compare = line_compare_keys(old_content, &before, ws, &mut old_normalized);
393 let new_compare = line_compare_keys(new_content, &after, ws, &mut new_normalized);
394 let ops = git_xdiff_compacted_line_ops(
395 &old_compare,
396 &new_compare,
397 &old_refs,
398 &new_refs,
399 indent_heuristic,
400 need_min,
401 );
402 let mut edits = if let Some(is_ignorable) = is_ignorable_change {
403 diff_ops_to_ignorable_edit_spans(&ops, &old_refs, &new_refs, is_ignorable)
404 } else {
405 diff_ops_to_edit_spans(&ops)
406 };
407 if edits.is_empty() {
408 if old_content != new_content && !ws.normalizes_line_compare() {
409 edits = line_ending_only_edits(&before, &after);
410 }
411 if edits.is_empty() {
412 return String::new();
413 }
414 }
415 emit_unified_body_from_edits(
416 &before,
417 &after,
418 &edits,
419 UnifiedEmitOptions {
420 context_lines,
421 inter_hunk_context,
422 function_context: false,
423 },
424 None,
425 )
426}
427
428#[allow(clippy::too_many_arguments)]
429fn git_patience_unified_body_raw_with_ignorable<F>(
430 old_content: &str,
431 new_content: &str,
432 context_lines: usize,
433 inter_hunk_context: usize,
434 indent_heuristic: bool,
435 ws: super::WhitespaceMode,
436 is_ignorable_change: Option<F>,
437) -> String
438where
439 F: Fn(&[&str], &[&str]) -> bool,
440{
441 let before = LineSide::from_text(old_content);
442 let after = LineSide::from_text(new_content);
443 if before.is_empty() && after.is_empty() {
444 return String::new();
445 }
446 let old_refs: Vec<&str> = before.lines.iter().map(|l| l.body).collect();
447 let new_refs: Vec<&str> = after.lines.iter().map(|l| l.body).collect();
448 let (mut old_normalized, mut new_normalized) = (Vec::new(), Vec::new());
449 let old_compare = line_compare_keys(old_content, &before, ws, &mut old_normalized);
450 let new_compare = line_compare_keys(new_content, &after, ws, &mut new_normalized);
451 let (changed1, changed2) = git_xdiff::patience_changed_flags(&old_compare, &new_compare);
452 let raw = changed_flags_to_ops(&changed1, &changed2, old_refs.len(), new_refs.len());
453 let ops = if ws.normalizes_line_compare() {
454 diff_indent_heuristic::apply_change_compact_to_ops_with_ws(
455 &raw,
456 &old_refs,
457 &new_refs,
458 indent_heuristic,
459 ws,
460 )
461 } else {
462 diff_indent_heuristic::apply_change_compact_to_ops(
463 &raw,
464 &old_refs,
465 &new_refs,
466 indent_heuristic,
467 )
468 };
469 let mut edits = if let Some(is_ignorable) = is_ignorable_change {
470 diff_ops_to_ignorable_edit_spans(&ops, &old_refs, &new_refs, is_ignorable)
471 } else {
472 diff_ops_to_edit_spans(&ops)
473 };
474 if edits.is_empty() {
475 if old_content != new_content && !ws.normalizes_line_compare() {
476 edits = line_ending_only_edits(&before, &after);
477 }
478 if edits.is_empty() {
479 return String::new();
480 }
481 }
482 emit_unified_body_from_edits(
483 &before,
484 &after,
485 &edits,
486 UnifiedEmitOptions {
487 context_lines,
488 inter_hunk_context,
489 function_context: false,
490 },
491 None,
492 )
493}
494
495fn blank_line_ignorable(ws: super::WhitespaceMode) -> impl Fn(&[&str], &[&str]) -> bool + Copy {
496 move |removed: &[&str], added: &[&str]| {
497 ws.ignore_blank_lines
498 && removed.iter().all(|l| crate::ws::ws_blank_line(l))
499 && added.iter().all(|l| crate::ws::ws_blank_line(l))
500 }
501}
502
503pub(crate) fn unified_hunk_body_for_diff_options(
505 old_content: &str,
506 new_content: &str,
507 options: &crate::diff::options::DiffOptions,
508) -> String {
509 use crate::diff::options::DiffAlgorithm;
510
511 let ws = options.whitespace;
512 if ws.is_active() {
513 let blank_only = blank_line_ignorable(ws);
514 if ws.normalizes_line_compare() {
515 let ignorable = ws.ignore_blank_lines.then_some(blank_only);
516 return match options.algorithm {
517 DiffAlgorithm::Myers => git_xdiff_unified_body_raw_with_ignorable(
518 old_content,
519 new_content,
520 options.context_lines,
521 options.inter_hunk_context,
522 options.indent_heuristic,
523 false,
524 ws,
525 ignorable,
526 ),
527 DiffAlgorithm::Minimal => git_xdiff_unified_body_raw_with_ignorable(
528 old_content,
529 new_content,
530 options.context_lines,
531 options.inter_hunk_context,
532 options.indent_heuristic,
533 true,
534 ws,
535 ignorable,
536 ),
537 DiffAlgorithm::Patience => git_patience_unified_body_raw_with_ignorable(
538 old_content,
539 new_content,
540 options.context_lines,
541 options.inter_hunk_context,
542 options.indent_heuristic,
543 ws,
544 ignorable,
545 ),
546 DiffAlgorithm::Histogram => {
547 if ws.ignore_blank_lines {
548 histogram_unified_body_ignore_fc(
549 old_content,
550 new_content,
551 options.context_lines,
552 options.inter_hunk_context,
553 false,
554 None,
555 options.indent_heuristic,
556 ws,
557 blank_only,
558 )
559 } else {
560 histogram_unified_body_raw(
561 old_content,
562 new_content,
563 options.context_lines,
564 options.inter_hunk_context,
565 options.indent_heuristic,
566 ws,
567 )
568 }
569 }
570 };
571 }
572 if ws.ignore_blank_lines {
573 return match options.algorithm {
574 DiffAlgorithm::Myers => git_xdiff_unified_body_raw_with_ignorable(
575 old_content,
576 new_content,
577 options.context_lines,
578 options.inter_hunk_context,
579 options.indent_heuristic,
580 false,
581 ws,
582 Some(blank_only),
583 ),
584 DiffAlgorithm::Minimal => git_xdiff_unified_body_raw_with_ignorable(
585 old_content,
586 new_content,
587 options.context_lines,
588 options.inter_hunk_context,
589 options.indent_heuristic,
590 true,
591 ws,
592 Some(blank_only),
593 ),
594 DiffAlgorithm::Patience => git_patience_unified_body_raw_with_ignorable(
595 old_content,
596 new_content,
597 options.context_lines,
598 options.inter_hunk_context,
599 options.indent_heuristic,
600 ws,
601 Some(blank_only),
602 ),
603 DiffAlgorithm::Histogram => histogram_unified_body_ignore_fc(
604 old_content,
605 new_content,
606 options.context_lines,
607 options.inter_hunk_context,
608 false,
609 None,
610 options.indent_heuristic,
611 ws,
612 blank_only,
613 ),
614 };
615 }
616 }
617 match options.algorithm {
618 DiffAlgorithm::Myers => git_xdiff_unified_body_raw(
619 old_content,
620 new_content,
621 options.context_lines,
622 options.inter_hunk_context,
623 options.indent_heuristic,
624 false,
625 ws,
626 ),
627 DiffAlgorithm::Minimal => git_xdiff_unified_body_raw(
628 old_content,
629 new_content,
630 options.context_lines,
631 options.inter_hunk_context,
632 options.indent_heuristic,
633 true,
634 ws,
635 ),
636 DiffAlgorithm::Patience => git_patience_unified_body_raw(
637 old_content,
638 new_content,
639 options.context_lines,
640 options.inter_hunk_context,
641 options.indent_heuristic,
642 ),
643 DiffAlgorithm::Histogram => histogram_unified_body_raw(
644 old_content,
645 new_content,
646 options.context_lines,
647 options.inter_hunk_context,
648 options.indent_heuristic,
649 ws,
650 ),
651 }
652}
653
654fn git_patience_unified_body_raw(
656 old_content: &str,
657 new_content: &str,
658 context_lines: usize,
659 inter_hunk_context: usize,
660 indent_heuristic: bool,
661) -> String {
662 let before = LineSide::from_text(old_content);
663 let after = LineSide::from_text(new_content);
664 if before.is_empty() && after.is_empty() {
665 return String::new();
666 }
667 let old_refs: Vec<&str> = before.lines.iter().map(|l| l.body).collect();
668 let new_refs: Vec<&str> = after.lines.iter().map(|l| l.body).collect();
669 let ws = super::WhitespaceMode::default();
670 let (mut old_normalized, mut new_normalized) = (Vec::new(), Vec::new());
671 let old_compare = line_compare_keys(old_content, &before, ws, &mut old_normalized);
672 let new_compare = line_compare_keys(new_content, &after, ws, &mut new_normalized);
673 let (changed1, changed2) = git_xdiff::patience_changed_flags(&old_compare, &new_compare);
674 let raw = changed_flags_to_ops(&changed1, &changed2, old_refs.len(), new_refs.len());
675 let ops = diff_indent_heuristic::apply_change_compact_to_ops(
676 &raw,
677 &old_refs,
678 &new_refs,
679 indent_heuristic,
680 );
681 let mut edits = diff_ops_to_edit_spans(&ops);
682 if edits.is_empty() {
683 if old_content != new_content {
684 edits = line_ending_only_edits(&before, &after);
685 }
686 if edits.is_empty() {
687 return String::new();
688 }
689 }
690 emit_unified_body_from_edits(
691 &before,
692 &after,
693 &edits,
694 UnifiedEmitOptions {
695 context_lines,
696 inter_hunk_context,
697 function_context: false,
698 },
699 None,
700 )
701}
702
703pub(crate) fn unified_hunk_header_counts(
705 header_line: &str,
706) -> Option<(usize, usize, usize, usize)> {
707 let rest = header_line.strip_prefix("@@ ")?;
708 let (old_part, rest2) = rest.split_once(" +")?;
709 let old_tok = old_part.strip_prefix('-')?.split(' ').next()?;
710 let new_tok = rest2.split(" @@").next()?.split(' ').next()?;
711
712 fn parse_side(spec: &str) -> (usize, usize) {
713 if let Some((start, count)) = spec.split_once(',') {
714 (start.parse().unwrap_or(0), count.parse().unwrap_or(0))
715 } else {
716 (spec.parse().unwrap_or(0), 1)
717 }
718 }
719
720 let (os, ol) = parse_side(old_tok);
721 let (ns, nl) = parse_side(new_tok);
722 Some((os, ol, ns, nl))
723}
724
725#[allow(clippy::too_many_arguments)]
727pub(crate) fn write_unified_diff_from_diff_options<W: std::io::Write>(
728 out: &mut W,
729 old_content: &str,
730 new_content: &str,
731 old_path: &str,
732 new_path: &str,
733 src_prefix: &str,
734 dst_prefix: &str,
735 funcname_matcher: Option<&FuncnameMatcher>,
736 options: &crate::diff::options::DiffOptions,
737 quote_path_fully: bool,
738) -> Result<()> {
739 use crate::quote_path::format_diff_path_with_prefix;
740
741 if old_path == "/dev/null" {
742 out.write_all(b"--- /dev/null\n")?;
743 } else if src_prefix.is_empty() {
744 writeln!(out, "--- {old_path}")?;
745 } else {
746 out.write_all(b"--- ")?;
747 out.write_all(
748 format_diff_path_with_prefix(src_prefix, old_path, quote_path_fully).as_bytes(),
749 )?;
750 out.write_all(b"\n")?;
751 }
752 if new_path == "/dev/null" {
753 out.write_all(b"+++ /dev/null\n")?;
754 } else if dst_prefix.is_empty() {
755 writeln!(out, "+++ {new_path}")?;
756 } else {
757 out.write_all(b"+++ ")?;
758 out.write_all(
759 format_diff_path_with_prefix(dst_prefix, new_path, quote_path_fully).as_bytes(),
760 )?;
761 out.write_all(b"\n")?;
762 }
763
764 let body = unified_hunk_body_for_diff_options(old_content, new_content, options);
765 if body.is_empty() {
766 return Ok(());
767 }
768 let old_lines: Vec<&str> = old_content.lines().collect();
769 for hunk_str in imara_unified_hunk_slices(&body) {
770 if hunk_str.is_empty() {
771 continue;
772 }
773 if let Some(first_newline) = hunk_str.find('\n') {
774 let header_line = &hunk_str[..first_newline];
775 let rest = &hunk_str[first_newline..];
776 if let Some(func_ctx) =
777 extract_function_context(header_line, &old_lines, funcname_matcher, Some(old_path))
778 {
779 out.write_all(header_line.as_bytes())?;
780 out.write_all(b" ")?;
781 out.write_all(func_ctx.as_bytes())?;
782 out.write_all(rest.as_bytes())?;
783 } else {
784 out.write_all(hunk_str.as_bytes())?;
785 }
786 } else {
787 out.write_all(hunk_str.as_bytes())?;
788 }
789 }
790 Ok(())
791}
792
793#[expect(clippy::too_many_arguments)]
794fn unified_diff_git_myers_with_prefix_and_funcname(
795 old_content: &str,
796 new_content: &str,
797 old_path: &str,
798 new_path: &str,
799 context_lines: usize,
800 inter_hunk_context: usize,
801 src_prefix: &str,
802 dst_prefix: &str,
803 funcname_matcher: Option<&FuncnameMatcher>,
804 indent_heuristic: bool,
805 quote_path_fully: bool,
806) -> String {
807 use crate::quote_path::format_diff_path_with_prefix;
808
809 let body = git_xdiff_unified_body_raw(
810 old_content,
811 new_content,
812 context_lines,
813 inter_hunk_context,
814 indent_heuristic,
815 false,
816 super::WhitespaceMode::default(),
817 );
818
819 let mut output = String::new();
820 if old_path == "/dev/null" {
821 output.push_str("--- /dev/null\n");
822 } else if src_prefix.is_empty() {
823 output.push_str(&format!("--- {old_path}\n"));
824 } else {
825 output.push_str("--- ");
826 output.push_str(&format_diff_path_with_prefix(
827 src_prefix,
828 old_path,
829 quote_path_fully,
830 ));
831 output.push('\n');
832 }
833 if new_path == "/dev/null" {
834 output.push_str("+++ /dev/null\n");
835 } else if dst_prefix.is_empty() {
836 output.push_str(&format!("+++ {new_path}\n"));
837 } else {
838 output.push_str("+++ ");
839 output.push_str(&format_diff_path_with_prefix(
840 dst_prefix,
841 new_path,
842 quote_path_fully,
843 ));
844 output.push('\n');
845 }
846
847 let old_lines: Vec<&str> = old_content.lines().collect();
848 for hunk_str in imara_unified_hunk_slices(&body) {
849 if hunk_str.is_empty() {
850 continue;
851 }
852 if let Some(first_newline) = hunk_str.find('\n') {
853 let header_line = &hunk_str[..first_newline];
854 let rest = &hunk_str[first_newline..];
855 if let Some(func_ctx) =
856 extract_function_context(header_line, &old_lines, funcname_matcher, Some(old_path))
857 {
858 output.push_str(header_line);
859 output.push(' ');
860 output.push_str(&func_ctx);
861 output.push_str(rest);
862 } else {
863 output.push_str(hunk_str);
864 }
865 } else {
866 output.push_str(hunk_str);
867 }
868 }
869
870 annotate_unified_diff_no_newline_at_eof(&output, old_content, new_content)
871}
872
873#[expect(clippy::too_many_arguments)]
874fn unified_diff_git_minimal_with_prefix_and_funcname(
875 old_content: &str,
876 new_content: &str,
877 old_path: &str,
878 new_path: &str,
879 context_lines: usize,
880 inter_hunk_context: usize,
881 src_prefix: &str,
882 dst_prefix: &str,
883 funcname_matcher: Option<&FuncnameMatcher>,
884 indent_heuristic: bool,
885 quote_path_fully: bool,
886) -> String {
887 use crate::quote_path::format_diff_path_with_prefix;
888
889 let body = git_xdiff_unified_body_raw(
890 old_content,
891 new_content,
892 context_lines,
893 inter_hunk_context,
894 indent_heuristic,
895 true,
896 super::WhitespaceMode::default(),
897 );
898
899 let mut output = String::new();
900 if old_path == "/dev/null" {
901 output.push_str("--- /dev/null\n");
902 } else if src_prefix.is_empty() {
903 output.push_str(&format!("--- {old_path}\n"));
904 } else {
905 output.push_str("--- ");
906 output.push_str(&format_diff_path_with_prefix(
907 src_prefix,
908 old_path,
909 quote_path_fully,
910 ));
911 output.push('\n');
912 }
913 if new_path == "/dev/null" {
914 output.push_str("+++ /dev/null\n");
915 } else if dst_prefix.is_empty() {
916 output.push_str(&format!("+++ {new_path}\n"));
917 } else {
918 output.push_str("+++ ");
919 output.push_str(&format_diff_path_with_prefix(
920 dst_prefix,
921 new_path,
922 quote_path_fully,
923 ));
924 output.push('\n');
925 }
926
927 let old_lines: Vec<&str> = old_content.lines().collect();
928 for hunk_str in imara_unified_hunk_slices(&body) {
929 if hunk_str.is_empty() {
930 continue;
931 }
932 if let Some(first_newline) = hunk_str.find('\n') {
933 let header_line = &hunk_str[..first_newline];
934 let rest = &hunk_str[first_newline..];
935 if let Some(func_ctx) =
936 extract_function_context(header_line, &old_lines, funcname_matcher, Some(old_path))
937 {
938 output.push_str(header_line);
939 output.push(' ');
940 output.push_str(&func_ctx);
941 output.push_str(rest);
942 } else {
943 output.push_str(hunk_str);
944 }
945 } else {
946 output.push_str(hunk_str);
947 }
948 }
949
950 annotate_unified_diff_no_newline_at_eof(&output, old_content, new_content)
951}
952
953#[expect(clippy::too_many_arguments)]
954fn unified_diff_git_patience_with_prefix_and_funcname(
955 old_content: &str,
956 new_content: &str,
957 old_path: &str,
958 new_path: &str,
959 context_lines: usize,
960 inter_hunk_context: usize,
961 src_prefix: &str,
962 dst_prefix: &str,
963 funcname_matcher: Option<&FuncnameMatcher>,
964 indent_heuristic: bool,
965 quote_path_fully: bool,
966) -> String {
967 use crate::quote_path::format_diff_path_with_prefix;
968
969 let body = git_patience_unified_body_raw(
970 old_content,
971 new_content,
972 context_lines,
973 inter_hunk_context,
974 indent_heuristic,
975 );
976
977 let mut output = String::new();
978 if old_path == "/dev/null" {
979 output.push_str("--- /dev/null\n");
980 } else if src_prefix.is_empty() {
981 output.push_str(&format!("--- {old_path}\n"));
982 } else {
983 output.push_str("--- ");
984 output.push_str(&format_diff_path_with_prefix(
985 src_prefix,
986 old_path,
987 quote_path_fully,
988 ));
989 output.push('\n');
990 }
991 if new_path == "/dev/null" {
992 output.push_str("+++ /dev/null\n");
993 } else if dst_prefix.is_empty() {
994 output.push_str(&format!("+++ {new_path}\n"));
995 } else {
996 output.push_str("+++ ");
997 output.push_str(&format_diff_path_with_prefix(
998 dst_prefix,
999 new_path,
1000 quote_path_fully,
1001 ));
1002 output.push('\n');
1003 }
1004
1005 let old_lines: Vec<&str> = old_content.lines().collect();
1006 for hunk_str in imara_unified_hunk_slices(&body) {
1007 if hunk_str.is_empty() {
1008 continue;
1009 }
1010 if let Some(first_newline) = hunk_str.find('\n') {
1011 let header_line = &hunk_str[..first_newline];
1012 let rest = &hunk_str[first_newline..];
1013 if let Some(func_ctx) =
1014 extract_function_context(header_line, &old_lines, funcname_matcher, Some(old_path))
1015 {
1016 output.push_str(header_line);
1017 output.push(' ');
1018 output.push_str(&func_ctx);
1019 output.push_str(rest);
1020 } else {
1021 output.push_str(hunk_str);
1022 }
1023 } else {
1024 output.push_str(hunk_str);
1025 }
1026 }
1027
1028 annotate_unified_diff_no_newline_at_eof(&output, old_content, new_content)
1029}
1030
1031#[must_use]
1035pub fn unified_diff_histogram_hunks_only(
1036 old_content: &str,
1037 new_content: &str,
1038 context_lines: usize,
1039 inter_hunk_context: usize,
1040 indent_heuristic: bool,
1041) -> String {
1042 histogram_unified_body_raw(
1043 old_content,
1044 new_content,
1045 context_lines,
1046 inter_hunk_context,
1047 indent_heuristic,
1048 super::WhitespaceMode::default(),
1049 )
1050}
1051#[expect(clippy::too_many_arguments)]
1052#[must_use]
1054pub fn unified_diff_histogram_with_prefix_and_funcname(
1055 old_content: &str,
1056 new_content: &str,
1057 old_path: &str,
1058 new_path: &str,
1059 context_lines: usize,
1060 inter_hunk_context: usize,
1061 src_prefix: &str,
1062 dst_prefix: &str,
1063 funcname_matcher: Option<&FuncnameMatcher>,
1064 indent_heuristic: bool,
1065 quote_path_fully: bool,
1066) -> String {
1067 unified_diff_histogram_with_prefix_and_funcname_indent(
1068 old_content,
1069 new_content,
1070 old_path,
1071 new_path,
1072 context_lines,
1073 inter_hunk_context,
1074 src_prefix,
1075 dst_prefix,
1076 funcname_matcher,
1077 indent_heuristic,
1078 quote_path_fully,
1079 )
1080}
1081
1082#[expect(clippy::too_many_arguments)]
1083fn unified_diff_histogram_with_prefix_and_funcname_indent(
1084 old_content: &str,
1085 new_content: &str,
1086 old_path: &str,
1087 new_path: &str,
1088 context_lines: usize,
1089 inter_hunk_context: usize,
1090 src_prefix: &str,
1091 dst_prefix: &str,
1092 funcname_matcher: Option<&FuncnameMatcher>,
1093 indent_heuristic: bool,
1094 quote_path_fully: bool,
1095) -> String {
1096 use crate::quote_path::format_diff_path_with_prefix;
1097
1098 let body = histogram_unified_body_raw(
1099 old_content,
1100 new_content,
1101 context_lines,
1102 inter_hunk_context,
1103 indent_heuristic,
1104 super::WhitespaceMode::default(),
1105 );
1106
1107 let mut output = String::new();
1108 if old_path == "/dev/null" {
1109 output.push_str("--- /dev/null\n");
1110 } else if src_prefix.is_empty() {
1111 output.push_str(&format!("--- {old_path}\n"));
1112 } else {
1113 output.push_str("--- ");
1114 output.push_str(&format_diff_path_with_prefix(
1115 src_prefix,
1116 old_path,
1117 quote_path_fully,
1118 ));
1119 output.push('\n');
1120 }
1121 if new_path == "/dev/null" {
1122 output.push_str("+++ /dev/null\n");
1123 } else if dst_prefix.is_empty() {
1124 output.push_str(&format!("+++ {new_path}\n"));
1125 } else {
1126 output.push_str("+++ ");
1127 output.push_str(&format_diff_path_with_prefix(
1128 dst_prefix,
1129 new_path,
1130 quote_path_fully,
1131 ));
1132 output.push('\n');
1133 }
1134
1135 let old_lines: Vec<&str> = old_content.lines().collect();
1136 for hunk_str in imara_unified_hunk_slices(&body) {
1137 if hunk_str.is_empty() {
1138 continue;
1139 }
1140 if let Some(first_newline) = hunk_str.find('\n') {
1141 let header_line = &hunk_str[..first_newline];
1142 let rest = &hunk_str[first_newline..];
1143 if let Some(func_ctx) =
1144 extract_function_context(header_line, &old_lines, funcname_matcher, Some(old_path))
1145 {
1146 output.push_str(header_line);
1147 output.push(' ');
1148 output.push_str(&func_ctx);
1149 output.push_str(rest);
1150 } else {
1151 output.push_str(hunk_str);
1152 }
1153 } else {
1154 output.push_str(hunk_str);
1155 }
1156 }
1157
1158 annotate_unified_diff_no_newline_at_eof(&output, old_content, new_content)
1159}
1160
1161#[allow(clippy::too_many_arguments)]
1171pub fn unified_diff_histogram_ignore_with_prefix_and_funcname<F>(
1172 old_content: &str,
1173 new_content: &str,
1174 old_path: &str,
1175 new_path: &str,
1176 context_lines: usize,
1177 inter_hunk_context: usize,
1178 src_prefix: &str,
1179 dst_prefix: &str,
1180 funcname_matcher: Option<&FuncnameMatcher>,
1181 quote_path_fully: bool,
1182 function_context: bool,
1183 indent_heuristic: bool,
1184 is_ignorable_change: F,
1185) -> String
1186where
1187 F: Fn(&[&str], &[&str]) -> bool,
1188{
1189 use crate::quote_path::format_diff_path_with_prefix;
1190
1191 let body = histogram_unified_body_ignore_fc(
1192 old_content,
1193 new_content,
1194 context_lines,
1195 inter_hunk_context,
1196 function_context,
1197 funcname_matcher,
1198 indent_heuristic,
1199 super::WhitespaceMode::default(),
1200 is_ignorable_change,
1201 );
1202 if body.is_empty() {
1203 return String::new();
1204 }
1205
1206 let mut output = String::new();
1207 if old_path == "/dev/null" {
1208 output.push_str("--- /dev/null\n");
1209 } else if src_prefix.is_empty() {
1210 output.push_str(&format!("--- {old_path}\n"));
1211 } else {
1212 output.push_str("--- ");
1213 output.push_str(&format_diff_path_with_prefix(
1214 src_prefix,
1215 old_path,
1216 quote_path_fully,
1217 ));
1218 output.push('\n');
1219 }
1220 if new_path == "/dev/null" {
1221 output.push_str("+++ /dev/null\n");
1222 } else if dst_prefix.is_empty() {
1223 output.push_str(&format!("+++ {new_path}\n"));
1224 } else {
1225 output.push_str("+++ ");
1226 output.push_str(&format_diff_path_with_prefix(
1227 dst_prefix,
1228 new_path,
1229 quote_path_fully,
1230 ));
1231 output.push('\n');
1232 }
1233
1234 let old_lines: Vec<&str> = old_content.lines().collect();
1235 for hunk_str in imara_unified_hunk_slices(&body) {
1236 if hunk_str.is_empty() {
1237 continue;
1238 }
1239 if let Some(first_newline) = hunk_str.find('\n') {
1240 let header_line = &hunk_str[..first_newline];
1241 let rest = &hunk_str[first_newline..];
1242 if let Some(func_ctx) =
1243 extract_function_context(header_line, &old_lines, funcname_matcher, Some(old_path))
1244 {
1245 output.push_str(header_line);
1246 output.push(' ');
1247 output.push_str(&func_ctx);
1248 output.push_str(rest);
1249 } else {
1250 output.push_str(hunk_str);
1251 }
1252 } else {
1253 output.push_str(hunk_str);
1254 }
1255 }
1256
1257 output
1258}
1259
1260#[must_use]
1262pub fn indent_heuristic_from_config(config: &ConfigSet) -> bool {
1263 match config.get_bool("diff.indentHeuristic") {
1264 Some(Ok(b)) => b,
1265 Some(Err(_)) | None => true,
1266 }
1267}
1268
1269#[must_use]
1271pub fn resolve_indent_heuristic(
1272 config: &ConfigSet,
1273 cli_indent_heuristic: bool,
1274 cli_no_indent_heuristic: bool,
1275) -> bool {
1276 if cli_no_indent_heuristic {
1277 false
1278 } else if cli_indent_heuristic {
1279 true
1280 } else {
1281 indent_heuristic_from_config(config)
1282 }
1283}
1284
1285#[must_use]
1287pub fn parse_indent_heuristic_cli_flags(argv: &[String]) -> (bool, bool) {
1288 let mut indent_heuristic = false;
1289 let mut no_indent_heuristic = false;
1290 for a in argv {
1291 match a.as_str() {
1292 "--indent-heuristic" => {
1293 indent_heuristic = true;
1294 no_indent_heuristic = false;
1295 }
1296 "--no-indent-heuristic" => {
1297 no_indent_heuristic = true;
1298 indent_heuristic = false;
1299 }
1300 _ => {}
1301 }
1302 }
1303 (indent_heuristic, no_indent_heuristic)
1304}
1305
1306mod git_xdiff {
1316 const XDL_MAX_COST_MIN: i64 = 256;
1317 const XDL_HEUR_MIN_COST: i64 = 256;
1318 const XDL_SNAKE_CNT: i64 = 20;
1319 const XDL_K_HEUR: i64 = 4;
1320 const XDL_LINE_MAX: i64 = i64::MAX;
1321 const XDL_KPDIS_RUN: i64 = 4;
1322 const XDL_MAX_EQLIMIT: i64 = 1024;
1323 const XDL_SIMSCAN_WINDOW: i64 = 100;
1324
1325 const DISCARD: u8 = 0;
1327 const KEEP: u8 = 1;
1328 const INVESTIGATE: u8 = 2;
1329
1330 fn bogosqrt(mut n: i64) -> i64 {
1332 let mut i: i64 = 1;
1333 while n > 0 {
1334 i <<= 1;
1335 n >>= 2;
1336 }
1337 i
1338 }
1339
1340 struct XdAlgoEnv {
1341 mxcost: i64,
1342 snake_cnt: i64,
1343 heur_min: i64,
1344 }
1345
1346 struct XdpSplit {
1347 i1: i64,
1348 i2: i64,
1349 min_lo: bool,
1350 min_hi: bool,
1351 }
1352
1353 struct XdFile<'a> {
1356 ids: &'a [u32],
1357 changed: Vec<bool>,
1358 reference_index: Vec<usize>,
1359 dstart: i64,
1360 dend: i64,
1361 nreff: i64,
1362 }
1363
1364 #[inline]
1366 fn get_id(xdf: &XdFile<'_>, idx: i64) -> u32 {
1367 xdf.ids[xdf.reference_index[idx as usize]]
1368 }
1369
1370 #[allow(clippy::too_many_arguments)]
1373 fn xdl_split(
1374 xdf1: &XdFile<'_>,
1375 off1: i64,
1376 lim1: i64,
1377 xdf2: &XdFile<'_>,
1378 off2: i64,
1379 lim2: i64,
1380 kvdf: &mut [i64],
1381 kvdb: &mut [i64],
1382 koff: i64,
1383 need_min: bool,
1384 spl: &mut XdpSplit,
1385 xenv: &XdAlgoEnv,
1386 ) -> i64 {
1387 let dmin = off1 - lim2;
1388 let dmax = lim1 - off2;
1389 let fmid = off1 - off2;
1390 let bmid = lim1 - lim2;
1391 let odd = ((fmid - bmid) & 1) != 0;
1392 let mut fmin = fmid;
1393 let mut fmax = fmid;
1394 let mut bmin = bmid;
1395 let mut bmax = bmid;
1396
1397 let kf = |k: i64| -> usize { (k + koff) as usize };
1398
1399 kvdf[kf(fmid)] = off1;
1400 kvdb[kf(bmid)] = lim1;
1401
1402 let mut ec: i64 = 1;
1403 loop {
1404 let mut got_snake = false;
1405
1406 if fmin > dmin {
1407 fmin -= 1;
1408 kvdf[kf(fmin - 1)] = -1;
1409 } else {
1410 fmin += 1;
1411 }
1412 if fmax < dmax {
1413 fmax += 1;
1414 kvdf[kf(fmax + 1)] = -1;
1415 } else {
1416 fmax -= 1;
1417 }
1418
1419 let mut d = fmax;
1420 while d >= fmin {
1421 let mut i1 = if kvdf[kf(d - 1)] >= kvdf[kf(d + 1)] {
1422 kvdf[kf(d - 1)] + 1
1423 } else {
1424 kvdf[kf(d + 1)]
1425 };
1426 let prev1 = i1;
1427 let mut i2 = i1 - d;
1428 while i1 < lim1 && i2 < lim2 && get_id(xdf1, i1) == get_id(xdf2, i2) {
1429 i1 += 1;
1430 i2 += 1;
1431 }
1432 if i1 - prev1 > xenv.snake_cnt {
1433 got_snake = true;
1434 }
1435 kvdf[kf(d)] = i1;
1436 if odd && bmin <= d && d <= bmax && kvdb[kf(d)] <= i1 {
1437 spl.i1 = i1;
1438 spl.i2 = i2;
1439 spl.min_lo = true;
1440 spl.min_hi = true;
1441 return ec;
1442 }
1443 d -= 2;
1444 }
1445
1446 if bmin > dmin {
1447 bmin -= 1;
1448 kvdb[kf(bmin - 1)] = XDL_LINE_MAX;
1449 } else {
1450 bmin += 1;
1451 }
1452 if bmax < dmax {
1453 bmax += 1;
1454 kvdb[kf(bmax + 1)] = XDL_LINE_MAX;
1455 } else {
1456 bmax -= 1;
1457 }
1458
1459 let mut d = bmax;
1460 while d >= bmin {
1461 let mut i1 = if kvdb[kf(d - 1)] < kvdb[kf(d + 1)] {
1462 kvdb[kf(d - 1)]
1463 } else {
1464 kvdb[kf(d + 1)] - 1
1465 };
1466 let prev1 = i1;
1467 let mut i2 = i1 - d;
1468 while i1 > off1 && i2 > off2 && get_id(xdf1, i1 - 1) == get_id(xdf2, i2 - 1) {
1469 i1 -= 1;
1470 i2 -= 1;
1471 }
1472 if prev1 - i1 > xenv.snake_cnt {
1473 got_snake = true;
1474 }
1475 kvdb[kf(d)] = i1;
1476 if !odd && fmin <= d && d <= fmax && i1 <= kvdf[kf(d)] {
1477 spl.i1 = i1;
1478 spl.i2 = i2;
1479 spl.min_lo = true;
1480 spl.min_hi = true;
1481 return ec;
1482 }
1483 d -= 2;
1484 }
1485
1486 if need_min {
1487 ec += 1;
1488 continue;
1489 }
1490
1491 if got_snake && ec > xenv.heur_min {
1492 let mut best = 0i64;
1493 let mut d = fmax;
1494 while d >= fmin {
1495 let dd = if d > fmid { d - fmid } else { fmid - d };
1496 let i1 = kvdf[kf(d)];
1497 let i2 = i1 - d;
1498 let v = (i1 - off1) + (i2 - off2) - dd;
1499 if v > XDL_K_HEUR * ec
1500 && v > best
1501 && off1 + xenv.snake_cnt <= i1
1502 && i1 < lim1
1503 && off2 + xenv.snake_cnt <= i2
1504 && i2 < lim2
1505 {
1506 let mut k = 1i64;
1507 while get_id(xdf1, i1 - k) == get_id(xdf2, i2 - k) {
1508 if k == xenv.snake_cnt {
1509 best = v;
1510 spl.i1 = i1;
1511 spl.i2 = i2;
1512 break;
1513 }
1514 k += 1;
1515 }
1516 }
1517 d -= 2;
1518 }
1519 if best > 0 {
1520 spl.min_lo = true;
1521 spl.min_hi = false;
1522 return ec;
1523 }
1524
1525 let mut best = 0i64;
1526 let mut d = bmax;
1527 while d >= bmin {
1528 let dd = if d > bmid { d - bmid } else { bmid - d };
1529 let i1 = kvdb[kf(d)];
1530 let i2 = i1 - d;
1531 let v = (lim1 - i1) + (lim2 - i2) - dd;
1532 if v > XDL_K_HEUR * ec
1533 && v > best
1534 && off1 < i1
1535 && i1 <= lim1 - xenv.snake_cnt
1536 && off2 < i2
1537 && i2 <= lim2 - xenv.snake_cnt
1538 {
1539 let mut k = 0i64;
1540 while get_id(xdf1, i1 + k) == get_id(xdf2, i2 + k) {
1541 if k == xenv.snake_cnt - 1 {
1542 best = v;
1543 spl.i1 = i1;
1544 spl.i2 = i2;
1545 break;
1546 }
1547 k += 1;
1548 }
1549 }
1550 d -= 2;
1551 }
1552 if best > 0 {
1553 spl.min_lo = false;
1554 spl.min_hi = true;
1555 return ec;
1556 }
1557 }
1558
1559 if ec >= xenv.mxcost {
1560 let mut fbest = -1i64;
1561 let mut fbest1 = -1i64;
1562 let mut d = fmax;
1563 while d >= fmin {
1564 let mut i1 = kvdf[kf(d)].min(lim1);
1565 let mut i2 = i1 - d;
1566 if lim2 < i2 {
1567 i1 = lim2 + d;
1568 i2 = lim2;
1569 }
1570 if fbest < i1 + i2 {
1571 fbest = i1 + i2;
1572 fbest1 = i1;
1573 }
1574 d -= 2;
1575 }
1576
1577 let mut bbest = XDL_LINE_MAX;
1578 let mut bbest1 = XDL_LINE_MAX;
1579 let mut d = bmax;
1580 while d >= bmin {
1581 let mut i1 = off1.max(kvdb[kf(d)]);
1582 let mut i2 = i1 - d;
1583 if i2 < off2 {
1584 i1 = off2 + d;
1585 i2 = off2;
1586 }
1587 if i1 + i2 < bbest {
1588 bbest = i1 + i2;
1589 bbest1 = i1;
1590 }
1591 d -= 2;
1592 }
1593
1594 if (lim1 + lim2) - bbest < fbest - (off1 + off2) {
1595 spl.i1 = fbest1;
1596 spl.i2 = fbest - fbest1;
1597 spl.min_lo = true;
1598 spl.min_hi = false;
1599 } else {
1600 spl.i1 = bbest1;
1601 spl.i2 = bbest - bbest1;
1602 spl.min_lo = false;
1603 spl.min_hi = true;
1604 }
1605 return ec;
1606 }
1607
1608 ec += 1;
1609 }
1610 }
1611
1612 #[allow(clippy::too_many_arguments)]
1614 fn xdl_recs_cmp(
1615 xdf1: &mut XdFile<'_>,
1616 mut off1: i64,
1617 mut lim1: i64,
1618 xdf2: &mut XdFile<'_>,
1619 mut off2: i64,
1620 mut lim2: i64,
1621 kvdf: &mut [i64],
1622 kvdb: &mut [i64],
1623 koff: i64,
1624 need_min: bool,
1625 xenv: &XdAlgoEnv,
1626 ) {
1627 while off1 < lim1 && off2 < lim2 && get_id(xdf1, off1) == get_id(xdf2, off2) {
1628 off1 += 1;
1629 off2 += 1;
1630 }
1631 while off1 < lim1 && off2 < lim2 && get_id(xdf1, lim1 - 1) == get_id(xdf2, lim2 - 1) {
1632 lim1 -= 1;
1633 lim2 -= 1;
1634 }
1635
1636 if off1 == lim1 {
1637 while off2 < lim2 {
1638 let r = xdf2.reference_index[off2 as usize];
1639 xdf2.changed[r] = true;
1640 off2 += 1;
1641 }
1642 } else if off2 == lim2 {
1643 while off1 < lim1 {
1644 let r = xdf1.reference_index[off1 as usize];
1645 xdf1.changed[r] = true;
1646 off1 += 1;
1647 }
1648 } else {
1649 let mut spl = XdpSplit {
1650 i1: 0,
1651 i2: 0,
1652 min_lo: false,
1653 min_hi: false,
1654 };
1655 xdl_split(
1656 xdf1, off1, lim1, xdf2, off2, lim2, kvdf, kvdb, koff, need_min, &mut spl, xenv,
1657 );
1658 xdl_recs_cmp(
1659 xdf1, off1, spl.i1, xdf2, off2, spl.i2, kvdf, kvdb, koff, spl.min_lo, xenv,
1660 );
1661 xdl_recs_cmp(
1662 xdf1, spl.i1, lim1, xdf2, spl.i2, lim2, kvdf, kvdb, koff, spl.min_hi, xenv,
1663 );
1664 }
1665 }
1666
1667 fn clean_mmatch(action: &[u8], i: i64, mut s: i64, mut e: i64) -> bool {
1669 if i - s > XDL_SIMSCAN_WINDOW {
1670 s = i - XDL_SIMSCAN_WINDOW;
1671 }
1672 if e - i > XDL_SIMSCAN_WINDOW {
1673 e = i + XDL_SIMSCAN_WINDOW;
1674 }
1675
1676 let mut rdis0 = 0i64;
1677 let mut rpdis0 = 1i64;
1678 let mut r = 1i64;
1679 while i - r >= s {
1680 match action[(i - r) as usize] {
1681 DISCARD => rdis0 += 1,
1682 INVESTIGATE => rpdis0 += 1,
1683 _ => break, }
1685 r += 1;
1686 }
1687 if rdis0 == 0 {
1688 return false;
1689 }
1690 let mut rdis1 = 0i64;
1691 let mut rpdis1 = 1i64;
1692 let mut r = 1i64;
1693 while i + r <= e {
1694 match action[(i + r) as usize] {
1695 DISCARD => rdis1 += 1,
1696 INVESTIGATE => rpdis1 += 1,
1697 _ => break, }
1699 r += 1;
1700 }
1701 if rdis1 == 0 {
1702 return false;
1703 }
1704 rdis1 += rdis0;
1705 rpdis1 += rpdis0;
1706 rpdis1 * XDL_KPDIS_RUN < (rpdis1 + rdis1)
1707 }
1708
1709 pub fn changed_flags(ids1: &[u32], ids2: &[u32], need_min: bool) -> (Vec<bool>, Vec<bool>) {
1715 let nrec1 = ids1.len();
1716 let nrec2 = ids2.len();
1717
1718 let nclasses = ids1
1720 .iter()
1721 .chain(ids2)
1722 .max()
1723 .map_or(0, |&max| max as usize + 1);
1724 let mut count1 = vec![0i64; nclasses];
1725 let mut count2 = vec![0i64; nclasses];
1726 for &id in ids1 {
1727 count1[id as usize] += 1;
1728 }
1729 for &id in ids2 {
1730 count2[id as usize] += 1;
1731 }
1732
1733 let mut xdf1 = XdFile {
1734 ids: ids1,
1735 changed: vec![false; nrec1],
1736 reference_index: Vec::new(),
1737 dstart: 0,
1738 dend: nrec1 as i64 - 1,
1739 nreff: 0,
1740 };
1741 let mut xdf2 = XdFile {
1742 ids: ids2,
1743 changed: vec![false; nrec2],
1744 reference_index: Vec::new(),
1745 dstart: 0,
1746 dend: nrec2 as i64 - 1,
1747 nreff: 0,
1748 };
1749
1750 let lim = nrec1.min(nrec2) as i64;
1752 let mut i = 0i64;
1753 while i < lim && ids1[i as usize] == ids2[i as usize] {
1754 i += 1;
1755 }
1756 xdf1.dstart = i;
1757 xdf2.dstart = i;
1758 let mut j = 0i64;
1759 let rem = lim - i;
1760 while j < rem && ids1[nrec1 - 1 - j as usize] == ids2[nrec2 - 1 - j as usize] {
1761 j += 1;
1762 }
1763 xdf1.dend = nrec1 as i64 - j - 1;
1764 xdf2.dend = nrec2 as i64 - j - 1;
1765
1766 let mut action1 = vec![0u8; nrec1 + 1];
1768 let mut action2 = vec![0u8; nrec2 + 1];
1769
1770 let mlim1 = if need_min {
1771 i64::MAX
1772 } else {
1773 let mut mlim = bogosqrt(nrec1 as i64);
1774 if mlim > XDL_MAX_EQLIMIT {
1775 mlim = XDL_MAX_EQLIMIT;
1776 }
1777 mlim
1778 };
1779 let mut idx = xdf1.dstart;
1780 while idx <= xdf1.dend {
1781 let id = ids1[idx as usize];
1782 let nm = count2[id as usize];
1783 action1[idx as usize] = if nm == 0 {
1784 DISCARD
1785 } else if nm >= mlim1 {
1786 INVESTIGATE
1787 } else {
1788 KEEP
1789 };
1790 idx += 1;
1791 }
1792
1793 let mlim2 = if need_min {
1794 i64::MAX
1795 } else {
1796 let mut mlim = bogosqrt(nrec2 as i64);
1797 if mlim > XDL_MAX_EQLIMIT {
1798 mlim = XDL_MAX_EQLIMIT;
1799 }
1800 mlim
1801 };
1802 let mut idx = xdf2.dstart;
1803 while idx <= xdf2.dend {
1804 let id = ids2[idx as usize];
1805 let nm = count1[id as usize];
1806 action2[idx as usize] = if nm == 0 {
1807 DISCARD
1808 } else if nm >= mlim2 {
1809 INVESTIGATE
1810 } else {
1811 KEEP
1812 };
1813 idx += 1;
1814 }
1815
1816 let mut idx = xdf1.dstart;
1817 while idx <= xdf1.dend {
1818 let a = action1[idx as usize];
1819 if a == KEEP
1820 || (a == INVESTIGATE && !clean_mmatch(&action1, idx, xdf1.dstart, xdf1.dend))
1821 {
1822 xdf1.reference_index.push(idx as usize);
1823 } else {
1824 xdf1.changed[idx as usize] = true;
1825 }
1826 idx += 1;
1827 }
1828 xdf1.nreff = xdf1.reference_index.len() as i64;
1829
1830 let mut idx = xdf2.dstart;
1831 while idx <= xdf2.dend {
1832 let a = action2[idx as usize];
1833 if a == KEEP
1834 || (a == INVESTIGATE && !clean_mmatch(&action2, idx, xdf2.dstart, xdf2.dend))
1835 {
1836 xdf2.reference_index.push(idx as usize);
1837 } else {
1838 xdf2.changed[idx as usize] = true;
1839 }
1840 idx += 1;
1841 }
1842 xdf2.nreff = xdf2.reference_index.len() as i64;
1843
1844 let ndiags = xdf1.nreff + xdf2.nreff + 3;
1846 let kvd_len = (2 * ndiags + 2) as usize;
1847 let mut kvd = vec![0i64; kvd_len];
1848 let koff = xdf2.nreff + 1;
1850 let (kvdf_slice, kvdb_slice) = kvd.split_at_mut(ndiags as usize);
1851 let xenv = XdAlgoEnv {
1854 mxcost: bogosqrt(ndiags).max(XDL_MAX_COST_MIN),
1855 snake_cnt: XDL_SNAKE_CNT,
1856 heur_min: XDL_HEUR_MIN_COST,
1857 };
1858
1859 let nreff1 = xdf1.nreff;
1860 let nreff2 = xdf2.nreff;
1861 xdl_recs_cmp(
1862 &mut xdf1, 0, nreff1, &mut xdf2, 0, nreff2, kvdf_slice, kvdb_slice, koff, need_min,
1863 &xenv,
1864 );
1865
1866 (xdf1.changed, xdf2.changed)
1867 }
1868
1869 pub fn patience_changed_flags(lines1: &[&str], lines2: &[&str]) -> (Vec<bool>, Vec<bool>) {
1871 let mut changed1 = vec![false; lines1.len()];
1872 let mut changed2 = vec![false; lines2.len()];
1873 patience_recurse(
1874 lines1,
1875 lines2,
1876 &mut changed1,
1877 &mut changed2,
1878 0,
1879 lines1.len(),
1880 0,
1881 lines2.len(),
1882 );
1883 (changed1, changed2)
1884 }
1885
1886 #[allow(clippy::too_many_arguments)]
1887 fn patience_recurse(
1888 lines1: &[&str],
1889 lines2: &[&str],
1890 changed1: &mut [bool],
1891 changed2: &mut [bool],
1892 o_start: usize,
1893 o_end: usize,
1894 n_start: usize,
1895 n_end: usize,
1896 ) {
1897 if o_start >= o_end && n_start >= n_end {
1898 return;
1899 }
1900 if o_start >= o_end {
1901 changed2[n_start..n_end].fill(true);
1902 return;
1903 }
1904 if n_start >= n_end {
1905 changed1[o_start..o_end].fill(true);
1906 return;
1907 }
1908
1909 let anchors = patience_anchors(lines1, lines2, o_start, o_end, n_start, n_end);
1910 if anchors.is_empty() {
1911 merge_myers_region(
1912 lines1, lines2, changed1, changed2, o_start, o_end, n_start, n_end,
1913 );
1914 return;
1915 }
1916
1917 let mut prev_o = o_start;
1918 let mut prev_n = n_start;
1919 for (ao, an) in anchors {
1920 patience_recurse(lines1, lines2, changed1, changed2, prev_o, ao, prev_n, an);
1921 prev_o = ao + 1;
1922 prev_n = an + 1;
1923 }
1924 patience_recurse(
1925 lines1, lines2, changed1, changed2, prev_o, o_end, prev_n, n_end,
1926 );
1927 }
1928
1929 #[allow(clippy::too_many_arguments)]
1930 fn patience_anchors(
1931 lines1: &[&str],
1932 lines2: &[&str],
1933 o_start: usize,
1934 o_end: usize,
1935 n_start: usize,
1936 n_end: usize,
1937 ) -> Vec<(usize, usize)> {
1938 use std::collections::HashMap;
1939
1940 let old_slice = &lines1[o_start..o_end];
1941 let new_slice = &lines2[n_start..n_end];
1942 let mut old_pos: HashMap<&str, Vec<usize>> = HashMap::new();
1943 let mut new_pos: HashMap<&str, Vec<usize>> = HashMap::new();
1944 for (i, line) in old_slice.iter().enumerate() {
1945 old_pos.entry(*line).or_default().push(o_start + i);
1946 }
1947 for (i, line) in new_slice.iter().enumerate() {
1948 new_pos.entry(*line).or_default().push(n_start + i);
1949 }
1950
1951 let mut pairs: Vec<(usize, usize)> = Vec::new();
1952 for (line, o_indices) in &old_pos {
1953 if o_indices.len() != 1 {
1954 continue;
1955 }
1956 if let Some(n_indices) = new_pos.get(line) {
1957 if n_indices.len() == 1 {
1958 pairs.push((o_indices[0], n_indices[0]));
1959 }
1960 }
1961 }
1962 pairs.sort_by_key(|p| p.0);
1963 longest_increasing_by_second(&pairs)
1964 }
1965
1966 fn longest_increasing_by_second(pairs: &[(usize, usize)]) -> Vec<(usize, usize)> {
1967 if pairs.is_empty() {
1968 return Vec::new();
1969 }
1970 let n = pairs.len();
1971 let mut tails = vec![0usize; n];
1972 let mut prev = vec![usize::MAX; n];
1973 let mut len = 0usize;
1974 for i in 0..n {
1975 let v = pairs[i].1;
1976 let mut lo = 0usize;
1977 let mut hi = len;
1978 while lo < hi {
1979 let mid = (lo + hi) / 2;
1980 if pairs[tails[mid]].1 < v {
1981 lo = mid + 1;
1982 } else {
1983 hi = mid;
1984 }
1985 }
1986 if lo > 0 {
1987 prev[i] = tails[lo - 1];
1988 }
1989 tails[lo] = i;
1990 if lo == len {
1991 len += 1;
1992 }
1993 }
1994 let mut out = Vec::with_capacity(len);
1995 let mut k = tails[len.saturating_sub(1)];
1996 while k != usize::MAX {
1997 out.push(pairs[k]);
1998 k = prev[k];
1999 }
2000 out.reverse();
2001 out
2002 }
2003
2004 pub(crate) fn histogram_changed_flags(
2005 lines1: &[&str],
2006 lines2: &[&str],
2007 ) -> (Vec<bool>, Vec<bool>) {
2008 crate::diff::git_histogram::histogram_changed_flags(
2009 lines1,
2010 lines2,
2011 |changed1, changed2, o_start, o_end, n_start, n_end| {
2012 merge_myers_region(
2013 lines1, lines2, changed1, changed2, o_start, o_end, n_start, n_end,
2014 );
2015 },
2016 )
2017 }
2018
2019 #[allow(clippy::too_many_arguments)]
2020 fn merge_myers_region(
2021 lines1: &[&str],
2022 lines2: &[&str],
2023 changed1: &mut [bool],
2024 changed2: &mut [bool],
2025 o_start: usize,
2026 o_end: usize,
2027 n_start: usize,
2028 n_end: usize,
2029 ) {
2030 use crate::line_interner::LineInterner;
2031
2032 let old_slice = &lines1[o_start..o_end];
2033 let new_slice = &lines2[n_start..n_end];
2034 if old_slice.is_empty() && new_slice.is_empty() {
2035 return;
2036 }
2037 let mut interner = LineInterner::with_capacity(old_slice.len() + new_slice.len());
2038 let ids1 = interner.intern_all(old_slice);
2039 let ids2 = interner.intern_all(new_slice);
2040 let (sub1, sub2) = changed_flags(&ids1, &ids2, false);
2041 for (i, ch) in sub1.iter().enumerate() {
2042 if *ch {
2043 changed1[o_start + i] = true;
2044 }
2045 }
2046 for (j, ch) in sub2.iter().enumerate() {
2047 if *ch {
2048 changed2[n_start + j] = true;
2049 }
2050 }
2051 }
2052}
2053
2054pub(crate) fn myers_rediff_changed_masks_for_region<'a>(
2056 old_lines: &[&'a str],
2057 new_lines: &[&'a str],
2058 ws: super::WhitespaceMode,
2059) -> (Vec<bool>, Vec<bool>) {
2060 use crate::line_interner::LineInterner;
2061
2062 if old_lines.is_empty() && new_lines.is_empty() {
2063 return (Vec::new(), Vec::new());
2064 }
2065 let normalize = |lines: &[&'a str]| -> Vec<Cow<'a, str>> {
2066 lines
2067 .iter()
2068 .map(|line| ws.normalize_line_for_compare(line))
2069 .collect()
2070 };
2071 let (old_normalized, new_normalized) = (normalize(old_lines), normalize(new_lines));
2072 let old_keys: Vec<&str> = old_normalized.iter().map(Cow::as_ref).collect();
2073 let new_keys: Vec<&str> = new_normalized.iter().map(Cow::as_ref).collect();
2074 let mut interner = LineInterner::with_capacity(old_keys.len() + new_keys.len());
2075 let ids1 = interner.intern_all(&old_keys);
2076 let ids2 = interner.intern_all(&new_keys);
2077 git_xdiff::changed_flags(&ids1, &ids2, false)
2078}
2079
2080fn changed_flags_to_ops(
2082 changed1: &[bool],
2083 changed2: &[bool],
2084 old_len: usize,
2085 new_len: usize,
2086) -> Vec<similar::DiffOp> {
2087 use similar::DiffOp;
2088 let mut ops: Vec<DiffOp> = Vec::new();
2089 let mut i = 0usize;
2090 let mut j = 0usize;
2091 while i < old_len || j < new_len {
2092 let del = i < old_len && changed1[i];
2093 let ins = j < new_len && changed2[j];
2094 if !del && !ins {
2095 if i < old_len && j < new_len {
2096 let start_i = i;
2098 let start_j = j;
2099 while i < old_len && j < new_len && !changed1[i] && !changed2[j] {
2100 i += 1;
2101 j += 1;
2102 }
2103 ops.push(DiffOp::Equal {
2104 old_index: start_i,
2105 new_index: start_j,
2106 len: i - start_i,
2107 });
2108 } else if i < old_len {
2109 let start_i = i;
2110 while i < old_len {
2111 i += 1;
2112 }
2113 ops.push(DiffOp::Delete {
2114 old_index: start_i,
2115 old_len: i - start_i,
2116 new_index: j,
2117 });
2118 } else if j < new_len {
2119 let start_j = j;
2120 while j < new_len {
2121 j += 1;
2122 }
2123 ops.push(DiffOp::Insert {
2124 old_index: i,
2125 new_index: start_j,
2126 new_len: j - start_j,
2127 });
2128 }
2129 } else {
2130 let start_i = i;
2132 let start_j = j;
2133 while i < old_len && changed1[i] {
2134 i += 1;
2135 }
2136 while j < new_len && changed2[j] {
2137 j += 1;
2138 }
2139 let dlen = i - start_i;
2140 let ilen = j - start_j;
2141 if dlen > 0 && ilen > 0 {
2142 ops.push(DiffOp::Replace {
2143 old_index: start_i,
2144 old_len: dlen,
2145 new_index: start_j,
2146 new_len: ilen,
2147 });
2148 } else if dlen > 0 {
2149 ops.push(DiffOp::Delete {
2150 old_index: start_i,
2151 old_len: dlen,
2152 new_index: start_j,
2153 });
2154 } else if ilen > 0 {
2155 ops.push(DiffOp::Insert {
2156 old_index: start_i,
2157 new_index: start_j,
2158 new_len: ilen,
2159 });
2160 }
2161 }
2162 }
2163 ops
2164}
2165
2166#[must_use]
2172pub fn word_diff_ops_imara(old_words: &[&str], new_words: &[&str]) -> Vec<similar::DiffOp> {
2173 use std::collections::HashMap;
2174
2175 let mut interner: HashMap<&str, u32> = HashMap::new();
2177 let mut ids1: Vec<u32> = Vec::with_capacity(old_words.len());
2178 for &w in old_words {
2179 let next = interner.len() as u32;
2180 let id = *interner.entry(w).or_insert(next);
2181 ids1.push(id);
2182 }
2183 let mut ids2: Vec<u32> = Vec::with_capacity(new_words.len());
2184 for &w in new_words {
2185 let next = interner.len() as u32;
2186 let id = *interner.entry(w).or_insert(next);
2187 ids2.push(id);
2188 }
2189
2190 let (changed1, changed2) = git_xdiff::changed_flags(&ids1, &ids2, false);
2191 let ops = changed_flags_to_ops(&changed1, &changed2, old_words.len(), new_words.len());
2192
2193 diff_indent_heuristic::apply_change_compact_to_ops(&ops, old_words, new_words, false)
2196}
2197
2198#[must_use]
2200pub fn diff_slice_ops_compacted(
2201 old_lines: &[&str],
2202 new_lines: &[&str],
2203 algorithm: similar::Algorithm,
2204 indent_heuristic: bool,
2205) -> Vec<similar::DiffOp> {
2206 diff_indent_heuristic::diff_slice_ops_compacted(
2207 old_lines,
2208 new_lines,
2209 algorithm,
2210 indent_heuristic,
2211 )
2212}
2213
2214#[must_use]
2216pub(crate) fn line_diff_ops_git_xdiff_compacted(
2217 old_lines: &[&str],
2218 new_lines: &[&str],
2219 indent_heuristic: bool,
2220) -> Vec<similar::DiffOp> {
2221 let mut interner =
2222 crate::line_interner::LineInterner::with_capacity(old_lines.len() + new_lines.len());
2223 let ids1 = interner.intern_all(old_lines);
2224 let ids2 = interner.intern_all(new_lines);
2225 line_diff_ops_git_xdiff_compacted_ids(
2226 &ids1,
2227 &ids2,
2228 old_lines,
2229 new_lines,
2230 indent_heuristic,
2231 super::WhitespaceMode::default(),
2232 )
2233}
2234
2235#[must_use]
2237pub(crate) fn line_diff_ops_git_xdiff_compacted_ids(
2238 old_ids: &[u32],
2239 new_ids: &[u32],
2240 old_lines: &[&str],
2241 new_lines: &[&str],
2242 indent_heuristic: bool,
2243 ws: super::WhitespaceMode,
2244) -> Vec<similar::DiffOp> {
2245 let (changed1, changed2) = git_xdiff::changed_flags(old_ids, new_ids, false);
2246 let ops = changed_flags_to_ops(&changed1, &changed2, old_lines.len(), new_lines.len());
2247 diff_indent_heuristic::apply_change_compact_to_ops_with_ws(
2248 &ops,
2249 old_lines,
2250 new_lines,
2251 indent_heuristic,
2252 ws,
2253 )
2254}
2255
2256#[cfg(test)]
2258#[must_use]
2259pub(crate) fn xdiff_new_line_changed_flags(
2260 old_lines: &[&str],
2261 new_lines: &[&str],
2262 indent_heuristic: bool,
2263) -> Vec<bool> {
2264 let mut interner =
2265 crate::line_interner::LineInterner::with_capacity(old_lines.len() + new_lines.len());
2266 let ids1 = interner.intern_all(old_lines);
2267 let ids2 = interner.intern_all(new_lines);
2268 let (changed1, changed2) = git_xdiff::changed_flags(&ids1, &ids2, false);
2269 let ops = changed_flags_to_ops(&changed1, &changed2, old_lines.len(), new_lines.len());
2270 let compacted = diff_indent_heuristic::apply_change_compact_to_ops(
2271 &ops,
2272 old_lines,
2273 new_lines,
2274 indent_heuristic,
2275 );
2276 let mut flags = vec![false; new_lines.len()];
2277 for op in compacted {
2278 match op.tag() {
2279 similar::DiffTag::Insert | similar::DiffTag::Replace => {
2280 for idx in op.new_range() {
2281 if idx < flags.len() {
2282 flags[idx] = true;
2283 }
2284 }
2285 }
2286 similar::DiffTag::Equal | similar::DiffTag::Delete => {}
2287 }
2288 }
2289 flags
2290}
2291
2292#[must_use]
2294pub fn map_new_to_old_lines_compacted(
2295 old_joined: &str,
2296 new_joined: &str,
2297 algorithm: similar::Algorithm,
2298 indent_heuristic: bool,
2299 new_line_count: usize,
2300) -> Vec<Option<usize>> {
2301 let ops = diff_indent_heuristic::diff_lines_ops_compacted(
2302 old_joined,
2303 new_joined,
2304 algorithm,
2305 indent_heuristic,
2306 );
2307 diff_indent_heuristic::map_new_to_old_from_ops(&ops, new_line_count)
2308}
2309
2310#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2312#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
2313pub enum DiffStatus {
2314 Added,
2316 Deleted,
2318 Modified,
2320 Renamed,
2322 Copied,
2324 TypeChanged,
2326 Unmerged,
2328}
2329
2330impl DiffStatus {
2331 #[must_use]
2333 pub fn letter(&self) -> char {
2334 match self {
2335 Self::Added => 'A',
2336 Self::Deleted => 'D',
2337 Self::Modified => 'M',
2338 Self::Renamed => 'R',
2339 Self::Copied => 'C',
2340 Self::TypeChanged => 'T',
2341 Self::Unmerged => 'U',
2342 }
2343 }
2344
2345 #[must_use]
2352 pub(crate) fn for_content_change(old_mode: u32, new_mode: u32) -> Self {
2353 if old_mode & 0o170000 != new_mode & 0o170000 {
2354 Self::TypeChanged
2355 } else {
2356 Self::Modified
2357 }
2358 }
2359}
2360
2361#[derive(Debug, Clone, PartialEq, Eq)]
2363#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
2364pub struct DiffEntry {
2365 pub status: DiffStatus,
2367 pub old_path: Option<String>,
2369 pub new_path: Option<String>,
2371 pub old_mode: String,
2373 pub new_mode: String,
2375 pub old_oid: ObjectId,
2377 pub new_oid: ObjectId,
2380 pub score: Option<u32>,
2382}
2383
2384impl DiffEntry {
2385 #[must_use]
2387 pub fn path(&self) -> &str {
2388 self.new_path
2389 .as_deref()
2390 .or(self.old_path.as_deref())
2391 .unwrap_or("")
2392 }
2393
2394 #[must_use]
2399 pub fn display_path(&self) -> String {
2400 match self.status {
2401 DiffStatus::Renamed | DiffStatus::Copied => {
2402 let old = self.old_path.as_deref().unwrap_or("");
2403 let new = self.new_path.as_deref().unwrap_or("");
2404 if old.is_empty() || new.is_empty() {
2405 self.path().to_owned()
2406 } else {
2407 format!("{old} -> {new}")
2408 }
2409 }
2410 _ => self.path().to_owned(),
2411 }
2412 }
2413}
2414
2415pub const ZERO_OID: &str = "0000000000000000000000000000000000000000";
2417
2418#[must_use]
2420pub fn zero_oid() -> ObjectId {
2421 ObjectId::from_bytes(&[0u8; 20]).unwrap_or_else(|_| {
2422 panic!("internal error: failed to create zero OID");
2424 })
2425}
2426
2427#[must_use]
2429pub fn empty_blob_oid() -> ObjectId {
2430 ObjectId::from_hex("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391").unwrap_or_else(|_| {
2431 panic!("internal error: failed to create empty blob OID");
2433 })
2434}
2435
2436pub fn diff_trees(
2451 odb: &Odb,
2452 old_tree_oid: Option<&ObjectId>,
2453 new_tree_oid: Option<&ObjectId>,
2454 prefix: &str,
2455) -> Result<Vec<DiffEntry>> {
2456 crate::trace::trace_span!("grit.diff.tree");
2457 diff_trees_opts(odb, old_tree_oid, new_tree_oid, prefix, false, None)
2458}
2459
2460pub(crate) fn diff_trees_with_index(
2463 odb: &Odb,
2464 index: &Index,
2465 old_tree_oid: Option<&ObjectId>,
2466 new_tree_oid: Option<&ObjectId>,
2467 prefix: &str,
2468 trust_index_for_old_tree: bool,
2469) -> Result<Vec<DiffEntry>> {
2470 let hint = (trust_index_for_old_tree
2471 && old_tree_oid.is_some()
2472 && index.entries_sorted
2473 && !index.entries.iter().any(|e| e.stage() != 0))
2474 .then_some(DiffTreesIndexHint { index });
2475 diff_trees_opts(
2476 odb,
2477 old_tree_oid,
2478 new_tree_oid,
2479 prefix,
2480 false,
2481 hint.as_ref(),
2482 )
2483}
2484
2485#[derive(Clone, Copy)]
2486struct DiffTreesIndexHint<'a> {
2487 index: &'a Index,
2488}
2489
2490pub fn diff_tree_path_change_count(
2498 odb: &Odb,
2499 old_tree_oid: Option<&ObjectId>,
2500 new_tree_oid: Option<&ObjectId>,
2501 prefix: &str,
2502) -> Result<usize> {
2503 if old_tree_oid == new_tree_oid {
2504 return Ok(0);
2505 }
2506 let old_entries = match old_tree_oid {
2507 Some(oid) => read_tree(odb, oid)?,
2508 None => Vec::new(),
2509 };
2510 let new_entries = match new_tree_oid {
2511 Some(oid) => read_tree(odb, oid)?,
2512 None => Vec::new(),
2513 };
2514 let mut count = 0usize;
2515 diff_tree_entries_count(odb, &old_entries, &new_entries, prefix, false, &mut count)?;
2516 Ok(count)
2517}
2518
2519pub fn diff_trees_with_renames(
2528 odb: &Odb,
2529 old_tree_oid: Option<&ObjectId>,
2530 new_tree_oid: Option<&ObjectId>,
2531 prefix: &str,
2532 rename_threshold: u32,
2533) -> Result<Vec<DiffEntry>> {
2534 let config = ConfigSet::new();
2535 let policy = ThreadPolicy::default();
2536 diff_trees_with_renames_and_policy(
2537 odb,
2538 old_tree_oid,
2539 new_tree_oid,
2540 prefix,
2541 rename_threshold,
2542 &config,
2543 &policy,
2544 )
2545}
2546
2547pub fn diff_trees_with_renames_and_policy(
2553 odb: &Odb,
2554 old_tree_oid: Option<&ObjectId>,
2555 new_tree_oid: Option<&ObjectId>,
2556 prefix: &str,
2557 rename_threshold: u32,
2558 config: &ConfigSet,
2559 thread_policy: &ThreadPolicy,
2560) -> Result<Vec<DiffEntry>> {
2561 let mut entries = diff_trees(odb, old_tree_oid, new_tree_oid, prefix)?;
2562 if rename_threshold <= 100 {
2563 entries = detect_renames_with_policy(
2564 odb,
2565 None,
2566 entries,
2567 rename_threshold,
2568 config,
2569 thread_policy,
2570 None,
2571 )?;
2572 }
2573 Ok(entries)
2574}
2575
2576pub fn tree_oid_of_commit(odb: &Odb, commit_oid: &ObjectId) -> Result<ObjectId> {
2582 let obj = odb.read(commit_oid)?;
2583 if obj.kind != ObjectKind::Commit {
2584 return Err(Error::InvalidRef(format!(
2585 "invalid ref: {commit_oid} is not a commit"
2586 )));
2587 }
2588 let commit = parse_commit(&obj.data)?;
2589 Ok(commit.tree)
2590}
2591
2592pub fn tree_oid_of_commit_in_repo(
2598 repo: &crate::repo::Repository,
2599 commit_oid: &ObjectId,
2600) -> Result<ObjectId> {
2601 if let Some(chain) = repo.caches().commit_graph_chain(repo.odb().objects_dir()) {
2602 if let Some(tree) = chain.graph_commit_tree(commit_oid) {
2603 return Ok(tree);
2604 }
2605 }
2606 tree_oid_of_commit(repo.odb(), commit_oid)
2607}
2608
2609pub fn diff_commits(odb: &Odb, base: &ObjectId, tip: &ObjectId) -> Result<Vec<DiffEntry>> {
2615 let base_tree = tree_oid_of_commit(odb, base)?;
2616 let tip_tree = tree_oid_of_commit(odb, tip)?;
2617 diff_trees(odb, Some(&base_tree), Some(&tip_tree), "")
2618}
2619
2620pub fn diff_commits_with_renames(
2629 odb: &Odb,
2630 base: &ObjectId,
2631 tip: &ObjectId,
2632 rename_threshold: u32,
2633) -> Result<Vec<DiffEntry>> {
2634 let config = ConfigSet::new();
2635 let policy = ThreadPolicy::default();
2636 diff_commits_with_renames_and_policy(odb, base, tip, rename_threshold, &config, &policy)
2637}
2638
2639pub fn diff_commits_with_renames_and_policy(
2645 odb: &Odb,
2646 base: &ObjectId,
2647 tip: &ObjectId,
2648 rename_threshold: u32,
2649 config: &ConfigSet,
2650 thread_policy: &ThreadPolicy,
2651) -> Result<Vec<DiffEntry>> {
2652 let base_tree = tree_oid_of_commit(odb, base)?;
2653 let tip_tree = tree_oid_of_commit(odb, tip)?;
2654 diff_trees_with_renames_and_policy(
2655 odb,
2656 Some(&base_tree),
2657 Some(&tip_tree),
2658 "",
2659 rename_threshold,
2660 config,
2661 thread_policy,
2662 )
2663}
2664
2665pub fn diff_trees_show_tree_entries(
2672 odb: &Odb,
2673 old_tree_oid: Option<&ObjectId>,
2674 new_tree_oid: Option<&ObjectId>,
2675 prefix: &str,
2676) -> Result<Vec<DiffEntry>> {
2677 diff_trees_opts(odb, old_tree_oid, new_tree_oid, prefix, true, None)
2678}
2679
2680struct TreeReadCache<'a> {
2681 odb: &'a Odb,
2682 parsed: std::collections::HashMap<ObjectId, Arc<Vec<TreeEntry>>>,
2683}
2684
2685impl<'a> TreeReadCache<'a> {
2686 fn new(odb: &'a Odb) -> Self {
2687 Self {
2688 odb,
2689 parsed: std::collections::HashMap::new(),
2690 }
2691 }
2692
2693 fn entries_arc(&mut self, oid: &ObjectId) -> Result<Arc<Vec<TreeEntry>>> {
2694 if let Some(e) = self.parsed.get(oid) {
2695 return Ok(Arc::clone(e));
2696 }
2697 let arc = self.odb.cached_tree_entries(oid)?;
2698 self.parsed.insert(*oid, Arc::clone(&arc));
2699 Ok(arc)
2700 }
2701}
2702
2703fn diff_subtrees_in_place(
2704 cache: &mut TreeReadCache<'_>,
2705 old_tree_oid: Option<&ObjectId>,
2706 new_tree_oid: Option<&ObjectId>,
2707 prefix: &str,
2708 show_trees: bool,
2709 index_hint: Option<&DiffTreesIndexHint<'_>>,
2710 result: &mut Vec<DiffEntry>,
2711) -> Result<()> {
2712 if !show_trees {
2713 if let (Some(a), Some(b)) = (old_tree_oid, new_tree_oid) {
2714 if a == b {
2715 return Ok(());
2716 }
2717 }
2718 }
2719 let empty: Arc<Vec<TreeEntry>> = Arc::new(Vec::new());
2720 let old_entries = match old_tree_oid {
2721 Some(oid) => cache.entries_arc(oid)?,
2722 None => Arc::clone(&empty),
2723 };
2724 let new_entries = match new_tree_oid {
2725 Some(oid) => cache.entries_arc(oid)?,
2726 None => Arc::clone(&empty),
2727 };
2728 diff_tree_entries_opts(
2729 cache,
2730 old_entries.as_slice(),
2731 new_entries.as_slice(),
2732 prefix,
2733 show_trees,
2734 index_hint,
2735 result,
2736 )
2737}
2738
2739fn diff_trees_opts(
2740 odb: &Odb,
2741 old_tree_oid: Option<&ObjectId>,
2742 new_tree_oid: Option<&ObjectId>,
2743 prefix: &str,
2744 show_trees: bool,
2745 index_hint: Option<&DiffTreesIndexHint<'_>>,
2746) -> Result<Vec<DiffEntry>> {
2747 let mut result = Vec::new();
2748 let mut cache = TreeReadCache::new(odb);
2749 diff_subtrees_in_place(
2750 &mut cache,
2751 old_tree_oid,
2752 new_tree_oid,
2753 prefix,
2754 show_trees,
2755 index_hint,
2756 &mut result,
2757 )?;
2758 Ok(result)
2759}
2760
2761#[cfg(test)]
2762use std::cell::RefCell;
2763#[cfg(test)]
2764use std::rc::Rc;
2765#[cfg(test)]
2766use std::sync::atomic::{AtomicUsize, Ordering};
2767
2768#[cfg(test)]
2769thread_local! {
2770 static DIFF_TREE_READ_HOOK: RefCell<Option<Rc<AtomicUsize>>> = const { RefCell::new(None) };
2771}
2772
2773#[cfg(test)]
2775pub(crate) fn set_diff_tree_read_hook_for_tests(hook: Option<Rc<AtomicUsize>>) {
2776 DIFF_TREE_READ_HOOK.with(|cell| *cell.borrow_mut() = hook);
2777}
2778
2779fn read_tree_from_odb(odb: &Odb, oid: &ObjectId) -> Result<Vec<TreeEntry>> {
2781 #[cfg(test)]
2782 if let Some(counter) = DIFF_TREE_READ_HOOK.with(|cell| cell.borrow().clone()) {
2783 counter.fetch_add(1, Ordering::Relaxed);
2784 }
2785 Ok((*odb.cached_tree_entries(oid)?).clone())
2786}
2787
2788fn read_tree(odb: &Odb, oid: &ObjectId) -> Result<Vec<TreeEntry>> {
2790 read_tree_from_odb(odb, oid)
2791}
2792
2793fn diff_tree_entries_count(
2794 odb: &Odb,
2795 old: &[TreeEntry],
2796 new: &[TreeEntry],
2797 prefix: &str,
2798 show_trees: bool,
2799 count: &mut usize,
2800) -> Result<()> {
2801 let mut oi = 0;
2802 let mut ni = 0;
2803
2804 while oi < old.len() || ni < new.len() {
2805 match (old.get(oi), new.get(ni)) {
2806 (Some(o), Some(n)) => {
2807 let cmp = crate::objects::tree_entry_cmp(
2808 &o.name,
2809 is_tree_mode(o.mode),
2810 &n.name,
2811 is_tree_mode(n.mode),
2812 );
2813 match cmp {
2814 std::cmp::Ordering::Less => {
2815 emit_deleted_count(odb, o, prefix, show_trees, count)?;
2816 oi += 1;
2817 }
2818 std::cmp::Ordering::Greater => {
2819 emit_added_count(odb, n, prefix, show_trees, count)?;
2820 ni += 1;
2821 }
2822 std::cmp::Ordering::Equal => {
2823 if o.oid != n.oid || o.mode != n.mode {
2824 let path = crate::repo_relpath::format_path_stored(prefix, &o.name);
2825 if is_tree_mode(o.mode) && is_tree_mode(n.mode) {
2826 if show_trees {
2827 *count += 1;
2828 }
2829 *count += diff_tree_path_change_count(
2830 odb,
2831 Some(&o.oid),
2832 Some(&n.oid),
2833 &path,
2834 )?;
2835 } else if is_tree_mode(o.mode) != is_tree_mode(n.mode) {
2836 emit_deleted_count(odb, o, prefix, show_trees, count)?;
2837 emit_added_count(odb, n, prefix, show_trees, count)?;
2838 } else {
2839 *count += 1;
2840 }
2841 }
2842 oi += 1;
2843 ni += 1;
2844 }
2845 }
2846 }
2847 (Some(o), None) => {
2848 emit_deleted_count(odb, o, prefix, show_trees, count)?;
2849 oi += 1;
2850 }
2851 (None, Some(n)) => {
2852 emit_added_count(odb, n, prefix, show_trees, count)?;
2853 ni += 1;
2854 }
2855 (None, None) => break,
2856 }
2857 }
2858
2859 Ok(())
2860}
2861
2862fn emit_deleted_count(
2863 odb: &Odb,
2864 entry: &TreeEntry,
2865 prefix: &str,
2866 show_trees: bool,
2867 count: &mut usize,
2868) -> Result<()> {
2869 let path = crate::repo_relpath::format_path_stored(prefix, &entry.name);
2870 if is_tree_mode(entry.mode) {
2871 if show_trees {
2872 *count += 1;
2873 }
2874 *count += diff_tree_path_change_count(odb, Some(&entry.oid), None, &path)?;
2875 } else {
2876 *count += 1;
2877 }
2878 Ok(())
2879}
2880
2881fn emit_added_count(
2882 odb: &Odb,
2883 entry: &TreeEntry,
2884 prefix: &str,
2885 show_trees: bool,
2886 count: &mut usize,
2887) -> Result<()> {
2888 let path = crate::repo_relpath::format_path_stored(prefix, &entry.name);
2889 if is_tree_mode(entry.mode) {
2890 if show_trees {
2891 *count += 1;
2892 }
2893 *count += diff_tree_path_change_count(odb, None, Some(&entry.oid), &path)?;
2894 } else {
2895 *count += 1;
2896 }
2897 Ok(())
2898}
2899
2900fn diff_tree_entries_opts(
2902 cache: &mut TreeReadCache<'_>,
2903 old: &[TreeEntry],
2904 new: &[TreeEntry],
2905 prefix: &str,
2906 show_trees: bool,
2907 index_hint: Option<&DiffTreesIndexHint<'_>>,
2908 result: &mut Vec<DiffEntry>,
2909) -> Result<()> {
2910 let mut oi = 0;
2911 let mut ni = 0;
2912
2913 while oi < old.len() || ni < new.len() {
2914 match (old.get(oi), new.get(ni)) {
2915 (Some(o), Some(n)) => {
2916 let cmp = crate::objects::tree_entry_cmp(
2917 &o.name,
2918 is_tree_mode(o.mode),
2919 &n.name,
2920 is_tree_mode(n.mode),
2921 );
2922 match cmp {
2923 std::cmp::Ordering::Less => {
2924 emit_deleted_opts(cache, o, prefix, show_trees, index_hint, result)?;
2926 oi += 1;
2927 }
2928 std::cmp::Ordering::Greater => {
2929 emit_added_opts(cache, n, prefix, show_trees, result)?;
2931 ni += 1;
2932 }
2933 std::cmp::Ordering::Equal => {
2934 if o.oid != n.oid || o.mode != n.mode {
2936 let path = crate::repo_relpath::format_path_stored(prefix, &o.name);
2937 if is_tree_mode(o.mode) && is_tree_mode(n.mode) {
2938 if show_trees {
2940 result.push(DiffEntry {
2941 status: DiffStatus::Modified,
2942 old_path: Some(path.clone()),
2943 new_path: Some(path.clone()),
2944 old_mode: format_mode(o.mode),
2945 new_mode: format_mode(n.mode),
2946 old_oid: o.oid,
2947 new_oid: n.oid,
2948 score: None,
2949 });
2950 }
2951 diff_subtrees_in_place(
2952 cache,
2953 Some(&o.oid),
2954 Some(&n.oid),
2955 &path,
2956 show_trees,
2957 index_hint,
2958 result,
2959 )?;
2960 } else if is_tree_mode(o.mode) && !is_tree_mode(n.mode) {
2961 emit_deleted_opts(
2963 cache, o, prefix, show_trees, index_hint, result,
2964 )?;
2965 emit_added_opts(cache, n, prefix, show_trees, result)?;
2966 } else if !is_tree_mode(o.mode) && is_tree_mode(n.mode) {
2967 emit_deleted_opts(
2969 cache, o, prefix, show_trees, index_hint, result,
2970 )?;
2971 emit_added_opts(cache, n, prefix, show_trees, result)?;
2972 } else {
2973 result.push(DiffEntry {
2974 status: DiffStatus::for_content_change(o.mode, n.mode),
2975 old_path: Some(path.clone()),
2976 new_path: Some(path),
2977 old_mode: format_mode(o.mode),
2978 new_mode: format_mode(n.mode),
2979 old_oid: o.oid,
2980 new_oid: n.oid,
2981 score: None,
2982 });
2983 }
2984 }
2985 oi += 1;
2986 ni += 1;
2987 }
2988 }
2989 }
2990 (Some(o), None) => {
2991 emit_deleted_opts(cache, o, prefix, show_trees, index_hint, result)?;
2992 oi += 1;
2993 }
2994 (None, Some(n)) => {
2995 emit_added_opts(cache, n, prefix, show_trees, result)?;
2996 ni += 1;
2997 }
2998 (None, None) => break,
2999 }
3000 }
3001
3002 Ok(())
3003}
3004
3005fn emit_deleted_tree_from_index(index: &Index, dir_prefix: &str, result: &mut Vec<DiffEntry>) {
3006 let scan_prefix: Vec<u8> = if dir_prefix.is_empty() {
3007 Vec::new()
3008 } else {
3009 format!("{dir_prefix}/").into_bytes()
3010 };
3011 if index.entries_sorted {
3012 let start = if scan_prefix.is_empty() {
3013 0
3014 } else {
3015 index
3016 .entries
3017 .partition_point(|e| e.path.as_slice() < scan_prefix.as_slice())
3018 };
3019 for ie in index.entries[start..].iter() {
3020 if ie.stage() != 0 {
3021 continue;
3022 }
3023 if !scan_prefix.is_empty() && !ie.path.starts_with(&scan_prefix) {
3024 break;
3025 }
3026 if scan_prefix.is_empty() && ie.path.contains(&b'/') {
3027 continue;
3028 }
3029 let path = String::from_utf8_lossy(&ie.path).into_owned();
3030 result.push(DiffEntry {
3031 status: DiffStatus::Deleted,
3032 old_path: Some(path),
3033 new_path: None,
3034 old_mode: format_mode(ie.mode),
3035 new_mode: "000000".to_owned(),
3036 old_oid: ie.oid,
3037 new_oid: zero_oid(),
3038 score: None,
3039 });
3040 }
3041 return;
3042 }
3043 for ie in &index.entries {
3044 if ie.stage() != 0 {
3045 continue;
3046 }
3047 let Ok(path) = std::str::from_utf8(&ie.path) else {
3048 continue;
3049 };
3050 let under = if dir_prefix.is_empty() {
3051 !path.contains('/')
3052 } else if path == dir_prefix {
3053 false
3054 } else {
3055 path.strip_prefix(dir_prefix)
3056 .is_some_and(|rest| rest.starts_with('/'))
3057 };
3058 if !under {
3059 continue;
3060 }
3061 result.push(DiffEntry {
3062 status: DiffStatus::Deleted,
3063 old_path: Some(path.to_owned()),
3064 new_path: None,
3065 old_mode: format_mode(ie.mode),
3066 new_mode: "000000".to_owned(),
3067 old_oid: ie.oid,
3068 new_oid: zero_oid(),
3069 score: None,
3070 });
3071 }
3072}
3073
3074fn emit_deleted_opts(
3075 cache: &mut TreeReadCache<'_>,
3076 entry: &TreeEntry,
3077 prefix: &str,
3078 show_trees: bool,
3079 index_hint: Option<&DiffTreesIndexHint<'_>>,
3080 result: &mut Vec<DiffEntry>,
3081) -> Result<()> {
3082 let path = crate::repo_relpath::format_path_stored(prefix, &entry.name);
3083 if is_tree_mode(entry.mode) {
3084 if show_trees {
3085 result.push(DiffEntry {
3086 status: DiffStatus::Deleted,
3087 old_path: Some(path.clone()),
3088 new_path: None,
3089 old_mode: format_mode(entry.mode),
3090 new_mode: "000000".to_owned(),
3091 old_oid: entry.oid,
3092 new_oid: zero_oid(),
3093 score: None,
3094 });
3095 }
3096 if let Some(hint) = index_hint {
3097 emit_deleted_tree_from_index(hint.index, &path, result);
3098 return Ok(());
3099 }
3100 diff_subtrees_in_place(
3101 cache,
3102 Some(&entry.oid),
3103 None,
3104 &path,
3105 show_trees,
3106 None,
3107 result,
3108 )?;
3109 } else {
3110 result.push(DiffEntry {
3111 status: DiffStatus::Deleted,
3112 old_path: Some(path.clone()),
3113 new_path: None,
3114 old_mode: format_mode(entry.mode),
3115 new_mode: "000000".to_owned(),
3116 old_oid: entry.oid,
3117 new_oid: zero_oid(),
3118 score: None,
3119 });
3120 }
3121 Ok(())
3122}
3123
3124fn emit_added_opts(
3125 cache: &mut TreeReadCache<'_>,
3126 entry: &TreeEntry,
3127 prefix: &str,
3128 show_trees: bool,
3129 result: &mut Vec<DiffEntry>,
3130) -> Result<()> {
3131 let path = crate::repo_relpath::format_path_stored(prefix, &entry.name);
3132 if is_tree_mode(entry.mode) {
3133 if show_trees {
3134 result.push(DiffEntry {
3135 status: DiffStatus::Added,
3136 old_path: None,
3137 new_path: Some(path.clone()),
3138 old_mode: "000000".to_owned(),
3139 new_mode: format_mode(entry.mode),
3140 old_oid: zero_oid(),
3141 new_oid: entry.oid,
3142 score: None,
3143 });
3144 }
3145 diff_subtrees_in_place(
3146 cache,
3147 None,
3148 Some(&entry.oid),
3149 &path,
3150 show_trees,
3151 None,
3152 result,
3153 )?;
3154 } else {
3155 result.push(DiffEntry {
3156 status: DiffStatus::Added,
3157 old_path: None,
3158 new_path: Some(path),
3159 old_mode: "000000".to_owned(),
3160 new_mode: format_mode(entry.mode),
3161 old_oid: zero_oid(),
3162 new_oid: entry.oid,
3163 score: None,
3164 });
3165 }
3166 Ok(())
3167}
3168
3169pub fn diff_index_to_tree(
3189 odb: &Odb,
3190 index: &Index,
3191 tree_oid: Option<&ObjectId>,
3192 ignore_submodules: bool,
3193) -> Result<Vec<DiffEntry>> {
3194 match index_cache_tree_walk(index, tree_oid) {
3195 CacheTreeWalk::Fast {
3196 head_tree,
3197 cache_root,
3198 } => {
3199 diff_index_to_tree_with_cache_tree(odb, index, head_tree, cache_root, ignore_submodules)
3200 }
3201 CacheTreeWalk::Flatten => {
3202 diff_index_to_tree_flatten(odb, index, tree_oid, ignore_submodules)
3203 }
3204 }
3205}
3206
3207pub fn diff_index_to_tree_with_renames(
3216 odb: &Odb,
3217 index: &Index,
3218 tree_oid: Option<&ObjectId>,
3219 ignore_submodules: bool,
3220 rename_threshold: u32,
3221 config: &ConfigSet,
3222 thread_policy: &ThreadPolicy,
3223) -> Result<Vec<DiffEntry>> {
3224 let entries = diff_index_to_tree(odb, index, tree_oid, ignore_submodules)?;
3225 apply_rename_detection_to_diff_entries(
3226 odb,
3227 entries,
3228 None,
3229 rename_threshold,
3230 config,
3231 thread_policy,
3232 )
3233}
3234
3235pub fn apply_rename_detection_to_diff_entries(
3244 odb: &Odb,
3245 entries: Vec<DiffEntry>,
3246 work_root: Option<&Path>,
3247 rename_threshold: u32,
3248 config: &ConfigSet,
3249 thread_policy: &ThreadPolicy,
3250) -> Result<Vec<DiffEntry>> {
3251 if entries.is_empty() || rename_threshold >= 100 {
3252 return Ok(entries);
3253 }
3254 if work_root.is_some() {
3255 let opts = DiffPostprocessOptions {
3256 rename_threshold,
3257 ..DiffPostprocessOptions::default()
3258 };
3259 postprocess_diff_entries(
3260 odb,
3261 entries,
3262 None,
3263 work_root,
3264 None,
3265 config,
3266 thread_policy,
3267 &opts,
3268 )
3269 } else {
3270 detect_renames_with_policy(
3271 odb,
3272 None,
3273 entries,
3274 rename_threshold,
3275 config,
3276 thread_policy,
3277 None,
3278 )
3279 }
3280}
3281
3282pub fn append_untracked_worktree_added_for_rename_detection(
3294 repo: &Repository,
3295 index: &Index,
3296 rules: &WorktreeRules,
3297 mut entries: Vec<DiffEntry>,
3298) -> Result<(Vec<DiffEntry>, HashSet<String>)> {
3299 let mut synthetic_new_paths = HashSet::new();
3300 if !entries.iter().any(|e| e.status == DiffStatus::Deleted) {
3301 return Ok((entries, synthetic_new_paths));
3302 }
3303 let mut existing_new: HashSet<String> =
3304 entries.iter().filter_map(|e| e.new_path.clone()).collect();
3305 let untracked = list_untracked_worktree_files_for_rename(repo, index, rules)?;
3306 for path in untracked {
3307 if existing_new.contains(&path) {
3308 continue;
3309 }
3310 existing_new.insert(path.clone());
3311 synthetic_new_paths.insert(path.clone());
3312 entries.push(DiffEntry {
3313 status: DiffStatus::Added,
3314 old_path: None,
3315 new_path: Some(path),
3316 old_mode: "000000".to_owned(),
3317 new_mode: "100644".to_owned(),
3318 old_oid: zero_oid(),
3319 new_oid: zero_oid(),
3320 score: None,
3321 });
3322 }
3323 Ok((entries, synthetic_new_paths))
3324}
3325
3326#[must_use]
3328pub fn drop_unused_synthetic_rename_candidates(
3329 entries: Vec<DiffEntry>,
3330 synthetic_new_paths: &HashSet<String>,
3331) -> Vec<DiffEntry> {
3332 if synthetic_new_paths.is_empty() {
3333 return entries;
3334 }
3335 entries
3336 .into_iter()
3337 .filter(|e| {
3338 !(e.status == DiffStatus::Added
3339 && e.new_path
3340 .as_ref()
3341 .is_some_and(|p| synthetic_new_paths.contains(p)))
3342 })
3343 .collect()
3344}
3345
3346fn list_untracked_worktree_files_for_rename(
3347 repo: &Repository,
3348 index: &Index,
3349 rules: &WorktreeRules,
3350) -> Result<Vec<String>> {
3351 let work_tree = repo.work_tree().ok_or(Error::RequiresWorkTree)?;
3352 let mut out = Vec::new();
3353 walk_untracked_worktree_files(repo, index, work_tree, "", rules, &mut out)?;
3354 out.sort();
3355 Ok(out)
3356}
3357
3358fn walk_untracked_worktree_files(
3359 repo: &Repository,
3360 index: &Index,
3361 dir: &Path,
3362 rel_prefix: &str,
3363 rules: &WorktreeRules,
3364 out: &mut Vec<String>,
3365) -> Result<()> {
3366 let read = match fs::read_dir(dir) {
3367 Ok(r) => r,
3368 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
3369 Err(e) => return Err(Error::Io(e)),
3370 };
3371 for entry in read {
3372 let entry = entry.map_err(Error::Io)?;
3373 let name = entry.file_name();
3374 let name = name.to_string_lossy();
3375 if rel_prefix.is_empty() && name == ".git" {
3376 continue;
3377 }
3378 let rel_path = if rel_prefix.is_empty() {
3379 name.to_string()
3380 } else {
3381 format!("{rel_prefix}/{name}")
3382 };
3383 let file_type = entry.file_type().map_err(Error::Io)?;
3384 if file_type.is_dir() {
3385 let (ignored, _) = rules
3386 .ignore_mut()
3387 .check_path(repo, Some(index), &rel_path, true)?;
3388 if ignored {
3389 continue;
3390 }
3391 walk_untracked_worktree_files(
3392 repo,
3393 index,
3394 &dir.join(name.as_ref()),
3395 &rel_path,
3396 rules,
3397 out,
3398 )?;
3399 } else if file_type.is_file() || file_type.is_symlink() {
3400 if index.get(rel_path.as_bytes(), 0).is_some() {
3401 continue;
3402 }
3403 let (ignored, _) =
3404 rules
3405 .ignore_mut()
3406 .check_path(repo, Some(index), &rel_path, false)?;
3407 if !ignored {
3408 out.push(rel_path);
3409 }
3410 }
3411 }
3412 Ok(())
3413}
3414
3415pub fn diff_index_to_worktree_with_renames(
3425 repo: &Repository,
3426 index: &mut Index,
3427 work_tree: &Path,
3428 options: DiffIndexToWorktreeOptions,
3429 rename_threshold: u32,
3430) -> Result<(Vec<DiffEntry>, bool)> {
3431 let config = repo.config()?.as_ref().clone();
3432 let thread_policy = repo.thread_policy();
3433 let odb = repo.odb();
3434 let rules_for_append = options.worktree_rules.clone();
3435 let (mut entries, changed) =
3436 diff_index_to_worktree_with_options(odb, index, work_tree, options)?;
3437 let mut synthetic_new_paths = HashSet::new();
3438 if rename_threshold < 100 {
3439 if let Some(rules_arc) = rules_for_append {
3440 let rules = rules_arc
3441 .lock()
3442 .map_err(|e| Error::Message(format!("worktree rules lock poisoned: {e}")))?;
3443 (entries, synthetic_new_paths) =
3444 append_untracked_worktree_added_for_rename_detection(repo, index, &rules, entries)?;
3445 } else {
3446 let rules = WorktreeRules::from_repository(repo, index)?;
3447 (entries, synthetic_new_paths) =
3448 append_untracked_worktree_added_for_rename_detection(repo, index, &rules, entries)?;
3449 }
3450 }
3451 let entries = apply_rename_detection_to_diff_entries(
3452 odb,
3453 entries,
3454 Some(work_tree),
3455 rename_threshold,
3456 &config,
3457 &thread_policy,
3458 )?;
3459 let entries = drop_unused_synthetic_rename_candidates(entries, &synthetic_new_paths);
3460 Ok((entries, changed))
3461}
3462
3463enum CacheTreeWalk<'a> {
3464 Fast {
3465 head_tree: &'a ObjectId,
3466 cache_root: &'a CacheTreeNode,
3467 },
3468 Flatten,
3469}
3470
3471fn index_cache_tree_walk<'a>(
3474 index: &'a Index,
3475 tree_oid: Option<&'a ObjectId>,
3476) -> CacheTreeWalk<'a> {
3477 let Some(head_tree) = tree_oid else {
3478 return CacheTreeWalk::Flatten;
3479 };
3480 let Some(cache_root) = index.cache_tree.as_ref() else {
3481 return CacheTreeWalk::Flatten;
3482 };
3483 if index.entries.iter().any(|e| e.stage() != 0) {
3484 return CacheTreeWalk::Flatten;
3485 }
3486 CacheTreeWalk::Fast {
3487 head_tree,
3488 cache_root,
3489 }
3490}
3491
3492fn diff_index_to_tree_flatten(
3494 odb: &Odb,
3495 index: &Index,
3496 tree_oid: Option<&ObjectId>,
3497 ignore_submodules: bool,
3498) -> Result<Vec<DiffEntry>> {
3499 let tree_entries = match tree_oid {
3501 Some(oid) => flatten_tree(odb, oid, "")?,
3502 None => Vec::new(),
3503 };
3504
3505 let mut tree_map: std::collections::BTreeMap<&str, &FlatEntry> =
3507 std::collections::BTreeMap::new();
3508 for entry in &tree_entries {
3509 tree_map.insert(&entry.path, entry);
3510 }
3511
3512 let mut result = Vec::new();
3513 let mut stage0_paths = std::collections::BTreeSet::new();
3514 let mut unmerged_modes: std::collections::BTreeMap<String, (u8, u32)> =
3515 std::collections::BTreeMap::new();
3516
3517 for ie in &index.entries {
3519 let path = String::from_utf8_lossy(&ie.path).to_string();
3520 if ie.stage() == 0 && ie.intent_to_add() {
3521 continue;
3524 }
3525 if ie.stage() != 0 {
3526 let rank = match ie.stage() {
3527 2 => 0u8,
3528 3 => 1u8,
3529 1 => 2u8,
3530 _ => 3u8,
3531 };
3532 match unmerged_modes.get(&path) {
3533 Some((existing_rank, _)) if *existing_rank <= rank => {}
3534 _ => {
3535 unmerged_modes.insert(path, (rank, ie.mode));
3536 }
3537 }
3538 continue;
3539 }
3540 if ignore_submodules && ie.mode == 0o160000 {
3541 let _ = tree_map.remove(path.as_str());
3542 stage0_paths.insert(path.clone());
3543 continue;
3544 }
3545 stage0_paths.insert(path.clone());
3546 match tree_map.remove(path.as_str()) {
3547 Some(te) => {
3548 if te.oid != ie.oid || te.mode != ie.mode {
3550 result.push(DiffEntry {
3551 status: DiffStatus::for_content_change(te.mode, ie.mode),
3552 old_path: Some(path.clone()),
3553 new_path: Some(path),
3554 old_mode: format_mode(te.mode),
3555 new_mode: format_mode(ie.mode),
3556 old_oid: te.oid,
3557 new_oid: ie.oid,
3558 score: None,
3559 });
3560 }
3561 }
3562 None => {
3563 result.push(DiffEntry {
3565 status: DiffStatus::Added,
3566 old_path: None,
3567 new_path: Some(path),
3568 old_mode: "000000".to_owned(),
3569 new_mode: format_mode(ie.mode),
3570 old_oid: zero_oid(),
3571 new_oid: ie.oid,
3572 score: None,
3573 });
3574 }
3575 }
3576 }
3577
3578 diff_index_to_tree_finish_unmerged_and_deleted(
3579 ignore_submodules,
3580 &mut tree_map,
3581 &unmerged_modes,
3582 &stage0_paths,
3583 &mut result,
3584 );
3585
3586 result.sort_by(|a, b| a.path().cmp(b.path()));
3587 Ok(result)
3588}
3589
3590fn diff_index_to_tree_finish_unmerged_and_deleted(
3591 ignore_submodules: bool,
3592 tree_map: &mut std::collections::BTreeMap<&str, &FlatEntry>,
3593 unmerged_modes: &std::collections::BTreeMap<String, (u8, u32)>,
3594 stage0_paths: &std::collections::BTreeSet<String>,
3595 result: &mut Vec<DiffEntry>,
3596) {
3597 for (path, (_, mode)) in unmerged_modes {
3598 if stage0_paths.contains(path) {
3599 continue;
3600 }
3601 tree_map.remove(path.as_str());
3602 result.push(DiffEntry {
3603 status: DiffStatus::Unmerged,
3604 old_path: Some(path.clone()),
3605 new_path: Some(path.clone()),
3606 old_mode: "000000".to_owned(),
3607 new_mode: format_mode(*mode),
3608 old_oid: zero_oid(),
3609 new_oid: zero_oid(),
3610 score: None,
3611 });
3612 }
3613
3614 for (path, te) in std::mem::take(tree_map) {
3616 if ignore_submodules && te.mode == 0o160000 {
3617 continue;
3618 }
3619 result.push(DiffEntry {
3620 status: DiffStatus::Deleted,
3621 old_path: Some(path.to_owned()),
3622 new_path: None,
3623 old_mode: format_mode(te.mode),
3624 new_mode: "000000".to_owned(),
3625 old_oid: te.oid,
3626 new_oid: zero_oid(),
3627 score: None,
3628 });
3629 }
3630}
3631
3632fn diff_index_to_tree_with_cache_tree(
3635 odb: &Odb,
3636 index: &Index,
3637 head_tree: &ObjectId,
3638 cache_root: &CacheTreeNode,
3639 ignore_submodules: bool,
3640) -> Result<Vec<DiffEntry>> {
3641 let mut result = Vec::new();
3642 let mut idx_cursor = 0usize;
3643
3644 let mut walk = DiffIndexTreeWalk {
3645 odb,
3646 index,
3647 idx_cursor: &mut idx_cursor,
3648 ignore_submodules,
3649 result: &mut result,
3650 abort_to_flatten: false,
3651 };
3652 diff_index_tree_at(&mut walk, head_tree, Some(cache_root), b"")?;
3653 if walk.abort_to_flatten {
3654 return diff_index_to_tree_flatten(odb, index, Some(head_tree), ignore_submodules);
3655 }
3656
3657 while idx_cursor < index.entries.len() {
3659 let ie = &index.entries[idx_cursor];
3660 idx_cursor += 1;
3661 let path = String::from_utf8_lossy(&ie.path).to_string();
3662 if ie.stage() == 0 && ie.intent_to_add() {
3663 continue;
3664 }
3665 if ie.stage() != 0 {
3666 continue;
3667 }
3668 if ignore_submodules && ie.mode == 0o160000 {
3669 continue;
3670 }
3671 result.push(DiffEntry {
3672 status: DiffStatus::Added,
3673 old_path: None,
3674 new_path: Some(path),
3675 old_mode: "000000".to_owned(),
3676 new_mode: format_mode(ie.mode),
3677 old_oid: zero_oid(),
3678 new_oid: ie.oid,
3679 score: None,
3680 });
3681 }
3682
3683 result.sort_by(|a, b| a.path().cmp(b.path()));
3684 Ok(result)
3685}
3686
3687enum IndexLevelKind<'a> {
3688 Blob {
3689 entry: &'a IndexEntry,
3690 name: &'a [u8],
3691 },
3692 Tree {
3693 name: &'a [u8],
3694 },
3695}
3696
3697fn index_relative_in_dir<'a>(path: &'a [u8], dir_prefix: &[u8]) -> Option<&'a [u8]> {
3700 if dir_prefix.is_empty() {
3701 return Some(path);
3702 }
3703 let rest = path.strip_prefix(dir_prefix)?;
3704 match rest.split_first() {
3705 None => Some(rest),
3706 Some((b'/', tail)) => Some(tail),
3707 Some(_) => None,
3708 }
3709}
3710
3711fn join_index_path(dir_prefix: &[u8], name: &[u8]) -> Vec<u8> {
3712 if dir_prefix.is_empty() {
3713 return name.to_vec();
3714 }
3715 let mut path = Vec::with_capacity(dir_prefix.len() + 1 + name.len());
3716 path.extend_from_slice(dir_prefix);
3717 path.push(b'/');
3718 path.extend_from_slice(name);
3719 path
3720}
3721
3722fn peek_index_level_entry<'a>(
3723 index: &'a Index,
3724 cursor: &mut usize,
3725 dir_prefix: &[u8],
3726 ignore_submodules: bool,
3727) -> Option<IndexLevelKind<'a>> {
3728 while let Some(ie) = index.entries.get(*cursor) {
3729 let rel = index_relative_in_dir(&ie.path, dir_prefix)?;
3730 if ie.stage() != 0 || ie.intent_to_add() || rel.is_empty() {
3731 *cursor += 1;
3732 continue;
3733 }
3734 if let Some(slash) = rel.iter().position(|&b| b == b'/') {
3735 return Some(IndexLevelKind::Tree {
3736 name: &rel[..slash],
3737 });
3738 }
3739 if ignore_submodules && ie.mode == 0o160000 {
3740 *cursor += 1;
3741 continue;
3742 }
3743 return Some(IndexLevelKind::Blob {
3744 entry: ie,
3745 name: rel,
3746 });
3747 }
3748 None
3749}
3750
3751fn skip_index_subtree(index: &Index, cursor: &mut usize, dir_prefix: &[u8]) {
3752 while index
3753 .entries
3754 .get(*cursor)
3755 .is_some_and(|ie| index_relative_in_dir(&ie.path, dir_prefix).is_some())
3756 {
3757 *cursor += 1;
3758 }
3759}
3760
3761const CACHE_CHILD_LINEAR_SCAN_MAX: usize = 16;
3763
3764struct CacheChildren<'a> {
3770 children: &'a [CacheTreeNode],
3771 sorted: Vec<&'a CacheTreeNode>,
3772}
3773
3774impl<'a> CacheChildren<'a> {
3775 fn new(node: Option<&'a CacheTreeNode>) -> Self {
3776 let children = node.map_or(&[][..], |n| n.children.as_slice());
3777 let sorted = if children.len() > CACHE_CHILD_LINEAR_SCAN_MAX {
3778 let mut sorted: Vec<&CacheTreeNode> = children.iter().collect();
3779 sorted.sort_by(|a, b| a.name.cmp(&b.name));
3780 sorted
3781 } else {
3782 Vec::new()
3783 };
3784 Self { children, sorted }
3785 }
3786
3787 fn get(&self, name: &[u8]) -> Option<&'a CacheTreeNode> {
3788 if self.sorted.is_empty() {
3789 return self.children.iter().find(|c| c.name == name);
3790 }
3791 let pos = self.sorted.partition_point(|c| c.name.as_slice() < name);
3792 self.sorted.get(pos).copied().filter(|c| c.name == name)
3793 }
3794}
3795
3796struct DiffIndexTreeWalk<'a, 'b> {
3797 odb: &'a Odb,
3798 index: &'a Index,
3799 idx_cursor: &'b mut usize,
3800 ignore_submodules: bool,
3801 result: &'b mut Vec<DiffEntry>,
3802 abort_to_flatten: bool,
3804}
3805
3806fn tree_entries_have_duplicate_names(entries: &[TreeEntry]) -> bool {
3807 let mut seen = std::collections::BTreeSet::<&[u8]>::new();
3808 for entry in entries {
3809 if !seen.insert(entry.name.as_slice()) {
3810 return true;
3811 }
3812 }
3813 false
3814}
3815
3816fn diff_index_tree_at(
3818 walk: &mut DiffIndexTreeWalk<'_, '_>,
3819 tree_oid: &ObjectId,
3820 cache_node: Option<&CacheTreeNode>,
3821 dir_prefix: &[u8],
3822) -> Result<()> {
3823 let tree_entries = read_tree(walk.odb, tree_oid)?;
3824 if tree_entries_have_duplicate_names(&tree_entries) {
3825 walk.abort_to_flatten = true;
3826 return Ok(());
3827 }
3828 let cache_children = CacheChildren::new(cache_node);
3829 let mut ti = 0usize;
3830
3831 loop {
3832 let te = tree_entries.get(ti);
3833 let ie_kind = peek_index_level_entry(
3834 walk.index,
3835 walk.idx_cursor,
3836 dir_prefix,
3837 walk.ignore_submodules,
3838 );
3839
3840 match (te, ie_kind) {
3841 (Some(t_entry), Some(ie)) => {
3842 let t_name = &t_entry.name;
3843 let (i_name, i_is_tree) = match &ie {
3844 IndexLevelKind::Blob { name, .. } => (*name, false),
3845 IndexLevelKind::Tree { name } => (*name, true),
3846 };
3847 let t_is_tree = is_tree_mode(t_entry.mode);
3848 let cmp = crate::objects::tree_entry_cmp(t_name, t_is_tree, i_name, i_is_tree);
3849 match cmp {
3850 std::cmp::Ordering::Less => {
3851 emit_index_tree_deleted(
3852 walk.odb,
3853 t_entry,
3854 dir_prefix,
3855 walk.ignore_submodules,
3856 walk.result,
3857 )?;
3858 ti += 1;
3859 }
3860 std::cmp::Ordering::Greater => {
3861 emit_index_tree_added(walk, dir_prefix)?;
3862 }
3863 std::cmp::Ordering::Equal => {
3864 if t_is_tree && i_is_tree {
3865 let full_path = join_index_path(dir_prefix, t_name);
3866 let child_cache = cache_children.get(t_name);
3867 if child_cache
3868 .is_some_and(|c| c.is_valid() && c.oid == Some(t_entry.oid))
3869 {
3870 skip_index_subtree(walk.index, walk.idx_cursor, &full_path);
3871 ti += 1;
3872 continue;
3873 }
3874 diff_index_tree_at(walk, &t_entry.oid, child_cache, &full_path)?;
3875 } else if t_is_tree != i_is_tree {
3876 emit_index_tree_deleted(
3877 walk.odb,
3878 t_entry,
3879 dir_prefix,
3880 walk.ignore_submodules,
3881 walk.result,
3882 )?;
3883 emit_index_tree_added(walk, dir_prefix)?;
3884 } else {
3885 let IndexLevelKind::Blob { entry: ie, .. } = ie else {
3886 ti += 1;
3887 continue;
3888 };
3889 *walk.idx_cursor += 1;
3890 if t_entry.oid != ie.oid || t_entry.mode != ie.mode {
3891 let path = String::from_utf8_lossy(&ie.path).into_owned();
3892 walk.result.push(DiffEntry {
3893 status: DiffStatus::for_content_change(t_entry.mode, ie.mode),
3894 old_path: Some(path.clone()),
3895 new_path: Some(path),
3896 old_mode: format_mode(t_entry.mode),
3897 new_mode: format_mode(ie.mode),
3898 old_oid: t_entry.oid,
3899 new_oid: ie.oid,
3900 score: None,
3901 });
3902 }
3903 }
3904 ti += 1;
3905 }
3906 }
3907 }
3908 (Some(t_entry), None) => {
3909 emit_index_tree_deleted(
3910 walk.odb,
3911 t_entry,
3912 dir_prefix,
3913 walk.ignore_submodules,
3914 walk.result,
3915 )?;
3916 ti += 1;
3917 }
3918 (None, Some(_)) => {
3919 emit_index_tree_added(walk, dir_prefix)?;
3920 }
3921 (None, None) => break,
3922 }
3923 }
3924
3925 Ok(())
3926}
3927
3928fn emit_index_tree_deleted(
3929 odb: &Odb,
3930 entry: &TreeEntry,
3931 dir_prefix: &[u8],
3932 ignore_submodules: bool,
3933 result: &mut Vec<DiffEntry>,
3934) -> Result<()> {
3935 if ignore_submodules && entry.mode == 0o160000 {
3936 return Ok(());
3937 }
3938 let path = String::from_utf8_lossy(&join_index_path(dir_prefix, &entry.name)).into_owned();
3939 if is_tree_mode(entry.mode) {
3940 for fe in flatten_tree(odb, &entry.oid, &path)? {
3941 if ignore_submodules && fe.mode == 0o160000 {
3942 continue;
3943 }
3944 result.push(DiffEntry {
3945 status: DiffStatus::Deleted,
3946 old_path: Some(fe.path.clone()),
3947 new_path: None,
3948 old_mode: format_mode(fe.mode),
3949 new_mode: "000000".to_owned(),
3950 old_oid: fe.oid,
3951 new_oid: zero_oid(),
3952 score: None,
3953 });
3954 }
3955 } else {
3956 result.push(DiffEntry {
3957 status: DiffStatus::Deleted,
3958 old_path: Some(path),
3959 new_path: None,
3960 old_mode: format_mode(entry.mode),
3961 new_mode: "000000".to_owned(),
3962 old_oid: entry.oid,
3963 new_oid: zero_oid(),
3964 score: None,
3965 });
3966 }
3967 Ok(())
3968}
3969
3970fn emit_index_tree_added(walk: &mut DiffIndexTreeWalk<'_, '_>, dir_prefix: &[u8]) -> Result<()> {
3971 let ie_kind = peek_index_level_entry(
3972 walk.index,
3973 walk.idx_cursor,
3974 dir_prefix,
3975 walk.ignore_submodules,
3976 )
3977 .ok_or_else(|| Error::IndexError("missing index entry during tree walk".to_owned()))?;
3978 match ie_kind {
3979 IndexLevelKind::Blob { entry: ie, .. } => {
3980 let path = String::from_utf8_lossy(&ie.path).into_owned();
3981 *walk.idx_cursor += 1;
3982 walk.result.push(DiffEntry {
3983 status: DiffStatus::Added,
3984 old_path: None,
3985 new_path: Some(path),
3986 old_mode: "000000".to_owned(),
3987 new_mode: format_mode(ie.mode),
3988 old_oid: zero_oid(),
3989 new_oid: ie.oid,
3990 score: None,
3991 });
3992 }
3993 IndexLevelKind::Tree { name } => {
3994 let full_path = join_index_path(dir_prefix, name);
3995 let start = *walk.idx_cursor;
3996 skip_index_subtree(walk.index, walk.idx_cursor, &full_path);
3997 for ie in &walk.index.entries[start..*walk.idx_cursor] {
3998 if ie.stage() != 0 || ie.intent_to_add() {
3999 continue;
4000 }
4001 if walk.ignore_submodules && ie.mode == 0o160000 {
4002 continue;
4003 }
4004 let path = String::from_utf8_lossy(&ie.path).into_owned();
4005 walk.result.push(DiffEntry {
4006 status: DiffStatus::Added,
4007 old_path: None,
4008 new_path: Some(path),
4009 old_mode: "000000".to_owned(),
4010 new_mode: format_mode(ie.mode),
4011 old_oid: zero_oid(),
4012 new_oid: ie.oid,
4013 score: None,
4014 });
4015 }
4016 }
4017 }
4018 Ok(())
4019}
4020
4021pub fn diff_index_to_worktree(
4046 odb: &Odb,
4047 index: &mut Index,
4048 work_tree: &Path,
4049 ignore_submodule_untracked: bool,
4050 simplify_gitlinks: bool,
4051) -> Result<Vec<DiffEntry>> {
4052 diff_index_to_worktree_with_options(
4053 odb,
4054 index,
4055 work_tree,
4056 DiffIndexToWorktreeOptions {
4057 ignore_submodule_untracked,
4058 simplify_gitlinks,
4059 ..DiffIndexToWorktreeOptions::default()
4060 },
4061 )
4062 .map(|(entries, _)| entries)
4063}
4064
4065#[derive(Clone)]
4067pub struct DiffIndexToWorktreeOptions {
4068 pub index_mtime: Option<(u32, u32)>,
4074 pub ignore_submodule_untracked: bool,
4076 pub simplify_gitlinks: bool,
4078 pub error_on_broken_gitlinks: bool,
4081 pub repository_git_dir: Option<std::path::PathBuf>,
4085 pub refresh_index_stat_in_pass: bool,
4088 pub config: Option<std::sync::Arc<crate::config::ConfigSet>>,
4090 pub stat_parallel_threads: Option<usize>,
4094 pub thread_policy: Option<crate::parallel::ThreadPolicy>,
4096 pub worktree_rules:
4098 Option<std::sync::Arc<std::sync::Mutex<crate::worktree_rules::WorktreeRules>>>,
4099 pub for_status: bool,
4101 pub refresh_fsmonitor: bool,
4103 pub command_runner: Option<std::sync::Arc<dyn crate::command_runner::CommandRunner>>,
4105 pub fsmonitor_query_boundary: Option<crate::fsmonitor::FsmonitorQueryBoundary>,
4107 pub restrict_to_paths: Option<std::sync::Arc<std::collections::BTreeSet<String>>>,
4114 pub hash_blobs_without_write: bool,
4116 pub cancel_watch: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
4119}
4120
4121impl Default for DiffIndexToWorktreeOptions {
4122 fn default() -> Self {
4123 Self {
4124 index_mtime: None,
4125 ignore_submodule_untracked: false,
4126 simplify_gitlinks: false,
4127 error_on_broken_gitlinks: false,
4128 repository_git_dir: None,
4129 refresh_index_stat_in_pass: false,
4130 config: None,
4131 stat_parallel_threads: None,
4132 thread_policy: None,
4133 worktree_rules: None,
4134 for_status: false,
4135 refresh_fsmonitor: true,
4136 command_runner: None,
4137 fsmonitor_query_boundary: None,
4138 restrict_to_paths: None,
4139 hash_blobs_without_write: false,
4140 cancel_watch: None,
4141 }
4142 }
4143}
4144
4145#[inline]
4146fn poll_diff_cancel_watch(
4147 watch: Option<&std::sync::Arc<std::sync::atomic::AtomicBool>>,
4148) -> crate::error::Result<()> {
4149 if let Some(flag) = watch {
4150 crate::progress::poll_watch(flag)?;
4151 }
4152 Ok(())
4153}
4154
4155struct StagingHashJob {
4156 entry_index: usize,
4157 rel_path: String,
4158 file_path: PathBuf,
4159 entry: IndexEntry,
4160 worktree_mode: u32,
4162 file_attrs: crate::crlf::FileAttrs,
4163}
4164
4165#[allow(clippy::too_many_arguments)]
4166fn parallel_process_blob_worktree_jobs(
4167 odb: &Odb,
4168 jobs: &[StagingHashJob],
4169 thread_policy: &crate::parallel::ThreadPolicy,
4170 config: &crate::config::ConfigSet,
4171 materialize_dirty_blobs: bool,
4172 hash_blobs_without_write: bool,
4173 conv: &crate::crlf::ConversionConfig,
4174 filter_process: Option<&crate::filter_process::FilterProcessState>,
4175 result: &mut Vec<DiffEntry>,
4176) -> Result<()> {
4177 use crate::parallel::{try_par_map, ParallelHashError, ParallelOp};
4178
4179 if jobs.is_empty() {
4180 return Ok(());
4181 }
4182 let mut par_opts = thread_policy.resolve(ParallelOp::IndexRefresh, config);
4183 par_opts.min_items = 0;
4184 par_opts.min_total_bytes = None;
4185 let est_bytes = jobs.len().saturating_mul(8192);
4186 let store_dirty_blobs = (materialize_dirty_blobs && !hash_blobs_without_write)
4187 .then_some(crate::odb::BULK_NEW_LOOSE_WRITE);
4188 let stream_from = if store_dirty_blobs.is_some() {
4189 odb.big_file_threshold()?
4190 } else {
4191 hash_stream_threshold(odb)?
4192 };
4193
4194 let rows = try_par_map::<StagingHashJob, Option<(usize, DiffEntry)>, Error, _>(
4195 jobs,
4196 par_opts,
4197 est_bytes,
4198 |job| {
4199 let eff_oid = read_regular_worktree_blob(
4200 odb,
4201 &job.file_path,
4202 conv,
4203 &job.file_attrs,
4204 &job.rel_path,
4205 Some(&job.entry),
4206 filter_process,
4207 stream_from,
4208 )?
4209 .object_id(odb, store_dirty_blobs)?;
4210 if eff_oid != job.entry.oid || job.worktree_mode != job.entry.mode {
4211 let path_owned = job.rel_path.clone();
4212 let new_oid = if eff_oid != job.entry.oid && !materialize_dirty_blobs {
4213 zero_oid()
4214 } else {
4215 eff_oid
4216 };
4217 Ok(Some((
4218 job.entry_index,
4219 DiffEntry {
4220 status: DiffStatus::for_content_change(job.entry.mode, job.worktree_mode),
4221 old_path: Some(path_owned.clone()),
4222 new_path: Some(path_owned),
4223 old_mode: format_mode(job.entry.mode),
4224 new_mode: format_mode(job.worktree_mode),
4225 old_oid: job.entry.oid,
4226 new_oid,
4227 score: None,
4228 },
4229 )))
4230 } else {
4231 Ok(None)
4232 }
4233 },
4234 )
4235 .map_err(|e| match e {
4236 ParallelHashError::Task(err) => err,
4237 })?;
4238
4239 let mut rows: Vec<(usize, DiffEntry)> = rows.into_iter().flatten().collect();
4240 rows.sort_by_key(|(idx, _)| *idx);
4241 result.extend(rows.into_iter().map(|(_, d)| d));
4242 Ok(())
4243}
4244
4245#[must_use]
4247fn try_push_status_modified_on_size_mismatch(
4248 ie: &IndexEntry,
4249 path_str_ref: &str,
4250 meta: &fs::Metadata,
4251 worktree_mode: u32,
4252 for_status: bool,
4253 result: &mut Vec<DiffEntry>,
4254) -> bool {
4255 if ie.stat_cache_unset() {
4256 return false;
4257 }
4258 if !for_status || crate::index::munge_index_size(meta.len()) == ie.size {
4259 return false;
4260 }
4261 let path_owned = path_str_ref.to_owned();
4262 result.push(DiffEntry {
4263 status: DiffStatus::for_content_change(ie.mode, worktree_mode),
4264 old_path: Some(path_owned.clone()),
4265 new_path: Some(path_owned),
4266 old_mode: format_mode(ie.mode),
4267 new_mode: format_mode(worktree_mode),
4268 old_oid: ie.oid,
4269 new_oid: ie.oid,
4270 score: None,
4271 });
4272 true
4273}
4274
4275fn try_push_status_modified_on_size_mismatch_stat(
4276 ie: &IndexEntry,
4277 path_str_ref: &str,
4278 stat: &crate::worktree_scan::BlobDiskStat,
4279 worktree_mode: u32,
4280 for_status: bool,
4281 result: &mut Vec<DiffEntry>,
4282) -> bool {
4283 if ie.stat_cache_unset() {
4284 return false;
4285 }
4286 if !for_status || stat.worktree_size() == ie.size {
4287 return false;
4288 }
4289 let path_owned = path_str_ref.to_owned();
4290 result.push(DiffEntry {
4291 status: DiffStatus::for_content_change(ie.mode, worktree_mode),
4292 old_path: Some(path_owned.clone()),
4293 new_path: Some(path_owned),
4294 old_mode: format_mode(ie.mode),
4295 new_mode: format_mode(worktree_mode),
4296 old_oid: ie.oid,
4297 new_oid: ie.oid,
4298 score: None,
4299 });
4300 true
4301}
4302
4303#[allow(clippy::too_many_arguments)]
4304fn push_index_blob_worktree_diff(
4305 odb: &Odb,
4306 ie: &IndexEntry,
4307 path_str_ref: &str,
4308 file_path: &Path,
4309 meta: fs::Metadata,
4310 index_mtime: Option<(u32, u32)>,
4311 mode_policy: crate::index::WorktreeModePolicy,
4312 stat_policy: crate::index::StatMatchPolicy,
4313 file_attrs_for: &dyn Fn(&str) -> crate::crlf::FileAttrs,
4314 conv: &crate::crlf::ConversionConfig,
4315 materialize_dirty_blobs: bool,
4316 hash_blobs_without_write: bool,
4317 for_status: bool,
4318 filter_process: Option<&crate::filter_process::FilterProcessState>,
4319 result: &mut Vec<DiffEntry>,
4320) -> Result<()> {
4321 if meta.is_dir() {
4322 if file_path.join(".git").exists() {
4323 let head = read_submodule_head_oid(file_path).unwrap_or_else(zero_oid);
4324 let path_owned = path_str_ref.to_owned();
4325 result.push(DiffEntry {
4326 status: DiffStatus::TypeChanged,
4327 old_path: Some(path_owned.clone()),
4328 new_path: Some(path_owned),
4329 old_mode: format_mode(ie.mode),
4330 new_mode: format_mode(0o160000),
4331 old_oid: ie.oid,
4332 new_oid: head,
4333 score: None,
4334 });
4335 return Ok(());
4336 }
4337 result.push(DiffEntry {
4338 status: DiffStatus::Deleted,
4339 old_path: Some(path_str_ref.to_owned()),
4340 new_path: None,
4341 old_mode: format_mode(ie.mode),
4342 new_mode: String::new(),
4343 old_oid: ie.oid,
4344 new_oid: zero_oid(),
4345 score: None,
4346 });
4347 return Ok(());
4348 }
4349
4350 let worktree_mode =
4351 mode_policy.index_mode_for_worktree(mode_from_metadata(&meta), Some(ie.mode));
4352 let stat_same = stat_matches_with_policy(ie, &meta, stat_policy);
4353 if stat_same && worktree_mode != ie.mode {
4354 let path_owned = path_str_ref.to_owned();
4355 result.push(DiffEntry {
4356 status: DiffStatus::for_content_change(ie.mode, worktree_mode),
4357 old_path: Some(path_owned.clone()),
4358 new_path: Some(path_owned),
4359 old_mode: format_mode(ie.mode),
4360 new_mode: format_mode(worktree_mode),
4361 old_oid: ie.oid,
4362 new_oid: ie.oid,
4363 score: None,
4364 });
4365 return Ok(());
4366 }
4367
4368 if stat_same
4369 && index_entry_skips_worktree_content_hash_after_stat_match(ie, index_mtime, stat_policy)
4370 {
4371 return Ok(());
4372 }
4373
4374 if try_push_status_modified_on_size_mismatch(
4375 ie,
4376 path_str_ref,
4377 &meta,
4378 worktree_mode,
4379 for_status,
4380 result,
4381 ) {
4382 return Ok(());
4383 }
4384
4385 let file_attrs = file_attrs_for(path_str_ref);
4386 let store = (materialize_dirty_blobs && !hash_blobs_without_write)
4387 .then_some(crate::odb::BULK_NEW_LOOSE_WRITE);
4388 let stream_from = if store.is_some() {
4389 odb.big_file_threshold()?
4390 } else {
4391 hash_stream_threshold(odb)?
4392 };
4393 let eff_oid = worktree_blob_content_from(
4394 odb,
4395 file_path,
4396 &meta,
4397 conv,
4398 &file_attrs,
4399 path_str_ref,
4400 Some(ie),
4401 filter_process,
4402 stream_from,
4403 )?
4404 .object_id(odb, store)?;
4405
4406 if eff_oid != ie.oid || worktree_mode != ie.mode {
4407 let path_owned = path_str_ref.to_owned();
4408 let new_oid = if eff_oid != ie.oid && !materialize_dirty_blobs {
4411 zero_oid()
4412 } else {
4413 eff_oid
4414 };
4415 result.push(DiffEntry {
4416 status: DiffStatus::for_content_change(ie.mode, worktree_mode),
4417 old_path: Some(path_owned.clone()),
4418 new_path: Some(path_owned),
4419 old_mode: format_mode(ie.mode),
4420 new_mode: format_mode(worktree_mode),
4421 old_oid: ie.oid,
4422 new_oid,
4423 score: None,
4424 });
4425 }
4426 Ok(())
4427}
4428
4429pub fn diff_index_to_worktree_with_options(
4433 odb: &Odb,
4434 index: &mut Index,
4435 work_tree: &Path,
4436 options: DiffIndexToWorktreeOptions,
4437) -> Result<(Vec<DiffEntry>, bool)> {
4438 let default_git_dir = work_tree.join(".git");
4439 let git_dir = options
4440 .repository_git_dir
4441 .clone()
4442 .unwrap_or(default_git_dir);
4443 diff_index_to_worktree_inner(odb, index, work_tree, &git_dir, options, false)
4444 .map(|(entries, changed, _)| (entries, changed))
4445}
4446
4447pub fn diff_index_to_worktree_for_staging(
4451 odb: &Odb,
4452 repository_git_dir: &Path,
4453 index: &mut Index,
4454 work_tree: &Path,
4455 options: DiffIndexToWorktreeOptions,
4456) -> Result<(
4457 Vec<DiffEntry>,
4458 bool,
4459 std::collections::BTreeMap<String, crate::index::IndexStatFields>,
4460)> {
4461 diff_index_to_worktree_inner(odb, index, work_tree, repository_git_dir, options, true)
4462}
4463
4464#[allow(clippy::too_many_arguments)]
4466fn worktree_diff_pass1_index_entry(
4467 odb: &Odb,
4468 work_tree: &Path,
4469 ie: &IndexEntry,
4470 path_str_ref: &str,
4471 simplify_gitlinks: bool,
4472 ignore_submodule_untracked: bool,
4473 error_on_broken_gitlinks: bool,
4474 materialize_dirty_blobs: bool,
4475 filter_fp: Option<&crate::filter_process::FilterProcessState>,
4476 conv: &crate::crlf::ConversionConfig,
4477 file_attrs_for: &dyn Fn(&str) -> crate::crlf::FileAttrs,
4478 precompose_unicode: bool,
4479 ignorecase: bool,
4480 result: &mut Vec<DiffEntry>,
4481) -> Result<()> {
4482 if ie.skip_worktree() || ie.assume_unchanged() {
4483 return Ok(());
4484 }
4485 let is_intent_to_add = ie.intent_to_add();
4486
4487 if ie.mode == 0o160000 {
4488 let sub_dir =
4489 index_entry_worktree_abs_bytes(work_tree, &ie.path, precompose_unicode, ignorecase);
4490 match fs::symlink_metadata(&sub_dir) {
4491 Err(e)
4492 if e.kind() == std::io::ErrorKind::NotFound
4493 || e.raw_os_error() == Some(20) =>
4494 {
4495 if !simplify_gitlinks {
4496 let path_owned = path_str_ref.to_owned();
4497 result.push(DiffEntry {
4498 status: DiffStatus::Deleted,
4499 old_path: Some(path_owned.clone()),
4500 new_path: Some(path_owned),
4501 old_mode: format_mode(ie.mode),
4502 new_mode: "000000".to_owned(),
4503 old_oid: ie.oid,
4504 new_oid: zero_oid(),
4505 score: None,
4506 });
4507 }
4508 return Ok(());
4509 }
4510 Err(e) => return Err(Error::Io(e)),
4511 Ok(meta) if !meta.is_dir() => {
4512 let worktree_mode = mode_from_metadata(&meta);
4513 let file_attrs = file_attrs_for(path_str_ref);
4514 let worktree_oid = worktree_file_oid(
4515 odb,
4516 &sub_dir,
4517 &meta,
4518 conv,
4519 &file_attrs,
4520 path_str_ref,
4521 None,
4522 materialize_dirty_blobs,
4523 filter_fp,
4524 )?;
4525 let path_owned = path_str_ref.to_owned();
4526 result.push(DiffEntry {
4527 status: DiffStatus::TypeChanged,
4528 old_path: Some(path_owned.clone()),
4529 new_path: Some(path_owned),
4530 old_mode: format_mode(ie.mode),
4531 new_mode: format_mode(worktree_mode),
4532 old_oid: ie.oid,
4533 new_oid: worktree_oid,
4534 score: None,
4535 });
4536 return Ok(());
4537 }
4538 Ok(_) => {}
4539 }
4540 let sub_head_oid = read_submodule_head_oid(&sub_dir);
4541 let ref_matches = if let Some(oid) = sub_head_oid {
4542 oid == ie.oid
4543 } else {
4544 let is_placeholder = submodule_worktree_is_unpopulated_placeholder(&sub_dir);
4545 if error_on_broken_gitlinks
4546 && !is_placeholder
4547 && submodule_embedded_git_dir(&sub_dir).is_some()
4548 {
4549 return Err(Error::Config(
4550 format!("could not read submodule HEAD for '{path_str_ref}'").into(),
4551 ));
4552 }
4553 is_placeholder
4554 };
4555 if simplify_gitlinks {
4556 if !ref_matches {
4557 let path_owned = path_str_ref.to_owned();
4558 let new_oid = sub_head_oid.unwrap_or_else(zero_oid);
4559 result.push(DiffEntry {
4560 status: DiffStatus::Modified,
4561 old_path: Some(path_owned.clone()),
4562 new_path: Some(path_owned),
4563 old_mode: format_mode(ie.mode),
4564 new_mode: format_mode(ie.mode),
4565 old_oid: ie.oid,
4566 new_oid,
4567 score: None,
4568 });
4569 }
4570 return Ok(());
4571 }
4572 if sub_head_oid.is_some() && submodule_head_object_broken(&sub_dir) {
4573 return Err(Error::Config(
4574 format!("'git status --porcelain=2' failed in submodule {path_str_ref}").into(),
4575 ));
4576 }
4577 let mut flags = submodule_porcelain_flags(work_tree, path_str_ref, ie.oid);
4578 if ignore_submodule_untracked {
4579 flags.untracked = false;
4580 }
4581 let inner_dirty = flags.modified || flags.untracked;
4582 if !ref_matches || inner_dirty {
4583 let path_owned = path_str_ref.to_owned();
4584 let new_oid = if !ref_matches {
4585 sub_head_oid.unwrap_or_else(zero_oid)
4586 } else {
4587 zero_oid()
4588 };
4589 result.push(DiffEntry {
4590 status: DiffStatus::Modified,
4591 old_path: Some(path_owned.clone()),
4592 new_path: Some(path_owned),
4593 old_mode: format_mode(ie.mode),
4594 new_mode: format_mode(ie.mode),
4595 old_oid: ie.oid,
4596 new_oid,
4597 score: None,
4598 });
4599 }
4600 return Ok(());
4601 }
4602
4603 let file_path =
4604 index_entry_worktree_abs_bytes(work_tree, &ie.path, precompose_unicode, ignorecase);
4605 let path_match = crate::repo_relpath::as_utf8_for_matching(path_str_ref);
4606
4607 if is_intent_to_add {
4608 match fs::symlink_metadata(&file_path) {
4609 Ok(meta) => {
4610 let file_attrs = file_attrs_for(path_match.as_ref());
4611 let worktree_oid = worktree_file_oid(
4612 odb,
4613 &file_path,
4614 &meta,
4615 conv,
4616 &file_attrs,
4617 path_match.as_ref(),
4618 None,
4619 materialize_dirty_blobs,
4620 filter_fp,
4621 )?;
4622 let worktree_mode = mode_from_metadata(&meta);
4623 result.push(DiffEntry {
4624 status: DiffStatus::Added,
4625 old_path: None,
4626 new_path: Some(path_str_ref.to_owned()),
4627 old_mode: "000000".to_owned(),
4628 new_mode: format_mode(worktree_mode),
4629 old_oid: zero_oid(),
4630 new_oid: worktree_oid,
4631 score: None,
4632 });
4633 }
4634 Err(e)
4635 if e.kind() == std::io::ErrorKind::NotFound
4636 || e.raw_os_error() == Some(20) =>
4637 {
4638 result.push(DiffEntry {
4639 status: DiffStatus::Deleted,
4640 old_path: Some(path_str_ref.to_owned()),
4641 new_path: None,
4642 old_mode: format_mode(ie.mode),
4643 new_mode: "000000".to_owned(),
4644 old_oid: ie.oid,
4645 new_oid: zero_oid(),
4646 score: None,
4647 });
4648 }
4649 Err(e) => return Err(Error::Io(e)),
4650 }
4651 }
4652 Ok(())
4653}
4654
4655#[inline]
4656fn index_entry_needs_worktree_diff_pass1(ie: &IndexEntry) -> bool {
4657 ie.mode == crate::index::MODE_GITLINK || ie.intent_to_add()
4658}
4659
4660fn diff_index_to_worktree_inner(
4661 odb: &Odb,
4662 index: &mut Index,
4663 work_tree: &Path,
4664 repository_git_dir: &Path,
4665 options: DiffIndexToWorktreeOptions,
4666 materialize_dirty_blobs: bool,
4667) -> Result<(
4668 Vec<DiffEntry>,
4669 bool,
4670 std::collections::BTreeMap<String, crate::index::IndexStatFields>,
4671)> {
4672 use crate::config::ConfigSet;
4673 use crate::crlf;
4674 use std::collections::BTreeMap;
4675
4676 let mut staging_stat_cache = BTreeMap::new();
4677 let cancel_watch = options.cancel_watch.as_ref();
4678 poll_diff_cancel_watch(cancel_watch)?;
4679 let index_mtime = index_mtime_for_diff(index, options.index_mtime);
4680
4681 let ignore_submodule_untracked = options.ignore_submodule_untracked;
4682 let simplify_gitlinks = options.simplify_gitlinks;
4683
4684 let rules_lock = options
4685 .worktree_rules
4686 .as_ref()
4687 .map(|shared| {
4688 shared
4689 .lock()
4690 .map_err(|e| Error::Message(format!("worktree rules lock poisoned: {e}")))
4691 })
4692 .transpose()?;
4693 let config = if let Some(rules) = rules_lock.as_ref() {
4694 rules.config().clone()
4695 } else if let Some(c) = options.config.as_ref() {
4696 c.as_ref().clone()
4697 } else {
4698 ConfigSet::load(
4699 &crate::environment::Environment::empty(),
4700 Some(repository_git_dir),
4701 true,
4702 )
4703 .unwrap_or_else(|_| ConfigSet::new())
4704 };
4705 let conv = rules_lock
4706 .as_ref()
4707 .map(|r| r.conversion().clone())
4708 .unwrap_or_else(|| crlf::ConversionConfig::from_config(&config));
4709 let legacy_attrs = if rules_lock.is_none() {
4710 Some(crlf::load_gitattributes_parsed(work_tree))
4711 } else {
4712 None
4713 };
4714 let file_attrs_for = |path: &str| -> crlf::FileAttrs {
4715 if let Some(rules) = rules_lock.as_ref() {
4716 rules.file_attrs(path, false)
4717 } else if let Some(parsed) = legacy_attrs.as_ref() {
4718 crlf::file_attrs_from_gitattributes(&parsed.rules, &parsed.macros, path, false, &config)
4719 } else {
4720 crlf::FileAttrs::default()
4721 }
4722 };
4723 let filter_fp = rules_lock.as_ref().map(|r| r.filter_process());
4724 let precompose_unicode = config
4725 .get_bool("core.precomposeunicode")
4726 .and_then(|r| r.ok())
4727 .unwrap_or(false);
4728 let mode_policy = crate::index::WorktreeModePolicy::from_config(&config);
4729 let stat_policy = crate::index::StatMatchPolicy::from_config(Some(&config));
4730 let ignorecase = config
4731 .get_bool("core.ignorecase")
4732 .and_then(|r| r.ok())
4733 .unwrap_or(false);
4734 let hash_blobs_without_write = options.hash_blobs_without_write;
4735
4736 let mut result = Vec::new();
4737 let refresh_index_stat_in_pass = options.refresh_index_stat_in_pass;
4738 let mut index_stat_refresh_changed = false;
4739 let mut honor_fsmonitor_valid = false;
4740 if options.refresh_fsmonitor {
4741 let default_runner = crate::command_runner::system_command_runner();
4742 let runner = options
4743 .command_runner
4744 .as_deref()
4745 .unwrap_or(default_runner.as_ref());
4746 let boundary = options.fsmonitor_query_boundary.unwrap_or_else(|| {
4747 crate::fsmonitor::FsmonitorQueryBoundary(
4748 crate::environment::wall_clock_unix_timestamp_nanos(),
4749 )
4750 });
4751 let refresh_opts = crate::fsmonitor::FsmonitorRefreshOptions::with_query_boundary(boundary);
4752 let refresh = crate::fsmonitor::refresh_index(
4753 index,
4754 repository_git_dir,
4755 work_tree,
4756 &config,
4757 Some(runner),
4758 &refresh_opts,
4759 )?;
4760 if refresh.index_changed {
4761 index_stat_refresh_changed = true;
4762 }
4763 honor_fsmonitor_valid = refresh.honor_valid_bits;
4764 }
4765 let use_fsmonitor_stat_skip = honor_fsmonitor_valid;
4766 let mut unmerged_base: std::collections::BTreeMap<String, (u8, IndexEntry)> =
4767 std::collections::BTreeMap::new();
4768
4769 let dir_symlinks = SymlinkDirCache::default();
4773
4774 let error_on_broken_gitlinks = options.error_on_broken_gitlinks;
4775 if options.restrict_to_paths.is_none() {
4776 for ie in &index.entries {
4777 poll_diff_cancel_watch(cancel_watch)?;
4778 if ie.stage() != 0 {
4779 let path = String::from_utf8_lossy(&ie.path).to_string();
4780 let rank = match ie.stage() {
4781 2 => 0u8,
4782 3 => 1u8,
4783 1 => 2u8,
4784 _ => 3u8,
4785 };
4786 match unmerged_base.get(&path) {
4787 Some((existing_rank, _)) if *existing_rank <= rank => {}
4788 _ => {
4789 unmerged_base.insert(path, (rank, ie.clone()));
4790 }
4791 }
4792 }
4793 }
4794 }
4795
4796 if let Some(limit) = options.restrict_to_paths.as_ref() {
4797 for path in limit.iter() {
4798 let Some(ie) = index.get(path.as_bytes(), 0) else {
4799 continue;
4800 };
4801 if !index_entry_needs_worktree_diff_pass1(ie) {
4802 continue;
4803 }
4804 worktree_diff_pass1_index_entry(
4805 odb,
4806 work_tree,
4807 ie,
4808 path.as_str(),
4809 simplify_gitlinks,
4810 ignore_submodule_untracked,
4811 error_on_broken_gitlinks,
4812 materialize_dirty_blobs,
4813 filter_fp,
4814 &conv,
4815 &file_attrs_for,
4816 precompose_unicode,
4817 ignorecase,
4818 &mut result,
4819 )?;
4820 }
4821 } else {
4822 for ie in &index.entries {
4823 poll_diff_cancel_watch(cancel_watch)?;
4824 if ie.stage() != 0 {
4825 continue;
4826 }
4827 if !index_entry_needs_worktree_diff_pass1(ie) {
4828 continue;
4829 }
4830 let path_stored = crate::repo_relpath::storage_string(&ie.path);
4831 worktree_diff_pass1_index_entry(
4832 odb,
4833 work_tree,
4834 ie,
4835 &path_stored,
4836 simplify_gitlinks,
4837 ignore_submodule_untracked,
4838 error_on_broken_gitlinks,
4839 materialize_dirty_blobs,
4840 filter_fp,
4841 &conv,
4842 &file_attrs_for,
4843 precompose_unicode,
4844 ignorecase,
4845 &mut result,
4846 )?;
4847 }
4848 }
4849
4850 use crate::worktree_scan::{
4851 collect_blob_lookup_batches_with_literal_paths, group_blob_entries_by_dir,
4852 stat_scan_thread_count, visit_blob_lookup_batches, BlobDiskLookup, BlobScanSparseMode,
4853 WorktreeBlobScanOptions, PARALLEL_STAT_MIN_ENTRIES,
4854 };
4855 let preload_index = config
4856 .get_bool("core.preloadIndex")
4857 .and_then(|r| r.ok())
4858 .unwrap_or(true);
4859 let blob_scan = WorktreeBlobScanOptions {
4860 preload_index,
4861 stat_parallel_threads: options.stat_parallel_threads,
4862 };
4863 let blob_dirs = if let Some(limit) = options.restrict_to_paths.as_ref() {
4864 crate::worktree_scan::group_blob_entries_by_dir_for_paths(index, limit)
4865 } else {
4866 group_blob_entries_by_dir(index, honor_fsmonitor_valid)
4867 };
4868 let work_tree_owned = work_tree.to_path_buf();
4869 let dir_abs = move |dir: &[u8]| {
4870 if dir.is_empty() {
4871 work_tree_owned.clone()
4872 } else {
4873 index_entry_worktree_abs_bytes(&work_tree_owned, dir, precompose_unicode, ignorecase)
4874 }
4875 };
4876 let work_tree_for_files = work_tree.to_path_buf();
4877 let file_abs = move |rel: &[u8]| {
4878 index_entry_worktree_abs_bytes(&work_tree_for_files, rel, precompose_unicode, ignorecase)
4879 };
4880 let blob_count = blob_dirs.len();
4881 let thread_policy = options.thread_policy.unwrap_or_default();
4882 let hash_threads = stat_scan_thread_count(&thread_policy, &config, blob_scan, blob_count);
4883 let parallel_blob_work = blob_count >= PARALLEL_STAT_MIN_ENTRIES
4884 && hash_threads > 1
4885 && (materialize_dirty_blobs || options.for_status);
4886 let mut staging_hash_jobs: Vec<StagingHashJob> = Vec::new();
4887 let mut refresh_content_verified = vec![false; index.entries.len()];
4889 let sparse = if parallel_blob_work {
4894 BlobScanSparseMode::StatusDiff {
4895 stat_policy,
4896 index_mtime,
4897 mode_policy,
4898 for_status: options.for_status,
4899 refresh_index_stat_in_pass,
4900 parallel_blob_work,
4901 honor_fsmonitor_stat_skip: use_fsmonitor_stat_skip,
4902 }
4903 } else {
4904 BlobScanSparseMode::Dense
4905 };
4906 let blob_stat_batches = collect_blob_lookup_batches_with_literal_paths(
4907 index,
4908 &blob_dirs,
4909 blob_scan,
4910 &thread_policy,
4911 &config,
4912 dir_abs,
4913 file_abs,
4914 !precompose_unicode && !ignorecase,
4915 |rel_path| {
4916 dir_symlinks.has_symlink_in_path_bytes(
4917 work_tree,
4918 rel_path,
4919 precompose_unicode,
4920 ignorecase,
4921 )
4922 },
4923 sparse,
4924 )?;
4925 let refresh_checks = if refresh_index_stat_in_pass {
4926 let par_opts =
4927 refresh_hash_parallel_options(&thread_policy, &config, options.stat_parallel_threads);
4928 refresh_content_checks(
4929 odb,
4930 index,
4931 &blob_stat_batches,
4932 index_mtime,
4933 stat_policy,
4934 &|ie, stat| {
4935 !(use_fsmonitor_stat_skip && crate::fsmonitor::entry_skips_worktree_stat(ie, index))
4936 && worktree_diff_refreshes_entry(
4937 ie,
4938 stat,
4939 index_mtime,
4940 stat_policy,
4941 options.for_status,
4942 )
4943 },
4944 &WorktreeHashContext {
4945 work_tree,
4946 precompose_unicode,
4947 ignorecase,
4948 conv: &conv,
4949 file_attrs_for: &file_attrs_for,
4950 filter_process: filter_fp,
4951 },
4952 par_opts,
4953 )
4954 } else {
4955 Vec::new()
4956 };
4957 poll_diff_cancel_watch(cancel_watch)?;
4958 visit_blob_lookup_batches(blob_stat_batches, |entry_index, lookup| {
4959 poll_diff_cancel_watch(cancel_watch)?;
4960 let path_owned = crate::repo_relpath::storage_string(&index.entries[entry_index].path);
4961 let path_str_ref = path_owned.as_str();
4962 if use_fsmonitor_stat_skip {
4963 let ie_skip = &index.entries[entry_index];
4964 if crate::fsmonitor::entry_skips_worktree_stat(ie_skip, index) {
4965 return Ok(());
4966 }
4967 }
4968 match lookup {
4969 BlobDiskLookup::Missing => {
4970 let ie = &index.entries[entry_index];
4971 result.push(DiffEntry {
4972 status: DiffStatus::Deleted,
4973 old_path: Some(path_str_ref.to_owned()),
4974 new_path: None,
4975 old_mode: format_mode(ie.mode),
4976 new_mode: "000000".to_owned(),
4977 old_oid: ie.oid,
4978 new_oid: zero_oid(),
4979 score: None,
4980 });
4981 }
4982 BlobDiskLookup::Io(e) => return Err(Error::Io(e)),
4983 BlobDiskLookup::Present(stat) => {
4984 if materialize_dirty_blobs {
4985 staging_stat_cache.insert(path_str_ref.to_owned(), stat.fields);
4986 }
4987 if refresh_index_stat_in_pass
4988 && worktree_diff_refreshes_entry(
4989 &index.entries[entry_index],
4990 &stat,
4991 index_mtime,
4992 stat_policy,
4993 options.for_status,
4994 )
4995 {
4996 let refresh = refresh_index_blob_stat_from_scan(
4997 &mut index.entries[entry_index],
4998 &stat,
4999 index_mtime,
5000 stat_policy,
5001 refresh_checks[entry_index].unwrap_or(false),
5002 );
5003 if refresh.index_changed {
5004 index_stat_refresh_changed = true;
5005 }
5006 if refresh.content_verified {
5007 refresh_content_verified[entry_index] = true;
5008 let path = index.entries[entry_index].path.clone();
5009 let oid = index.entries[entry_index].oid;
5010 index.mark_content_verified(&path, oid);
5011 }
5012 if index.fsmonitor_last_update.is_some()
5013 && stat_matches_blob_stat(&index.entries[entry_index], &stat, stat_policy)
5014 && !refresh.index_changed
5015 {
5016 index.entries[entry_index].set_fsmonitor_valid(true);
5017 }
5018 }
5019 let ie = &index.entries[entry_index];
5020 let worktree_mode =
5021 mode_policy.index_mode_for_worktree(stat.git_mode, Some(ie.mode));
5022 if parallel_blob_work {
5023 let stat_same = stat_matches_blob_stat(ie, &stat, stat_policy);
5024 if stat_same
5025 && worktree_mode == ie.mode
5026 && index_entry_skips_worktree_content_hash_after_stat_match(
5027 ie,
5028 index_mtime,
5029 stat_policy,
5030 )
5031 {
5032 return Ok(());
5033 }
5034 if stat_same && worktree_mode != ie.mode {
5035 let path_owned = path_str_ref.to_owned();
5036 result.push(DiffEntry {
5037 status: DiffStatus::for_content_change(ie.mode, worktree_mode),
5038 old_path: Some(path_owned.clone()),
5039 new_path: Some(path_owned),
5040 old_mode: format_mode(ie.mode),
5041 new_mode: format_mode(worktree_mode),
5042 old_oid: ie.oid,
5043 new_oid: ie.oid,
5044 score: None,
5045 });
5046 } else if try_push_status_modified_on_size_mismatch_stat(
5047 ie,
5048 path_str_ref,
5049 &stat,
5050 worktree_mode,
5051 options.for_status,
5052 &mut result,
5053 ) {
5054 } else if !refresh_content_verified[entry_index] {
5055 let file_path = index_entry_worktree_abs_bytes(
5056 work_tree,
5057 &index.entries[entry_index].path,
5058 precompose_unicode,
5059 ignorecase,
5060 );
5061 staging_hash_jobs.push(StagingHashJob {
5062 entry_index,
5063 rel_path: path_str_ref.to_owned(),
5064 file_path,
5065 entry: ie.clone(),
5066 worktree_mode,
5067 file_attrs: file_attrs_for(path_str_ref),
5068 });
5069 }
5070 } else {
5071 let same_file_type = (worktree_mode == crate::index::MODE_SYMLINK)
5075 == (ie.mode == crate::index::MODE_SYMLINK);
5076 if !stat.is_dir
5077 && worktree_mode == ie.mode
5078 && ((refresh_content_verified[entry_index] && same_file_type)
5079 || (stat_matches_blob_stat(ie, &stat, stat_policy)
5080 && index_entry_skips_worktree_content_hash_after_stat_match(
5081 ie,
5082 index_mtime,
5083 stat_policy,
5084 )))
5085 {
5086 return Ok(());
5087 }
5088 let file_path = index_entry_worktree_abs_bytes(
5089 work_tree,
5090 &index.entries[entry_index].path,
5091 precompose_unicode,
5092 ignorecase,
5093 );
5094 let meta = fs::symlink_metadata(&file_path).map_err(Error::Io)?;
5095 push_index_blob_worktree_diff(
5096 odb,
5097 ie,
5098 path_str_ref,
5099 &file_path,
5100 meta,
5101 index_mtime,
5102 mode_policy,
5103 stat_policy,
5104 &file_attrs_for,
5105 &conv,
5106 materialize_dirty_blobs,
5107 hash_blobs_without_write,
5108 options.for_status,
5109 filter_fp,
5110 &mut result,
5111 )?;
5112 }
5113 }
5114 }
5115 Ok(())
5116 })?;
5117 if parallel_blob_work && !staging_hash_jobs.is_empty() {
5118 parallel_process_blob_worktree_jobs(
5119 odb,
5120 &staging_hash_jobs,
5121 &thread_policy,
5122 &config,
5123 materialize_dirty_blobs,
5124 hash_blobs_without_write,
5125 &conv,
5126 filter_fp,
5127 &mut result,
5128 )?;
5129 }
5130
5131 for (path, (_, base_entry)) in unmerged_base {
5132 let file_path = index_entry_worktree_abs(work_tree, &path, precompose_unicode, ignorecase);
5133 let wt_meta = match fs::symlink_metadata(&file_path) {
5134 Ok(meta) => Some(meta),
5135 Err(e)
5136 if e.kind() == std::io::ErrorKind::NotFound
5137 || e.raw_os_error() == Some(20) =>
5138 {
5139 None
5140 }
5141 Err(e) => return Err(Error::Io(e)),
5142 };
5143
5144 let new_mode = wt_meta.as_ref().map_or_else(
5145 || "000000".to_owned(),
5146 |meta| format_mode(mode_from_metadata(meta)),
5147 );
5148 result.push(DiffEntry {
5149 status: DiffStatus::Unmerged,
5150 old_path: Some(path.clone()),
5151 new_path: Some(path.clone()),
5152 old_mode: "000000".to_owned(),
5153 new_mode,
5154 old_oid: zero_oid(),
5155 new_oid: zero_oid(),
5156 score: None,
5157 });
5158
5159 if !options.for_status {
5160 if let Some(meta) = wt_meta {
5161 let file_attrs = file_attrs_for(&path);
5162 let wt_oid = worktree_file_oid(
5163 odb,
5164 &file_path,
5165 &meta,
5166 &conv,
5167 &file_attrs,
5168 &path,
5169 Some(&base_entry),
5170 materialize_dirty_blobs,
5171 filter_fp,
5172 )?;
5173 let wt_mode = mode_from_metadata(&meta);
5174 if wt_oid != base_entry.oid || wt_mode != base_entry.mode {
5175 result.push(DiffEntry {
5176 status: DiffStatus::for_content_change(base_entry.mode, wt_mode),
5177 old_path: Some(path.clone()),
5178 new_path: Some(path),
5179 old_mode: format_mode(base_entry.mode),
5180 new_mode: format_mode(wt_mode),
5181 old_oid: base_entry.oid,
5182 new_oid: wt_oid,
5183 score: None,
5184 });
5185 }
5186 }
5187 }
5188 }
5189
5190 Ok((result, index_stat_refresh_changed, staging_stat_cache))
5191}
5192
5193#[derive(Default)]
5196pub(crate) struct SymlinkDirCache {
5197 dirs: std::sync::Mutex<hashbrown::HashMap<Vec<u8>, bool>>,
5199}
5200
5201pub(crate) fn index_entry_worktree_abs(
5202 work_tree: &Path,
5203 index_relpath: &str,
5204 precompose_unicode: bool,
5205 ignorecase: bool,
5206) -> PathBuf {
5207 index_entry_worktree_abs_bytes(
5208 work_tree,
5209 index_relpath.as_bytes(),
5210 precompose_unicode,
5211 ignorecase,
5212 )
5213}
5214
5215pub(crate) fn index_entry_worktree_abs_bytes(
5216 work_tree: &Path,
5217 index_relpath: &[u8],
5218 precompose_unicode: bool,
5219 ignorecase: bool,
5220) -> PathBuf {
5221 if precompose_unicode || ignorecase {
5222 if let Ok(s) = std::str::from_utf8(index_relpath) {
5223 if precompose_unicode {
5224 return crate::unicode_normalization::resolve_worktree_path_for_staging(
5225 work_tree, s, true,
5226 )
5227 .abs;
5228 }
5229 return crate::path_icase::worktree_path_for_index_entry(work_tree, s, ignorecase);
5230 }
5231 }
5232 crate::index::worktree_path_from_index_rel(work_tree, index_relpath)
5233}
5234
5235#[must_use]
5240pub fn index_mtime_for_diff(index: &Index, explicit: Option<(u32, u32)>) -> Option<(u32, u32)> {
5241 explicit.or(index.source_mtime)
5242}
5243
5244impl SymlinkDirCache {
5245 pub(crate) fn has_symlink_in_path_bytes(
5250 &self,
5251 work_tree: &Path,
5252 rel_path: &[u8],
5253 precompose_unicode: bool,
5254 ignorecase: bool,
5255 ) -> bool {
5256 let Some(dir_len) = rel_path.iter().rposition(|&b| b == b'/') else {
5257 return false;
5258 };
5259 let prefix_ends = rel_path[..dir_len]
5260 .iter()
5261 .enumerate()
5262 .filter_map(|(i, &b)| (b == b'/').then_some(i))
5263 .chain(std::iter::once(dir_len));
5264 for end in prefix_ends {
5265 let prefix = &rel_path[..end];
5266 if prefix.is_empty() || prefix.ends_with(b"/") {
5267 continue;
5268 }
5269 let cached = self
5270 .dirs
5271 .lock()
5272 .unwrap_or_else(|e| e.into_inner())
5273 .get(prefix)
5274 .copied();
5275 let is_symlink = cached.unwrap_or_else(|| {
5276 let abs = index_entry_worktree_abs_bytes(
5277 work_tree,
5278 prefix,
5279 precompose_unicode,
5280 ignorecase,
5281 );
5282 let probed = crate::worktree_scan::path_is_symlink(&abs);
5283 self.dirs
5284 .lock()
5285 .unwrap_or_else(|e| e.into_inner())
5286 .insert(prefix.to_vec(), probed);
5287 probed
5288 });
5289 if is_symlink {
5290 return true;
5291 }
5292 }
5293 false
5294 }
5295}
5296
5297#[must_use]
5303pub fn entry_is_racy(ie: &IndexEntry, index_mtime: Option<(u32, u32)>) -> bool {
5304 entry_is_racy_with_policy(ie, index_mtime, crate::index::StatMatchPolicy::default())
5305}
5306
5307#[must_use]
5309pub fn entry_is_racy_with_policy(
5310 ie: &IndexEntry,
5311 index_mtime: Option<(u32, u32)>,
5312 policy: crate::index::StatMatchPolicy,
5313) -> bool {
5314 let Some((index_mtime_sec, index_mtime_nsec)) = index_mtime else {
5315 return false;
5316 };
5317 if index_mtime_sec == 0 {
5318 return false;
5319 }
5320 if policy.use_nanosec {
5321 index_mtime_sec < ie.mtime_sec
5322 || (index_mtime_sec == ie.mtime_sec && index_mtime_nsec <= ie.mtime_nsec)
5323 } else {
5324 index_mtime_sec <= ie.mtime_sec
5325 }
5326}
5327
5328pub fn smudge_racily_clean_entries(
5345 odb: &Odb,
5346 git_dir: &Path,
5347 index: &mut Index,
5348 work_tree: &Path,
5349 index_mtime: Option<(u32, u32)>,
5350 config: Option<&ConfigSet>,
5351 filter_process: Option<&crate::filter_process::FilterProcessState>,
5352) -> bool {
5353 use crate::crlf;
5354 use crate::index::{MODE_EXECUTABLE, MODE_REGULAR, MODE_SYMLINK};
5355
5356 let Some(index_mtime) = index_mtime else {
5357 return false;
5358 };
5359
5360 let config = config.cloned().unwrap_or_else(|| {
5361 ConfigSet::load(
5362 &crate::environment::Environment::empty(),
5363 Some(git_dir),
5364 true,
5365 )
5366 .unwrap_or_default()
5367 });
5368 let conv = crlf::ConversionConfig::from_config(&config);
5369 let attrs = crlf::load_gitattributes(work_tree);
5370 let verified = std::mem::take(&mut index.content_verified);
5371
5372 let mut changed = false;
5373 for ie in &mut index.entries {
5374 if ie.stage() != 0 || ie.skip_worktree() || ie.assume_unchanged() || ie.intent_to_add() {
5375 continue;
5376 }
5377 if ie.mode != MODE_REGULAR && ie.mode != MODE_EXECUTABLE && ie.mode != MODE_SYMLINK {
5378 continue;
5379 }
5380 if ie.size == 0 {
5381 continue;
5382 }
5383 if !entry_is_racy(ie, Some(index_mtime)) {
5384 continue;
5385 }
5386 if verified.get(&ie.path) == Some(&ie.oid) {
5387 continue;
5388 }
5389 let Ok(path) = std::str::from_utf8(&ie.path) else {
5390 continue;
5391 };
5392 let abs = work_tree.join(path);
5393 let Ok(meta) = fs::symlink_metadata(&abs) else {
5394 continue;
5395 };
5396 if !stat_matches(ie, &meta) {
5397 continue;
5398 }
5399 let file_attrs = crlf::get_file_attrs(&attrs, path, false, &config);
5400 let content_matches = hash_worktree_file(
5401 odb,
5402 &abs,
5403 &meta,
5404 &conv,
5405 &file_attrs,
5406 path,
5407 Some(ie),
5408 filter_process,
5409 )
5410 .map(|oid| oid == ie.oid)
5411 .unwrap_or(false);
5412 if content_matches {
5413 continue;
5414 }
5415 ie.size = 0;
5416 changed = true;
5417 }
5418 changed
5419}
5420pub fn worktree_differs_from_index_entry(
5428 odb: &Odb,
5429 repository_git_dir: &Path,
5430 work_tree: &Path,
5431 ie: &IndexEntry,
5432 ignore_submodule_untracked: bool,
5433 config: Option<&ConfigSet>,
5434 filter_process: Option<&crate::filter_process::FilterProcessState>,
5435) -> Result<bool> {
5436 WorktreeEntryComparer::new(repository_git_dir, work_tree, config).differs(
5437 odb,
5438 ie,
5439 ignore_submodule_untracked,
5440 filter_process,
5441 )
5442}
5443
5444pub(crate) struct WorktreeEntryComparer<'a> {
5448 work_tree: &'a Path,
5449 config: ConfigSet,
5450 mode_policy: crate::index::WorktreeModePolicy,
5451 precompose_unicode: bool,
5452 ignorecase: bool,
5453 conv: std::cell::OnceCell<crate::crlf::ConversionConfig>,
5454 attrs: std::cell::OnceCell<Vec<crate::crlf::AttrRule>>,
5455}
5456
5457impl<'a> WorktreeEntryComparer<'a> {
5458 pub(crate) fn new(
5460 repository_git_dir: &Path,
5461 work_tree: &'a Path,
5462 config: Option<&ConfigSet>,
5463 ) -> Self {
5464 let config = config.cloned().unwrap_or_else(|| {
5465 ConfigSet::load(
5466 &crate::environment::Environment::empty(),
5467 Some(repository_git_dir),
5468 true,
5469 )
5470 .unwrap_or_else(|_| ConfigSet::new())
5471 });
5472 let flag =
5473 |key: &str, default: bool| config.get_bool(key).and_then(|r| r.ok()).unwrap_or(default);
5474 Self {
5475 work_tree,
5476 mode_policy: crate::index::WorktreeModePolicy::from_config(&config),
5477 precompose_unicode: flag("core.precomposeunicode", false),
5478 ignorecase: flag("core.ignorecase", false),
5479 config,
5480 conv: std::cell::OnceCell::new(),
5481 attrs: std::cell::OnceCell::new(),
5482 }
5483 }
5484
5485 pub(crate) fn differs(
5487 &self,
5488 odb: &Odb,
5489 ie: &IndexEntry,
5490 ignore_submodule_untracked: bool,
5491 filter_process: Option<&crate::filter_process::FilterProcessState>,
5492 ) -> Result<bool> {
5493 use crate::crlf;
5494
5495 let work_tree = self.work_tree;
5496 let path_stored = crate::repo_relpath::storage_string(&ie.path);
5497 let path_str_ref = crate::repo_relpath::as_utf8_for_matching(&path_stored);
5498 let file_path = index_entry_worktree_abs_bytes(
5499 work_tree,
5500 &ie.path,
5501 self.precompose_unicode,
5502 self.ignorecase,
5503 );
5504
5505 if ie.mode == 0o160000 {
5506 let sub_head_oid = read_submodule_head(&file_path);
5507 let ref_matches = match sub_head_oid {
5508 Some(oid) => oid == ie.oid,
5509 None => submodule_worktree_is_unpopulated_placeholder(&file_path),
5510 };
5511 let mut flags = submodule_porcelain_flags(work_tree, path_str_ref.as_ref(), ie.oid);
5512 if ignore_submodule_untracked {
5513 flags.untracked = false;
5514 }
5515 return Ok(!ref_matches || flags.modified || flags.untracked);
5516 }
5517
5518 let meta = match fs::symlink_metadata(&file_path) {
5519 Ok(m) => m,
5520 Err(e)
5521 if e.kind() == std::io::ErrorKind::NotFound
5522 || e.raw_os_error() == Some(20) =>
5523 {
5524 return Ok(true);
5525 }
5526 Err(e) => return Err(Error::Io(e)),
5527 };
5528
5529 if meta.is_dir() {
5530 return Ok(true);
5531 }
5532
5533 if !self
5534 .mode_policy
5535 .worktree_mode_matches(mode_from_metadata(&meta), ie.mode)
5536 {
5537 return Ok(true);
5538 }
5539
5540 let conv = self
5541 .conv
5542 .get_or_init(|| crlf::ConversionConfig::from_config(&self.config));
5543 let attrs = self
5544 .attrs
5545 .get_or_init(|| crlf::load_gitattributes(work_tree));
5546 let file_attrs = crlf::get_file_attrs(attrs, path_str_ref.as_ref(), false, &self.config);
5547 let worktree_oid = hash_worktree_file(
5548 odb,
5549 &file_path,
5550 &meta,
5551 conv,
5552 &file_attrs,
5553 path_str_ref.as_ref(),
5554 Some(ie),
5555 filter_process,
5556 )?;
5557
5558 let mut eff_oid = worktree_oid;
5559 if eff_oid != ie.oid {
5560 if let Ok(raw) = fs::read(&file_path) {
5561 let raw_oid = odb.hash(ObjectKind::Blob, &raw)?;
5562 if raw_oid == ie.oid {
5563 eff_oid = ie.oid;
5564 }
5565 }
5566 }
5567
5568 Ok(eff_oid != ie.oid)
5569 }
5570}
5571
5572pub struct WorktreeAddRefreshParams<'a> {
5574 pub odb: &'a Odb,
5576 pub ie: &'a IndexEntry,
5578 pub meta: &'a fs::Metadata,
5580 pub abs_path: &'a Path,
5582 pub rel_path: &'a str,
5584 pub conv: &'a crate::crlf::ConversionConfig,
5586 pub file_attrs: &'a crate::crlf::FileAttrs,
5588 pub index_mtime: Option<(u32, u32)>,
5590 pub staged_mode: u32,
5593 pub filter_process: Option<&'a crate::filter_process::FilterProcessState>,
5595}
5596
5597#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5599pub enum WorktreeAddRefresh {
5600 UpToDate,
5602 ModeOnly {
5604 mode: u32,
5606 },
5607 StatOnly,
5609 NeedsRestage,
5611}
5612
5613#[must_use]
5615pub(crate) fn index_stat_cache_trustworthy(index_mtime: Option<(u32, u32)>) -> bool {
5616 match index_mtime {
5617 None => false,
5618 Some((0, _)) => false,
5619 Some(_) => true,
5620 }
5621}
5622
5623#[must_use]
5632pub(crate) fn index_entry_skips_worktree_content_hash_after_stat_match(
5633 ie: &IndexEntry,
5634 index_mtime: Option<(u32, u32)>,
5635 stat_policy: crate::index::StatMatchPolicy,
5636) -> bool {
5637 if ie.stat_cache_unset() || (ie.size == 0 && !ie.oid.is_empty_blob()) {
5638 return false;
5639 }
5640 if !index_stat_cache_trustworthy(index_mtime) {
5641 return false;
5642 }
5643 !entry_is_racy_with_policy(ie, index_mtime, stat_policy)
5644}
5645
5646pub fn classify_worktree_entry_for_add(
5660 params: &WorktreeAddRefreshParams<'_>,
5661) -> Result<WorktreeAddRefresh> {
5662 use crate::index::{MODE_EXECUTABLE, MODE_REGULAR, MODE_SYMLINK};
5663
5664 let ie = params.ie;
5665 let meta = params.meta;
5666 let staged_mode = params.staged_mode;
5667
5668 if ie.intent_to_add() {
5669 return Ok(WorktreeAddRefresh::NeedsRestage);
5670 }
5671
5672 let racy = entry_is_racy(ie, params.index_mtime);
5673 let stat_same = stat_matches(ie, meta);
5674 let trust_stat = index_stat_cache_trustworthy(params.index_mtime);
5675
5676 if ie.mode == MODE_SYMLINK {
5677 if !meta.file_type().is_symlink() && staged_mode != MODE_SYMLINK {
5680 return Ok(WorktreeAddRefresh::NeedsRestage);
5681 }
5682 if stat_same && !racy && trust_stat && staged_mode == ie.mode {
5683 return Ok(WorktreeAddRefresh::UpToDate);
5684 }
5685 if stat_same && !racy && trust_stat && staged_mode != ie.mode {
5686 return Ok(WorktreeAddRefresh::ModeOnly { mode: staged_mode });
5687 }
5688 return worktree_effective_oid_matches_index(params);
5689 }
5690
5691 if ie.mode != MODE_REGULAR && ie.mode != MODE_EXECUTABLE {
5692 return Ok(WorktreeAddRefresh::NeedsRestage);
5693 }
5694
5695 if meta.file_type().is_symlink() || meta.is_dir() {
5696 return Ok(WorktreeAddRefresh::NeedsRestage);
5697 }
5698
5699 if stat_same && !racy && trust_stat {
5700 if staged_mode == ie.mode {
5701 return Ok(WorktreeAddRefresh::UpToDate);
5702 }
5703 return Ok(WorktreeAddRefresh::ModeOnly { mode: staged_mode });
5704 }
5705
5706 worktree_effective_oid_matches_index(params)
5707}
5708
5709fn worktree_effective_oid_matches_index(
5710 params: &WorktreeAddRefreshParams<'_>,
5711) -> Result<WorktreeAddRefresh> {
5712 let WorktreeAddRefreshParams {
5713 odb,
5714 ie,
5715 meta,
5716 abs_path,
5717 rel_path,
5718 conv,
5719 file_attrs,
5720 staged_mode,
5721 ..
5722 } = params;
5723 let worktree_oid = hash_worktree_file(
5724 odb,
5725 abs_path,
5726 meta,
5727 conv,
5728 file_attrs,
5729 rel_path,
5730 Some(ie),
5731 params.filter_process,
5732 )?;
5733 let mut eff_oid = worktree_oid;
5734 if eff_oid != ie.oid {
5735 if let Ok(raw) = fs::read(abs_path) {
5736 let raw_oid = odb.hash(ObjectKind::Blob, &raw)?;
5737 if raw_oid == ie.oid {
5738 eff_oid = ie.oid;
5739 }
5740 }
5741 }
5742 if eff_oid != ie.oid {
5743 return Ok(WorktreeAddRefresh::NeedsRestage);
5744 }
5745 if *staged_mode != ie.mode {
5746 return Ok(WorktreeAddRefresh::ModeOnly { mode: *staged_mode });
5747 }
5748 if stat_matches(ie, meta) {
5749 Ok(WorktreeAddRefresh::UpToDate)
5750 } else {
5751 Ok(WorktreeAddRefresh::StatOnly)
5752 }
5753}
5754
5755#[must_use]
5757pub fn stat_matches(ie: &IndexEntry, meta: &fs::Metadata) -> bool {
5758 stat_matches_with_policy(ie, meta, crate::index::StatMatchPolicy::default())
5759}
5760
5761pub(crate) fn stat_matches_blob_stat(
5763 ie: &IndexEntry,
5764 stat: &crate::worktree_scan::BlobDiskStat,
5765 policy: crate::index::StatMatchPolicy,
5766) -> bool {
5767 stat.fields.matches_index_entry(ie, policy)
5768}
5769
5770#[must_use]
5772pub fn stat_matches_with_policy(
5773 ie: &IndexEntry,
5774 meta: &fs::Metadata,
5775 policy: crate::index::StatMatchPolicy,
5776) -> bool {
5777 if crate::index::munge_index_size(meta.len()) != ie.size {
5778 return false;
5779 }
5780 #[cfg(unix)]
5781 {
5782 use std::os::unix::fs::MetadataExt;
5783 if meta.mtime() as u32 != ie.mtime_sec {
5784 return false;
5785 }
5786 if policy.use_nanosec && policy.check_stat && meta.mtime_nsec() as u32 != ie.mtime_nsec {
5787 return false;
5788 }
5789 if policy.trust_ctime && policy.check_stat {
5790 if meta.ctime() as u32 != ie.ctime_sec {
5791 return false;
5792 }
5793 if policy.use_nanosec && meta.ctime_nsec() as u32 != ie.ctime_nsec {
5794 return false;
5795 }
5796 }
5797 if policy.check_stat && meta.ino() as u32 != ie.ino {
5798 return false;
5799 }
5800 }
5801 #[cfg(not(unix))]
5802 {
5803 let ((ctime_sec, ctime_nsec), (mtime_sec, mtime_nsec)) = crate::index::metadata_times(meta);
5804 if mtime_sec != ie.mtime_sec {
5805 return false;
5806 }
5807 if policy.use_nanosec && policy.check_stat && mtime_nsec != ie.mtime_nsec {
5808 return false;
5809 }
5810 if policy.trust_ctime && policy.check_stat {
5811 if ctime_sec != ie.ctime_sec {
5812 return false;
5813 }
5814 if policy.use_nanosec && ctime_nsec != ie.ctime_nsec {
5815 return false;
5816 }
5817 }
5818 }
5819 true
5820}
5821
5822pub fn refresh_index_stat_content_verified(
5845 odb: &Odb,
5846 git_dir: &Path,
5847 index: &mut Index,
5848 work_tree: &Path,
5849 index_mtime: Option<(u32, u32)>,
5850 config: Option<&ConfigSet>,
5851 _stat_parallel_threads: Option<usize>,
5852) -> Result<bool> {
5853 let _ = _stat_parallel_threads;
5854 refresh_index_stat_content_verified_with_rules(
5855 odb,
5856 git_dir,
5857 index,
5858 work_tree,
5859 index_mtime,
5860 config,
5861 _stat_parallel_threads,
5862 None,
5863 )
5864}
5865
5866#[allow(clippy::too_many_arguments)]
5874pub fn refresh_index_stat_content_verified_with_rules(
5875 odb: &Odb,
5876 git_dir: &Path,
5877 index: &mut Index,
5878 work_tree: &Path,
5879 index_mtime: Option<(u32, u32)>,
5880 config: Option<&ConfigSet>,
5881 stat_parallel_threads: Option<usize>,
5882 worktree_rules: Option<&std::sync::Arc<std::sync::Mutex<crate::worktree_rules::WorktreeRules>>>,
5883) -> Result<bool> {
5884 use crate::config::ConfigSet;
5885 use crate::crlf;
5886 use crate::worktree_scan::{
5887 collect_blob_lookup_batches, group_blob_entries_by_dir, visit_blob_lookup_batches,
5888 BlobDiskLookup, BlobScanSparseMode, WorktreeBlobScanOptions,
5889 };
5890
5891 let index_mtime = index_mtime_for_diff(index, index_mtime);
5892 let rules_lock = worktree_rules
5893 .map(|shared| {
5894 shared
5895 .lock()
5896 .map_err(|e| Error::Message(format!("worktree rules lock poisoned: {e}")))
5897 })
5898 .transpose()?;
5899 let config = rules_lock
5900 .as_ref()
5901 .map(|r| r.config().clone())
5902 .unwrap_or_else(|| {
5903 config.cloned().unwrap_or_else(|| {
5904 ConfigSet::load(
5905 &crate::environment::Environment::empty(),
5906 Some(git_dir),
5907 true,
5908 )
5909 .unwrap_or_default()
5910 })
5911 });
5912 let preload_index = config
5913 .get_bool("core.preloadIndex")
5914 .and_then(|r| r.ok())
5915 .unwrap_or(true);
5916 let blob_scan = WorktreeBlobScanOptions {
5917 preload_index,
5918 stat_parallel_threads,
5919 };
5920 let conv = rules_lock
5921 .as_ref()
5922 .map(|r| r.conversion().clone())
5923 .unwrap_or_else(|| crlf::ConversionConfig::from_config(&config));
5924 let legacy_attrs = if rules_lock.is_none() {
5925 Some(crlf::load_gitattributes_parsed(work_tree))
5926 } else {
5927 None
5928 };
5929 let file_attrs_for = |path: &str| -> crlf::FileAttrs {
5930 if let Some(rules) = rules_lock.as_ref() {
5931 rules.file_attrs(path, false)
5932 } else if let Some(parsed) = legacy_attrs.as_ref() {
5933 crlf::file_attrs_from_gitattributes(&parsed.rules, &parsed.macros, path, false, &config)
5934 } else {
5935 crlf::FileAttrs::default()
5936 }
5937 };
5938 let precompose_unicode = config
5939 .get_bool("core.precomposeunicode")
5940 .and_then(|r| r.ok())
5941 .unwrap_or(false);
5942 let ignorecase = config
5943 .get_bool("core.ignorecase")
5944 .and_then(|r| r.ok())
5945 .unwrap_or(false);
5946 let stat_policy = crate::index::StatMatchPolicy::from_config(Some(&config));
5947
5948 let blob_dirs = group_blob_entries_by_dir(index, false);
5949 let work_tree_owned = work_tree.to_path_buf();
5950 let dir_abs = move |dir: &[u8]| {
5951 if dir.is_empty() {
5952 work_tree_owned.clone()
5953 } else {
5954 index_entry_worktree_abs_bytes(&work_tree_owned, dir, precompose_unicode, ignorecase)
5955 }
5956 };
5957 let work_tree_for_files = work_tree.to_path_buf();
5958 let file_abs = move |rel: &[u8]| {
5959 index_entry_worktree_abs_bytes(&work_tree_for_files, rel, precompose_unicode, ignorecase)
5960 };
5961 let dir_symlinks = SymlinkDirCache::default();
5962 let mut changed = false;
5963 let filter_fp = rules_lock.as_ref().map(|r| r.filter_process());
5964 let thread_policy = crate::parallel::ThreadPolicy::default();
5965 let refresh_batches = collect_blob_lookup_batches(
5967 index,
5968 &blob_dirs,
5969 blob_scan,
5970 &thread_policy,
5971 &config,
5972 dir_abs,
5973 file_abs,
5974 |rel_path| {
5975 dir_symlinks.has_symlink_in_path_bytes(
5976 work_tree,
5977 rel_path,
5978 precompose_unicode,
5979 ignorecase,
5980 )
5981 },
5982 BlobScanSparseMode::StatRefresh {
5983 stat_policy,
5984 index_mtime,
5985 },
5986 )?;
5987 let par_opts = refresh_hash_parallel_options(&thread_policy, &config, stat_parallel_threads);
5988 let refresh_checks = refresh_content_checks(
5989 odb,
5990 index,
5991 &refresh_batches,
5992 index_mtime,
5993 stat_policy,
5994 &|_, _| true,
5995 &WorktreeHashContext {
5996 work_tree,
5997 precompose_unicode,
5998 ignorecase,
5999 conv: &conv,
6000 file_attrs_for: &file_attrs_for,
6001 filter_process: filter_fp,
6002 },
6003 par_opts,
6004 );
6005 visit_blob_lookup_batches(refresh_batches, |entry_index, lookup| {
6006 let BlobDiskLookup::Present(stat) = lookup else {
6007 return Ok(());
6008 };
6009 let refresh = refresh_index_blob_stat_from_scan(
6010 &mut index.entries[entry_index],
6011 &stat,
6012 index_mtime,
6013 stat_policy,
6014 refresh_checks[entry_index].unwrap_or(false),
6015 );
6016 if refresh.index_changed {
6017 changed = true;
6018 }
6019 if refresh.content_verified {
6020 let path = index.entries[entry_index].path.clone();
6021 let oid = index.entries[entry_index].oid;
6022 index.mark_content_verified(&path, oid);
6023 }
6024 Ok(())
6025 })?;
6026 Ok(changed)
6027}
6028
6029struct BlobStatRefreshOutcome {
6031 index_changed: bool,
6033 content_verified: bool,
6035}
6036
6037fn refresh_index_entry_eligible(ie: &IndexEntry) -> bool {
6039 use crate::index::{MODE_EXECUTABLE, MODE_REGULAR, MODE_SYMLINK};
6040 ie.stage() == 0
6041 && !ie.skip_worktree()
6042 && !ie.assume_unchanged()
6043 && !ie.intent_to_add()
6044 && matches!(ie.mode, MODE_REGULAR | MODE_EXECUTABLE | MODE_SYMLINK)
6045}
6046
6047fn refresh_needs_content_check(
6050 ie: &IndexEntry,
6051 stat: &crate::worktree_scan::BlobDiskStat,
6052 index_mtime: Option<(u32, u32)>,
6053 stat_policy: crate::index::StatMatchPolicy,
6054) -> bool {
6055 refresh_index_entry_eligible(ie)
6056 && !stat.is_dir
6057 && (!stat_matches_blob_stat(ie, stat, stat_policy)
6058 || entry_is_racy_with_policy(ie, index_mtime, stat_policy))
6059}
6060
6061pub(crate) fn worktree_diff_refreshes_entry(
6066 ie: &IndexEntry,
6067 stat: &crate::worktree_scan::BlobDiskStat,
6068 index_mtime: Option<(u32, u32)>,
6069 stat_policy: crate::index::StatMatchPolicy,
6070 for_status: bool,
6071) -> bool {
6072 if !for_status {
6073 return true;
6074 }
6075 if stat_matches_blob_stat(ie, stat, stat_policy) {
6076 return entry_is_racy_with_policy(ie, index_mtime, stat_policy);
6077 }
6078 stat.worktree_size() == ie.size || ie.stat_cache_unset()
6079}
6080
6081fn refresh_hash_parallel_options(
6088 thread_policy: &crate::parallel::ThreadPolicy,
6089 config: &ConfigSet,
6090 stat_parallel_threads: Option<usize>,
6091) -> crate::parallel::ParallelOptions {
6092 let policy = crate::parallel::ThreadPolicy {
6093 min_items_override: thread_policy.min_items_override.or(Some(0)),
6094 min_total_bytes_override: thread_policy.min_total_bytes_override.or(Some(0)),
6095 ..*thread_policy
6096 };
6097 let mut par_opts = policy.resolve(crate::parallel::ParallelOp::IndexRefresh, config);
6098 if let Some(cap) = stat_parallel_threads {
6099 par_opts.threads = par_opts
6100 .threads
6101 .min(std::num::NonZeroUsize::new(cap).unwrap_or(std::num::NonZeroUsize::MIN));
6102 }
6103 par_opts.min_items = 0;
6104 par_opts.min_total_bytes = None;
6105 par_opts
6106}
6107
6108struct WorktreeHashContext<'a> {
6110 work_tree: &'a Path,
6111 precompose_unicode: bool,
6112 ignorecase: bool,
6113 conv: &'a crate::crlf::ConversionConfig,
6114 file_attrs_for: &'a dyn Fn(&str) -> crate::crlf::FileAttrs,
6115 filter_process: Option<&'a crate::filter_process::FilterProcessState>,
6116}
6117
6118#[allow(clippy::too_many_arguments)]
6128fn refresh_content_checks(
6129 odb: &Odb,
6130 index: &Index,
6131 batches: &[Vec<(usize, crate::worktree_scan::BlobDiskLookup)>],
6132 index_mtime: Option<(u32, u32)>,
6133 stat_policy: crate::index::StatMatchPolicy,
6134 include: &dyn Fn(&IndexEntry, &crate::worktree_scan::BlobDiskStat) -> bool,
6135 ctx: &WorktreeHashContext<'_>,
6136 par_opts: crate::parallel::ParallelOptions,
6137) -> Vec<Option<bool>> {
6138 use crate::worktree_scan::BlobDiskLookup;
6139
6140 struct ContentCheck {
6142 entry_index: usize,
6143 worktree_mode: u32,
6144 file_attrs: crate::crlf::FileAttrs,
6145 }
6146
6147 let mut jobs = Vec::new();
6148 let mut total_bytes = 0usize;
6149 for (entry_index, lookup) in batches.iter().flatten() {
6150 let BlobDiskLookup::Present(stat) = lookup else {
6151 continue;
6152 };
6153 let ie = &index.entries[*entry_index];
6154 if !refresh_needs_content_check(ie, stat, index_mtime, stat_policy) || !include(ie, stat) {
6155 continue;
6156 }
6157 total_bytes = total_bytes.saturating_add(ie.size as usize);
6158 jobs.push(ContentCheck {
6159 entry_index: *entry_index,
6160 worktree_mode: stat.git_mode,
6161 file_attrs: (ctx.file_attrs_for)(&crate::repo_relpath::storage_string(&ie.path)),
6162 });
6163 }
6164 let mut checks = vec![None; index.entries.len()];
6165 if jobs.is_empty() {
6166 return checks;
6167 }
6168 let (work_tree, precompose_unicode, ignorecase) =
6169 (ctx.work_tree, ctx.precompose_unicode, ctx.ignorecase);
6170 let (conv, filter_process) = (ctx.conv, ctx.filter_process);
6171 let entries = index.entries.as_slice();
6172 let matches = if par_opts.threads.get() > 1 && jobs.len() > 1 {
6173 crate::parallel::try_par_map_force::<_, _, std::convert::Infallible, _>(
6174 &jobs,
6175 par_opts,
6176 |job| {
6177 let ie = &entries[job.entry_index];
6178 let file_path = index_entry_worktree_abs_bytes(
6179 work_tree,
6180 &ie.path,
6181 precompose_unicode,
6182 ignorecase,
6183 );
6184 Ok(hash_worktree_file_with_mode(
6185 odb,
6186 &file_path,
6187 job.worktree_mode,
6188 conv,
6189 &job.file_attrs,
6190 &crate::repo_relpath::storage_string(&ie.path),
6191 Some(ie),
6192 filter_process,
6193 )
6194 .is_ok_and(|oid| oid == ie.oid))
6195 },
6196 )
6197 .unwrap_or_else(|e| match e {
6198 crate::parallel::ParallelHashError::Task(infallible) => match infallible {},
6199 })
6200 } else {
6201 jobs.iter()
6202 .map(|job| {
6203 let ie = &entries[job.entry_index];
6204 let file_path = index_entry_worktree_abs_bytes(
6205 work_tree,
6206 &ie.path,
6207 precompose_unicode,
6208 ignorecase,
6209 );
6210 hash_worktree_file_with_mode(
6211 odb,
6212 &file_path,
6213 job.worktree_mode,
6214 conv,
6215 &job.file_attrs,
6216 &crate::repo_relpath::storage_string(&ie.path),
6217 Some(ie),
6218 filter_process,
6219 )
6220 .is_ok_and(|oid| oid == ie.oid)
6221 })
6222 .collect()
6223 };
6224 for (job, matched) in jobs.iter().zip(matches) {
6225 checks[job.entry_index] = Some(matched);
6226 }
6227 checks
6228}
6229
6230fn refresh_index_blob_stat_from_scan(
6236 ie: &mut IndexEntry,
6237 stat: &crate::worktree_scan::BlobDiskStat,
6238 index_mtime: Option<(u32, u32)>,
6239 stat_policy: crate::index::StatMatchPolicy,
6240 content_matches: bool,
6241) -> BlobStatRefreshOutcome {
6242 const UNCHANGED: BlobStatRefreshOutcome = BlobStatRefreshOutcome {
6243 index_changed: false,
6244 content_verified: false,
6245 };
6246 if !refresh_index_entry_eligible(ie) || stat.is_dir {
6247 return UNCHANGED;
6248 }
6249 let cached = crate::index::IndexStatFields::of_entry(ie);
6250 let stat_same = cached.equivalent_to(&stat.fields, stat_policy);
6251 let stat_matches = stat_matches_blob_stat(ie, stat, stat_policy);
6252 if !stat_matches || entry_is_racy_with_policy(ie, index_mtime, stat_policy) {
6253 if !content_matches {
6254 if stat_matches {
6255 invalidate_index_stat_cache(ie);
6256 return BlobStatRefreshOutcome {
6257 index_changed: true,
6258 content_verified: false,
6259 };
6260 }
6261 return UNCHANGED;
6262 }
6263 if !stat_same {
6264 stat.fields.store_in(ie);
6265 }
6266 return BlobStatRefreshOutcome {
6267 index_changed: !stat_same,
6268 content_verified: true,
6269 };
6270 }
6271 if stat_same {
6272 return BlobStatRefreshOutcome {
6273 index_changed: false,
6274 content_verified: true,
6275 };
6276 }
6277 stat.fields.store_in(ie);
6278 BlobStatRefreshOutcome {
6279 index_changed: true,
6280 content_verified: false,
6281 }
6282}
6283
6284#[allow(clippy::too_many_arguments)] pub fn path_checkout_skip_blob_write_when_up_to_date(
6297 odb: &Odb,
6298 git_dir: &Path,
6299 work_tree: &Path,
6300 index: &Index,
6301 entry: &IndexEntry,
6302 expected_oid: &ObjectId,
6303 expected_mode: u32,
6304 rel_path: &str,
6305 filter_process: Option<&crate::filter_process::FilterProcessState>,
6306) -> Result<bool> {
6307 path_checkout_skip_blob_write_when_up_to_date_with_config(
6308 odb,
6309 git_dir,
6310 work_tree,
6311 index,
6312 entry,
6313 expected_oid,
6314 expected_mode,
6315 rel_path,
6316 filter_process,
6317 None,
6318 )
6319}
6320
6321#[allow(clippy::too_many_arguments)]
6323pub(crate) fn path_checkout_skip_blob_write_when_up_to_date_with_config(
6324 odb: &Odb,
6325 git_dir: &Path,
6326 work_tree: &Path,
6327 index: &Index,
6328 entry: &IndexEntry,
6329 expected_oid: &ObjectId,
6330 expected_mode: u32,
6331 rel_path: &str,
6332 filter_process: Option<&crate::filter_process::FilterProcessState>,
6333 preloaded_config: Option<&crate::config::ConfigSet>,
6334) -> Result<bool> {
6335 use crate::config::ConfigSet;
6336 use crate::crlf;
6337 use crate::index::{MODE_EXECUTABLE, MODE_REGULAR};
6338
6339 if entry.oid != *expected_oid || entry.mode != expected_mode {
6340 return Ok(false);
6341 }
6342 if expected_mode != MODE_REGULAR && expected_mode != MODE_EXECUTABLE {
6343 return Ok(false);
6344 }
6345 let abs_path = crate::repo_relpath::worktree_abs(work_tree, rel_path);
6346 let meta = match fs::symlink_metadata(&abs_path) {
6347 Ok(m) => m,
6348 Err(_) => return Ok(false),
6349 };
6350 if !meta.is_file() || !stat_matches(entry, &meta) {
6351 return Ok(false);
6352 }
6353 let owned_config;
6354 let config = match preloaded_config {
6355 Some(cfg) => cfg,
6356 None => {
6357 owned_config = ConfigSet::load(
6358 &crate::environment::Environment::empty(),
6359 Some(git_dir),
6360 true,
6361 )
6362 .unwrap_or_default();
6363 &owned_config
6364 }
6365 };
6366 if !crate::index::WorktreeModePolicy::from_config(config)
6367 .worktree_mode_matches(mode_from_metadata(&meta), expected_mode)
6368 {
6369 return Ok(false);
6370 }
6371 let index_mtime = index_mtime_for_diff(index, None);
6372 if !entry_is_racy(entry, index_mtime) {
6373 return Ok(true);
6374 }
6375 let conv = crlf::ConversionConfig::from_config(config);
6376 let attrs = crlf::load_gitattributes(work_tree);
6377 let file_attrs = crlf::get_file_attrs(&attrs, rel_path, false, config);
6378 let wt_oid = hash_worktree_file(
6379 odb,
6380 &abs_path,
6381 &meta,
6382 &conv,
6383 &file_attrs,
6384 rel_path,
6385 Some(entry),
6386 filter_process,
6387 )?;
6388 Ok(wt_oid == *expected_oid)
6389}
6390
6391fn invalidate_index_stat_cache(ie: &mut IndexEntry) {
6393 ie.ctime_sec = 0;
6394 ie.ctime_nsec = 0;
6395 ie.mtime_sec = 0;
6396 ie.mtime_nsec = 0;
6397 ie.dev = 0;
6398 ie.ino = 0;
6399 ie.size = 0;
6400}
6401
6402pub fn read_worktree_bytes_for_mode(path: &Path, mode: u32) -> std::io::Result<Vec<u8>> {
6413 if mode == MODE_SYMLINK && fs::symlink_metadata(path)?.file_type().is_symlink() {
6414 let target = fs::read_link(path)?;
6415 return Ok(symlink_target_bytes(&target));
6416 }
6417 fs::read(path)
6418}
6419
6420pub fn read_worktree_side_bytes_for_diff(
6430 odb: &Odb,
6431 work_tree: &Path,
6432 rel_path: &str,
6433 mode: u32,
6434 config: &crate::config::ConfigSet,
6435 index_entry: Option<&IndexEntry>,
6436 worktree_rules: Option<&crate::worktree_rules::WorktreeRules>,
6437) -> Result<Vec<u8>> {
6438 let file_path = crate::repo_relpath::worktree_abs(work_tree, rel_path);
6439 let rel_match = crate::repo_relpath::as_utf8_for_matching(rel_path);
6440 let meta = fs::symlink_metadata(&file_path).map_err(|source| {
6441 crate::worktree::map_worktree_io(
6442 rel_match.as_ref(),
6443 crate::worktree::WorktreePathOperation::Metadata,
6444 source,
6445 )
6446 })?;
6447 if mode == MODE_SYMLINK && meta.file_type().is_symlink() {
6448 let target = fs::read_link(&file_path).map_err(|source| {
6449 crate::worktree::map_worktree_io(
6450 rel_match.as_ref(),
6451 crate::worktree::WorktreePathOperation::Read,
6452 source,
6453 )
6454 })?;
6455 return Ok(symlink_target_bytes(&target));
6456 }
6457 if let Some(rules) = worktree_rules {
6458 return worktree_blob_bytes(
6459 odb,
6460 &file_path,
6461 &meta,
6462 rules.conversion(),
6463 &rules.file_attrs(rel_match.as_ref(), meta.is_dir()),
6464 rel_match.as_ref(),
6465 index_entry,
6466 Some(rules.filter_process()),
6467 );
6468 }
6469 let conv = crate::crlf::ConversionConfig::from_config(config);
6470 let attrs = crate::crlf::load_gitattributes(work_tree);
6471 let file_attrs = crate::crlf::get_file_attrs(&attrs, rel_match.as_ref(), meta.is_dir(), config);
6472 worktree_blob_bytes(
6473 odb,
6474 &file_path,
6475 &meta,
6476 &conv,
6477 &file_attrs,
6478 rel_match.as_ref(),
6479 index_entry,
6480 None,
6481 )
6482}
6483
6484pub(crate) fn symlink_target_bytes(target: &Path) -> Vec<u8> {
6490 #[cfg(unix)]
6491 {
6492 use std::os::unix::ffi::OsStrExt as _;
6493 target.as_os_str().as_bytes().to_vec()
6494 }
6495 #[cfg(not(unix))]
6496 {
6497 target.as_os_str().to_string_lossy().as_bytes().to_vec()
6498 }
6499}
6500
6501pub(crate) enum WorktreeBlobContent {
6508 Bytes(Vec<u8>),
6510 Stream {
6512 file: fs::File,
6514 size: u64,
6516 },
6517}
6518
6519impl WorktreeBlobContent {
6520 pub(crate) fn hash(self, odb: &Odb) -> Result<ObjectId> {
6522 match self {
6523 Self::Bytes(data) => odb.hash(ObjectKind::Blob, &data),
6524 Self::Stream { mut file, size } => odb.hash_stream(ObjectKind::Blob, size, &mut file),
6525 }
6526 }
6527
6528 pub(crate) fn store(self, odb: &Odb, options: crate::odb::WriteOptions) -> Result<ObjectId> {
6530 match self {
6531 Self::Bytes(data) => odb.write_with_options(ObjectKind::Blob, &data, options),
6532 Self::Stream { mut file, size } => {
6533 odb.write_stream(ObjectKind::Blob, size, &mut file, options)
6534 }
6535 }
6536 }
6537
6538 fn object_id(self, odb: &Odb, store: Option<crate::odb::WriteOptions>) -> Result<ObjectId> {
6540 match store {
6541 Some(options) => self.store(odb, options),
6542 None => self.hash(odb),
6543 }
6544 }
6545}
6546
6547#[allow(clippy::too_many_arguments)]
6549fn read_regular_worktree_bytes(
6550 odb: &Odb,
6551 mut file: fs::File,
6552 size: u64,
6553 conv: &crate::crlf::ConversionConfig,
6554 file_attrs: &crate::crlf::FileAttrs,
6555 rel_path: &str,
6556 index_entry: Option<&IndexEntry>,
6557 filter_process: Option<&crate::filter_process::FilterProcessState>,
6558) -> Result<Vec<u8>> {
6559 let needs_index_blob =
6560 index_entry.is_some_and(|_| crate::crlf::clean_uses_autocrlf_index_guard(file_attrs, conv));
6561 let prior_blob: Option<Vec<u8>> = if needs_index_blob {
6562 index_entry
6563 .filter(|e| e.oid != zero_oid())
6564 .and_then(|e| odb.read(&e.oid).ok().map(|o| o.data))
6565 } else {
6566 None
6567 };
6568 let mut raw = Vec::with_capacity(usize::try_from(size).unwrap_or_default());
6569 std::io::Read::read_to_end(&mut file, &mut raw).map_err(|source| {
6570 crate::worktree::map_worktree_io(
6571 rel_path,
6572 crate::worktree::WorktreePathOperation::Read,
6573 source,
6574 )
6575 })?;
6576 #[cfg(test)]
6577 odb.hot_path_metrics().record_blob_content_read();
6578 let opts = crate::crlf::ConvertToGitOpts {
6579 index_blob: prior_blob.as_deref(),
6580 renormalize: false,
6581 check_safecrlf: false,
6582 filter_process,
6583 command_runner: None,
6584 };
6585 Ok(crate::crlf::convert_to_git_owned(
6586 raw, rel_path, conv, file_attrs, opts,
6587 )?)
6588}
6589
6590fn open_with_size(path: &Path, rel_path: &str) -> Result<(fs::File, u64)> {
6591 let file = fs::File::open(path).map_err(|source| {
6592 crate::worktree::map_worktree_io(
6593 rel_path,
6594 crate::worktree::WorktreePathOperation::Read,
6595 source,
6596 )
6597 })?;
6598 let size = file
6599 .metadata()
6600 .map_err(|source| {
6601 crate::worktree::map_worktree_io(
6602 rel_path,
6603 crate::worktree::WorktreePathOperation::Metadata,
6604 source,
6605 )
6606 })?
6607 .len();
6608 Ok((file, size))
6609}
6610
6611#[allow(clippy::too_many_arguments)]
6614fn read_regular_worktree_blob(
6615 odb: &Odb,
6616 path: &Path,
6617 conv: &crate::crlf::ConversionConfig,
6618 file_attrs: &crate::crlf::FileAttrs,
6619 rel_path: &str,
6620 index_entry: Option<&IndexEntry>,
6621 filter_process: Option<&crate::filter_process::FilterProcessState>,
6622 stream_from: u64,
6623) -> Result<WorktreeBlobContent> {
6624 let (mut file, size) = open_with_size(path, rel_path)?;
6625 if size >= stream_from
6626 && crate::crlf::worktree_allows_raw_byte_stream(
6627 conv,
6628 file_attrs,
6629 index_entry,
6630 &mut file,
6631 size,
6632 )?
6633 {
6634 #[cfg(test)]
6635 odb.hot_path_metrics().record_blob_content_read();
6636 return Ok(WorktreeBlobContent::Stream { file, size });
6637 }
6638 read_regular_worktree_bytes(
6639 odb,
6640 file,
6641 size,
6642 conv,
6643 file_attrs,
6644 rel_path,
6645 index_entry,
6646 filter_process,
6647 )
6648 .map(WorktreeBlobContent::Bytes)
6649}
6650
6651#[allow(clippy::too_many_arguments)]
6654pub(crate) fn worktree_blob_content(
6655 odb: &Odb,
6656 path: &Path,
6657 meta: &fs::Metadata,
6658 conv: &crate::crlf::ConversionConfig,
6659 file_attrs: &crate::crlf::FileAttrs,
6660 rel_path: &str,
6661 index_entry: Option<&IndexEntry>,
6662 filter_process: Option<&crate::filter_process::FilterProcessState>,
6663) -> Result<WorktreeBlobContent> {
6664 worktree_blob_content_from(
6665 odb,
6666 path,
6667 meta,
6668 conv,
6669 file_attrs,
6670 rel_path,
6671 index_entry,
6672 filter_process,
6673 hash_stream_threshold(odb)?,
6674 )
6675}
6676
6677#[allow(clippy::too_many_arguments)]
6679fn worktree_blob_content_from(
6680 odb: &Odb,
6681 path: &Path,
6682 meta: &fs::Metadata,
6683 conv: &crate::crlf::ConversionConfig,
6684 file_attrs: &crate::crlf::FileAttrs,
6685 rel_path: &str,
6686 index_entry: Option<&IndexEntry>,
6687 filter_process: Option<&crate::filter_process::FilterProcessState>,
6688 stream_from: u64,
6689) -> Result<WorktreeBlobContent> {
6690 if meta.file_type().is_symlink() {
6691 let target = fs::read_link(path).map_err(|source| {
6692 crate::worktree::map_worktree_io(
6693 rel_path,
6694 crate::worktree::WorktreePathOperation::Read,
6695 source,
6696 )
6697 })?;
6698 return Ok(WorktreeBlobContent::Bytes(symlink_target_bytes(&target)));
6699 }
6700 if meta.is_dir() {
6701 return Ok(WorktreeBlobContent::Bytes(Vec::new()));
6702 }
6703 read_regular_worktree_blob(
6704 odb,
6705 path,
6706 conv,
6707 file_attrs,
6708 rel_path,
6709 index_entry,
6710 filter_process,
6711 stream_from,
6712 )
6713}
6714
6715#[allow(clippy::too_many_arguments)]
6718pub(crate) fn worktree_blob_bytes(
6719 odb: &Odb,
6720 path: &Path,
6721 meta: &fs::Metadata,
6722 conv: &crate::crlf::ConversionConfig,
6723 file_attrs: &crate::crlf::FileAttrs,
6724 rel_path: &str,
6725 index_entry: Option<&IndexEntry>,
6726 filter_process: Option<&crate::filter_process::FilterProcessState>,
6727) -> Result<Vec<u8>> {
6728 if meta.file_type().is_symlink() {
6729 let target = fs::read_link(path).map_err(|source| {
6730 crate::worktree::map_worktree_io(
6731 rel_path,
6732 crate::worktree::WorktreePathOperation::Read,
6733 source,
6734 )
6735 })?;
6736 return Ok(symlink_target_bytes(&target));
6737 }
6738 if meta.is_dir() {
6739 return Ok(Vec::new());
6740 }
6741 let (file, size) = open_with_size(path, rel_path)?;
6742 read_regular_worktree_bytes(
6743 odb,
6744 file,
6745 size,
6746 conv,
6747 file_attrs,
6748 rel_path,
6749 index_entry,
6750 filter_process,
6751 )
6752}
6753
6754#[allow(clippy::too_many_arguments)]
6755fn worktree_file_oid(
6756 odb: &Odb,
6757 path: &Path,
6758 meta: &fs::Metadata,
6759 conv: &crate::crlf::ConversionConfig,
6760 file_attrs: &crate::crlf::FileAttrs,
6761 rel_path: &str,
6762 index_entry: Option<&IndexEntry>,
6763 materialize: bool,
6764 filter_process: Option<&crate::filter_process::FilterProcessState>,
6765) -> Result<ObjectId> {
6766 let stream_from = if materialize {
6767 odb.big_file_threshold()?
6768 } else {
6769 hash_stream_threshold(odb)?
6770 };
6771 worktree_blob_content_from(
6772 odb,
6773 path,
6774 meta,
6775 conv,
6776 file_attrs,
6777 rel_path,
6778 index_entry,
6779 filter_process,
6780 stream_from,
6781 )?
6782 .object_id(odb, materialize.then_some(crate::odb::BULK_NEW_LOOSE_WRITE))
6783}
6784
6785#[allow(clippy::too_many_arguments)]
6796pub fn hash_worktree_file(
6797 odb: &Odb,
6798 path: &Path,
6799 meta: &fs::Metadata,
6800 conv: &crate::crlf::ConversionConfig,
6801 file_attrs: &crate::crlf::FileAttrs,
6802 rel_path: &str,
6803 index_entry: Option<&IndexEntry>,
6804 filter_process: Option<&crate::filter_process::FilterProcessState>,
6805) -> Result<ObjectId> {
6806 worktree_file_oid(
6807 odb,
6808 path,
6809 meta,
6810 conv,
6811 file_attrs,
6812 rel_path,
6813 index_entry,
6814 false,
6815 filter_process,
6816 )
6817}
6818
6819const STREAMED_HASH_MIN_SIZE: u64 = 1024 * 1024;
6822
6823fn hash_stream_threshold(odb: &Odb) -> Result<u64> {
6825 Ok(odb.big_file_threshold()?.min(STREAMED_HASH_MIN_SIZE))
6826}
6827
6828#[allow(clippy::too_many_arguments)]
6832pub(crate) fn hash_worktree_file_with_mode(
6833 odb: &Odb,
6834 path: &Path,
6835 worktree_mode: u32,
6836 conv: &crate::crlf::ConversionConfig,
6837 file_attrs: &crate::crlf::FileAttrs,
6838 rel_path: &str,
6839 index_entry: Option<&IndexEntry>,
6840 filter_process: Option<&crate::filter_process::FilterProcessState>,
6841) -> Result<ObjectId> {
6842 if worktree_mode == MODE_SYMLINK {
6843 let data = read_worktree_bytes_for_mode(path, worktree_mode)?;
6844 return odb.hash(ObjectKind::Blob, &data);
6845 }
6846 read_regular_worktree_blob(
6847 odb,
6848 path,
6849 conv,
6850 file_attrs,
6851 rel_path,
6852 index_entry,
6853 filter_process,
6854 hash_stream_threshold(odb)?,
6855 )?
6856 .hash(odb)
6857}
6858
6859#[allow(clippy::too_many_arguments)]
6867pub fn materialize_worktree_blob(
6868 odb: &Odb,
6869 path: &Path,
6870 meta: &fs::Metadata,
6871 conv: &crate::crlf::ConversionConfig,
6872 file_attrs: &crate::crlf::FileAttrs,
6873 rel_path: &str,
6874 index_entry: Option<&IndexEntry>,
6875 filter_process: Option<&crate::filter_process::FilterProcessState>,
6876) -> Result<ObjectId> {
6877 worktree_file_oid(
6878 odb,
6879 path,
6880 meta,
6881 conv,
6882 file_attrs,
6883 rel_path,
6884 index_entry,
6885 true,
6886 filter_process,
6887 )
6888}
6889
6890pub fn mode_from_metadata(meta: &fs::Metadata) -> u32 {
6892 if meta.file_type().is_symlink() {
6893 0o120000
6894 } else {
6895 #[cfg(unix)]
6896 {
6897 if meta.mode() & 0o111 != 0 {
6898 return 0o100755;
6899 }
6900 }
6901 0o100644
6902 }
6903}
6904
6905pub fn diff_tree_to_worktree(
6922 odb: &Odb,
6923 tree_oid: Option<&ObjectId>,
6924 work_tree: &Path,
6925 index: &Index,
6926) -> Result<Vec<DiffEntry>> {
6927 diff_tree_to_worktree_with_git_dir(
6928 odb,
6929 tree_oid,
6930 work_tree,
6931 &work_tree.join(".git"),
6932 index,
6933 None,
6934 )
6935}
6936
6937pub fn diff_tree_to_worktree_with_git_dir(
6941 odb: &Odb,
6942 tree_oid: Option<&ObjectId>,
6943 work_tree: &Path,
6944 repository_git_dir: &Path,
6945 index: &Index,
6946 config: Option<&ConfigSet>,
6947) -> Result<Vec<DiffEntry>> {
6948 diff_tree_to_worktree_with_git_dir_and_rules(
6949 odb,
6950 tree_oid,
6951 work_tree,
6952 repository_git_dir,
6953 index,
6954 config,
6955 None,
6956 )
6957}
6958
6959pub fn diff_tree_to_worktree_with_git_dir_and_rules(
6961 odb: &Odb,
6962 tree_oid: Option<&ObjectId>,
6963 work_tree: &Path,
6964 repository_git_dir: &Path,
6965 index: &Index,
6966 config: Option<&ConfigSet>,
6967 worktree_rules: Option<&crate::worktree_rules::WorktreeRules>,
6968) -> Result<Vec<DiffEntry>> {
6969 use crate::config::ConfigSet;
6970 use crate::crlf;
6971
6972 if worktree_rules.is_none() {
6973 if let Some(tree_oid) = tree_oid {
6974 if index.cache_tree_root.as_ref() == Some(tree_oid)
6975 && crate::write_tree::cache_tree_fully_valid(odb, index.cache_tree.as_ref())
6976 {
6977 let mut idx = index.clone();
6978 let options = DiffIndexToWorktreeOptions {
6979 repository_git_dir: Some(repository_git_dir.to_path_buf()),
6980 refresh_index_stat_in_pass: false,
6981 for_status: false,
6985 config: config.cloned().map(std::sync::Arc::new),
6986 ..DiffIndexToWorktreeOptions::default()
6987 };
6988 let (entries, _, _) = diff_index_to_worktree_inner(
6989 odb,
6990 &mut idx,
6991 work_tree,
6992 repository_git_dir,
6993 options,
6994 false,
6995 )?;
6996 return Ok(entries);
6997 }
6998 }
6999 }
7000
7001 let config = worktree_rules
7002 .map(|r| r.config().clone())
7003 .unwrap_or_else(|| {
7004 config.cloned().unwrap_or_else(|| {
7005 ConfigSet::load(
7006 &crate::environment::Environment::empty(),
7007 Some(repository_git_dir),
7008 true,
7009 )
7010 .unwrap_or_else(|_| ConfigSet::new())
7011 })
7012 });
7013 let conv = worktree_rules
7014 .map(|r| r.conversion().clone())
7015 .unwrap_or_else(|| crlf::ConversionConfig::from_config(&config));
7016 let legacy_attrs = if worktree_rules.is_none() {
7017 Some(crlf::load_gitattributes_parsed(work_tree))
7018 } else {
7019 None
7020 };
7021 let file_attrs_for = |path: &str| -> crlf::FileAttrs {
7022 if let Some(rules) = worktree_rules {
7023 rules.file_attrs(path, false)
7024 } else if let Some(parsed) = legacy_attrs.as_ref() {
7025 crlf::file_attrs_from_gitattributes(&parsed.rules, &parsed.macros, path, false, &config)
7026 } else {
7027 crlf::FileAttrs::default()
7028 }
7029 };
7030 let filter_fp = worktree_rules.map(|r| r.filter_process());
7031
7032 let tree_flat = match tree_oid {
7034 Some(oid) => flatten_tree(odb, oid, "")?,
7035 None => Vec::new(),
7036 };
7037 let tree_map: std::collections::BTreeMap<String, &FlatEntry> =
7038 tree_flat.iter().map(|e| (e.path.clone(), e)).collect();
7039
7040 let mut index_entries: std::collections::BTreeMap<&[u8], &IndexEntry> =
7042 std::collections::BTreeMap::new();
7043 let mut index_paths: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
7044 let mut stage0_paths: std::collections::BTreeSet<Vec<u8>> = std::collections::BTreeSet::new();
7045 for ie in &index.entries {
7046 if ie.stage() != 0 {
7047 continue;
7048 }
7049 let path = String::from_utf8_lossy(&ie.path).to_string();
7050 index_entries.insert(&ie.path, ie);
7051 index_paths.insert(path);
7052 stage0_paths.insert(ie.path.clone());
7053 }
7054
7055 let mut unmerged_only_paths: std::collections::BTreeSet<String> =
7058 std::collections::BTreeSet::new();
7059 for ie in &index.entries {
7060 if !(1..=3).contains(&ie.stage()) {
7061 continue;
7062 }
7063 if stage0_paths.contains(&ie.path) {
7064 continue;
7065 }
7066 unmerged_only_paths.insert(String::from_utf8_lossy(&ie.path).into_owned());
7067 }
7068
7069 let mut all_paths: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
7071 all_paths.extend(tree_map.keys().cloned());
7072 all_paths.extend(index_paths.iter().cloned());
7073 all_paths.extend(unmerged_only_paths.iter().cloned());
7074
7075 let index_mtime = index_mtime_for_diff(index, None);
7076 let stat_policy = crate::index::StatMatchPolicy::from_config(Some(&config));
7077 let mode_policy = crate::index::WorktreeModePolicy::from_config(&config);
7078 let mut result = Vec::new();
7079
7080 for path in &all_paths {
7081 if index_entries
7082 .get(path.as_bytes())
7083 .is_some_and(|ie| ie.skip_worktree())
7084 {
7085 continue;
7088 }
7089 if index_entries
7090 .get(path.as_bytes())
7091 .is_some_and(|ie| ie.assume_unchanged())
7092 {
7093 continue;
7094 }
7095
7096 let tree_entry = tree_map.get(path.as_str());
7097
7098 let is_gitlink = tree_entry.is_some_and(|te| te.mode == 0o160000)
7100 || index_entries
7101 .get(path.as_bytes())
7102 .is_some_and(|ie| ie.mode == 0o160000);
7103 if is_gitlink {
7104 let sub_dir = work_tree.join(path);
7105 let index_gitlink_oid = index_entries
7106 .get(path.as_bytes())
7107 .filter(|ie| ie.mode == 0o160000)
7108 .map(|ie| ie.oid);
7109 match (tree_entry, index_gitlink_oid) {
7110 (Some(te), _) => {
7111 let sub_head = read_submodule_head_oid(&sub_dir);
7112 if sub_head.is_none() && !sub_dir.exists() {
7116 result.push(DiffEntry {
7117 status: DiffStatus::Deleted,
7118 old_path: Some(path.clone()),
7119 new_path: Some(path.clone()),
7120 old_mode: format_mode(te.mode),
7121 new_mode: "000000".to_string(),
7122 old_oid: te.oid,
7123 new_oid: zero_oid(),
7124 score: None,
7125 });
7126 continue;
7127 }
7128 let ref_matches = if let Some(head) = sub_head {
7129 head == te.oid
7130 } else {
7131 submodule_worktree_is_unpopulated_placeholder(&sub_dir)
7132 };
7133 let index_matches_tree = index_gitlink_oid.is_some_and(|oid| oid == te.oid);
7134 let sub_flags = submodule_porcelain_flags(work_tree, path, te.oid);
7137 let dirty_while_aligned =
7138 index_matches_tree && ref_matches && sub_flags.modified;
7139 if !ref_matches || dirty_while_aligned {
7140 let new_oid = if !ref_matches {
7144 sub_head.unwrap_or_else(zero_oid)
7145 } else {
7146 zero_oid()
7147 };
7148 result.push(DiffEntry {
7149 status: DiffStatus::Modified,
7150 old_path: Some(path.clone()),
7151 new_path: Some(path.clone()),
7152 old_mode: format_mode(te.mode),
7153 new_mode: format_mode(te.mode),
7154 old_oid: te.oid,
7155 new_oid,
7156 score: None,
7157 });
7158 }
7159 }
7160 (None, Some(idx_oid)) => {
7161 let new_oid = read_submodule_head_oid(&sub_dir).unwrap_or(idx_oid);
7166 result.push(DiffEntry {
7167 status: DiffStatus::Added,
7168 old_path: Some(path.clone()),
7169 new_path: Some(path.clone()),
7170 old_mode: "000000".to_string(),
7171 new_mode: format_mode(0o160000),
7172 old_oid: zero_oid(),
7173 new_oid,
7174 score: None,
7175 });
7176 }
7177 (None, None) => {}
7178 }
7179 continue;
7180 }
7181
7182 let file_path = work_tree.join(path);
7183
7184 let wt_meta = match fs::symlink_metadata(&file_path) {
7185 Ok(m) => Some(m),
7186 Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
7187 Err(e) => return Err(Error::Io(e)),
7188 };
7189
7190 if unmerged_only_paths.contains(path) {
7191 if let (Some(te), Some(meta)) = (tree_entry, wt_meta.as_ref()) {
7192 let file_attrs = file_attrs_for(path);
7193 let wt_oid = hash_worktree_file(
7194 odb,
7195 &file_path,
7196 meta,
7197 &conv,
7198 &file_attrs,
7199 path,
7200 None,
7201 filter_fp,
7202 )?;
7203 let wt_mode =
7204 mode_policy.index_mode_for_worktree(mode_from_metadata(meta), Some(te.mode));
7205 if wt_oid != te.oid || wt_mode != te.mode {
7206 result.push(DiffEntry {
7207 status: DiffStatus::for_content_change(te.mode, wt_mode),
7208 old_path: Some(path.clone()),
7209 new_path: Some(path.clone()),
7210 old_mode: format_mode(te.mode),
7211 new_mode: format_mode(wt_mode),
7212 old_oid: te.oid,
7213 new_oid: wt_oid,
7214 score: None,
7215 });
7216 }
7217 }
7218 continue;
7219 }
7220
7221 match (tree_entry, wt_meta) {
7222 (Some(te), Some(ref meta)) => {
7223 let Some(ie) = index_entries.get(path.as_bytes()) else {
7224 continue;
7225 };
7226 let wt_mode =
7227 mode_policy.index_mode_for_worktree(mode_from_metadata(meta), Some(ie.mode));
7228
7229 let index_matches_tree = ie.oid == te.oid && ie.mode == te.mode;
7230
7231 let stat_trusted = stat_matches_with_policy(ie, meta, stat_policy)
7233 && !entry_is_racy_with_policy(ie, index_mtime, stat_policy);
7234 if index_matches_tree && stat_trusted && wt_mode == te.mode {
7235 continue;
7236 }
7237
7238 let file_attrs = file_attrs_for(path);
7239 let idx_ent = index_entries.get(path.as_bytes()).copied();
7240
7241 if ie.oid == te.oid && ie.mode != te.mode && stat_trusted && wt_mode == ie.mode {
7246 result.push(DiffEntry {
7247 status: DiffStatus::for_content_change(te.mode, ie.mode),
7248 old_path: Some(path.clone()),
7249 new_path: Some(path.clone()),
7250 old_mode: format_mode(te.mode),
7251 new_mode: format_mode(ie.mode),
7252 old_oid: te.oid,
7253 new_oid: te.oid,
7254 score: None,
7255 });
7256 continue;
7257 }
7258
7259 if index_matches_tree {
7262 let wt_oid = hash_worktree_file(
7263 odb,
7264 &file_path,
7265 meta,
7266 &conv,
7267 &file_attrs,
7268 path,
7269 None,
7270 filter_fp,
7271 )?;
7272 if wt_oid != te.oid {
7273 result.push(DiffEntry {
7274 status: DiffStatus::for_content_change(te.mode, wt_mode),
7275 old_path: Some(path.clone()),
7276 new_path: Some(path.clone()),
7277 old_mode: format_mode(te.mode),
7278 new_mode: format_mode(wt_mode),
7279 old_oid: te.oid,
7280 new_oid: zero_oid(),
7283 score: None,
7284 });
7285 } else if wt_mode != te.mode {
7286 result.push(DiffEntry {
7287 status: DiffStatus::for_content_change(te.mode, wt_mode),
7288 old_path: Some(path.clone()),
7289 new_path: Some(path.clone()),
7290 old_mode: format_mode(te.mode),
7291 new_mode: format_mode(wt_mode),
7292 old_oid: te.oid,
7293 new_oid: te.oid,
7294 score: None,
7295 });
7296 }
7297 continue;
7298 }
7299
7300 let wt_oid = hash_worktree_file(
7302 odb,
7303 &file_path,
7304 meta,
7305 &conv,
7306 &file_attrs,
7307 path,
7308 idx_ent,
7309 filter_fp,
7310 )?;
7311 if wt_oid != te.oid || wt_mode != te.mode {
7312 let new_oid = if wt_oid != te.oid { zero_oid() } else { te.oid };
7313 result.push(DiffEntry {
7314 status: DiffStatus::for_content_change(te.mode, wt_mode),
7315 old_path: Some(path.clone()),
7316 new_path: Some(path.clone()),
7317 old_mode: format_mode(te.mode),
7318 new_mode: format_mode(wt_mode),
7319 old_oid: te.oid,
7320 new_oid,
7321 score: None,
7322 });
7323 }
7324 }
7325 (Some(te), None) => {
7326 result.push(DiffEntry {
7328 status: DiffStatus::Deleted,
7329 old_path: Some(path.clone()),
7330 new_path: None,
7331 old_mode: format_mode(te.mode),
7332 new_mode: "000000".to_owned(),
7333 old_oid: te.oid,
7334 new_oid: zero_oid(),
7335 score: None,
7336 });
7337 }
7338 (None, Some(ref meta)) => {
7339 let file_attrs = file_attrs_for(path);
7341 let idx_ent = index_entries.get(path.as_bytes()).copied();
7342 let wt_oid = materialize_worktree_blob(
7343 odb,
7344 &file_path,
7345 meta,
7346 &conv,
7347 &file_attrs,
7348 path,
7349 idx_ent,
7350 filter_fp,
7351 )?;
7352 let wt_mode = mode_from_metadata(meta);
7353 result.push(DiffEntry {
7354 status: DiffStatus::Added,
7355 old_path: None,
7356 new_path: Some(path.clone()),
7357 old_mode: "000000".to_owned(),
7358 new_mode: format_mode(wt_mode),
7359 old_oid: zero_oid(),
7360 new_oid: wt_oid,
7361 score: None,
7362 });
7363 }
7364 (None, None) => {
7365 }
7367 }
7368 }
7369
7370 result.sort_by(|a, b| a.path().cmp(b.path()));
7371 Ok(result)
7372}
7373
7374fn read_added_entry_bytes(
7377 odb: &Odb,
7378 entry: &DiffEntry,
7379 work_root: Option<&Path>,
7380) -> Option<Vec<u8>> {
7381 if entry.new_oid != zero_oid() {
7382 if let Ok(obj) = odb.read(&entry.new_oid) {
7383 return Some(obj.data);
7384 }
7385 }
7386 let path = entry.new_path.as_deref()?;
7387 let root = work_root?;
7388 fs::read(root.join(path)).ok()
7389}
7390
7391fn modified_as_copy_from_sources(
7392 odb: &Odb,
7393 work_root: Option<&Path>,
7394 e: &DiffEntry,
7395 threshold: u32,
7396 sources: &[(String, ObjectId, bool)],
7397 source_contents: &[Option<Vec<u8>>],
7398 source_tree_entries: &[(String, String, ObjectId)],
7399) -> Result<Option<DiffEntry>> {
7400 fn regular_file_mode(mode: &str) -> bool {
7401 mode == "100644" || mode == "100755"
7402 }
7403
7404 if e.status != DiffStatus::Modified || !regular_file_mode(&e.new_mode) {
7405 return Ok(None);
7406 }
7407 let Some(new_data) = read_added_entry_bytes(odb, e, work_root) else {
7408 return Ok(None);
7409 };
7410 let new_oid_eff = if e.new_oid != zero_oid() {
7411 e.new_oid
7412 } else {
7413 odb.hash(ObjectKind::Blob, &new_data)?
7414 };
7415
7416 let mut best: Option<(usize, u32)> = None;
7417 for (si, (src_path, src_oid, is_deleted)) in sources.iter().enumerate() {
7418 if *is_deleted {
7419 continue;
7420 }
7421 if e.new_path.as_deref() == Some(src_path.as_str()) {
7422 continue;
7423 }
7424 let src_mode_str = source_tree_entries
7425 .iter()
7426 .find(|(p, _, _)| p == src_path)
7427 .map(|(_, m, _)| m.as_str())
7428 .unwrap_or("100644");
7429 if !regular_file_mode(src_mode_str) {
7430 continue;
7431 }
7432
7433 let score = if *src_oid == new_oid_eff {
7434 100
7435 } else {
7436 match (&source_contents[si], Some(new_data.as_slice())) {
7437 (Some(old_data), Some(nd)) => compute_similarity(old_data, nd),
7438 _ => 0,
7439 }
7440 };
7441 if score >= threshold {
7442 let replace = match best {
7443 None => true,
7444 Some((_, s)) => score > s,
7445 };
7446 if replace {
7447 best = Some((si, score));
7448 }
7449 }
7450 }
7451
7452 let Some((si, score)) = best else {
7453 return Ok(None);
7454 };
7455 let (src_path, src_oid, _) = &sources[si];
7456 let src_mode = source_tree_entries
7457 .iter()
7458 .find(|(p, _, _)| p == src_path)
7459 .map(|(_, m, _)| m.clone())
7460 .unwrap_or_else(|| e.old_mode.clone());
7461
7462 Ok(Some(DiffEntry {
7463 status: DiffStatus::Copied,
7464 old_path: Some(src_path.clone()),
7465 new_path: e.new_path.clone(),
7466 old_mode: src_mode,
7467 new_mode: e.new_mode.clone(),
7468 old_oid: *src_oid,
7469 new_oid: e.new_oid,
7470 score: Some(score),
7471 }))
7472}
7473
7474#[allow(clippy::expect_used)]
7486pub fn detect_renames(
7487 odb: &Odb,
7488 work_root: Option<&Path>,
7489 entries: Vec<DiffEntry>,
7490 threshold: u32,
7491) -> Vec<DiffEntry> {
7492 let config = ConfigSet::new();
7493 let policy = ThreadPolicy::default();
7494 detect_renames_with_policy(odb, work_root, entries, threshold, &config, &policy, None)
7495 .expect("rename detection")
7496}
7497
7498#[inline]
7499fn poll_rename_cancel(
7500 cancel_watch: Option<&std::sync::atomic::AtomicBool>,
7501) -> crate::error::Result<()> {
7502 if let Some(watch) = cancel_watch {
7503 crate::progress::poll_watch(watch)?;
7504 }
7505 Ok(())
7506}
7507
7508pub fn detect_renames_with_policy(
7510 odb: &Odb,
7511 work_root: Option<&Path>,
7512 entries: Vec<DiffEntry>,
7513 threshold: u32,
7514 config: &ConfigSet,
7515 thread_policy: &ThreadPolicy,
7516 cancel_watch: Option<&std::sync::atomic::AtomicBool>,
7517) -> Result<Vec<DiffEntry>> {
7518 poll_rename_cancel(cancel_watch)?;
7519 let mut deleted: Vec<DiffEntry> = Vec::new();
7521 let mut added: Vec<DiffEntry> = Vec::new();
7522 let mut others: Vec<DiffEntry> = Vec::new();
7523
7524 for entry in entries {
7525 match entry.status {
7526 DiffStatus::Deleted => deleted.push(entry),
7527 DiffStatus::Added => added.push(entry),
7528 _ => others.push(entry),
7529 }
7530 }
7531
7532 if deleted.is_empty() || added.is_empty() {
7533 let mut result = others;
7535 result.extend(deleted);
7536 result.extend(added);
7537 if !entries_sorted_by_path(&result) {
7538 result.sort_by(|a, b| a.path().cmp(b.path()));
7539 }
7540 return Ok(result);
7541 }
7542
7543 fn is_regularish_mode(mode: &str) -> bool {
7544 mode == "100644" || mode == "100755"
7545 }
7546
7547 fn same_path_same_blob(del: &DiffEntry, add: &DiffEntry) -> bool {
7548 del.old_path == add.new_path && del.old_oid == add.new_oid && del.old_mode == add.new_mode
7549 }
7550
7551 fn sort_rename_scores(
7552 scores: &mut [(u32, usize, usize)],
7553 deleted: &[DiffEntry],
7554 added: &[DiffEntry],
7555 ) {
7556 scores.sort_by(|a, b| {
7557 let a_noop = same_path_same_blob(&deleted[a.1], &added[a.2]);
7558 let b_noop = same_path_same_blob(&deleted[b.1], &added[b.2]);
7559 let a_same = same_basename(&deleted[a.1], &added[a.2]);
7560 let b_same = same_basename(&deleted[b.1], &added[b.2]);
7561 a_noop
7562 .cmp(&b_noop)
7563 .then_with(|| b_same.cmp(&a_same))
7564 .then_with(|| b.0.cmp(&a.0))
7565 });
7566 }
7567
7568 fn apply_rename_scores(
7569 scores: &[(u32, usize, usize)],
7570 deleted: &[DiffEntry],
7571 added: &[DiffEntry],
7572 used_deleted: &mut [bool],
7573 used_added: &mut [bool],
7574 renames: &mut Vec<DiffEntry>,
7575 ) {
7576 for (score, di, ai) in scores {
7577 if used_deleted[*di] || used_added[*ai] {
7578 continue;
7579 }
7580 used_deleted[*di] = true;
7581 used_added[*ai] = true;
7582
7583 let del = &deleted[*di];
7584 let add = &added[*ai];
7585 if same_path_same_blob(del, add) {
7586 continue;
7587 }
7588
7589 renames.push(DiffEntry {
7590 status: DiffStatus::Renamed,
7591 old_path: del.old_path.clone(),
7592 new_path: add.new_path.clone(),
7593 old_mode: del.old_mode.clone(),
7594 new_mode: add.new_mode.clone(),
7595 old_oid: del.old_oid,
7596 new_oid: add.new_oid,
7597 score: Some(*score),
7598 });
7599 }
7600 }
7601
7602 let mut used_deleted = vec![false; deleted.len()];
7603 let mut used_added = vec![false; added.len()];
7604 let mut renames: Vec<DiffEntry> = Vec::new();
7605
7606 let mut exact_scores: Vec<(u32, usize, usize)> = Vec::new();
7608 for (di, del) in deleted.iter().enumerate() {
7609 poll_rename_cancel(cancel_watch)?;
7610 for (ai, add) in added.iter().enumerate() {
7611 if del.old_oid == add.new_oid {
7612 exact_scores.push((100, di, ai));
7613 }
7614 }
7615 }
7616 sort_rename_scores(&mut exact_scores, &deleted, &added);
7617 apply_rename_scores(
7618 &exact_scores,
7619 &deleted,
7620 &added,
7621 &mut used_deleted,
7622 &mut used_added,
7623 &mut renames,
7624 );
7625
7626 let remaining_deleted: Vec<usize> = used_deleted
7627 .iter()
7628 .enumerate()
7629 .filter_map(|(i, used)| (!*used).then_some(i))
7630 .collect();
7631 let remaining_added: Vec<usize> = used_added
7632 .iter()
7633 .enumerate()
7634 .filter_map(|(i, used)| (!*used).then_some(i))
7635 .collect();
7636
7637 let rename_limit = parse_rename_limit(config);
7638 if !remaining_deleted.is_empty()
7639 && !remaining_added.is_empty()
7640 && !inexact_matrix_exceeds_limit(
7641 remaining_added.len(),
7642 remaining_deleted.len(),
7643 rename_limit,
7644 )
7645 {
7646 let par_opts = diff_rename_parallel_options(config, thread_policy);
7647
7648 let deleted_sizes: Vec<Option<usize>> = remaining_deleted
7649 .iter()
7650 .map(|&di| blob_payload_size(odb, &deleted[di].old_oid))
7651 .collect();
7652 let added_sizes: Vec<Option<usize>> = remaining_added
7653 .iter()
7654 .map(|&ai| added_entry_payload_size(odb, &added[ai], work_root))
7655 .collect();
7656
7657 let mut load_deleted = vec![false; remaining_deleted.len()];
7658 let mut load_added = vec![false; remaining_added.len()];
7659 let mut candidate_pairs: Vec<(usize, usize)> = Vec::new();
7660 for (row, &di) in remaining_deleted.iter().enumerate() {
7661 poll_rename_cancel(cancel_watch)?;
7662 let del = &deleted[di];
7663 if !is_regularish_mode(&del.old_mode) {
7664 continue;
7665 }
7666 let Some(ds) = deleted_sizes[row] else {
7667 continue;
7668 };
7669 for (col, &ai) in remaining_added.iter().enumerate() {
7670 let add = &added[ai];
7671 if !is_regularish_mode(&add.new_mode) {
7672 continue;
7673 }
7674 let Some(asz) = added_sizes[col] else {
7675 continue;
7676 };
7677 if rename_pair_size_reachable(ds, asz, threshold) {
7678 load_deleted[row] = true;
7679 load_added[col] = true;
7680 candidate_pairs.push((row, col));
7681 }
7682 }
7683 }
7684
7685 if !candidate_pairs.is_empty() {
7686 let loads_needed = load_deleted.iter().filter(|&&b| b).count()
7687 + load_added.iter().filter(|&&b| b).count();
7688 let load_deleted_blob = |i: usize| -> Option<Vec<u8>> {
7689 if !load_deleted[i] {
7690 return None;
7691 }
7692 odb.read(&deleted[remaining_deleted[i]].old_oid)
7693 .ok()
7694 .map(|obj| normalize_bytes_for_rename(obj.data))
7695 };
7696 let load_added_blob = |i: usize| -> Option<Vec<u8>> {
7697 if !load_added[i] {
7698 return None;
7699 }
7700 read_added_entry_bytes(odb, &added[remaining_added[i]], work_root)
7701 .map(normalize_bytes_for_rename)
7702 };
7703 let (deleted_contents, added_contents) = if loads_needed <= 32 {
7704 (
7705 (0..remaining_deleted.len())
7706 .map(load_deleted_blob)
7707 .collect(),
7708 (0..remaining_added.len()).map(load_added_blob).collect(),
7709 )
7710 } else {
7711 (
7712 parallel_load_optional_blobs(
7713 odb,
7714 remaining_deleted.len(),
7715 par_opts,
7716 0,
7717 load_deleted_blob,
7718 ),
7719 parallel_load_optional_blobs(
7720 odb,
7721 remaining_added.len(),
7722 par_opts,
7723 0,
7724 load_added_blob,
7725 ),
7726 )
7727 };
7728
7729 let est_table_bytes: usize = deleted_contents
7730 .iter()
7731 .chain(added_contents.iter())
7732 .filter_map(|opt| opt.as_ref().map(|b| b.len()))
7733 .sum();
7734 let build_deleted_table = |row: usize| -> std::result::Result<
7735 Option<RenameLineTable>,
7736 std::convert::Infallible,
7737 > {
7738 Ok(deleted_contents[row]
7739 .as_ref()
7740 .map(|d| RenameLineTable::from_normalized_blob(d)))
7741 };
7742 let build_added_table = |col: usize| -> std::result::Result<
7743 Option<RenameLineTable>,
7744 std::convert::Infallible,
7745 > {
7746 Ok(added_contents[col]
7747 .as_ref()
7748 .map(|d| RenameLineTable::from_normalized_blob(d)))
7749 };
7750 let deleted_tables: Vec<Option<RenameLineTable>> = if loads_needed <= 32 {
7751 (0..remaining_deleted.len())
7752 .map(|row| build_deleted_table(row).unwrap())
7753 .collect()
7754 } else {
7755 parallel_map_indices(
7756 odb,
7757 remaining_deleted.len(),
7758 par_opts,
7759 est_table_bytes,
7760 build_deleted_table,
7761 )
7762 .expect("deleted rename line tables")
7763 };
7764 let added_tables: Vec<Option<RenameLineTable>> = if loads_needed <= 32 {
7765 (0..remaining_added.len())
7766 .map(|col| build_added_table(col).unwrap())
7767 .collect()
7768 } else {
7769 parallel_map_indices(
7770 odb,
7771 remaining_added.len(),
7772 par_opts,
7773 est_table_bytes,
7774 build_added_table,
7775 )
7776 .expect("added rename line tables")
7777 };
7778
7779 let mut inexact_scores: Vec<(u32, usize, usize)> = Vec::new();
7780 for (row, col) in candidate_pairs {
7781 poll_rename_cancel(cancel_watch)?;
7782 let di = remaining_deleted[row];
7783 let ai = remaining_added[col];
7784 let (Some(old_table), Some(new_table)) = (&deleted_tables[row], &added_tables[col])
7785 else {
7786 continue;
7787 };
7788 let score = RenameLineTable::similarity_score(old_table, new_table);
7789 if score >= threshold {
7790 inexact_scores.push((score, di, ai));
7791 }
7792 }
7793 sort_rename_scores(&mut inexact_scores, &deleted, &added);
7794 apply_rename_scores(
7795 &inexact_scores,
7796 &deleted,
7797 &added,
7798 &mut used_deleted,
7799 &mut used_added,
7800 &mut renames,
7801 );
7802 }
7803 }
7804
7805 let mut result = others;
7807 result.extend(renames);
7808 for (i, entry) in deleted.into_iter().enumerate() {
7809 if !used_deleted[i] {
7810 result.push(entry);
7811 }
7812 }
7813 for (i, entry) in added.into_iter().enumerate() {
7814 if !used_added[i] {
7815 result.push(entry);
7816 }
7817 }
7818
7819 result = prune_break_deletions_obviated_by_renames(result);
7820 result.sort_by(|a, b| a.path().cmp(b.path()));
7821 Ok(result)
7822}
7823
7824pub fn detect_copies(
7839 odb: &Odb,
7840 work_root: Option<&Path>,
7841 entries: Vec<DiffEntry>,
7842 threshold: u32,
7843 find_copies_harder: bool,
7844 source_tree_entries: &[(String, String, ObjectId)],
7845) -> Result<Vec<DiffEntry>> {
7846 let config = ConfigSet::new();
7847 let policy = ThreadPolicy::default();
7848 detect_copies_with_policy(
7849 odb,
7850 work_root,
7851 entries,
7852 threshold,
7853 find_copies_harder,
7854 source_tree_entries,
7855 &config,
7856 &policy,
7857 )
7858}
7859
7860#[expect(clippy::too_many_arguments)]
7866pub fn detect_copies_with_policy(
7867 odb: &Odb,
7868 work_root: Option<&Path>,
7869 entries: Vec<DiffEntry>,
7870 threshold: u32,
7871 find_copies_harder: bool,
7872 source_tree_entries: &[(String, String, ObjectId)],
7873 config: &ConfigSet,
7874 thread_policy: &ThreadPolicy,
7875) -> Result<Vec<DiffEntry>> {
7876 use std::collections::{HashMap, HashSet};
7877
7878 let mut deleted: Vec<DiffEntry> = Vec::new();
7880 let mut added: Vec<DiffEntry> = Vec::new();
7881 let mut others: Vec<DiffEntry> = Vec::new();
7882
7883 for entry in entries {
7884 match entry.status {
7885 DiffStatus::Deleted => deleted.push(entry),
7886 DiffStatus::Added => added.push(entry),
7887 _ => others.push(entry),
7888 }
7889 }
7890
7891 let mut sources: Vec<(String, ObjectId, bool)> = Vec::new(); let mut deleted_source_idx: HashMap<String, usize> = HashMap::new();
7895
7896 for entry in &deleted {
7897 if let Some(ref path) = entry.old_path {
7898 deleted_source_idx.insert(path.clone(), sources.len());
7899 sources.push((path.clone(), entry.old_oid, true));
7900 }
7901 }
7902
7903 for entry in &others {
7906 if matches!(entry.status, DiffStatus::Modified | DiffStatus::TypeChanged) {
7907 if let Some(ref old_path) = entry.old_path {
7908 if !sources.iter().any(|(p, _, _)| p == old_path) {
7909 sources.push((old_path.clone(), entry.old_oid, false));
7910 }
7911 }
7912 }
7913 }
7914
7915 if find_copies_harder {
7917 for (path, _mode, oid) in source_tree_entries {
7918 if !sources.iter().any(|(p, _, _)| p == path) {
7919 sources.push((path.clone(), *oid, false));
7920 }
7921 }
7922 }
7923
7924 if sources.is_empty() {
7925 let mut result = others;
7926 result.extend(deleted);
7927 result.extend(added);
7928 result.sort_by(|a, b| a.path().cmp(b.path()));
7929 return Ok(result);
7930 }
7931
7932 let par_opts = diff_rename_parallel_options(config, thread_policy);
7933 let rename_limit = parse_rename_limit(config);
7934
7935 fn apply_copy_score_assignments(
7936 scores: &[(u32, usize, usize)],
7937 sources: &[(String, ObjectId, bool)],
7938 added: &[DiffEntry],
7939 source_tree_entries: &[(String, String, ObjectId)],
7940 used_added2: &mut [bool],
7941 renamed_deleted: &mut HashSet<usize>,
7942 result_entries: &mut Vec<DiffEntry>,
7943 ) {
7944 let mut used_added = vec![false; added.len()];
7945 let mut source_to_added: HashMap<usize, Vec<(usize, u32)>> = HashMap::new();
7946 for &(score, si, ai) in scores {
7947 if used_added[ai] {
7948 continue;
7949 }
7950 used_added[ai] = true;
7951 source_to_added.entry(si).or_default().push((ai, score));
7952 }
7953
7954 for (&si, assignments_for_src) in &source_to_added {
7955 let (_, _, is_deleted) = &sources[si];
7956 if *is_deleted && !assignments_for_src.is_empty() {
7957 let rename_ai = assignments_for_src
7958 .iter()
7959 .max_by_key(|(ai, _score)| added[*ai].path().to_string())
7960 .map(|(ai, _)| *ai);
7961
7962 for &(ai, score) in assignments_for_src {
7963 let (ref src_path, _, _) = sources[si];
7964 let add = &added[ai];
7965 let src_mode = source_tree_entries
7966 .iter()
7967 .find(|(p, _, _)| p == src_path)
7968 .map(|(_, m, _)| m.clone())
7969 .unwrap_or_else(|| add.old_mode.clone());
7970
7971 let is_rename = Some(ai) == rename_ai;
7972 result_entries.push(DiffEntry {
7973 status: if is_rename {
7974 DiffStatus::Renamed
7975 } else {
7976 DiffStatus::Copied
7977 },
7978 old_path: Some(src_path.clone()),
7979 new_path: add.new_path.clone(),
7980 old_mode: src_mode,
7981 new_mode: add.new_mode.clone(),
7982 old_oid: sources[si].1,
7983 new_oid: add.new_oid,
7984 score: Some(score),
7985 });
7986 used_added2[ai] = true;
7987 }
7988 renamed_deleted.insert(si);
7989 } else {
7990 for &(ai, score) in assignments_for_src {
7991 let (ref src_path, _, _) = sources[si];
7992 let add = &added[ai];
7993 let src_mode = source_tree_entries
7994 .iter()
7995 .find(|(p, _, _)| p == src_path)
7996 .map(|(_, m, _)| m.clone())
7997 .unwrap_or_else(|| add.old_mode.clone());
7998
7999 result_entries.push(DiffEntry {
8000 status: DiffStatus::Copied,
8001 old_path: Some(src_path.clone()),
8002 new_path: add.new_path.clone(),
8003 old_mode: src_mode,
8004 new_mode: add.new_mode.clone(),
8005 old_oid: sources[si].1,
8006 new_oid: add.new_oid,
8007 score: Some(score),
8008 });
8009 used_added2[ai] = true;
8010 }
8011 }
8012 }
8013 }
8014
8015 fn added_entry_oid(
8016 odb: &Odb,
8017 add: &DiffEntry,
8018 added_contents: &[Option<Vec<u8>>],
8019 ai: usize,
8020 ) -> Result<ObjectId> {
8021 if add.new_oid != zero_oid() {
8022 Ok(add.new_oid)
8023 } else if let Some(ref data) = added_contents[ai] {
8024 odb.hash(ObjectKind::Blob, data)
8025 } else {
8026 Ok(zero_oid())
8027 }
8028 }
8029
8030 let mut result_entries: Vec<DiffEntry> = Vec::new();
8031 let mut renamed_deleted: HashSet<usize> = HashSet::new();
8032 let mut used_added2 = vec![false; added.len()];
8033 let mut source_contents = vec![None; sources.len()];
8034
8035 if !added.is_empty() {
8036 let added_contents = parallel_load_optional_blobs(odb, added.len(), par_opts, 0, |ai| {
8037 read_added_entry_bytes(odb, &added[ai], work_root)
8038 });
8039
8040 let mut exact_scores: Vec<(u32, usize, usize)> = Vec::new();
8042 for (si, (src_path, src_oid, _)) in sources.iter().enumerate() {
8043 for (ai, add) in added.iter().enumerate() {
8044 if add.new_path.as_deref() == Some(src_path.as_str()) {
8045 continue;
8046 }
8047 let add_oid = added_entry_oid(odb, add, &added_contents, ai)?;
8048 if *src_oid == add_oid && add_oid != zero_oid() {
8049 exact_scores.push((100, si, ai));
8050 }
8051 }
8052 }
8053 exact_scores.sort_by_key(|a| std::cmp::Reverse(a.0));
8054 apply_copy_score_assignments(
8055 &exact_scores,
8056 &sources,
8057 &added,
8058 source_tree_entries,
8059 &mut used_added2,
8060 &mut renamed_deleted,
8061 &mut result_entries,
8062 );
8063
8064 let remaining_added: Vec<usize> = used_added2
8065 .iter()
8066 .enumerate()
8067 .filter_map(|(i, used)| (!*used).then_some(i))
8068 .collect();
8069 let remaining_source_count = sources
8070 .iter()
8071 .enumerate()
8072 .filter(|(si, (_, _, is_deleted))| !*is_deleted || !renamed_deleted.contains(si))
8073 .count();
8074
8075 let skip_inexact = remaining_added.is_empty()
8076 || inexact_matrix_exceeds_limit(
8077 remaining_added.len(),
8078 remaining_source_count,
8079 rename_limit,
8080 );
8081
8082 if !skip_inexact {
8083 source_contents = parallel_load_optional_blobs(odb, sources.len(), par_opts, 0, |si| {
8084 odb.read(&sources[si].1).ok().map(|obj| obj.data)
8085 });
8086
8087 let est_bytes: usize = source_contents
8088 .iter()
8089 .chain(added_contents.iter())
8090 .filter_map(|opt| opt.as_ref().map(|b| b.len()))
8091 .sum();
8092
8093 let source_tables: Vec<Option<RenameLineTable>> = parallel_map_indices(
8094 odb,
8095 sources.len(),
8096 par_opts,
8097 est_bytes,
8098 |si| -> std::result::Result<Option<RenameLineTable>, std::convert::Infallible> {
8099 Ok(source_contents[si]
8100 .as_ref()
8101 .map(|d| RenameLineTable::from_similarity_blob(d)))
8102 },
8103 )
8104 .expect("copy source line tables");
8105 let added_tables: Vec<Option<RenameLineTable>> = parallel_map_indices(
8106 odb,
8107 added.len(),
8108 par_opts,
8109 est_bytes,
8110 |ai| -> std::result::Result<Option<RenameLineTable>, std::convert::Infallible> {
8111 Ok(added_contents[ai]
8112 .as_ref()
8113 .map(|d| RenameLineTable::from_similarity_blob(d)))
8114 },
8115 )
8116 .expect("copy added line tables");
8117
8118 let score_rows: Vec<Vec<(u32, usize, usize)>> = parallel_map_indices(
8119 odb,
8120 sources.len(),
8121 par_opts,
8122 est_bytes,
8123 |si| -> std::result::Result<Vec<(u32, usize, usize)>, std::convert::Infallible> {
8124 if renamed_deleted.contains(&si) {
8125 return Ok(Vec::new());
8126 }
8127 let (src_path, _, _) = &sources[si];
8128 let mut row: Vec<(u32, usize, usize)> = Vec::new();
8129 for &ai in &remaining_added {
8130 let add = &added[ai];
8131 if add.new_path.as_deref() == Some(src_path.as_str()) {
8132 continue;
8133 }
8134 let score = match (&source_tables[si], &added_tables[ai]) {
8135 (Some(old_table), Some(new_table)) => {
8136 RenameLineTable::similarity_score(old_table, new_table)
8137 }
8138 _ => 0,
8139 };
8140 if score >= threshold {
8141 row.push((score, si, ai));
8142 }
8143 }
8144 Ok(row)
8145 },
8146 )
8147 .expect("copy score rows");
8148 let mut inexact_scores: Vec<(u32, usize, usize)> =
8149 score_rows.into_iter().flatten().collect();
8150 inexact_scores.sort_by_key(|a| std::cmp::Reverse(a.0));
8151 apply_copy_score_assignments(
8152 &inexact_scores,
8153 &sources,
8154 &added,
8155 source_tree_entries,
8156 &mut used_added2,
8157 &mut renamed_deleted,
8158 &mut result_entries,
8159 );
8160 }
8161 }
8162
8163 for entry in deleted.into_iter() {
8165 if let Some(ref path) = entry.old_path {
8166 if let Some(&si) = deleted_source_idx.get(path) {
8167 if renamed_deleted.contains(&si) {
8168 continue;
8170 }
8171 }
8172 }
8173 result_entries.push(entry);
8174 }
8175
8176 let mut result = others;
8177 result.extend(result_entries);
8178 for (i, entry) in added.into_iter().enumerate() {
8180 if !used_added2[i] {
8181 result.push(entry);
8182 }
8183 }
8184
8185 let mut final_result = Vec::with_capacity(result.len());
8186 for e in result {
8187 if let Some(c) = modified_as_copy_from_sources(
8188 odb,
8189 work_root,
8190 &e,
8191 threshold,
8192 &sources,
8193 &source_contents,
8194 source_tree_entries,
8195 )? {
8196 final_result.push(c);
8197 } else {
8198 final_result.push(e);
8199 }
8200 }
8201
8202 final_result.sort_by(|a, b| a.path().cmp(b.path()));
8203 Ok(final_result)
8204}
8205
8206#[derive(Debug, Clone, Copy)]
8208pub struct DiffPostprocessOptions {
8209 pub break_score: Option<u64>,
8212 pub rename_threshold: u32,
8214 pub copy_threshold: Option<u32>,
8216 pub find_copies_harder: bool,
8218 pub materialize_break_worktree_new_oid: bool,
8221}
8222
8223impl Default for DiffPostprocessOptions {
8224 fn default() -> Self {
8225 Self {
8226 break_score: None,
8227 rename_threshold: 100,
8228 copy_threshold: None,
8229 find_copies_harder: false,
8230 materialize_break_worktree_new_oid: false,
8231 }
8232 }
8233}
8234
8235#[derive(Debug, Clone, Copy)]
8238pub struct DiffPostprocessWorktree<'a> {
8239 pub repository_git_dir: &'a Path,
8241 pub index: &'a Index,
8243}
8244
8245fn blob_bytes(odb: &Odb, oid: &ObjectId) -> Result<Vec<u8>> {
8246 Ok(odb.read(oid)?.data)
8247}
8248
8249fn read_new_side_bytes(
8254 odb: &Odb,
8255 entry: &DiffEntry,
8256 work_root: Option<&Path>,
8257 worktree: Option<&DiffPostprocessWorktree<'_>>,
8258 config: &ConfigSet,
8259) -> Result<Vec<u8>> {
8260 if entry.new_oid != zero_oid() {
8261 return blob_bytes(odb, &entry.new_oid);
8262 }
8263 let path = entry.new_path.as_deref().ok_or_else(|| {
8264 Error::ObjectNotFound("modified entry missing new_path for worktree read".into())
8265 })?;
8266 let root = work_root.ok_or_else(|| {
8267 Error::ObjectNotFound(format!(
8268 "worktree bytes required for {} but no work_root was provided",
8269 path
8270 ))
8271 })?;
8272 if let Some(wctx) = worktree {
8273 let mode = u32::from_str_radix(&entry.new_mode, 8).unwrap_or(crate::index::MODE_REGULAR);
8274 let index_entry = wctx.index.get(path.as_bytes(), 0);
8275 return read_worktree_side_bytes_for_diff(odb, root, path, mode, config, index_entry, None);
8276 }
8277 fs::read(root.join(path)).map_err(Error::Io)
8278}
8279
8280fn worktree_side_differs_after_normalize(
8282 odb: &Odb,
8283 entry: &DiffEntry,
8284 work_root: &Path,
8285 worktree: &DiffPostprocessWorktree<'_>,
8286 config: &ConfigSet,
8287) -> Result<bool> {
8288 let path = entry.new_path.as_deref().ok_or_else(|| {
8289 Error::ObjectNotFound("modified entry missing new_path for worktree read".into())
8290 })?;
8291 let raw = fs::read(work_root.join(path)).map_err(Error::Io)?;
8292 let canonical = read_new_side_bytes(odb, entry, Some(work_root), Some(worktree), config)?;
8293 Ok(raw != canonical)
8294}
8295
8296fn split_broken_modifications(
8297 odb: &Odb,
8298 work_root: Option<&Path>,
8299 entries: Vec<DiffEntry>,
8300 break_score: u64,
8301 worktree: Option<&DiffPostprocessWorktree<'_>>,
8302 config: &ConfigSet,
8303) -> Result<Vec<DiffEntry>> {
8304 let mut out = Vec::with_capacity(entries.len());
8305 for e in entries {
8306 if e.status != DiffStatus::Modified {
8307 out.push(e);
8308 continue;
8309 }
8310 let old = blob_bytes(odb, &e.old_oid)?;
8311 let new = read_new_side_bytes(odb, &e, work_root, worktree, config)?;
8312 if should_break_rewrite_pair(&old, &new, break_score) {
8313 let path = e.path().to_owned();
8314 out.push(DiffEntry {
8315 status: DiffStatus::Deleted,
8316 old_path: Some(path.clone()),
8317 new_path: None,
8318 old_mode: e.old_mode.clone(),
8319 new_mode: "000000".to_owned(),
8320 old_oid: e.old_oid,
8321 new_oid: zero_oid(),
8322 score: None,
8323 });
8324 out.push(DiffEntry {
8325 status: DiffStatus::Added,
8326 old_path: None,
8327 new_path: Some(path),
8328 old_mode: "000000".to_owned(),
8329 new_mode: e.new_mode.clone(),
8330 old_oid: zero_oid(),
8331 new_oid: e.new_oid,
8332 score: None,
8333 });
8334 } else {
8335 out.push(e);
8336 }
8337 }
8338 Ok(out)
8339}
8340
8341fn annotate_broken_modifications(
8342 odb: &Odb,
8343 work_root: Option<&Path>,
8344 entries: Vec<DiffEntry>,
8345 break_score: u64,
8346 worktree: Option<&DiffPostprocessWorktree<'_>>,
8347 config: &ConfigSet,
8348 materialize_worktree_oid: bool,
8349) -> Result<Vec<DiffEntry>> {
8350 let mut out = Vec::with_capacity(entries.len());
8351 for mut e in entries {
8352 if e.status == DiffStatus::Modified {
8353 let old = blob_bytes(odb, &e.old_oid)?;
8354 let new = read_new_side_bytes(odb, &e, work_root, worktree, config)?;
8355 if should_break_rewrite_pair(&old, &new, break_score) {
8356 let show_score = if e.new_oid != zero_oid() {
8360 true
8361 } else if materialize_worktree_oid {
8362 match (work_root, worktree) {
8363 (Some(root), Some(wctx)) => {
8364 worktree_side_differs_after_normalize(odb, &e, root, wctx, config)?
8365 }
8366 _ => false,
8367 }
8368 } else {
8369 false
8370 };
8371 if show_score {
8372 e.score = rewrite_dissimilarity_index_percent(&old, &new);
8373 }
8374 }
8375 }
8376 out.push(e);
8377 }
8378 Ok(out)
8379}
8380
8381fn materialize_break_worktree_new_oids(
8382 odb: &Odb,
8383 work_root: Option<&Path>,
8384 entries: &mut [DiffEntry],
8385 worktree: Option<&DiffPostprocessWorktree<'_>>,
8386 config: &ConfigSet,
8387) -> Result<()> {
8388 for e in entries {
8389 if e.new_oid != zero_oid() {
8390 continue;
8391 }
8392 if !matches!(e.status, DiffStatus::Modified | DiffStatus::Added) {
8393 continue;
8394 }
8395 let bytes = read_new_side_bytes(odb, e, work_root, worktree, config)?;
8396 e.new_oid = odb.hash(ObjectKind::Blob, &bytes)?;
8397 }
8398 Ok(())
8399}
8400
8401fn prune_break_deletions_obviated_by_renames(entries: Vec<DiffEntry>) -> Vec<DiffEntry> {
8404 use std::collections::HashSet;
8405 let rename_targets: HashSet<String> = entries
8406 .iter()
8407 .filter(|e| e.status == DiffStatus::Renamed)
8408 .filter_map(|e| e.new_path.clone())
8409 .collect();
8410 if rename_targets.is_empty() {
8411 return entries;
8412 }
8413 entries
8414 .into_iter()
8415 .filter(|e| {
8416 if e.status != DiffStatus::Deleted {
8417 return true;
8418 }
8419 e.old_path
8420 .as_ref()
8421 .is_none_or(|p| !rename_targets.contains(p))
8422 })
8423 .collect()
8424}
8425
8426#[allow(clippy::too_many_arguments)]
8436pub fn postprocess_diff_entries(
8437 odb: &Odb,
8438 entries: Vec<DiffEntry>,
8439 source_tree_oid: Option<&ObjectId>,
8440 work_root: Option<&Path>,
8441 worktree: Option<&DiffPostprocessWorktree<'_>>,
8442 config: &ConfigSet,
8443 thread_policy: &ThreadPolicy,
8444 opts: &DiffPostprocessOptions,
8445) -> Result<Vec<DiffEntry>> {
8446 let mut entries = entries;
8447 if let Some(break_score) = opts.break_score {
8448 let rename_or_copy = opts.rename_threshold < 100 || opts.copy_threshold.is_some();
8449 entries = if rename_or_copy {
8450 split_broken_modifications(odb, work_root, entries, break_score, worktree, config)?
8451 } else {
8452 annotate_broken_modifications(
8453 odb,
8454 work_root,
8455 entries,
8456 break_score,
8457 worktree,
8458 config,
8459 opts.materialize_break_worktree_new_oid,
8460 )?
8461 };
8462 if opts.materialize_break_worktree_new_oid {
8463 materialize_break_worktree_new_oids(odb, work_root, &mut entries, worktree, config)?;
8464 }
8465 }
8466
8467 let source_tree_entries = source_tree_oid
8468 .map(|t| flatten_tree_entries(odb, t))
8469 .transpose()?
8470 .unwrap_or_default();
8471
8472 if opts.copy_threshold.is_some() || opts.find_copies_harder {
8473 let threshold = opts.copy_threshold.unwrap_or(opts.rename_threshold.min(99));
8474 detect_copies_with_policy(
8475 odb,
8476 work_root,
8477 entries,
8478 threshold,
8479 opts.find_copies_harder,
8480 &source_tree_entries,
8481 config,
8482 thread_policy,
8483 )
8484 } else if opts.rename_threshold < 100 {
8485 detect_renames_with_policy(
8486 odb,
8487 work_root,
8488 entries,
8489 opts.rename_threshold,
8490 config,
8491 thread_policy,
8492 None,
8493 )
8494 } else {
8495 Ok(entries)
8496 }
8497}
8498
8499pub fn flatten_tree_entries(
8501 odb: &Odb,
8502 tree_oid: &ObjectId,
8503) -> Result<Vec<(String, String, ObjectId)>> {
8504 fn walk(
8505 odb: &Odb,
8506 tree_oid: &ObjectId,
8507 prefix: &str,
8508 out: &mut Vec<(String, String, ObjectId)>,
8509 ) -> Result<()> {
8510 let obj = odb.read(tree_oid)?;
8511 let oid_len = odb.hash_algo().len();
8512 for e in TreeRefIter::with_oid_len(&obj.data, oid_len) {
8513 let e = e?;
8514 let name = String::from_utf8_lossy(e.name);
8515 let path = if prefix.is_empty() {
8516 name.to_string()
8517 } else {
8518 format!("{prefix}/{name}")
8519 };
8520 if e.mode == 0o040000 {
8521 walk(odb, &e.oid, &path, out)?;
8522 } else {
8523 out.push((path, format!("{:o}", e.mode), e.oid));
8524 }
8525 }
8526 Ok(())
8527 }
8528 let mut out = Vec::new();
8529 walk(odb, tree_oid, "", &mut out)?;
8530 Ok(out)
8531}
8532
8533#[allow(clippy::too_many_arguments)]
8543pub fn status_apply_rename_copy_detection(
8544 odb: &Odb,
8545 unstaged_raw: Vec<DiffEntry>,
8546 threshold: u32,
8547 copies: bool,
8548 head_tree: Option<&ObjectId>,
8549 config: &ConfigSet,
8550 thread_policy: &ThreadPolicy,
8551 cancel_watch: Option<&std::sync::atomic::AtomicBool>,
8552) -> Result<Vec<DiffEntry>> {
8553 if !copies {
8554 return detect_renames_with_policy(
8555 odb,
8556 None,
8557 unstaged_raw,
8558 threshold,
8559 config,
8560 thread_policy,
8561 cancel_watch,
8562 );
8563 }
8564 let source_tree_entries: Vec<(String, String, ObjectId)> = match head_tree {
8565 Some(oid) => flatten_tree(odb, oid, "")?
8566 .into_iter()
8567 .map(|e| (e.path, format_mode(e.mode), e.oid))
8568 .collect(),
8569 None => Vec::new(),
8570 };
8571 detect_copies_with_policy(
8572 odb,
8573 None,
8574 unstaged_raw,
8575 threshold,
8576 false,
8577 &source_tree_entries,
8578 config,
8579 thread_policy,
8580 )
8581}
8582
8583pub fn format_rename_path(old: &str, new: &str) -> String {
8591 let ob = old.as_bytes();
8592 let nb = new.as_bytes();
8593
8594 let pfx = {
8596 let mut last_sep = 0usize;
8597 let min_len = ob.len().min(nb.len());
8598 for i in 0..min_len {
8599 if ob[i] != nb[i] {
8600 break;
8601 }
8602 if ob[i] == b'/' {
8603 last_sep = i + 1;
8604 }
8605 }
8606 last_sep
8607 };
8608
8609 let mut sfx = {
8611 let mut last_sep = 0usize;
8612 let min_len = ob.len().min(nb.len());
8613 for i in 0..min_len {
8614 let oi = ob.len() - 1 - i;
8615 let ni = nb.len() - 1 - i;
8616 if ob[oi] != nb[ni] {
8617 break;
8618 }
8619 if ob[oi] == b'/' {
8620 last_sep = i + 1;
8621 }
8622 }
8623 last_sep
8624 };
8625
8626 let mut sfx_at_old = ob.len() - sfx;
8628 let mut sfx_at_new = nb.len() - sfx;
8629
8630 while pfx > sfx_at_old && pfx > sfx_at_new && sfx > 0 {
8633 let suffix_bytes = &ob[sfx_at_old..];
8635 let mut new_sfx = 0;
8636 for (i, &b) in suffix_bytes.iter().enumerate().skip(1) {
8638 if b == b'/' {
8639 new_sfx = sfx - i;
8640 break;
8641 }
8642 }
8643 if new_sfx == 0 || new_sfx >= sfx {
8644 sfx_at_old = ob.len();
8645 sfx_at_new = nb.len();
8646 break;
8647 }
8648 sfx = new_sfx;
8649 sfx_at_old = ob.len() - sfx;
8650 sfx_at_new = nb.len() - sfx;
8651 }
8652
8653 let prefix = &old[..pfx];
8660 let suffix = &old[sfx_at_old..];
8661 let old_mid = if pfx <= sfx_at_old {
8662 &old[pfx..sfx_at_old]
8663 } else {
8664 ""
8665 };
8666 let new_mid = if pfx <= sfx_at_new {
8667 &new[pfx..sfx_at_new]
8668 } else {
8669 ""
8670 };
8671
8672 if prefix.is_empty() && suffix.is_empty() {
8673 return format!("{old} => {new}");
8674 }
8675
8676 format!("{prefix}{{{old_mid} => {new_mid}}}{suffix}")
8677}
8678
8679fn same_basename(del: &DiffEntry, add: &DiffEntry) -> bool {
8681 let old = del.old_path.as_deref().unwrap_or("");
8682 let new = add.new_path.as_deref().unwrap_or("");
8683 let old_base = old.rsplit('/').next().unwrap_or(old);
8684 let new_base = new.rsplit('/').next().unwrap_or(new);
8685 old_base == new_base && !old_base.is_empty()
8686}
8687
8688const RENAME_SCORE_MAX: u32 = 100;
8689
8690fn normalize_bytes_for_rename(data: Vec<u8>) -> Vec<u8> {
8692 if data.contains(&b'\r') {
8693 crate::crlf::crlf_to_lf(&data)
8694 } else {
8695 data
8696 }
8697}
8698
8699fn blob_payload_size(odb: &Odb, oid: &ObjectId) -> Option<usize> {
8701 if oid.is_zero() {
8702 return None;
8703 }
8704 odb.read_info(oid).ok().map(|info| info.size as usize)
8705}
8706
8707fn added_entry_payload_size(odb: &Odb, add: &DiffEntry, work_root: Option<&Path>) -> Option<usize> {
8709 if !add.new_oid.is_zero() {
8710 if let Some(size) = blob_payload_size(odb, &add.new_oid) {
8711 return Some(size);
8712 }
8713 }
8714 read_added_entry_bytes(odb, add, work_root).map(|data| data.len())
8715}
8716
8717fn rename_pair_size_reachable(src_size: usize, dst_size: usize, minimum_score: u32) -> bool {
8719 let max_size = src_size.max(dst_size);
8720 let min_size = src_size.min(dst_size);
8721 if max_size == 0 {
8722 return minimum_score <= RENAME_SCORE_MAX;
8723 }
8724 (max_size - min_size).saturating_mul(RENAME_SCORE_MAX as usize)
8725 <= max_size.saturating_mul((RENAME_SCORE_MAX - minimum_score) as usize)
8726}
8727
8728fn compute_similarity(old: &[u8], new: &[u8]) -> u32 {
8733 RenameLineTable::similarity_score(
8734 &RenameLineTable::from_similarity_blob(old),
8735 &RenameLineTable::from_similarity_blob(new),
8736 )
8737}
8738
8739#[must_use]
8743pub fn rename_similarity_score(old: &[u8], new: &[u8]) -> u32 {
8744 compute_similarity(old, new)
8745}
8746
8747pub fn format_raw(entry: &DiffEntry) -> String {
8753 let path = match entry.status {
8754 DiffStatus::Renamed | DiffStatus::Copied => {
8755 format!(
8756 "{}\t{}",
8757 entry.old_path.as_deref().unwrap_or(""),
8758 entry.new_path.as_deref().unwrap_or("")
8759 )
8760 }
8761 _ => entry.path().to_owned(),
8762 };
8763
8764 let status_str = match (entry.status, entry.score) {
8765 (DiffStatus::Renamed, Some(s)) => format!("R{:03}", s),
8766 (DiffStatus::Copied, Some(s)) => format!("C{:03}", s),
8767 (DiffStatus::Modified, Some(s)) => format!("M{s:03}"),
8768 (DiffStatus::TypeChanged, Some(s)) => format!("T{s:03}"),
8769 _ => entry.status.letter().to_string(),
8770 };
8771
8772 let (old_hex, new_hex) = raw_oid_hex_pair(&entry.old_oid, &entry.new_oid);
8773 format!(
8774 ":{} {} {} {} {}\t{}",
8775 entry.old_mode, entry.new_mode, old_hex, new_hex, status_str, path
8776 )
8777}
8778
8779fn raw_oid_hex_pair(old: &ObjectId, new: &ObjectId) -> (String, String) {
8787 let width = if !old.is_zero() {
8788 old.algo().hex_len()
8789 } else if !new.is_zero() {
8790 new.algo().hex_len()
8791 } else {
8792 old.algo().hex_len()
8793 };
8794 let render = |oid: &ObjectId| {
8795 if oid.is_zero() {
8796 "0".repeat(width)
8797 } else {
8798 oid.to_hex()
8799 }
8800 };
8801 (render(old), render(new))
8802}
8803
8804pub fn format_raw_abbrev(entry: &DiffEntry, abbrev_len: usize) -> String {
8806 let ellipsis = if crate::environment::Environment::empty()
8807 .var("GIT_PRINT_SHA1_ELLIPSIS")
8808 .as_deref()
8809 == Some("yes")
8810 {
8811 "..."
8812 } else {
8813 ""
8814 };
8815 let old_hex = format!("{}", entry.old_oid);
8816 let new_hex = format!("{}", entry.new_oid);
8817 let old_abbrev = &old_hex[..abbrev_len.min(old_hex.len())];
8818 let new_abbrev = &new_hex[..abbrev_len.min(new_hex.len())];
8819
8820 let path = match entry.status {
8822 DiffStatus::Renamed | DiffStatus::Copied => format!(
8823 "{}\t{}",
8824 entry.old_path.as_deref().unwrap_or(""),
8825 entry.new_path.as_deref().unwrap_or("")
8826 ),
8827 _ => entry.path().to_owned(),
8828 };
8829 let status_str = match (entry.status, entry.score) {
8830 (DiffStatus::Renamed, Some(s)) => format!("R{s:03}"),
8831 (DiffStatus::Copied, Some(s)) => format!("C{s:03}"),
8832 (DiffStatus::Modified, Some(s)) => format!("M{s:03}"),
8833 (DiffStatus::TypeChanged, Some(s)) => format!("T{s:03}"),
8834 _ => entry.status.letter().to_string(),
8835 };
8836
8837 format!(
8838 ":{} {} {}{} {}{} {}\t{}",
8839 entry.old_mode,
8840 entry.new_mode,
8841 old_abbrev,
8842 ellipsis,
8843 new_abbrev,
8844 ellipsis,
8845 status_str,
8846 path
8847 )
8848}
8849
8850pub fn unified_diff(
8865 old_content: &str,
8866 new_content: &str,
8867 old_path: &str,
8868 new_path: &str,
8869 context_lines: usize,
8870 indent_heuristic: bool,
8871 quote_path_fully: bool,
8872) -> String {
8873 unified_diff_with_prefix(
8874 old_content,
8875 new_content,
8876 old_path,
8877 new_path,
8878 context_lines,
8879 0,
8880 "a/",
8881 "b/",
8882 indent_heuristic,
8883 quote_path_fully,
8884 )
8885}
8886
8887#[allow(clippy::too_many_arguments)] pub fn unified_diff_with_prefix(
8893 old_content: &str,
8894 new_content: &str,
8895 old_path: &str,
8896 new_path: &str,
8897 context_lines: usize,
8898 inter_hunk_context: usize,
8899 src_prefix: &str,
8900 dst_prefix: &str,
8901 indent_heuristic: bool,
8902 quote_path_fully: bool,
8903) -> String {
8904 unified_diff_with_prefix_and_funcname(
8905 old_content,
8906 new_content,
8907 old_path,
8908 new_path,
8909 context_lines,
8910 inter_hunk_context,
8911 src_prefix,
8912 dst_prefix,
8913 None,
8914 indent_heuristic,
8915 quote_path_fully,
8916 )
8917}
8918
8919#[allow(clippy::too_many_arguments)]
8922pub fn unified_diff_with_prefix_and_funcname(
8923 old_content: &str,
8924 new_content: &str,
8925 old_path: &str,
8926 new_path: &str,
8927 context_lines: usize,
8928 inter_hunk_context: usize,
8929 src_prefix: &str,
8930 dst_prefix: &str,
8931 funcname_matcher: Option<&FuncnameMatcher>,
8932 indent_heuristic: bool,
8933 quote_path_fully: bool,
8934) -> String {
8935 unified_diff_with_prefix_and_funcname_and_algorithm(
8936 old_content,
8937 new_content,
8938 old_path,
8939 new_path,
8940 context_lines,
8941 inter_hunk_context,
8942 src_prefix,
8943 dst_prefix,
8944 funcname_matcher,
8945 similar::Algorithm::Myers,
8946 false,
8947 false,
8948 indent_heuristic,
8949 quote_path_fully,
8950 )
8951}
8952
8953#[allow(clippy::too_many_arguments)]
8959pub fn write_unified_diff_with_prefix_and_funcname_and_algorithm<W: std::io::Write>(
8960 out: &mut W,
8961 old_content: &str,
8962 new_content: &str,
8963 old_path: &str,
8964 new_path: &str,
8965 context_lines: usize,
8966 inter_hunk_context: usize,
8967 src_prefix: &str,
8968 dst_prefix: &str,
8969 funcname_matcher: Option<&FuncnameMatcher>,
8970 algorithm: similar::Algorithm,
8971 function_context: bool,
8972 use_git_histogram: bool,
8973 indent_heuristic: bool,
8974 quote_path_fully: bool,
8975) -> Result<()> {
8976 if function_context {
8977 let patch = unified_diff_with_function_context(
8978 old_content,
8979 new_content,
8980 old_path,
8981 new_path,
8982 context_lines,
8983 inter_hunk_context,
8984 src_prefix,
8985 dst_prefix,
8986 funcname_matcher,
8987 algorithm,
8988 indent_heuristic,
8989 quote_path_fully,
8990 );
8991 out.write_all(patch.as_bytes())?;
8992 return Ok(());
8993 }
8994
8995 if use_git_histogram {
8996 let patch = unified_diff_histogram_with_prefix_and_funcname_indent(
8997 old_content,
8998 new_content,
8999 old_path,
9000 new_path,
9001 context_lines,
9002 inter_hunk_context,
9003 src_prefix,
9004 dst_prefix,
9005 funcname_matcher,
9006 indent_heuristic,
9007 quote_path_fully,
9008 );
9009 out.write_all(patch.as_bytes())?;
9010 return Ok(());
9011 }
9012
9013 if algorithm == similar::Algorithm::Myers && !function_context {
9014 let patch = unified_diff_git_myers_with_prefix_and_funcname(
9015 old_content,
9016 new_content,
9017 old_path,
9018 new_path,
9019 context_lines,
9020 inter_hunk_context,
9021 src_prefix,
9022 dst_prefix,
9023 funcname_matcher,
9024 indent_heuristic,
9025 quote_path_fully,
9026 );
9027 out.write_all(patch.as_bytes())?;
9028 return Ok(());
9029 }
9030
9031 if algorithm == similar::Algorithm::Lcs && !function_context {
9032 let patch = unified_diff_git_minimal_with_prefix_and_funcname(
9033 old_content,
9034 new_content,
9035 old_path,
9036 new_path,
9037 context_lines,
9038 inter_hunk_context,
9039 src_prefix,
9040 dst_prefix,
9041 funcname_matcher,
9042 indent_heuristic,
9043 quote_path_fully,
9044 );
9045 out.write_all(patch.as_bytes())?;
9046 return Ok(());
9047 }
9048
9049 if algorithm == similar::Algorithm::Patience && !function_context {
9050 let patch = unified_diff_git_patience_with_prefix_and_funcname(
9051 old_content,
9052 new_content,
9053 old_path,
9054 new_path,
9055 context_lines,
9056 inter_hunk_context,
9057 src_prefix,
9058 dst_prefix,
9059 funcname_matcher,
9060 indent_heuristic,
9061 quote_path_fully,
9062 );
9063 out.write_all(patch.as_bytes())?;
9064 return Ok(());
9065 }
9066
9067 use crate::quote_path::format_diff_path_with_prefix;
9068 use similar::{udiff::UnifiedDiffHunk, TextDiff};
9069
9070 let diff = TextDiff::configure()
9071 .algorithm(algorithm)
9072 .diff_lines(old_content, new_content);
9073 let compacted_ops = diff_indent_heuristic::diff_lines_ops_compacted(
9074 old_content,
9075 new_content,
9076 algorithm,
9077 indent_heuristic,
9078 );
9079
9080 if old_path == "/dev/null" {
9081 out.write_all(b"--- /dev/null\n")?;
9082 } else if src_prefix.is_empty() {
9083 writeln!(out, "--- {old_path}")?;
9084 } else {
9085 out.write_all(b"--- ")?;
9086 out.write_all(
9087 format_diff_path_with_prefix(src_prefix, old_path, quote_path_fully).as_bytes(),
9088 )?;
9089 out.write_all(b"\n")?;
9090 }
9091 if new_path == "/dev/null" {
9092 out.write_all(b"+++ /dev/null\n")?;
9093 } else if dst_prefix.is_empty() {
9094 writeln!(out, "+++ {new_path}")?;
9095 } else {
9096 out.write_all(b"+++ ")?;
9097 out.write_all(
9098 format_diff_path_with_prefix(dst_prefix, new_path, quote_path_fully).as_bytes(),
9099 )?;
9100 out.write_all(b"\n")?;
9101 }
9102
9103 let old_lines: Vec<&str> = old_content.lines().collect();
9104 let max_common_gap = context_lines
9105 .saturating_mul(2)
9106 .saturating_add(inter_hunk_context);
9107 let op_groups = group_diff_ops_gap(compacted_ops, context_lines, max_common_gap);
9108
9109 let mut hunk_buf = String::new();
9110 let mut eof_annot = UnifiedDiffNoNewlineAnnotator::new(old_content, new_content);
9111 for ops in op_groups {
9112 if ops.is_empty() {
9113 continue;
9114 }
9115 let hunk = UnifiedDiffHunk::new(ops, &diff, true);
9116 hunk_buf.clear();
9117 use std::fmt::Write as FmtWrite;
9118 let _ = FmtWrite::write_fmt(&mut hunk_buf, format_args!("{hunk}"));
9119 let mut fragment = hunk_buf.clone();
9120 if let Some(first_newline) = hunk_buf.find('\n') {
9121 let header_line = &hunk_buf[..first_newline];
9122 let rest = &hunk_buf[first_newline..];
9123 if let Some(func_ctx) =
9124 extract_function_context(header_line, &old_lines, funcname_matcher, Some(old_path))
9125 {
9126 fragment.clear();
9127 fragment.push_str(header_line);
9128 fragment.push(' ');
9129 fragment.push_str(&func_ctx);
9130 fragment.push_str(rest);
9131 }
9132 }
9133 eof_annot.write_hunk(out, &fragment)?;
9134 }
9135 Ok(())
9136}
9137
9138#[allow(clippy::too_many_arguments)]
9144pub fn unified_diff_with_prefix_and_funcname_and_algorithm(
9145 old_content: &str,
9146 new_content: &str,
9147 old_path: &str,
9148 new_path: &str,
9149 context_lines: usize,
9150 inter_hunk_context: usize,
9151 src_prefix: &str,
9152 dst_prefix: &str,
9153 funcname_matcher: Option<&FuncnameMatcher>,
9154 algorithm: similar::Algorithm,
9155 function_context: bool,
9156 use_git_histogram: bool,
9157 indent_heuristic: bool,
9158 quote_path_fully: bool,
9159) -> String {
9160 let mut output = Vec::new();
9161 if write_unified_diff_with_prefix_and_funcname_and_algorithm(
9162 &mut output,
9163 old_content,
9164 new_content,
9165 old_path,
9166 new_path,
9167 context_lines,
9168 inter_hunk_context,
9169 src_prefix,
9170 dst_prefix,
9171 funcname_matcher,
9172 algorithm,
9173 function_context,
9174 use_git_histogram,
9175 indent_heuristic,
9176 quote_path_fully,
9177 )
9178 .is_err()
9179 {
9180 output.clear();
9181 }
9182 String::from_utf8_lossy(&output).into_owned()
9183}
9184
9185pub(crate) fn group_diff_ops_gap(
9191 mut ops: Vec<similar::DiffOp>,
9192 context: usize,
9193 max_common_gap: usize,
9194) -> Vec<Vec<similar::DiffOp>> {
9195 use similar::DiffOp;
9196 if ops.is_empty() {
9197 return vec![];
9198 }
9199
9200 let mut pending_group = Vec::new();
9201 let mut rv = Vec::new();
9202
9203 if let Some(DiffOp::Equal {
9204 old_index,
9205 new_index,
9206 len,
9207 }) = ops.first_mut()
9208 {
9209 let offset = (*len).saturating_sub(context);
9210 *old_index += offset;
9211 *new_index += offset;
9212 *len -= offset;
9213 }
9214
9215 if let Some(DiffOp::Equal { len, .. }) = ops.last_mut() {
9216 *len -= (*len).saturating_sub(context);
9217 }
9218
9219 for op in ops.into_iter() {
9220 if let DiffOp::Equal {
9221 old_index,
9222 new_index,
9223 len,
9224 } = op
9225 {
9226 if len > max_common_gap {
9229 pending_group.push(DiffOp::Equal {
9230 old_index,
9231 new_index,
9232 len: context,
9233 });
9234 rv.push(pending_group);
9235 let offset = len.saturating_sub(context);
9236 pending_group = vec![DiffOp::Equal {
9237 old_index: old_index + offset,
9238 new_index: new_index + offset,
9239 len: len - offset,
9240 }];
9241 continue;
9242 }
9243 }
9244 pending_group.push(op);
9245 }
9246
9247 match &pending_group[..] {
9248 &[] | &[similar::DiffOp::Equal { .. }] => {}
9249 _ => rv.push(pending_group),
9250 }
9251
9252 rv
9253}
9254#[expect(clippy::too_many_arguments)]
9255fn unified_diff_with_function_context(
9257 old_content: &str,
9258 new_content: &str,
9259 old_path: &str,
9260 new_path: &str,
9261 context_lines: usize,
9262 inter_hunk_context: usize,
9263 src_prefix: &str,
9264 dst_prefix: &str,
9265 funcname_matcher: Option<&FuncnameMatcher>,
9266 algorithm: similar::Algorithm,
9267 indent_heuristic: bool,
9268 quote_path_fully: bool,
9269) -> String {
9270 use crate::quote_path::format_diff_path_with_prefix;
9271 use similar::{udiff::UnifiedDiffHunk, TextDiff};
9272
9273 let diff = TextDiff::configure()
9274 .algorithm(algorithm)
9275 .diff_lines(old_content, new_content);
9276
9277 let old_lines: Vec<&str> = old_content.lines().collect();
9278 let new_lines: Vec<&str> = new_content.lines().collect();
9279 let n_old = old_lines.len();
9280 let n_new = new_lines.len();
9281
9282 let max_common_gap = context_lines
9289 .saturating_mul(2)
9290 .saturating_add(inter_hunk_context);
9291 let all_ops = diff.ops().to_vec();
9292 let op_groups = group_diff_ops_gap(all_ops.clone(), context_lines, max_common_gap);
9293
9294 let mut ranges: Vec<(usize, usize, usize, usize)> = Vec::new();
9295
9296 for ops in op_groups {
9297 if ops.is_empty() {
9298 continue;
9299 }
9300 let i1_anchor = func_context_old_anchor(&ops, n_old);
9301 let i1_end = hunk_old_change_end_exclusive(&ops);
9302 let skip_preimage_pull =
9303 append_with_whole_function_added(&ops, n_old, n_new, &new_lines, funcname_matcher);
9304 let hunk = UnifiedDiffHunk::new(ops, &diff, true);
9305 let hunk_str = format!("{hunk}");
9306 let header_line = hunk_str
9307 .lines()
9308 .next()
9309 .unwrap_or("")
9310 .trim_end_matches(['\r', '\n']);
9311 let Some((base_s1, _base_e1, _base_s2, _base_e2)) =
9312 parse_unified_hunk_header_ranges(header_line)
9313 else {
9314 continue;
9315 };
9316
9317 let ctx = context_lines;
9318 let (s1, e1, s2, e2) = if skip_preimage_pull {
9319 let s = n_old.saturating_sub(ctx);
9320 let s2 = map_old_line_to_new(&all_ops, s, n_new).min(n_new);
9321 (s, n_old, s2, n_new)
9322 } else {
9323 let mut s1 = base_s1.saturating_sub(ctx);
9324 let mut s2 = map_old_line_to_new(&all_ops, s1, n_new).min(n_new);
9325
9326 let base_pre_s1 = i1_anchor.saturating_sub(ctx);
9327 if base_pre_s1 < s1 {
9328 s1 = base_pre_s1;
9329 s2 = map_old_line_to_new(&all_ops, s1, n_new).min(n_new);
9330 }
9331
9332 let fs1 = expand_func_pre_start(s1, i1_anchor, n_old, &old_lines, funcname_matcher);
9333 if fs1 < s1 {
9334 s1 = fs1;
9335 s2 = map_old_line_to_new(&all_ops, s1, n_new).min(n_new);
9336 }
9337
9338 let mut e1 = (i1_end + ctx).min(n_old);
9344 let mut e2 = map_old_line_to_new(&all_ops, e1, n_new).min(n_new);
9345 let fe1 = expand_func_post_end(e1, i1_end, n_old, &old_lines, funcname_matcher);
9346 if fe1 > e1 {
9347 e1 = fe1;
9348 e2 = map_old_line_to_new(&all_ops, e1, n_new).min(n_new);
9349 }
9350 (s1, e1, s2, e2)
9351 };
9352
9353 ranges.push((s1, e1, s2, e2));
9354 }
9355
9356 ranges.sort_by_key(|r| (r.0, r.2));
9359 let mut merged: Vec<(usize, usize, usize, usize)> = Vec::with_capacity(ranges.len());
9360 for (s1, e1, s2, e2) in ranges {
9361 if let Some(last) = merged.last_mut() {
9362 if s1 < last.1 {
9363 last.1 = last.1.max(e1);
9364 last.3 = last.3.max(e2);
9365 continue;
9366 }
9367 }
9368 merged.push((s1, e1, s2, e2));
9369 }
9370 let ranges = merged;
9371
9372 let mut output = String::new();
9373 if old_path == "/dev/null" {
9374 output.push_str("--- /dev/null\n");
9375 } else if src_prefix.is_empty() {
9376 output.push_str(&format!("--- {old_path}\n"));
9377 } else {
9378 output.push_str("--- ");
9379 output.push_str(&format_diff_path_with_prefix(
9380 src_prefix,
9381 old_path,
9382 quote_path_fully,
9383 ));
9384 output.push('\n');
9385 }
9386 if new_path == "/dev/null" {
9387 output.push_str("+++ /dev/null\n");
9388 } else if dst_prefix.is_empty() {
9389 output.push_str(&format!("+++ {new_path}\n"));
9390 } else {
9391 output.push_str("+++ ");
9392 output.push_str(&format_diff_path_with_prefix(
9393 dst_prefix,
9394 new_path,
9395 quote_path_fully,
9396 ));
9397 output.push('\n');
9398 }
9399
9400 for (s1, e1, s2, e2) in ranges {
9401 if s1 >= e1 && s2 >= e2 {
9402 continue;
9403 }
9404 let old_seg =
9405 line_slice_for_diff_with_eof_nl(&old_lines, s1, e1, old_content.ends_with('\n'));
9406 let new_seg =
9407 line_slice_for_diff_with_eof_nl(&new_lines, s2, e2, new_content.ends_with('\n'));
9408 let inner_ctx = old_seg.lines().count().max(new_seg.lines().count()).max(1);
9409 let piece = unified_diff_with_prefix_and_funcname_and_algorithm(
9410 &old_seg,
9411 &new_seg,
9412 old_path,
9413 new_path,
9414 inner_ctx,
9415 0,
9416 src_prefix,
9417 dst_prefix,
9418 funcname_matcher,
9419 algorithm,
9420 false,
9421 false,
9422 indent_heuristic,
9423 quote_path_fully,
9424 );
9425 let shifted = shift_unified_hunk_headers_to_full_file(&piece, s1, s2);
9426 let with_func =
9427 enrich_unified_hunk_headers_funcname(&shifted, &old_lines, funcname_matcher);
9428 for line in with_func.lines() {
9429 if line.starts_with("--- ") || line.starts_with("+++ ") {
9430 continue;
9431 }
9432 output.push_str(line);
9433 output.push('\n');
9434 }
9435 }
9436
9437 output
9438}
9439
9440fn shift_unified_hunk_headers_to_full_file(
9445 patch: &str,
9446 delta_old: usize,
9447 delta_new: usize,
9448) -> String {
9449 if delta_old == 0 && delta_new == 0 {
9450 return patch.to_owned();
9451 }
9452 let mut out = String::with_capacity(patch.len());
9453 for line in patch.lines() {
9454 if let Some(shifted) = shift_one_unified_hunk_header(line, delta_old, delta_new) {
9455 out.push_str(&shifted);
9456 } else {
9457 out.push_str(line);
9458 }
9459 out.push('\n');
9460 }
9461 out
9462}
9463
9464fn shift_one_unified_hunk_header(line: &str, delta_old: usize, delta_new: usize) -> Option<String> {
9465 let rest = line.strip_prefix("@@ ")?;
9466 let (old_chunk, after_plus) = rest.split_once(" +")?;
9467 let old_spec = old_chunk.strip_prefix('-')?;
9468 let (new_spec, suffix) = after_plus.split_once(" @@")?;
9469 let shifted_old = shift_unified_range_spec(old_spec, delta_old)?;
9470 let shifted_new = shift_unified_range_spec(new_spec, delta_new)?;
9471 Some(format!("@@ -{shifted_old} +{shifted_new} @@{suffix}"))
9472}
9473
9474fn shift_unified_range_spec(spec: &str, delta: usize) -> Option<String> {
9475 let spec = spec.trim();
9476 if let Some((start_s, count_s)) = spec.split_once(',') {
9477 let start: usize = start_s.parse().ok()?;
9478 let count: usize = count_s.parse().ok()?;
9479 Some(format!("{},{}", start.saturating_add(delta), count))
9480 } else {
9481 let start: usize = spec.parse().ok()?;
9482 Some(format!("{}", start.saturating_add(delta)))
9483 }
9484}
9485
9486fn enrich_unified_hunk_headers_funcname(
9488 patch: &str,
9489 full_old_lines: &[&str],
9490 funcname_matcher: Option<&FuncnameMatcher>,
9491) -> String {
9492 let mut out = String::with_capacity(patch.len());
9493 for line in patch.lines() {
9494 if let Some(fixed) = enrich_one_hunk_header_funcname(line, full_old_lines, funcname_matcher)
9495 {
9496 out.push_str(&fixed);
9497 } else {
9498 out.push_str(line);
9499 }
9500 out.push('\n');
9501 }
9502 out
9503}
9504
9505fn enrich_one_hunk_header_funcname(
9506 line: &str,
9507 full_old_lines: &[&str],
9508 funcname_matcher: Option<&FuncnameMatcher>,
9509) -> Option<String> {
9510 let after_at = line.strip_prefix("@@ ")?;
9511 let idx = after_at.find(" @@")?;
9512 let mid = after_at[..idx].trim();
9513 let tail = after_at[idx + 3..].trim_start();
9514 let header_for_parse = format!("@@ {mid} @@");
9515 let func = extract_function_context(&header_for_parse, full_old_lines, funcname_matcher, None);
9516 Some(if let Some(f) = func {
9517 format!("@@ {mid} @@ {f}")
9518 } else if !tail.is_empty() {
9519 format!("@@ {mid} @@ {tail}")
9520 } else {
9521 format!("@@ {mid} @@")
9522 })
9523}
9524
9525fn line_slice_for_diff_with_eof_nl(
9526 lines: &[&str],
9527 start: usize,
9528 end: usize,
9529 full_file_ends_with_newline: bool,
9530) -> String {
9531 if start >= end {
9532 return String::new();
9533 }
9534 let mut s = lines[start..end].join("\n");
9535 let slice_is_suffix_of_file = end == lines.len();
9536 let need_trailing_nl = if slice_is_suffix_of_file {
9537 full_file_ends_with_newline
9538 } else {
9539 true
9540 };
9541 if need_trailing_nl && !s.ends_with('\n') {
9542 s.push('\n');
9543 }
9544 s
9545}
9546
9547fn map_old_line_to_new(ops: &[similar::DiffOp], old_line: usize, n_new: usize) -> usize {
9550 use similar::DiffOp;
9551 let mut n = 0usize;
9552 for op in ops {
9553 match *op {
9554 DiffOp::Equal {
9555 old_index,
9556 new_index,
9557 len,
9558 } => {
9559 if old_index + len <= old_line {
9560 n = new_index + len;
9561 continue;
9562 }
9563 if old_index < old_line {
9564 let take = old_line - old_index;
9565 return (new_index + take).min(n_new);
9566 }
9567 return new_index.min(n_new);
9568 }
9569 DiffOp::Delete {
9570 old_index,
9571 old_len,
9572 new_index,
9573 } => {
9574 if old_index + old_len <= old_line {
9575 n = new_index;
9576 continue;
9577 }
9578 if old_index < old_line {
9579 return new_index.min(n_new);
9580 }
9581 }
9582 DiffOp::Insert {
9583 old_index,
9584 new_index,
9585 new_len,
9586 } => {
9587 if old_index < old_line {
9588 n = new_index + new_len;
9589 continue;
9590 }
9591 if old_index == old_line {
9592 return (new_index + new_len).min(n_new);
9595 }
9596 return new_index.min(n_new);
9597 }
9598 DiffOp::Replace {
9599 old_index,
9600 old_len,
9601 new_index,
9602 new_len,
9603 } => {
9604 if old_index + old_len <= old_line {
9605 n = new_index + new_len;
9606 continue;
9607 }
9608 if old_index < old_line {
9609 let into_old = old_line - old_index;
9610 let mapped = new_index + into_old.min(new_len);
9611 return mapped.min(n_new);
9612 }
9613 return new_index.min(n_new);
9614 }
9615 }
9616 }
9617 n.min(n_new)
9618}
9619
9620fn parse_unified_hunk_header_ranges(header: &str) -> Option<(usize, usize, usize, usize)> {
9622 let rest = header.strip_prefix("@@ ")?;
9623 let (old_tok, rest2) = rest.split_once(" +")?;
9624 let old_tok = old_tok.strip_prefix('-')?;
9625 let new_tok = rest2.split_once(" @@").map(|(a, _)| a)?;
9626
9627 fn parse_side(spec: &str) -> Option<(usize, usize)> {
9628 let spec = spec.trim();
9629 let (start_one_based, count) = if let Some((a, b)) = spec.split_once(',') {
9630 (a.parse::<usize>().ok()?, b.parse::<usize>().ok()?)
9631 } else {
9632 let s = spec.parse::<usize>().ok()?;
9633 (s, 1usize)
9634 };
9635 let s0 = start_one_based.saturating_sub(1);
9636 let e0 = s0.saturating_add(count);
9637 Some((s0, e0))
9638 }
9639
9640 let (os, oe) = parse_side(old_tok)?;
9641 let (ns, ne) = parse_side(new_tok)?;
9642 Some((os, oe, ns, ne))
9643}
9644
9645fn append_with_whole_function_added(
9648 ops: &[similar::DiffOp],
9649 n_old: usize,
9650 n_new: usize,
9651 new_lines: &[&str],
9652 matcher: Option<&FuncnameMatcher>,
9653) -> bool {
9654 use similar::DiffOp;
9655 if n_old == 0 {
9656 return false;
9657 }
9658 let mut only_ins_or_eq = true;
9659 let mut min_new_ins = usize::MAX;
9660 for op in ops {
9661 match *op {
9662 DiffOp::Equal { .. } => {}
9663 DiffOp::Insert {
9664 new_index, new_len, ..
9665 } => {
9666 min_new_ins = min_new_ins.min(new_index);
9667 if new_len == 0 {
9668 only_ins_or_eq = false;
9669 }
9670 }
9671 DiffOp::Delete { .. } | DiffOp::Replace { .. } => {
9672 only_ins_or_eq = false;
9673 }
9674 }
9675 }
9676 let mut insert_at_eof = false;
9677 for op in ops {
9678 if let DiffOp::Insert { old_index, .. } = *op {
9679 if old_index == n_old {
9680 insert_at_eof = true;
9681 break;
9682 }
9683 }
9684 }
9685 let append_at_eof = min_new_ins == n_old || insert_at_eof;
9686 if !only_ins_or_eq || !append_at_eof || min_new_ins == usize::MAX {
9687 return false;
9688 }
9689 let mut j = min_new_ins;
9694 while j < n_new {
9695 let line = new_lines[j];
9696 if line.trim().is_empty() {
9697 j += 1;
9698 continue;
9699 }
9700 if let Some(m) = matcher {
9701 if m.match_line(line).is_some() {
9702 return true;
9703 }
9704 } else if inserted_block_starts_with_c_like_function_definition(line) {
9705 return true;
9706 }
9707 j += 1;
9708 }
9709 false
9710}
9711
9712fn inserted_block_starts_with_c_like_function_definition(line: &str) -> bool {
9713 let t = line.trim_start();
9714 let Some(open_paren) = t.find('(') else {
9715 return false;
9716 };
9717 let head = &t[..open_paren];
9718 let tokens: Vec<&str> = head.split_whitespace().collect();
9719 if tokens.len() < 2 {
9720 return false;
9722 }
9723 let nameish = tokens.last().copied().unwrap_or("");
9724 let name = nameish.trim_end_matches(['*', '&']);
9725 if name.is_empty() || !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
9726 return false;
9727 }
9728 let type_or_modifier = |tok: &str| {
9729 matches!(
9730 tok,
9731 "static"
9732 | "extern"
9733 | "inline"
9734 | "void"
9735 | "int"
9736 | "char"
9737 | "short"
9738 | "long"
9739 | "float"
9740 | "double"
9741 | "unsigned"
9742 | "signed"
9743 | "struct"
9744 | "enum"
9745 | "union"
9746 | "const"
9747 | "volatile"
9748 | "typedef"
9749 )
9750 };
9751 tokens[..tokens.len() - 1]
9752 .iter()
9753 .any(|tok| type_or_modifier(tok))
9754}
9755
9756fn hunk_old_change_end_exclusive(ops: &[similar::DiffOp]) -> usize {
9757 use similar::DiffOp;
9758 let mut max_o = 0usize;
9759 for op in ops {
9760 match *op {
9761 DiffOp::Delete {
9762 old_index, old_len, ..
9763 } => {
9764 max_o = max_o.max(old_index + old_len);
9765 }
9766 DiffOp::Replace {
9767 old_index, old_len, ..
9768 } => {
9769 max_o = max_o.max(old_index + old_len);
9770 }
9771 DiffOp::Insert { old_index, .. } => {
9772 max_o = max_o.max(old_index);
9775 }
9776 DiffOp::Equal { .. } => {}
9777 }
9778 }
9779 max_o
9780}
9781
9782fn func_context_old_anchor(ops: &[similar::DiffOp], n_old: usize) -> usize {
9783 use similar::DiffOp;
9784 let mut has_delete_or_replace = false;
9785 let mut min_del = usize::MAX;
9786 let mut min_ins_old = usize::MAX;
9787
9788 for op in ops {
9789 match *op {
9790 DiffOp::Delete {
9791 old_index, old_len, ..
9792 } => {
9793 has_delete_or_replace = true;
9794 min_del = min_del.min(old_index);
9795 min_del = min_del.min(old_index + old_len.saturating_sub(1));
9796 }
9797 DiffOp::Replace {
9798 old_index, old_len, ..
9799 } => {
9800 has_delete_or_replace = true;
9801 min_del = min_del.min(old_index);
9802 min_del = min_del.min(old_index + old_len.saturating_sub(1));
9803 }
9804 DiffOp::Insert { old_index, .. } => {
9805 min_ins_old = min_ins_old.min(old_index);
9806 }
9807 DiffOp::Equal { .. } => {}
9808 }
9809 }
9810
9811 let mut i1 = if has_delete_or_replace {
9812 min_del
9813 } else if min_ins_old != usize::MAX {
9814 min_ins_old
9815 } else {
9816 0
9817 };
9818
9819 let pure_insert = ops
9820 .iter()
9821 .all(|op| matches!(op, DiffOp::Insert { .. } | DiffOp::Equal { .. }))
9822 && ops.iter().any(|op| matches!(op, DiffOp::Insert { .. }));
9823
9824 if pure_insert && i1 >= n_old && n_old > 0 {
9825 i1 = n_old - 1;
9826 }
9827
9828 i1.min(n_old.saturating_sub(1))
9829}
9830
9831fn expand_func_pre_start(
9832 s1: usize,
9833 i1: usize,
9834 n_old: usize,
9835 old_lines: &[&str],
9836 matcher: Option<&FuncnameMatcher>,
9837) -> usize {
9838 if n_old == 0 {
9839 return s1;
9840 }
9841 let i1 = i1.min(n_old.saturating_sub(1));
9842 let mut fs1 = get_func_line_backward(old_lines, i1, matcher).unwrap_or(i1);
9843 while fs1 > 0
9844 && !is_line_empty_for_func_context(old_lines[fs1 - 1])
9845 && !is_func_line(old_lines[fs1 - 1], matcher)
9846 {
9847 fs1 -= 1;
9848 }
9849 s1.min(fs1)
9850}
9851
9852fn expand_func_post_end(
9853 e1: usize,
9854 i1_end: usize,
9855 n_old: usize,
9856 old_lines: &[&str],
9857 matcher: Option<&FuncnameMatcher>,
9858) -> usize {
9859 let from = i1_end.min(n_old);
9860 let fe1 = get_func_line_forward(old_lines, from, matcher).unwrap_or(n_old);
9861 let mut fe1_adj = fe1;
9862 while fe1_adj > 0 && is_line_empty_for_func_context(old_lines[fe1_adj - 1]) {
9863 fe1_adj -= 1;
9864 }
9865 e1.max(fe1_adj).min(n_old)
9866}
9867
9868fn is_line_empty_for_func_context(line: &str) -> bool {
9869 line.chars().all(|c| c.is_whitespace())
9870}
9871
9872pub(crate) fn is_func_line(line: &str, matcher: Option<&FuncnameMatcher>) -> bool {
9873 if let Some(m) = matcher {
9874 return m.match_line(line).is_some();
9875 }
9876 let t = line.trim_end_matches(['\n', '\r']);
9877 if t.is_empty() {
9878 return false;
9879 }
9880 let b = t.as_bytes()[0];
9881 if !(b.is_ascii_alphabetic() || b == b'_' || b == b'$') {
9882 return false;
9883 }
9884 true
9885}
9886
9887fn get_func_line_backward(
9888 old_lines: &[&str],
9889 start: usize,
9890 matcher: Option<&FuncnameMatcher>,
9891) -> Option<usize> {
9892 let mut l = start.min(old_lines.len().saturating_sub(1));
9893 if old_lines.is_empty() {
9894 return None;
9895 }
9896 loop {
9897 if is_func_line(old_lines[l], matcher) {
9898 return Some(l);
9899 }
9900 if l == 0 {
9901 break;
9902 }
9903 l -= 1;
9904 }
9905 None
9906}
9907
9908fn get_func_line_forward(
9909 old_lines: &[&str],
9910 start: usize,
9911 matcher: Option<&FuncnameMatcher>,
9912) -> Option<usize> {
9913 let mut l = start;
9914 while l < old_lines.len() {
9915 if is_func_line(old_lines[l], matcher) {
9916 return Some(l);
9917 }
9918 l += 1;
9919 }
9920 None
9921}
9922#[expect(clippy::too_many_arguments)]
9923pub fn anchored_unified_diff(
9933 old_content: &str,
9934 new_content: &str,
9935 old_path: &str,
9936 new_path: &str,
9937 context_lines: usize,
9938 anchors: &[String],
9939 algorithm: similar::Algorithm,
9940 use_git_histogram: bool,
9941 indent_heuristic: bool,
9942 quote_path_fully: bool,
9943) -> String {
9944 use crate::quote_path::format_diff_path_with_prefix;
9945 use similar::TextDiff;
9946
9947 let old_lines: Vec<&str> = old_content.lines().collect();
9948 let new_lines: Vec<&str> = new_content.lines().collect();
9949
9950 let mut anchor_pairs: Vec<(usize, usize)> = Vec::new(); for anchor in anchors {
9954 let anchor_str = anchor.as_str();
9955
9956 let old_positions: Vec<usize> = old_lines
9958 .iter()
9959 .enumerate()
9960 .filter(|(_, l)| l.trim_end() == anchor_str)
9961 .map(|(i, _)| i)
9962 .collect();
9963
9964 let new_positions: Vec<usize> = new_lines
9966 .iter()
9967 .enumerate()
9968 .filter(|(_, l)| l.trim_end() == anchor_str)
9969 .map(|(i, _)| i)
9970 .collect();
9971
9972 if old_positions.len() == 1 && new_positions.len() == 1 {
9974 anchor_pairs.push((old_positions[0], new_positions[0]));
9975 }
9976 }
9977
9978 if anchor_pairs.is_empty() {
9980 return unified_diff_with_prefix_and_funcname_and_algorithm(
9981 old_content,
9982 new_content,
9983 old_path,
9984 new_path,
9985 context_lines,
9986 0,
9987 "a/",
9988 "b/",
9989 None,
9990 algorithm,
9991 false,
9992 use_git_histogram,
9993 indent_heuristic,
9994 quote_path_fully,
9995 );
9996 }
9997
9998 anchor_pairs.sort_by_key(|&(old_idx, _)| old_idx);
10000
10001 let mut filtered: Vec<(usize, usize)> = Vec::new();
10004 for &pair in &anchor_pairs {
10005 if filtered.is_empty() || filtered.last().is_some_and(|last| pair.1 > last.1) {
10006 filtered.push(pair);
10007 }
10008 }
10009 let anchor_pairs = filtered;
10010
10011 struct LineDiffOp {
10020 tag: char, line: String,
10022 }
10023
10024 let append_segment_diff =
10025 |ops: &mut Vec<LineDiffOp>, old_seg_input: &str, new_seg_input: &str| {
10026 use similar::ChangeTag;
10027 let old_ls: Vec<&str> = old_seg_input.lines().collect();
10028 let new_ls: Vec<&str> = new_seg_input.lines().collect();
10029 if old_ls.is_empty() && new_ls.is_empty() {
10030 return;
10031 }
10032 let seg_diff = TextDiff::configure()
10033 .algorithm(algorithm)
10034 .diff_slices(&old_ls, &new_ls);
10035 let raw = seg_diff.ops().to_vec();
10036 let compacted = diff_indent_heuristic::apply_change_compact_to_ops(
10037 &raw,
10038 &old_ls,
10039 &new_ls,
10040 indent_heuristic,
10041 );
10042 for op in &compacted {
10043 for ch in op.iter_changes(&old_ls, &new_ls) {
10044 let t = match ch.tag() {
10045 ChangeTag::Equal => ' ',
10046 ChangeTag::Delete => '-',
10047 ChangeTag::Insert => '+',
10048 };
10049 ops.push(LineDiffOp {
10050 tag: t,
10051 line: ch.value().to_string(),
10052 });
10053 }
10054 }
10055 };
10056
10057 let mut ops: Vec<LineDiffOp> = Vec::new();
10058 let mut old_pos = 0usize;
10059 let mut new_pos = 0usize;
10060
10061 for &(old_anchor, new_anchor) in &anchor_pairs {
10062 let old_segment: Vec<&str> = old_lines[old_pos..old_anchor].to_vec();
10064 let new_segment: Vec<&str> = new_lines[new_pos..new_anchor].to_vec();
10065
10066 let old_seg_text = old_segment.join("\n");
10067 let new_seg_text = new_segment.join("\n");
10068
10069 if !old_seg_text.is_empty() || !new_seg_text.is_empty() {
10070 let old_seg_input = if old_seg_text.is_empty() {
10071 String::new()
10072 } else {
10073 format!("{}\n", old_seg_text)
10074 };
10075 let new_seg_input = if new_seg_text.is_empty() {
10076 String::new()
10077 } else {
10078 format!("{}\n", new_seg_text)
10079 };
10080 append_segment_diff(&mut ops, &old_seg_input, &new_seg_input);
10081 }
10082
10083 ops.push(LineDiffOp {
10085 tag: ' ',
10086 line: old_lines[old_anchor].to_string(),
10087 });
10088
10089 old_pos = old_anchor + 1;
10090 new_pos = new_anchor + 1;
10091 }
10092
10093 let old_segment: Vec<&str> = old_lines[old_pos..].to_vec();
10095 let new_segment: Vec<&str> = new_lines[new_pos..].to_vec();
10096 let old_seg_text = old_segment.join("\n");
10097 let new_seg_text = new_segment.join("\n");
10098
10099 if !old_seg_text.is_empty() || !new_seg_text.is_empty() {
10100 let old_seg_input = if old_seg_text.is_empty() {
10101 String::new()
10102 } else {
10103 format!("{}\n", old_seg_text)
10104 };
10105 let new_seg_input = if new_seg_text.is_empty() {
10106 String::new()
10107 } else {
10108 format!("{}\n", new_seg_text)
10109 };
10110 append_segment_diff(&mut ops, &old_seg_input, &new_seg_input);
10111 }
10112
10113 let mut output = String::new();
10115 if old_path == "/dev/null" {
10116 output.push_str("--- /dev/null\n");
10117 } else {
10118 output.push_str("--- ");
10119 output.push_str(&format_diff_path_with_prefix(
10120 "a/",
10121 old_path,
10122 quote_path_fully,
10123 ));
10124 output.push('\n');
10125 }
10126 if new_path == "/dev/null" {
10127 output.push_str("+++ /dev/null\n");
10128 } else {
10129 output.push_str("+++ ");
10130 output.push_str(&format_diff_path_with_prefix(
10131 "b/",
10132 new_path,
10133 quote_path_fully,
10134 ));
10135 output.push('\n');
10136 }
10137
10138 let total_ops = ops.len();
10140 if total_ops == 0 {
10141 return output;
10142 }
10143
10144 let mut hunks: Vec<(usize, usize)> = Vec::new(); let mut i = 0;
10147 while i < total_ops {
10148 if ops[i].tag != ' ' {
10149 let start = i.saturating_sub(context_lines);
10150 let mut end = i;
10151 while end < total_ops {
10153 if ops[end].tag != ' ' {
10154 end += 1;
10155 continue;
10156 }
10157 let mut next_change = end;
10159 while next_change < total_ops && ops[next_change].tag == ' ' {
10160 next_change += 1;
10161 }
10162 if next_change < total_ops && next_change - end <= context_lines * 2 {
10163 end = next_change + 1;
10164 } else {
10165 end = (end + context_lines).min(total_ops);
10166 break;
10167 }
10168 }
10169 if let Some(last) = hunks.last_mut() {
10171 if start <= last.1 {
10172 last.1 = end;
10173 } else {
10174 hunks.push((start, end));
10175 }
10176 } else {
10177 hunks.push((start, end));
10178 }
10179 i = end;
10180 } else {
10181 i += 1;
10182 }
10183 }
10184
10185 for (start, end) in hunks {
10187 let mut old_start = 1usize;
10189 let mut new_start = 1usize;
10190 for op in &ops[..start] {
10192 match op.tag {
10193 ' ' => {
10194 old_start += 1;
10195 new_start += 1;
10196 }
10197 '-' => {
10198 old_start += 1;
10199 }
10200 '+' => {
10201 new_start += 1;
10202 }
10203 _ => {}
10204 }
10205 }
10206 let mut old_count = 0usize;
10207 let mut new_count = 0usize;
10208 for op in &ops[start..end] {
10209 match op.tag {
10210 ' ' => {
10211 old_count += 1;
10212 new_count += 1;
10213 }
10214 '-' => {
10215 old_count += 1;
10216 }
10217 '+' => {
10218 new_count += 1;
10219 }
10220 _ => {}
10221 }
10222 }
10223
10224 output.push_str(&format!(
10225 "@@ -{},{} +{},{} @@\n",
10226 old_start, old_count, new_start, new_count
10227 ));
10228 for op in &ops[start..end] {
10229 output.push(op.tag);
10230 output.push_str(&op.line);
10231 output.push('\n');
10232 }
10233 }
10234
10235 output
10236}
10237
10238fn func_context_line_matches(line: &str, funcname_matcher: Option<&FuncnameMatcher>) -> bool {
10244 if let Some(m) = funcname_matcher {
10245 return m.match_line(line).is_some();
10246 }
10247 is_func_line(line, None)
10248}
10249
10250fn extract_function_context(
10251 header: &str,
10252 old_lines: &[&str],
10253 funcname_matcher: Option<&FuncnameMatcher>,
10254 _source_path: Option<&str>,
10255) -> Option<String> {
10256 let at_pos = header.find("-")?;
10258 let rest = &header[at_pos + 1..];
10259 let comma_or_space = rest.find([',', ' '])?;
10260 let start_str = &rest[..comma_or_space];
10261 let start_line: usize = start_str.parse().ok()?;
10262
10263 let old_token_end = rest.find([' ', '\t']).unwrap_or(rest.len());
10267 let old_token = &rest[..old_token_end];
10268 let old_count: usize = if let Some(comma) = old_token.find(',') {
10269 old_token[comma + 1..].parse().unwrap_or(1)
10270 } else {
10271 1
10272 };
10273
10274 if start_line == 0 {
10275 return None;
10276 }
10277
10278 if start_line == 0 {
10285 return None;
10286 }
10287 let search_end = if old_count == 0 {
10288 start_line.min(old_lines.len())
10289 } else {
10290 if start_line <= 1 {
10291 return None;
10292 }
10293 (start_line - 1).min(old_lines.len())
10294 };
10295 let truncate = |line: &str| {
10296 let mut end = line.len().min(80);
10297 while end > 0 && !line.is_char_boundary(end) {
10298 end -= 1;
10299 }
10300 let mut out = line[..end].to_owned();
10301 let tail = line[end..].trim_start();
10302 if tail.is_empty()
10307 || (!tail.starts_with('"')
10308 && !tail.starts_with('\'')
10309 && !line.starts_with("msgstr")
10310 && !line.starts_with("msgid"))
10311 {
10312 out = out.trim_end().to_owned();
10313 }
10314 out
10315 };
10316
10317 for i in (0..search_end).rev() {
10318 let line = old_lines[i];
10319 if line.is_empty() {
10320 continue;
10321 }
10322 if func_context_line_matches(line, funcname_matcher) {
10323 if let Some(matcher) = funcname_matcher {
10324 if let Some(matched) = matcher.match_line(line) {
10325 return Some(truncate(&matched));
10326 }
10327 continue;
10328 }
10329 return Some(truncate(line));
10330 }
10331 }
10332 None
10333}
10334
10335pub fn format_stat_line(
10339 path: &str,
10340 insertions: usize,
10341 deletions: usize,
10342 max_path_len: usize,
10343) -> String {
10344 format_stat_line_width(path, insertions, deletions, max_path_len, 0)
10345}
10346
10347pub fn format_stat_line_width(
10349 path: &str,
10350 insertions: usize,
10351 deletions: usize,
10352 max_path_len: usize,
10353 count_width: usize,
10354) -> String {
10355 let mut line =
10356 crate::diffstat::format_single_stat_line(path, insertions, deletions, max_path_len)
10357 .unwrap_or_else(|_| String::new());
10358 if count_width > 0 && line.contains(" | ") {
10359 let total = insertions + deletions;
10360 let cw = count_width;
10361 if let Some((left, rest)) = line.split_once(" | ") {
10362 if let Some((_, bar)) = rest.split_once(' ') {
10363 line = format!(" {left} | {total:>cw$} {bar}", cw = cw);
10364 } else {
10365 line = format!(" {left} | {total:>cw$}", cw = cw);
10366 }
10367 }
10368 }
10369 line
10370}
10371
10372fn last_line_index_without_trailing_newline(text: &str) -> Option<usize> {
10373 LineSide::from_text(text).incomplete_last_line_index()
10374}
10375
10376fn parse_unified_hunk_starts(header: &str) -> Option<(usize, usize)> {
10377 let rest = header.strip_prefix("@@ ")?;
10378 let (old_part, new_part) = rest.split_once(" +")?;
10379 let old_start = parse_unified_range_start(old_part.strip_prefix('-')?)?;
10380 let new_start = parse_unified_range_start(new_part.split(" @@").next()?)?;
10381 Some((old_start, new_start))
10382}
10383
10384fn parse_unified_range_start(spec: &str) -> Option<usize> {
10385 let spec = spec.trim();
10386 if let Some((start, _)) = spec.split_once(',') {
10387 start.parse().ok()
10388 } else {
10389 spec.parse().ok()
10390 }
10391}
10392
10393struct UnifiedDiffNoNewlineAnnotator {
10395 old_last: Option<usize>,
10396 new_last: Option<usize>,
10397 old_line: usize,
10398 new_line: usize,
10399 passthrough: bool,
10400}
10401
10402impl UnifiedDiffNoNewlineAnnotator {
10403 fn new(old: &str, new: &str) -> Self {
10404 let old_last = last_line_index_without_trailing_newline(old);
10405 let new_last = last_line_index_without_trailing_newline(new);
10406 Self {
10407 old_last,
10408 new_last,
10409 old_line: 1,
10410 new_line: 1,
10411 passthrough: old_last.is_none() && new_last.is_none(),
10412 }
10413 }
10414
10415 fn write_hunk<W: std::io::Write>(&mut self, out: &mut W, patch: &str) -> Result<()> {
10416 if self.passthrough {
10417 out.write_all(patch.as_bytes())?;
10418 return Ok(());
10419 }
10420 let mut lines = patch.lines().peekable();
10421 while let Some(line) = lines.next() {
10422 self.write_annotated_line(out, line, &mut lines)?;
10423 }
10424 Ok(())
10425 }
10426
10427 fn write_annotated_line<W: std::io::Write>(
10428 &mut self,
10429 out: &mut W,
10430 line: &str,
10431 lines: &mut std::iter::Peekable<std::str::Lines<'_>>,
10432 ) -> Result<()> {
10433 if let Some((o, n)) = line
10434 .strip_prefix("@@ ")
10435 .and_then(|_| parse_unified_hunk_starts(line))
10436 {
10437 self.old_line = o;
10438 self.new_line = n;
10439 writeln!(out, "{line}")?;
10440 return Ok(());
10441 }
10442 if line.starts_with("--- ")
10443 || line.starts_with("+++ ")
10444 || line.starts_with("diff --git ")
10445 || line.starts_with("diff --cc ")
10446 || line.starts_with("diff --combined ")
10447 {
10448 writeln!(out, "{line}")?;
10449 return Ok(());
10450 }
10451 if line.starts_with('-') {
10452 writeln!(out, "{line}")?;
10453 let already_marked = lines
10454 .peek()
10455 .is_some_and(|next| *next == "\\ No newline at end of file");
10456 if !already_marked && self.old_last == Some(self.old_line.saturating_sub(1)) {
10457 writeln!(out, "\\ No newline at end of file")?;
10458 }
10459 self.old_line += 1;
10460 return Ok(());
10461 }
10462 if line.starts_with('+') {
10463 writeln!(out, "{line}")?;
10464 let already_marked = lines
10465 .peek()
10466 .is_some_and(|next| *next == "\\ No newline at end of file");
10467 if !already_marked && self.new_last == Some(self.new_line.saturating_sub(1)) {
10468 writeln!(out, "\\ No newline at end of file")?;
10469 }
10470 self.new_line += 1;
10471 return Ok(());
10472 }
10473 if line.starts_with(' ') {
10474 writeln!(out, "{line}")?;
10475 let already_marked = lines
10476 .peek()
10477 .is_some_and(|next| *next == "\\ No newline at end of file");
10478 if !already_marked {
10479 let old_idx = self.old_line.saturating_sub(1);
10480 let new_idx = self.new_line.saturating_sub(1);
10481 if self.old_last == Some(old_idx) || self.new_last == Some(new_idx) {
10482 writeln!(out, "\\ No newline at end of file")?;
10483 }
10484 }
10485 self.old_line += 1;
10486 self.new_line += 1;
10487 return Ok(());
10488 }
10489 writeln!(out, "{line}")?;
10490 Ok(())
10491 }
10492}
10493
10494#[must_use]
10497pub fn annotate_unified_diff_no_newline_at_eof(patch: &str, old: &str, new: &str) -> String {
10498 let mut annotator = UnifiedDiffNoNewlineAnnotator::new(old, new);
10499 if annotator.passthrough {
10500 return patch.to_string();
10501 }
10502 let mut out = Vec::with_capacity(patch.len() + 64);
10503 let mut lines = patch.lines().peekable();
10504 while let Some(line) = lines.next() {
10505 if annotator
10506 .write_annotated_line(&mut out, line, &mut lines)
10507 .is_err()
10508 {
10509 return patch.to_string();
10510 }
10511 }
10512 String::from_utf8(out).unwrap_or_else(|_| patch.to_string())
10513}
10514
10515#[must_use]
10517pub fn normalize_ignore_space_change_line(line: &str) -> String {
10518 let mut result = String::with_capacity(line.len());
10519 let mut in_space = false;
10520 for c in line.chars() {
10521 if c.is_whitespace() {
10522 if !in_space {
10523 result.push(' ');
10524 in_space = true;
10525 }
10526 } else {
10527 result.push(c);
10528 in_space = false;
10529 }
10530 }
10531 while result.ends_with(' ') {
10532 result.pop();
10533 }
10534 result
10535}
10536
10537#[must_use]
10543pub fn normalize_ignore_space_change(content: &str) -> String {
10544 content
10545 .lines()
10546 .map(normalize_ignore_space_change_line)
10547 .collect::<Vec<_>>()
10548 .join("\n")
10549}
10550
10551pub fn count_changes(old_content: &str, new_content: &str) -> (usize, usize) {
10555 count_changes_with_algorithm(old_content, new_content, similar::Algorithm::Myers, false)
10556}
10557
10558#[must_use]
10563pub fn count_changes_with_algorithm(
10564 old_content: &str,
10565 new_content: &str,
10566 algorithm: similar::Algorithm,
10567 use_git_histogram: bool,
10568) -> (usize, usize) {
10569 if use_git_histogram {
10570 use imara_diff::{Algorithm, Diff, InternedInput};
10571 let input = InternedInput::new(old_content, new_content);
10572 let mut d = Diff::compute(Algorithm::Histogram, &input);
10573 d.postprocess_lines(&input);
10574 return (d.count_additions() as usize, d.count_removals() as usize);
10575 }
10576
10577 use similar::{ChangeTag, TextDiff};
10578
10579 let diff = TextDiff::configure()
10580 .algorithm(algorithm)
10581 .diff_lines(old_content, new_content);
10582 let mut ins = 0;
10583 let mut del = 0;
10584
10585 for change in diff.iter_all_changes() {
10586 match change.tag() {
10587 ChangeTag::Insert => ins += 1,
10588 ChangeTag::Delete => del += 1,
10589 ChangeTag::Equal => {}
10590 }
10591 }
10592
10593 (ins, del)
10594}
10595
10596#[must_use]
10598pub fn count_changes_git_histogram(old_content: &str, new_content: &str) -> (usize, usize) {
10599 count_changes_with_algorithm(old_content, new_content, similar::Algorithm::Myers, true)
10600}
10601
10602#[must_use]
10607pub fn count_git_lines(data: &[u8]) -> usize {
10608 if data.is_empty() {
10609 return 0;
10610 }
10611 let mut count = 0usize;
10612 let mut nl_just_seen = false;
10613 for &ch in data {
10614 if ch == b'\n' {
10615 count += 1;
10616 nl_just_seen = true;
10617 } else {
10618 nl_just_seen = false;
10619 }
10620 }
10621 if !nl_just_seen {
10622 count += 1;
10623 }
10624 count
10625}
10626
10627pub const GIT_DIFF_MAX_SCORE: u64 = 60_000;
10629const DIFF_MAX_SCORE: u64 = GIT_DIFF_MAX_SCORE;
10630const DIFF_MINIMUM_BREAK_SIZE: usize = 400;
10631const DIFF_DEFAULT_BREAK_SCORE: u64 = 30_000;
10632pub const GIT_DIFF_DEFAULT_BREAK_SCORE: u64 = DIFF_DEFAULT_BREAK_SCORE;
10634pub const GIT_DIFF_DEFAULT_MERGE_SCORE_AFTER_BREAK: u64 = 36_000;
10637const DIFF_HASHBASE: u32 = 107_927;
10638
10639#[derive(Clone, Copy, Default)]
10640struct SpanSlot {
10641 hashval: u32,
10642 cnt: u32,
10643}
10644
10645struct SpanHashTop {
10646 alloc_log2: u8,
10647 free_slots: i32,
10648 data: Vec<SpanSlot>,
10649}
10650
10651impl SpanHashTop {
10652 fn new(initial_log2: u8) -> Self {
10653 let cap = 1usize << initial_log2;
10654 Self {
10655 alloc_log2: initial_log2,
10656 free_slots: initial_free(initial_log2),
10657 data: vec![SpanSlot::default(); cap],
10658 }
10659 }
10660
10661 fn len(&self) -> usize {
10662 1usize << self.alloc_log2
10663 }
10664
10665 fn add_span(&mut self, hashval: u32, cnt: u32) {
10666 loop {
10667 let lim = self.len();
10668 let mut bucket = (hashval as usize) & (lim - 1);
10669 loop {
10670 let h = &mut self.data[bucket];
10671 if h.cnt == 0 {
10672 h.hashval = hashval;
10673 h.cnt = cnt;
10674 self.free_slots -= 1;
10675 if self.free_slots < 0 {
10676 self.rehash();
10677 break;
10678 }
10679 return;
10680 }
10681 if h.hashval == hashval {
10682 h.cnt = h.cnt.saturating_add(cnt);
10683 return;
10684 }
10685 bucket += 1;
10686 if bucket >= lim {
10687 bucket = 0;
10688 }
10689 }
10690 }
10691 }
10692
10693 fn rehash(&mut self) {
10694 let old = std::mem::take(&mut self.data);
10695 let old_log = self.alloc_log2;
10696 self.alloc_log2 = old_log.saturating_add(1);
10697 let new_len = 1usize << self.alloc_log2;
10698 self.free_slots = initial_free(self.alloc_log2);
10699 self.data = vec![SpanSlot::default(); new_len];
10700 let old_sz = 1usize << old_log;
10701 for o in old.iter().take(old_sz) {
10702 let o = *o;
10703 if o.cnt == 0 {
10704 continue;
10705 }
10706 self.add_span_after_rehash(o.hashval, o.cnt);
10707 }
10708 }
10709
10710 fn add_span_after_rehash(&mut self, hashval: u32, cnt: u32) {
10711 loop {
10712 let lim = self.len();
10713 let mut bucket = (hashval as usize) & (lim - 1);
10714 loop {
10715 let h = &mut self.data[bucket];
10716 if h.cnt == 0 {
10717 h.hashval = hashval;
10718 h.cnt = cnt;
10719 self.free_slots -= 1;
10720 if self.free_slots < 0 {
10721 self.rehash();
10722 break;
10723 }
10724 return;
10725 }
10726 if h.hashval == hashval {
10727 h.cnt = h.cnt.saturating_add(cnt);
10728 return;
10729 }
10730 bucket += 1;
10731 if bucket >= lim {
10732 bucket = 0;
10733 }
10734 }
10735 }
10736 }
10737
10738 fn sort_by_hashval(&mut self) {
10739 let sz = self.len();
10740 self.data[..sz].sort_by(|a, b| {
10741 if a.cnt == 0 {
10742 return std::cmp::Ordering::Greater;
10743 }
10744 if b.cnt == 0 {
10745 return std::cmp::Ordering::Less;
10746 }
10747 a.hashval.cmp(&b.hashval)
10748 });
10749 }
10750}
10751
10752fn initial_free(sz_log2: u8) -> i32 {
10753 let sz = sz_log2 as i32;
10754 ((1i32 << sz_log2) * (sz - 3) / sz).max(0)
10755}
10756
10757fn hash_blob_spans(buf: &[u8], is_text: bool) -> SpanHashTop {
10758 let mut hash = SpanHashTop::new(9);
10759 let mut n = 0u32;
10760 let mut accum1: u32 = 0;
10761 let mut accum2: u32 = 0;
10762 let mut i = 0usize;
10763 while i < buf.len() {
10764 let c = buf[i] as u32;
10765 let old_1 = accum1;
10766 i += 1;
10767
10768 if is_text && c == b'\r' as u32 && i < buf.len() && buf[i] == b'\n' {
10769 continue;
10770 }
10771
10772 accum1 = accum1.wrapping_shl(7) ^ accum2.wrapping_shr(25);
10773 accum2 = accum2.wrapping_shl(7) ^ old_1.wrapping_shr(25);
10774 accum1 = accum1.wrapping_add(c);
10775 n += 1;
10776 if n < 64 && c != b'\n' as u32 {
10777 continue;
10778 }
10779 let hashval = (accum1.wrapping_add(accum2.wrapping_mul(0x61))) % DIFF_HASHBASE;
10780 hash.add_span(hashval, n);
10781 n = 0;
10782 accum1 = 0;
10783 accum2 = 0;
10784 }
10785 if n > 0 {
10786 let hashval = (accum1.wrapping_add(accum2.wrapping_mul(0x61))) % DIFF_HASHBASE;
10787 hash.add_span(hashval, n);
10788 }
10789 hash.sort_by_hashval();
10790 hash
10791}
10792
10793#[must_use]
10798pub fn diffcore_count_changes(old: &[u8], new: &[u8]) -> (u64, u64) {
10799 crate::trace::trace_span!("grit.diff.blob");
10800 let src_is_text = !crate::merge_file::is_binary(old);
10801 let dst_is_text = !crate::merge_file::is_binary(new);
10802 let src_count = hash_blob_spans(old, src_is_text);
10803 let dst_count = hash_blob_spans(new, dst_is_text);
10804 let mut sc: u64 = 0;
10805 let mut la: u64 = 0;
10806 let mut si = 0usize;
10807 let mut di = 0usize;
10808 let src_len = src_count.len();
10809 let dst_len = dst_count.len();
10810 loop {
10811 if si >= src_len || src_count.data[si].cnt == 0 {
10812 break;
10813 }
10814 let s_hash = src_count.data[si].hashval;
10815 let s_cnt = u64::from(src_count.data[si].cnt);
10816 while di < dst_len && dst_count.data[di].cnt != 0 && dst_count.data[di].hashval < s_hash {
10817 la += u64::from(dst_count.data[di].cnt);
10818 di += 1;
10819 }
10820 let mut dst_cnt = 0u64;
10821 if di < dst_len && dst_count.data[di].cnt != 0 && dst_count.data[di].hashval == s_hash {
10822 dst_cnt = u64::from(dst_count.data[di].cnt);
10823 di += 1;
10824 }
10825 if s_cnt < dst_cnt {
10826 la += dst_cnt - s_cnt;
10827 sc += s_cnt;
10828 } else {
10829 sc += dst_cnt;
10830 }
10831 si += 1;
10832 }
10833 while di < dst_len && dst_count.data[di].cnt != 0 {
10834 la += u64::from(dst_count.data[di].cnt);
10835 di += 1;
10836 }
10837 (sc, la)
10838}
10839
10840#[must_use]
10843pub fn should_break_rewrite_for_stat(old: &[u8], new: &[u8]) -> bool {
10844 should_break_rewrite_inner(old, new, DIFF_DEFAULT_BREAK_SCORE)
10845}
10846
10847#[must_use]
10851pub fn should_break_rewrite_pair(old: &[u8], new: &[u8], break_score: u64) -> bool {
10852 should_break_rewrite_inner(old, new, break_score)
10853}
10854
10855#[must_use]
10857pub fn apply_diff_filter(
10858 entries: Vec<DiffEntry>,
10859 filter: Option<super::filter::DiffFilter>,
10860) -> Vec<DiffEntry> {
10861 match filter {
10862 Some(f) => f.filter_entries(entries),
10863 None => entries,
10864 }
10865}
10866
10867#[must_use]
10869pub fn parse_diff_rename_threshold_percent(arg: &str) -> Option<u8> {
10870 let token = if arg.is_empty() { "50" } else { arg };
10871 let internal = parse_diff_rename_score_token(token)?;
10872 Some((internal.saturating_mul(100) / GIT_DIFF_MAX_SCORE).min(100) as u8)
10873}
10874
10875pub fn parse_diff_rename_score_token(arg: &str) -> Option<u64> {
10878 let mut num: u64 = 0;
10879 let mut scale: u64 = 1;
10880 let mut dot = false;
10881 let mut saw_digit = false;
10882 for ch in arg.chars() {
10883 if !dot && ch == '.' {
10884 scale = 1;
10885 dot = true;
10886 continue;
10887 }
10888 if ch == '%' {
10889 scale = if dot { scale.saturating_mul(100) } else { 100 };
10890 break;
10891 }
10892 if ch.is_ascii_digit() {
10893 saw_digit = true;
10894 if scale < 100_000 {
10895 scale = scale.saturating_mul(10);
10896 num = num.saturating_mul(10) + u64::from(ch as u8 - b'0');
10897 }
10898 } else {
10899 break;
10900 }
10901 }
10902 if !saw_digit {
10903 return None;
10904 }
10905 Some(if num >= scale {
10906 GIT_DIFF_MAX_SCORE
10907 } else {
10908 GIT_DIFF_MAX_SCORE * num / scale
10909 })
10910}
10911
10912#[must_use]
10915pub fn rewrite_merge_score(old: &[u8], new: &[u8]) -> Option<u64> {
10916 if old.is_empty() {
10917 return None;
10918 }
10919 let max_size = old.len().max(new.len());
10920 if max_size < DIFF_MINIMUM_BREAK_SIZE {
10921 return None;
10922 }
10923 let (src_copied, _) = diffcore_count_changes(old, new);
10924 let src_copied = src_copied.min(old.len() as u64);
10925 let src_removed = (old.len() as u64).saturating_sub(src_copied);
10926 Some(src_removed * DIFF_MAX_SCORE / old.len() as u64)
10927}
10928
10929#[must_use]
10931pub fn rewrite_dissimilarity_index_percent(old: &[u8], new: &[u8]) -> Option<u32> {
10932 let score = rewrite_merge_score(old, new)?;
10933 Some((score * 100 / DIFF_MAX_SCORE).min(100) as u32)
10934}
10935
10936fn should_break_rewrite_inner(src: &[u8], dst: &[u8], break_score: u64) -> bool {
10937 if src.is_empty() {
10938 return false;
10939 }
10940 let max_size = src.len().max(dst.len());
10941 if max_size < DIFF_MINIMUM_BREAK_SIZE {
10942 return false;
10943 }
10944 let (src_copied, literal_added) = diffcore_count_changes(src, dst);
10945 let src_copied = src_copied.min(src.len() as u64);
10946 let mut literal_added = literal_added;
10947 let dst_len = dst.len() as u64;
10948 if src_copied < dst_len && literal_added + src_copied > dst_len {
10949 literal_added = dst_len.saturating_sub(src_copied);
10950 }
10951 let src_removed = (src.len() as u64).saturating_sub(src_copied);
10952 let merge_score = src_removed * DIFF_MAX_SCORE / src.len() as u64;
10953 if merge_score > break_score {
10954 return true;
10955 }
10956 let delta_size = src_removed.saturating_add(literal_added);
10957 if delta_size * DIFF_MAX_SCORE / (max_size as u64) < break_score {
10958 return false;
10959 }
10960 let s = src.len() as u64;
10961 if (s * break_score < src_removed * DIFF_MAX_SCORE)
10962 && (literal_added * 20 < src_removed)
10963 && (literal_added * 20 < src_copied)
10964 {
10965 return false;
10966 }
10967 true
10968}
10969
10970struct FlatEntry {
10974 path: String,
10975 mode: u32,
10976 oid: ObjectId,
10977}
10978
10979fn flatten_tree(odb: &Odb, tree_oid: &ObjectId, prefix: &str) -> Result<Vec<FlatEntry>> {
10980 let entries = read_tree(odb, tree_oid)?;
10981 let mut result = Vec::new();
10982
10983 for entry in entries {
10984 let name_str = String::from_utf8_lossy(&entry.name);
10985 let path = format_path(prefix, &name_str);
10986 if is_tree_mode(entry.mode) {
10987 let nested = flatten_tree(odb, &entry.oid, &path)?;
10988 result.extend(nested);
10989 } else {
10990 result.push(FlatEntry {
10991 path,
10992 mode: entry.mode,
10993 oid: entry.oid,
10994 });
10995 }
10996 }
10997
10998 Ok(result)
10999}
11000
11001pub fn head_path_states(
11003 odb: &Odb,
11004 head_tree: Option<&ObjectId>,
11005) -> Result<std::collections::BTreeMap<String, (u32, ObjectId)>> {
11006 let mut m = std::collections::BTreeMap::new();
11007 let Some(t) = head_tree else {
11008 return Ok(m);
11009 };
11010 for fe in flatten_tree(odb, t, "")? {
11011 m.insert(fe.path, (fe.mode, fe.oid));
11012 }
11013 Ok(m)
11014}
11015
11016fn is_tree_mode(mode: u32) -> bool {
11018 mode == 0o040000
11019}
11020
11021fn entries_sorted_by_path(entries: &[DiffEntry]) -> bool {
11023 entries
11024 .windows(2)
11025 .all(|pair| pair[0].path().cmp(pair[1].path()) != std::cmp::Ordering::Greater)
11026}
11027
11028fn format_path(prefix: &str, name: &str) -> String {
11029 if prefix.is_empty() {
11030 name.to_owned()
11031 } else {
11032 let mut path = String::with_capacity(prefix.len() + 1 + name.len());
11033 path.push_str(prefix);
11034 path.push('/');
11035 path.push_str(name);
11036 path
11037 }
11038}
11039
11040pub fn format_mode(mode: u32) -> String {
11042 format!("{mode:06o}")
11043}
11044
11045#[must_use]
11049pub fn read_submodule_head_for_checkout(sub_dir: &Path) -> Option<ObjectId> {
11050 read_submodule_head(sub_dir)
11051}
11052
11053#[must_use]
11058pub fn submodule_commit_subject_line(c: &CommitData) -> String {
11059 let enc = c.encoding.as_deref().unwrap_or("UTF-8");
11060 let is_latin1 = enc.eq_ignore_ascii_case("ISO8859-1")
11061 || enc.eq_ignore_ascii_case("ISO-8859-1")
11062 || enc.eq_ignore_ascii_case("LATIN1")
11063 || enc.eq_ignore_ascii_case("ISO-8859-15");
11064 if let Some(raw) = c.raw_message.as_deref() {
11065 let line = raw.split(|b| *b == b'\n').next().unwrap_or(raw);
11066 if is_latin1 {
11067 return line
11068 .iter()
11069 .map(|&b| b as char)
11070 .collect::<String>()
11071 .trim()
11072 .to_owned();
11073 }
11074 return String::from_utf8_lossy(line).trim().to_string();
11075 }
11076 c.message.lines().next().unwrap_or("").trim().to_owned()
11077}
11078
11079fn submodule_worktree_is_unpopulated_placeholder(sub_dir: &Path) -> bool {
11082 match fs::read_dir(sub_dir) {
11083 Ok(mut it) => it.next().is_none(),
11084 Err(e) if e.kind() == std::io::ErrorKind::NotFound => true,
11085 Err(_) => false,
11086 }
11087}
11088
11089fn read_submodule_head(sub_dir: &Path) -> Option<ObjectId> {
11090 read_submodule_head_oid(sub_dir)
11091}
11092
11093#[must_use]
11095pub fn submodule_embedded_git_dir(sub_dir: &Path) -> Option<PathBuf> {
11096 let gitfile = sub_dir.join(".git");
11097 if gitfile.is_file() {
11098 let content = fs::read_to_string(&gitfile).ok()?;
11099 let gitdir = content
11100 .lines()
11101 .find_map(|l| l.strip_prefix("gitdir: "))?
11102 .trim();
11103 Some(if Path::new(gitdir).is_absolute() {
11104 PathBuf::from(gitdir)
11105 } else {
11106 sub_dir.join(gitdir)
11107 })
11108 } else if gitfile.is_dir() {
11109 Some(gitfile)
11110 } else {
11111 None
11112 }
11113}
11114
11115#[must_use]
11121pub(crate) fn is_unregistered_embedded_git_repo(index: &Index, rel: &str, abs: &Path) -> bool {
11122 if rel.is_empty() {
11123 return false;
11124 }
11125 if index
11126 .get(rel.as_bytes(), 0)
11127 .is_some_and(|e| e.mode == crate::index::MODE_GITLINK)
11128 {
11129 return false;
11130 }
11131 let Some(git_dir) = submodule_embedded_git_dir(abs) else {
11132 return false;
11133 };
11134 crate::submodule_gitdir::is_valid_git_dir(&git_dir)
11135}
11136
11137fn find_superproject_git(sub_dir: &Path) -> Option<(PathBuf, PathBuf)> {
11139 let mut cur = sub_dir.parent()?;
11140 loop {
11141 let git_path = cur.join(".git");
11142 if git_path.exists() {
11143 let gd = if git_path.is_file() {
11144 let content = fs::read_to_string(&git_path).ok()?;
11145 let line = content
11146 .lines()
11147 .find_map(|l| l.strip_prefix("gitdir: "))?
11148 .trim();
11149 if Path::new(line).is_absolute() {
11150 PathBuf::from(line)
11151 } else {
11152 cur.join(line)
11153 }
11154 } else {
11155 git_path
11156 };
11157 return Some((cur.to_path_buf(), gd));
11158 }
11159 cur = cur.parent()?;
11160 }
11161}
11162
11163pub fn read_submodule_head_oid(sub_dir: &Path) -> Option<ObjectId> {
11169 let mut git_dir = submodule_embedded_git_dir(sub_dir)?;
11172 if let Some((super_wt, super_git_dir)) = find_superproject_git(sub_dir) {
11173 let rel = sub_dir.strip_prefix(&super_wt).ok()?;
11174 let rel_str = rel.to_string_lossy().replace('\\', "/");
11175 let local_mod = super_git_dir
11176 .join("modules")
11177 .join(rel_str.trim_start_matches('/'));
11178 if local_mod.join("HEAD").exists() {
11179 let sg = super_git_dir.canonicalize().unwrap_or(super_git_dir);
11180 let cur = git_dir.canonicalize().unwrap_or_else(|_| git_dir.clone());
11181 if !cur.starts_with(&sg) {
11182 git_dir = local_mod;
11183 }
11184 }
11185 }
11186 let head_content = fs::read_to_string(git_dir.join("HEAD")).ok()?;
11187 let head_trimmed = head_content.trim();
11188 if head_trimmed.starts_with("ref: ") {
11189 match crate::refs::resolve_ref(&git_dir, "HEAD") {
11193 Ok(oid) => Some(oid),
11194 Err(_) => {
11195 let mut found = None;
11196 for branch in ["main", "master"] {
11197 let p = git_dir.join("refs/heads").join(branch);
11198 if let Ok(s) = fs::read_to_string(&p) {
11199 if let Ok(o) = ObjectId::from_hex(s.trim()) {
11200 found = Some(o);
11201 break;
11202 }
11203 }
11204 }
11205 found
11206 }
11207 }
11208 } else {
11209 ObjectId::from_hex(head_trimmed).ok()
11210 }
11211}
11212
11213#[must_use]
11225pub fn submodule_head_object_broken(sub_dir: &Path) -> bool {
11226 let Some(sub_git_dir) = submodule_embedded_git_dir(sub_dir) else {
11227 return false;
11228 };
11229 let Some(head_oid) = read_submodule_head_oid(sub_dir) else {
11230 return false;
11231 };
11232 let odb = Odb::new(&sub_git_dir.join("objects"));
11233 match odb.read(&head_oid) {
11234 Ok(obj) => parse_commit(&obj.data).is_err(),
11237 Err(_) => true,
11238 }
11239}
11240
11241#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
11243pub struct SubmodulePorcelainFlags {
11244 pub new_commits: bool,
11246 pub modified: bool,
11248 pub untracked: bool,
11250}
11251
11252pub fn submodule_porcelain_flags(
11258 super_worktree: &Path,
11259 rel_path: &str,
11260 recorded_oid: ObjectId,
11261) -> SubmodulePorcelainFlags {
11262 let sub_dir = super_worktree.join(rel_path);
11263 let Some(sub_git_dir) = submodule_embedded_git_dir(&sub_dir) else {
11264 return SubmodulePorcelainFlags::default();
11265 };
11266 let Some(sub_head) = read_submodule_head_oid(&sub_dir) else {
11267 return SubmodulePorcelainFlags::default();
11268 };
11269
11270 let new_commits = sub_head != recorded_oid;
11271
11272 let index_path = sub_git_dir.join("index");
11273 let mut sub_index = match crate::index::Index::load(&index_path) {
11274 Ok(ix) => ix,
11275 Err(_) => {
11276 return SubmodulePorcelainFlags {
11277 new_commits,
11278 ..Default::default()
11279 }
11280 }
11281 };
11282
11283 let tracked: std::collections::BTreeSet<String> = sub_index
11284 .entries
11285 .iter()
11286 .filter(|e| e.stage() == 0)
11287 .map(|e| String::from_utf8_lossy(&e.path).into_owned())
11288 .collect();
11289 let untracked = submodule_dir_has_untracked_inner(&sub_dir, &sub_dir, &tracked, &sub_index);
11290
11291 let objects_dir = sub_git_dir.join("objects");
11292 let odb = Odb::new(&objects_dir);
11293
11294 let sub_head_tree = (|| -> Option<ObjectId> {
11295 let h = fs::read_to_string(sub_git_dir.join("HEAD")).ok()?;
11296 let h_str = h.trim();
11297 let commit_oid = if let Some(r) = h_str.strip_prefix("ref: ") {
11298 let oid_hex = fs::read_to_string(sub_git_dir.join(r)).ok()?;
11299 ObjectId::from_hex(oid_hex.trim()).ok()?
11300 } else {
11301 ObjectId::from_hex(h_str).ok()?
11302 };
11303 let obj = odb.read(&commit_oid).ok()?;
11304 let commit = parse_commit(&obj.data).ok()?;
11305 Some(commit.tree)
11306 })();
11307
11308 let staged_dirty = sub_head_tree
11309 .as_ref()
11310 .map(|t| diff_index_to_tree(&odb, &sub_index, Some(t), false).map(|v| !v.is_empty()))
11311 .unwrap_or(Ok(false));
11312 let staged_dirty = staged_dirty.unwrap_or(false);
11313
11314 let unstaged_dirty = diff_index_to_worktree(&odb, &mut sub_index, &sub_dir, false, true)
11315 .map(|v| !v.is_empty())
11316 .unwrap_or(false);
11317
11318 let mut modified = staged_dirty || unstaged_dirty;
11319
11320 for e in &sub_index.entries {
11325 if e.stage() != 0 || e.mode != 0o160000 {
11326 continue;
11327 }
11328 let child = String::from_utf8_lossy(&e.path).into_owned();
11329 let full_rel = if rel_path.is_empty() {
11330 child
11331 } else {
11332 format!("{rel_path}/{child}")
11333 };
11334 let nested = submodule_porcelain_flags(super_worktree, &full_rel, e.oid);
11335 modified |= nested.modified;
11336 }
11337
11338 SubmodulePorcelainFlags {
11339 new_commits,
11340 modified,
11341 untracked,
11342 }
11343}
11344
11345fn submodule_dir_has_untracked_inner(
11346 dir: &Path,
11347 root: &Path,
11348 tracked: &std::collections::BTreeSet<String>,
11349 owning_index: &Index,
11350) -> bool {
11351 let entries = match fs::read_dir(dir) {
11352 Ok(e) => e,
11353 Err(_) => return false,
11354 };
11355 let mut sorted: Vec<_> = entries.filter_map(|e| e.ok()).collect();
11356 sorted.sort_by_key(|e| e.file_name());
11357
11358 for entry in sorted {
11359 let name = entry.file_name().to_string_lossy().to_string();
11360 if name == ".git" {
11361 continue;
11362 }
11363 let path = entry.path();
11364 let rel = path
11365 .strip_prefix(root)
11366 .map(|p| p.to_string_lossy().to_string())
11367 .unwrap_or_else(|_| name.clone());
11368
11369 let is_dir = entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false);
11370 if is_dir {
11371 let is_gitlink = owning_index
11372 .get(rel.as_bytes(), 0)
11373 .is_some_and(|e| e.mode == 0o160000);
11374 if is_gitlink {
11375 let Some(nested_git) = submodule_embedded_git_dir(&path) else {
11376 continue;
11377 };
11378 let nested_index_path = nested_git.join("index");
11379 let Ok(nested_ix) = crate::index::Index::load(&nested_index_path) else {
11380 continue;
11381 };
11382 let nested_tracked: std::collections::BTreeSet<String> = nested_ix
11383 .entries
11384 .iter()
11385 .filter(|e| e.stage() == 0)
11386 .map(|e| String::from_utf8_lossy(&e.path).into_owned())
11387 .collect();
11388 if submodule_dir_has_untracked_inner(&path, &path, &nested_tracked, &nested_ix) {
11389 return true;
11390 }
11391 } else if submodule_dir_has_untracked_inner(&path, root, tracked, owning_index) {
11392 return true;
11393 }
11394 } else if !tracked.contains(&rel) {
11395 return true;
11396 }
11397 }
11398 false
11399}
11400
11401pub fn apply_orderfile_entries(
11404 entries: Vec<DiffEntry>,
11405 order_path: &str,
11406 cwd: &Path,
11407) -> Result<Vec<DiffEntry>> {
11408 apply_orderfile(entries, order_path, cwd)
11409}
11410
11411fn apply_orderfile(
11412 mut entries: Vec<DiffEntry>,
11413 order_path: &str,
11414 cwd: &Path,
11415) -> Result<Vec<DiffEntry>> {
11416 let patterns = read_orderfile_patterns(order_path, cwd)?;
11417 let sort_key = |entry: &DiffEntry| -> usize {
11418 let path = entry
11419 .new_path
11420 .as_ref()
11421 .or(entry.old_path.as_ref())
11422 .cloned()
11423 .unwrap_or_default();
11424 for (i, pat) in patterns.iter().enumerate() {
11425 if orderfile_pattern_matches(pat, &path) {
11426 return i;
11427 }
11428 }
11429 patterns.len()
11430 };
11431 entries.sort_by_key(|e| sort_key(e));
11432 Ok(entries)
11433}
11434
11435pub fn read_orderfile_patterns(order_path: &str, cwd: &Path) -> Result<Vec<String>> {
11437 let path = Path::new(order_path);
11438 let resolved = if path.is_absolute() {
11439 path.to_path_buf()
11440 } else {
11441 cwd.join(path)
11442 };
11443 let _meta = std::fs::metadata(&resolved)
11444 .map_err(|e| Error::Message(format!("could not read orderfile {order_path}: {e}")))?;
11445 let mut f = std::fs::File::open(&resolved)
11446 .map_err(|e| Error::Message(format!("could not read orderfile {order_path}: {e}")))?;
11447 let mut content = String::new();
11448 std::io::Read::read_to_string(&mut f, &mut content)
11449 .map_err(|e| Error::Message(format!("could not read orderfile {order_path}: {e}")))?;
11450 Ok(content
11451 .lines()
11452 .map(|l| l.trim().to_string())
11453 .filter(|l| !l.is_empty() && !l.starts_with('#'))
11454 .collect())
11455}
11456
11457pub fn apply_rotate_skip_entries(
11459 mut entries: Vec<DiffEntry>,
11460 rotate_to: Option<&str>,
11461 skip_to: Option<&str>,
11462) -> Result<Vec<DiffEntry>> {
11463 let Some(needle) = rotate_to.or(skip_to) else {
11464 return Ok(entries);
11465 };
11466 let needle = needle.trim();
11467 if needle.is_empty() {
11468 return Ok(entries);
11469 }
11470 let idx = entries
11471 .iter()
11472 .position(|e| e.path() == needle)
11473 .ok_or_else(|| {
11474 Error::DiffPath(crate::error::DiffPathError::NoSuchPathInDiff {
11475 path: needle.to_owned(),
11476 })
11477 })?;
11478 if rotate_to.is_some() {
11479 entries.rotate_left(idx);
11480 }
11481 if let Some(skip) = skip_to.filter(|s| !s.trim().is_empty()) {
11482 let pos = entries
11483 .iter()
11484 .position(|e| e.path() == skip)
11485 .ok_or_else(|| {
11486 Error::DiffPath(crate::error::DiffPathError::NoSuchPathInDiff {
11487 path: skip.to_owned(),
11488 })
11489 })?;
11490 entries.drain(..pos);
11491 }
11492 Ok(entries)
11493}
11494
11495pub fn apply_rotate_skip_log_entries(
11498 odb: &Odb,
11499 commit_tree: &ObjectId,
11500 entries: Vec<DiffEntry>,
11501 rotate_to: Option<&str>,
11502 skip_to: Option<&str>,
11503) -> Result<Vec<DiffEntry>> {
11504 fn trimmed(s: Option<&str>) -> Option<&str> {
11509 s.map(str::trim).filter(|t| !t.is_empty())
11510 }
11511 if trimmed(rotate_to).is_none() && trimmed(skip_to).is_none() {
11512 return Ok(entries);
11513 }
11514 let tree_paths = crate::merge_diff::all_blob_paths_in_tree_order(odb, commit_tree);
11515 apply_rotate_skip_ordered_paths(&tree_paths, entries, rotate_to, skip_to)
11516}
11517
11518fn apply_rotate_skip_ordered_paths(
11519 tree_paths: &[String],
11520 entries: Vec<DiffEntry>,
11521 rotate_to: Option<&str>,
11522 skip_to: Option<&str>,
11523) -> Result<Vec<DiffEntry>> {
11524 let rotate = rotate_to.and_then(|s| {
11525 let t = s.trim();
11526 (!t.is_empty()).then_some(t)
11527 });
11528 let skip = skip_to.and_then(|s| {
11529 let t = s.trim();
11530 (!t.is_empty()).then_some(t)
11531 });
11532 if rotate.is_none() && skip.is_none() {
11533 return Ok(entries);
11534 }
11535
11536 use std::collections::HashMap;
11537 let mut by_path: HashMap<String, DiffEntry> = HashMap::new();
11538 for e in entries {
11539 by_path.insert(e.path().to_string(), e);
11540 }
11541
11542 if rotate.is_none() {
11545 let Some(skip_path) = skip else {
11546 return Ok(by_path.into_values().collect());
11547 };
11548 let idx = tree_paths
11549 .iter()
11550 .position(|p| p == skip_path)
11551 .ok_or_else(|| {
11552 Error::DiffPath(crate::error::DiffPathError::NoSuchPathInDiff {
11553 path: skip_path.to_owned(),
11554 })
11555 })?;
11556 let mut out = Vec::new();
11557 for p in tree_paths.iter().skip(idx) {
11558 if let Some(e) = by_path.remove(p) {
11559 out.push(e);
11560 }
11561 }
11562 return Ok(out);
11563 }
11564
11565 let Some(needle) = rotate else {
11566 return Ok(by_path.into_values().collect());
11567 };
11568 let idx = tree_paths.iter().position(|p| p == needle).ok_or_else(|| {
11569 Error::DiffPath(crate::error::DiffPathError::NoSuchPathInDiff {
11570 path: needle.to_owned(),
11571 })
11572 })?;
11573 let mut order: Vec<String> = tree_paths.to_vec();
11574 order.rotate_left(idx);
11575 if let Some(skip_path) = skip {
11576 let pos = order.iter().position(|p| p == skip_path).ok_or_else(|| {
11577 Error::DiffPath(crate::error::DiffPathError::NoSuchPathInDiff {
11578 path: skip_path.to_owned(),
11579 })
11580 })?;
11581 order.drain(..pos);
11582 }
11583 let mut out = Vec::new();
11584 for p in order {
11585 if let Some(e) = by_path.remove(&p) {
11586 out.push(e);
11587 }
11588 }
11589 Ok(out)
11590}
11591
11592pub fn orderfile_pattern_matches(pattern: &str, path: &str) -> bool {
11595 let name = path.rsplit('/').next().unwrap_or(path);
11596 orderfile_glob_match(pattern, name) || orderfile_glob_match(pattern, path)
11597}
11598
11599fn orderfile_glob_match(pattern: &str, text: &str) -> bool {
11601 let mut pi = 0;
11602 let mut ti = 0;
11603 let pb = pattern.as_bytes();
11604 let tb = text.as_bytes();
11605 let mut star_pi = usize::MAX;
11606 let mut star_ti = 0;
11607
11608 while ti < tb.len() {
11609 if pi < pb.len() && (pb[pi] == b'?' || pb[pi] == tb[ti]) {
11610 pi += 1;
11611 ti += 1;
11612 } else if pi < pb.len() && pb[pi] == b'*' {
11613 star_pi = pi;
11614 star_ti = ti;
11615 pi += 1;
11616 } else if star_pi != usize::MAX {
11617 pi = star_pi + 1;
11618 star_ti += 1;
11619 ti = star_ti;
11620 } else {
11621 return false;
11622 }
11623 }
11624 while pi < pb.len() && pb[pi] == b'*' {
11625 pi += 1;
11626 }
11627 pi == pb.len()
11628}
11629
11630#[cfg(test)]
11631mod line_compare_key_tests {
11632 use super::*;
11633 use crate::diff::unified_emit::line_diff_token;
11634 use crate::diff::WhitespaceMode;
11635
11636 #[test]
11637 fn borrowed_keys_match_owned_line_tokens() {
11638 let texts = [
11639 "",
11640 "\n",
11641 "a",
11642 "a\n",
11643 "a\nb",
11644 "a\r\nb\r\n",
11645 "a\r\nb\r",
11646 "\n\n\nlast",
11647 "x\n\r\n\ry\n",
11648 ];
11649 for text in texts {
11650 let side = LineSide::from_text(text);
11651 let mut normalized = Vec::new();
11652 let keys = line_compare_keys(text, &side, WhitespaceMode::default(), &mut normalized);
11653 let expected: Vec<String> = side.lines.iter().map(line_diff_token).collect();
11654 assert_eq!(keys, expected, "text {text:?}");
11655 let source = text.as_bytes().as_ptr_range();
11656 assert!(
11657 keys.iter()
11658 .all(|key| source.contains(&key.as_ptr()) || key.is_empty()),
11659 "text {text:?}"
11660 );
11661 }
11662 }
11663
11664 #[test]
11665 fn whitespace_modes_compare_normalized_bodies() {
11666 let text = "a b\nab \n";
11667 let side = LineSide::from_text(text);
11668 let ws = WhitespaceMode {
11669 ignore_all_space: true,
11670 ..WhitespaceMode::default()
11671 };
11672 let mut normalized = Vec::new();
11673 let keys = line_compare_keys(text, &side, ws, &mut normalized);
11674 assert_eq!(keys, vec!["ab", "ab"]);
11675 }
11676
11677 #[test]
11678 fn xdiff_marks_only_edited_lines_in_large_file() {
11679 let old: String = (0..5_000).map(|i| format!("line{i}\n")).collect();
11680 let new: String = (0..5_000)
11681 .map(|i| {
11682 if i % 100 == 0 {
11683 format!("LINE{i}\n")
11684 } else {
11685 format!("line{i}\n")
11686 }
11687 })
11688 .collect();
11689 let body =
11690 git_xdiff_unified_body_raw(&old, &new, 3, 0, true, false, WhitespaceMode::default());
11691 assert_eq!(body.matches("\n@@ ").count() + 1, 50);
11692 assert_eq!(body.lines().filter(|l| l.starts_with('-')).count(), 50);
11693 assert_eq!(body.lines().filter(|l| l.starts_with('+')).count(), 50);
11694 assert!(body.starts_with("@@ -1,4 +1,4 @@\n-line0\n+LINE0\n line1\n"));
11695 }
11696}
11697
11698#[cfg(test)]
11699mod diff_index_to_tree_cache_tree_tests {
11700 use std::rc::Rc;
11701 use std::sync::atomic::{AtomicUsize, Ordering};
11702
11703 use super::*;
11704 use crate::index::{Index, IndexEntry, MODE_EXECUTABLE, MODE_GITLINK, MODE_REGULAR};
11705 use crate::objects::ObjectKind;
11706 use crate::write_tree::{build_cache_tree_from_index, write_tree_from_index};
11707 use tempfile::TempDir;
11708
11709 struct TreeReadCounter {
11710 counter: Rc<AtomicUsize>,
11711 }
11712
11713 impl TreeReadCounter {
11714 fn start() -> Self {
11715 let counter = Rc::new(AtomicUsize::new(0));
11716 set_diff_tree_read_hook_for_tests(Some(counter.clone()));
11717 Self { counter }
11718 }
11719
11720 fn get(&self) -> usize {
11721 self.counter.load(Ordering::Relaxed)
11722 }
11723 }
11724
11725 impl Drop for TreeReadCounter {
11726 fn drop(&mut self) {
11727 set_diff_tree_read_hook_for_tests(None);
11728 }
11729 }
11730
11731 fn diff_index_to_tree_flatten_reference(
11732 odb: &Odb,
11733 index: &Index,
11734 head_tree: &ObjectId,
11735 ignore_submodules: bool,
11736 ) -> Result<Vec<DiffEntry>> {
11737 let mut flat_index = index.clone();
11738 flat_index.clear_cache_tree();
11739 diff_index_to_tree(odb, &flat_index, Some(head_tree), ignore_submodules)
11740 }
11741
11742 fn index_entry(path: &str, mode: u32, oid: ObjectId) -> IndexEntry {
11743 IndexEntry {
11744 ctime_sec: 0,
11745 ctime_nsec: 0,
11746 mtime_sec: 0,
11747 mtime_nsec: 0,
11748 dev: 0,
11749 ino: 0,
11750 mode,
11751 uid: 0,
11752 gid: 0,
11753 size: 0,
11754 oid,
11755 flags: path.len().min(0xfff) as u16,
11756 flags_extended: None,
11757 path: path.as_bytes().to_vec(),
11758 base_index_pos: 0,
11759 }
11760 }
11761
11762 fn repo_with_tree(depth: usize, width: usize) -> (TempDir, Odb, ObjectId, Index) {
11763 let temp = TempDir::new().expect("tempdir");
11764 let odb = Odb::new(temp.path());
11765 let mut index = Index::new();
11766 for d in 0..depth {
11767 for w in 0..width {
11768 let path = format!("unchanged/d{d}/f{w}.txt");
11769 let oid = odb
11770 .write(ObjectKind::Blob, format!("body-{d}-{w}").as_bytes())
11771 .expect("blob");
11772 index.add_or_replace(index_entry(&path, MODE_REGULAR, oid));
11773 }
11774 }
11775 let head_tree = write_tree_from_index(&odb, &index, "").expect("write tree");
11776 let cache = build_cache_tree_from_index(&odb, &index).expect("cache-tree");
11777 index.set_cache_tree(cache);
11778 index.cache_tree_root = Some(head_tree);
11779 (temp, odb, head_tree, index)
11780 }
11781
11782 #[test]
11783 fn diff_index_to_tree_cache_tree_fast_path_matches_slow() {
11784 let (_temp, odb, head_tree, mut index) = repo_with_tree(4, 5);
11785 assert!(index.cache_tree.as_ref().is_some_and(|c| c.is_valid()));
11786
11787 let mut state: u64 = 0xDEAD_BEEF;
11788 for _ in 0..24 {
11789 state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
11790 let d = (state % 4) as usize;
11791 state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
11792 let w = (state % 5) as usize;
11793 let path = format!("unchanged/d{d}/f{w}.txt");
11794 index.invalidate_cache_tree_for_path(path.as_bytes());
11795 assert!(
11796 index.cache_tree_root.is_none(),
11797 "partial invalidation clears cache_tree_root like Index::add"
11798 );
11799 assert!(
11800 index.cache_tree.as_ref().is_some_and(|r| !r.is_valid()),
11801 "root cache node must be invalid after partial invalidation"
11802 );
11803 if let Some(e) = index.get_mut(path.as_bytes(), 0) {
11804 let new_oid = odb
11805 .write(ObjectKind::Blob, format!("new-{d}-{w}").as_bytes())
11806 .expect("blob");
11807 e.oid = new_oid;
11808 }
11809 let fast = diff_index_to_tree(&odb, &index, Some(&head_tree), false).expect("fast");
11810 let flat = diff_index_to_tree_flatten_reference(&odb, &index, &head_tree, false)
11811 .expect("flat");
11812 assert_eq!(fast, flat, "mismatch after mutating {path}");
11813 }
11814 }
11815
11816 #[test]
11817 fn diff_index_to_tree_skips_unchanged_subtrees() {
11818 let (_temp, odb, head_tree, index) = repo_with_tree(5, 6);
11819 let reads = TreeReadCounter::start();
11820 diff_index_to_tree(&odb, &index, Some(&head_tree), false).expect("cached diff");
11821 let cached_reads = reads.get();
11822
11823 let mut no_cache = index.clone();
11824 no_cache.clear_cache_tree();
11825 let reads = TreeReadCounter::start();
11826 diff_index_to_tree(&odb, &no_cache, Some(&head_tree), false).expect("flat diff");
11827 let flat_reads = reads.get();
11828
11829 assert!(
11830 cached_reads < flat_reads,
11831 "expected fewer tree reads with cache-tree (cached={cached_reads}, flat={flat_reads})"
11832 );
11833 assert_eq!(
11834 cached_reads, 1,
11835 "unchanged repo should only read the root tree"
11836 );
11837 }
11838
11839 #[test]
11840 fn diff_index_to_tree_partial_invalidation_skips_sibling_subtree() {
11841 let (_temp, odb, head_tree, mut index) = repo_with_tree(5, 6);
11842 let path = "unchanged/d1/f2.txt";
11843 index.invalidate_cache_tree_for_path(path.as_bytes());
11844 let new_oid = odb.write(ObjectKind::Blob, b"changed").expect("blob");
11845 index.get_mut(path.as_bytes(), 0).expect("entry").oid = new_oid;
11846
11847 let flat =
11848 diff_index_to_tree_flatten_reference(&odb, &index, &head_tree, false).expect("flat");
11849 let reads = TreeReadCounter::start();
11850 let fast = diff_index_to_tree(&odb, &index, Some(&head_tree), false).expect("fast");
11851 let cached_reads = reads.get();
11852 assert_eq!(fast, flat);
11853
11854 let reads = TreeReadCounter::start();
11855 diff_index_to_tree_flatten_reference(&odb, &index, &head_tree, false).expect("flat");
11856 let flat_reads = reads.get();
11857
11858 assert!(
11859 cached_reads < flat_reads,
11860 "partial cache-tree should skip sibling dirs (cached={cached_reads}, flat={flat_reads})"
11861 );
11862 }
11863
11864 fn cache_leaf(name: &str) -> CacheTreeNode {
11865 CacheTreeNode::valid(name.as_bytes().to_vec(), 1, ObjectId::zero(), Vec::new())
11866 }
11867
11868 #[test]
11869 fn cache_children_lookup_matches_linear_scan_on_wide_unsorted_nodes() {
11870 let mut children: Vec<CacheTreeNode> = (0..CACHE_CHILD_LINEAR_SCAN_MAX * 3)
11871 .rev()
11872 .map(|i| cache_leaf(&format!("d{i:03}")))
11873 .collect();
11874 let mut first_dup = cache_leaf("d007");
11875 first_dup.entry_count = 7;
11876 children.insert(3, first_dup);
11877 let root = CacheTreeNode::valid(Vec::new(), 0, ObjectId::zero(), children);
11878
11879 let lookup = CacheChildren::new(Some(&root));
11880 assert!(
11881 !lookup.sorted.is_empty(),
11882 "wide nodes must use the sorted lookup"
11883 );
11884 for name in ["d000", "d007", "d020", "d047", "zzz", "d", ""] {
11885 let linear = root.children.iter().find(|c| c.name == name.as_bytes());
11886 let fast = lookup.get(name.as_bytes());
11887 assert_eq!(
11888 fast.map(|c| (&c.name, c.entry_count)),
11889 linear.map(|c| (&c.name, c.entry_count)),
11890 "lookup of {name:?}"
11891 );
11892 }
11893 assert_eq!(lookup.get(b"d007").map(|c| c.entry_count), Some(7));
11894
11895 let narrow = CacheTreeNode::valid(
11896 Vec::new(),
11897 0,
11898 ObjectId::zero(),
11899 vec![cache_leaf("b"), cache_leaf("a")],
11900 );
11901 let lookup = CacheChildren::new(Some(&narrow));
11902 assert!(lookup.sorted.is_empty());
11903 assert_eq!(lookup.get(b"a").map(|c| c.name.as_slice()), Some(&b"a"[..]));
11904 assert!(lookup.get(b"c").is_none());
11905 assert!(CacheChildren::new(None).get(b"a").is_none());
11906 }
11907
11908 #[test]
11909 fn index_relative_in_dir_requires_a_component_boundary() {
11910 assert_eq!(index_relative_in_dir(b"a/b", b""), Some(&b"a/b"[..]));
11911 assert_eq!(index_relative_in_dir(b"a/b", b"a"), Some(&b"b"[..]));
11912 assert_eq!(index_relative_in_dir(b"a", b"a"), Some(&b""[..]));
11913 assert_eq!(index_relative_in_dir(b"ab/c", b"a"), None);
11914 assert_eq!(index_relative_in_dir(b"a.c", b"a"), None);
11915 assert_eq!(index_relative_in_dir(b"b", b"a"), None);
11916 assert_eq!(join_index_path(b"", b"x"), b"x".to_vec());
11917 assert_eq!(join_index_path(b"a/b", b"x"), b"a/b/x".to_vec());
11918 }
11919
11920 #[test]
11923 fn diff_index_to_tree_wide_unsorted_cache_tree_matches_flatten() {
11924 let temp = TempDir::new().expect("tempdir");
11925 let odb = Odb::new(temp.path());
11926 let mut index = Index::new();
11927 let width = CACHE_CHILD_LINEAR_SCAN_MAX * 4;
11928 for d in 0..width {
11929 for f in 0..2 {
11930 let path = format!("dir{d:03}/sub/f{f}.txt");
11931 let oid = odb.write(ObjectKind::Blob, path.as_bytes()).expect("blob");
11932 index.add_or_replace(index_entry(&path, MODE_REGULAR, oid));
11933 }
11934 }
11935 let head_tree = write_tree_from_index(&odb, &index, "").expect("write tree");
11936 let mut cache = build_cache_tree_from_index(&odb, &index).expect("cache-tree");
11937 cache.children.reverse();
11938 index.set_cache_tree(cache);
11939 index.cache_tree_root = Some(head_tree);
11940
11941 let changed = [
11942 "dir003/sub/f1.txt",
11943 "dir031/sub/f0.txt",
11944 "dir063/sub/f1.txt",
11945 ];
11946 for path in changed {
11947 index.invalidate_cache_tree_for_path(path.as_bytes());
11948 let oid = odb.write(ObjectKind::Blob, b"changed").expect("blob");
11949 index.get_mut(path.as_bytes(), 0).expect("entry").oid = oid;
11950 }
11951 for path in ["dir010/sub/new.txt", "dir999/new.txt", "top.txt"] {
11952 index.invalidate_cache_tree_for_path(path.as_bytes());
11953 let oid = odb.write(ObjectKind::Blob, path.as_bytes()).expect("blob");
11954 index.add_or_replace(index_entry(path, MODE_REGULAR, oid));
11955 }
11956 for path in [
11957 "dir020/sub/f0.txt",
11958 "dir020/sub/f1.txt",
11959 "dir021/sub/f0.txt",
11960 ] {
11961 index.invalidate_cache_tree_for_path(path.as_bytes());
11962 index.remove(path.as_bytes());
11963 }
11964
11965 let flat =
11966 diff_index_to_tree_flatten_reference(&odb, &index, &head_tree, false).expect("flat");
11967 let reads = TreeReadCounter::start();
11968 let fast = diff_index_to_tree(&odb, &index, Some(&head_tree), false).expect("fast");
11969 let cached_reads = reads.get();
11970 assert_eq!(fast, flat);
11971 assert_eq!(fast.len(), changed.len() + 3 + 3);
11972 assert!(
11973 cached_reads <= 1 + 2 * 6,
11974 "unchanged siblings must be skipped through the cache-tree (reads={cached_reads})"
11975 );
11976 }
11977
11978 #[test]
11979 fn diff_index_to_tree_cache_tree_handles_non_utf8_directory_names() {
11980 let temp = TempDir::new().expect("tempdir");
11981 let odb = Odb::new(temp.path());
11982 let mut index = Index::new();
11983 let raw_dirs: [&[u8]; 3] = [b"caf\xe9", b"caf\xef\xbf\xbd", b"plain"];
11984 for dir in raw_dirs {
11985 for f in [&b"a.txt"[..], b"b.txt"] {
11986 let path = [dir, b"/", f].concat();
11987 let oid = odb.write(ObjectKind::Blob, &path).expect("blob");
11988 let mut entry = index_entry("x", MODE_REGULAR, oid);
11989 entry.flags = path.len().min(0xfff) as u16;
11990 entry.path = path;
11991 index.add_or_replace(entry);
11992 }
11993 }
11994 let head_tree = write_tree_from_index(&odb, &index, "").expect("write tree");
11995 let cache = build_cache_tree_from_index(&odb, &index).expect("cache-tree");
11996 index.set_cache_tree(cache);
11997 index.cache_tree_root = Some(head_tree);
11998
11999 let changed: &[u8] = b"caf\xe9/b.txt";
12000 index.invalidate_cache_tree_for_path(changed);
12001 let oid = odb.write(ObjectKind::Blob, b"changed").expect("blob");
12002 index.get_mut(changed, 0).expect("entry").oid = oid;
12003
12004 let fast = diff_index_to_tree(&odb, &index, Some(&head_tree), false).expect("fast");
12007 assert_eq!(fast.len(), 1, "{fast:?}");
12008 assert_eq!(fast[0].status, DiffStatus::Modified);
12009 assert_eq!(
12010 fast[0].old_oid,
12011 odb.write(ObjectKind::Blob, changed).expect("blob")
12012 );
12013 assert_eq!(fast[0].new_oid, oid);
12014 }
12015
12016 #[test]
12017 fn diff_index_to_tree_reports_mode_gitlink_and_intent_to_add() {
12018 let temp = TempDir::new().expect("tempdir");
12019 let odb = Odb::new(temp.path());
12020 let blob = odb.write(ObjectKind::Blob, b"x").expect("blob");
12021 let mut index = Index::new();
12022 index.add_or_replace(index_entry("tracked.txt", MODE_REGULAR, blob));
12023 let head_tree = write_tree_from_index(&odb, &index, "").expect("tree");
12024 let cache = build_cache_tree_from_index(&odb, &index).expect("cache");
12025 index.set_cache_tree(cache);
12026
12027 let mode_entry = index_entry("tracked.txt", MODE_EXECUTABLE, blob);
12028 index.add_or_replace(mode_entry);
12029 index.invalidate_cache_tree_for_path(b"tracked.txt");
12030
12031 let diffs = diff_index_to_tree(&odb, &index, Some(&head_tree), false).expect("diff");
12032 assert_eq!(diffs.len(), 1);
12033 assert_eq!(diffs[0].status, DiffStatus::Modified);
12034 assert_eq!(diffs[0].old_mode, format_mode(MODE_REGULAR));
12035 assert_eq!(diffs[0].new_mode, format_mode(MODE_EXECUTABLE));
12036
12037 let mut ita = index_entry("new.txt", MODE_REGULAR, blob);
12039 ita.set_intent_to_add(true);
12040 index.add_or_replace(ita);
12041 let diffs = diff_index_to_tree(&odb, &index, Some(&head_tree), false).expect("diff");
12042 assert!(
12043 diffs.iter().all(|d| d.path() != "new.txt"),
12044 "intent-to-add must not appear in staged diff"
12045 );
12046
12047 let gitlink: ObjectId = "855827c583bc30645ba427885caa40c5b81764d2"
12049 .parse()
12050 .expect("oid");
12051 let mut sub_index = Index::new();
12052 sub_index.add_or_replace(index_entry("sub", MODE_GITLINK, gitlink));
12053 let sub_tree = write_tree_from_index(&odb, &sub_index, "").expect("tree");
12054 let sub_cache = build_cache_tree_from_index(&odb, &sub_index).expect("cache");
12055 sub_index.set_cache_tree(sub_cache);
12056 let diffs =
12057 diff_index_to_tree(&odb, &sub_index, Some(&sub_tree), true).expect("gitlink diff");
12058 assert!(
12059 diffs.is_empty(),
12060 "ignored gitlink should not diff: {diffs:?}"
12061 );
12062 }
12063
12064 #[test]
12065 fn ignore_submodules_skips_gitlink_in_added_directory_cache_walk() {
12066 let temp = TempDir::new().expect("tempdir");
12067 let odb = Odb::new(temp.path());
12068 let gitlink: ObjectId = "855827c583bc30645ba427885caa40c5b81764d2"
12069 .parse()
12070 .expect("oid");
12071 let head_tree = write_tree_from_index(&odb, &Index::new(), "").expect("empty tree");
12072 let mut index = Index::new();
12073 index.add_or_replace(index_entry("newdir/sub", MODE_GITLINK, gitlink));
12074 let cache = build_cache_tree_from_index(&odb, &index).expect("cache");
12075 index.set_cache_tree(cache);
12076 index.invalidate_cache_tree_for_path(b"newdir/sub");
12077
12078 let fast = diff_index_to_tree(&odb, &index, Some(&head_tree), true).expect("fast");
12079 let flat =
12080 diff_index_to_tree_flatten_reference(&odb, &index, &head_tree, true).expect("flat");
12081 assert_eq!(fast, flat);
12082 assert!(
12083 fast.is_empty(),
12084 "nested gitlinks in added dirs must be ignored: {fast:?}"
12085 );
12086 }
12087
12088 #[test]
12089 fn ignore_submodules_skips_gitlink_in_deleted_directory_cache_walk() {
12090 let temp = TempDir::new().expect("tempdir");
12091 let odb = Odb::new(temp.path());
12092 let blob = odb.write(ObjectKind::Blob, b"keep").expect("blob");
12093 let gitlink: ObjectId = "855827c583bc30645ba427885caa40c5b81764d2"
12094 .parse()
12095 .expect("oid");
12096 let mut index = Index::new();
12097 index.add_or_replace(index_entry("keep.txt", MODE_REGULAR, blob));
12098 index.add_or_replace(index_entry("olddir/sub", MODE_GITLINK, gitlink));
12099 let head_tree = write_tree_from_index(&odb, &index, "").expect("head");
12100 let cache = build_cache_tree_from_index(&odb, &index).expect("cache");
12101 index.set_cache_tree(cache);
12102 index
12103 .entries
12104 .retain(|e| !String::from_utf8_lossy(&e.path).starts_with("olddir/"));
12105 index.invalidate_cache_tree_for_path(b"olddir/sub");
12106
12107 let fast = diff_index_to_tree(&odb, &index, Some(&head_tree), true).expect("fast");
12108 let flat =
12109 diff_index_to_tree_flatten_reference(&odb, &index, &head_tree, true).expect("flat");
12110 assert_eq!(fast, flat);
12111 assert!(
12112 fast.is_empty(),
12113 "nested gitlinks in deleted dirs must be ignored: {fast:?}"
12114 );
12115 }
12116
12117 #[test]
12118 fn diff_trees_skips_identical_subtree_oids() {
12119 let temp = TempDir::new().expect("tempdir");
12120 let odb = Odb::new(temp.path());
12121 let leaf = odb.write(ObjectKind::Blob, b"leaf").expect("blob");
12122 let mut deep = Index::new();
12123 for i in 0..20 {
12124 deep.add_or_replace(index_entry(&format!("deep/n{i}.txt"), MODE_REGULAR, leaf));
12125 }
12126 let tree = write_tree_from_index(&odb, &deep, "").expect("tree");
12127 let reads = TreeReadCounter::start();
12128 let entries = diff_trees(&odb, Some(&tree), Some(&tree), "").expect("diff");
12129 assert!(entries.is_empty());
12130 assert_eq!(
12131 reads.get(),
12132 0,
12133 "identical tree oids should short-circuit without reading tree objects"
12134 );
12135 }
12136
12137 #[test]
12138 fn diff_tree_path_change_count_matches_diff_trees() {
12139 let temp = TempDir::new().expect("tempdir");
12140 let odb = Odb::new(temp.path());
12141 let a = odb.write(ObjectKind::Blob, b"a").expect("blob");
12142 let b = odb.write(ObjectKind::Blob, b"b").expect("blob");
12143 let mut left = Index::new();
12144 left.add_or_replace(index_entry("f.txt", MODE_REGULAR, a));
12145 let mut right = Index::new();
12146 right.add_or_replace(index_entry("f.txt", MODE_REGULAR, b));
12147 right.add_or_replace(index_entry("g.txt", MODE_REGULAR, a));
12148 let old_tree = write_tree_from_index(&odb, &left, "").expect("old");
12149 let new_tree = write_tree_from_index(&odb, &right, "").expect("new");
12150 let entries = diff_trees(&odb, Some(&old_tree), Some(&new_tree), "").expect("diff");
12151 let count =
12152 diff_tree_path_change_count(&odb, Some(&old_tree), Some(&new_tree), "").expect("count");
12153 assert_eq!(count, entries.len());
12154 assert_eq!(count, 2);
12155 }
12156}
12157
12158#[cfg(test)]
12159mod gitlink_tree_worktree_tests {
12160 use std::fs;
12161
12162 use crate::index::{Index, IndexEntry, MODE_GITLINK};
12163 use crate::objects::{ObjectId, ObjectKind};
12164 use crate::repo::init_repository;
12165
12166 use super::diff_tree_to_worktree;
12167
12168 fn encode_tree_gitlink(name: &str, oid: &ObjectId) -> Vec<u8> {
12169 let mut data = format!("160000 {name}\0").into_bytes();
12170 data.extend_from_slice(oid.as_bytes());
12171 data
12172 }
12173
12174 #[test]
12175 fn unpopulated_gitlink_matching_index_produces_no_diff_entry() {
12176 let dir = tempfile::tempdir().expect("tempdir");
12177 let work_tree = dir.path();
12178 fs::create_dir(work_tree.join("sub")).expect("sub placeholder");
12179
12180 let repo = init_repository(
12181 work_tree,
12182 false,
12183 "main",
12184 None,
12185 crate::RefStorageFormat::Files,
12186 )
12187 .expect("init");
12188 let gitlink: ObjectId = "855827c583bc30645ba427885caa40c5b81764d2"
12189 .parse()
12190 .expect("gitlink oid");
12191 let tree_oid = repo
12192 .odb()
12193 .write(ObjectKind::Tree, &encode_tree_gitlink("sub", &gitlink))
12194 .expect("write tree");
12195
12196 let path = b"sub".to_vec();
12197 let mut index = Index::new();
12198 index.add_or_replace(IndexEntry {
12199 ctime_sec: 0,
12200 ctime_nsec: 0,
12201 mtime_sec: 0,
12202 mtime_nsec: 0,
12203 dev: 0,
12204 ino: 0,
12205 mode: MODE_GITLINK,
12206 uid: 0,
12207 gid: 0,
12208 size: 0,
12209 oid: gitlink,
12210 flags: (path.len().min(0xfff)) as u16,
12211 flags_extended: None,
12212 path,
12213 base_index_pos: 0,
12214 });
12215
12216 let entries = diff_tree_to_worktree(&repo.odb(), Some(&tree_oid), work_tree, &index)
12217 .expect("diff_tree_to_worktree");
12218 assert!(
12219 entries.is_empty(),
12220 "clean uninitialized gitlink should not appear in tree→worktree diff: {entries:?}"
12221 );
12222 }
12223
12224 #[test]
12225 fn untracked_only_in_checked_out_submodule_produces_no_diff_entry() {
12226 use std::process::Command;
12227
12228 let dir = tempfile::tempdir().expect("tempdir");
12229 let work_tree = dir.path();
12230 fs::create_dir(work_tree.join("sub")).expect("sub dir");
12231
12232 let repo = init_repository(
12233 work_tree,
12234 false,
12235 "main",
12236 None,
12237 crate::RefStorageFormat::Files,
12238 )
12239 .expect("init");
12240 let sub = work_tree.join("sub");
12241 for args in [
12242 &["init", "-q"][..],
12243 &["config", "user.name", "T"][..],
12244 &["config", "user.email", "t@e.com"][..],
12245 ] {
12246 let status = Command::new("git")
12247 .current_dir(&sub)
12248 .args(args)
12249 .status()
12250 .expect("git");
12251 assert!(status.success(), "git {args:?} failed");
12252 }
12253 fs::write(sub.join("tracked.txt"), b"v1\n").expect("write");
12254 for args in [&["add", "tracked.txt"][..], &["commit", "-qm", "sub"][..]] {
12255 let status = Command::new("git")
12256 .current_dir(&sub)
12257 .args(args)
12258 .status()
12259 .expect("git");
12260 assert!(status.success());
12261 }
12262 let head_out = Command::new("git")
12263 .current_dir(&sub)
12264 .args(["rev-parse", "HEAD"])
12265 .output()
12266 .expect("rev-parse");
12267 let gitlink = std::str::from_utf8(&head_out.stdout)
12268 .expect("utf8")
12269 .trim()
12270 .parse::<ObjectId>()
12271 .expect("oid");
12272
12273 let tree_oid = repo
12274 .odb()
12275 .write(ObjectKind::Tree, &encode_tree_gitlink("sub", &gitlink))
12276 .expect("tree");
12277 let path = b"sub".to_vec();
12278 let mut index = Index::new();
12279 index.add_or_replace(IndexEntry {
12280 ctime_sec: 0,
12281 ctime_nsec: 0,
12282 mtime_sec: 0,
12283 mtime_nsec: 0,
12284 dev: 0,
12285 ino: 0,
12286 mode: MODE_GITLINK,
12287 uid: 0,
12288 gid: 0,
12289 size: 0,
12290 oid: gitlink,
12291 flags: (path.len().min(0xfff)) as u16,
12292 flags_extended: None,
12293 path,
12294 base_index_pos: 0,
12295 });
12296
12297 fs::write(sub.join("untracked.txt"), b"extra\n").expect("untracked");
12298
12299 let entries = diff_tree_to_worktree(&repo.odb(), Some(&tree_oid), work_tree, &index)
12300 .expect("diff_tree_to_worktree");
12301 assert!(
12302 entries.is_empty(),
12303 "untracked-only submodule dirt must not appear in default tree→worktree diff: {entries:?}"
12304 );
12305 }
12306}
12307
12308#[cfg(test)]
12309mod racy_index_mtime_diff_tests {
12310 use super::*;
12311 use crate::index::{entry_from_metadata, Index, MODE_REGULAR};
12312 use crate::objects::{HashAlgo, ObjectKind};
12313 use filetime::FileTime;
12314 use std::fs;
12315 use std::path::PathBuf;
12316
12317 const INDEX_MTIME: (u32, u32) = (1_700_000_000, 123_456_789);
12318
12319 fn pin_mtime(path: &Path, sec: u32, nsec: u32) {
12320 filetime::set_file_mtime(path, FileTime::from_unix_time(i64::from(sec), nsec))
12321 .expect("set mtime");
12322 }
12323
12324 fn minimal_sha256_repo(work_tree: &Path) -> (PathBuf, Odb) {
12325 let git_dir = work_tree.join(".git");
12326 fs::create_dir_all(git_dir.join("objects")).expect("objects dir");
12327 fs::write(
12328 git_dir.join("config"),
12329 "[extensions]\n\tobjectformat = sha256\n",
12330 )
12331 .expect("config");
12332 let odb = Odb::new(&git_dir.join("objects")).with_config_git_dir(git_dir.clone());
12333 (git_dir, odb)
12334 }
12335
12336 #[test]
12337 fn detects_same_size_change_when_entry_mtime_matches_index_mtime() {
12338 let td = tempfile::tempdir().expect("tempdir");
12339 let wt = td.path();
12340 let content_a = b"same length!!";
12341 let content_b = b"same length??";
12342 assert_eq!(content_a.len(), content_b.len());
12343
12344 fs::write(wt.join("f.txt"), content_a).expect("write");
12345 pin_mtime(&wt.join("f.txt"), INDEX_MTIME.0, INDEX_MTIME.1);
12346 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
12347 let git_dir = wt.join(".git");
12348 fs::create_dir_all(git_dir.join("objects")).expect("objects dir");
12349 let odb = Odb::new(&git_dir.join("objects"));
12350 let oid_a = odb.hash(ObjectKind::Blob, content_a).expect("hash");
12351 let entry = entry_from_metadata(&meta, b"f.txt", oid_a, MODE_REGULAR);
12352
12353 let mut index = Index::new();
12354 index.source_mtime = Some(INDEX_MTIME);
12355 index.entries.push(entry);
12356
12357 fs::write(wt.join("f.txt"), content_b).expect("rewrite");
12358 pin_mtime(&wt.join("f.txt"), INDEX_MTIME.0, INDEX_MTIME.1);
12359
12360 let diff = diff_index_to_worktree(&odb, &mut index, wt, false, false).expect("diff");
12361 assert_eq!(
12362 diff.len(),
12363 1,
12364 "racy entry must be re-hashed, not stat-trusted"
12365 );
12366 assert_eq!(diff[0].path(), "f.txt");
12367 }
12368
12369 #[test]
12370 #[cfg(unix)]
12371 fn non_utf8_symlink_clean_racy_equal_mtime_reports_no_diff() {
12372 use crate::index::MODE_SYMLINK;
12373 use std::os::unix::ffi::OsStrExt as _;
12374 use std::os::unix::fs::symlink;
12375
12376 let td = tempfile::tempdir().expect("tempdir");
12377 let wt = td.path();
12378 let target = std::ffi::OsStr::from_bytes(b"bad-\xff-target");
12379 let link_path = wt.join("link");
12380 symlink(target, &link_path).expect("symlink");
12381
12382 let ft = FileTime::from_unix_time(i64::from(INDEX_MTIME.0), INDEX_MTIME.1);
12383 filetime::set_symlink_file_times(&link_path, ft, ft).expect("set symlink mtime");
12384 let meta = fs::symlink_metadata(&link_path).expect("stat");
12385 let git_dir = wt.join(".git");
12386 fs::create_dir_all(git_dir.join("objects")).expect("objects dir");
12387 let odb = Odb::new(&git_dir.join("objects"));
12388 let oid = odb.hash(ObjectKind::Blob, target.as_bytes()).expect("hash");
12389 let entry = entry_from_metadata(&meta, b"link", oid, MODE_SYMLINK);
12390
12391 let mut index = Index::new();
12392 index.source_mtime = Some(INDEX_MTIME);
12393 index.entries.push(entry);
12394
12395 let diff = diff_index_to_worktree(&odb, &mut index, wt, false, false).expect("diff");
12396 assert!(
12397 diff.is_empty(),
12398 "clean non-UTF-8 symlink must not be reported modified: {diff:?}"
12399 );
12400 }
12401
12402 #[test]
12403 fn sha256_clean_racy_equal_mtime_reports_no_diff() {
12404 let td = tempfile::tempdir().expect("tempdir");
12405 let wt = td.path();
12406 let (_git_dir, odb) = minimal_sha256_repo(wt);
12407 assert_eq!(odb.hash_algo(), HashAlgo::Sha256);
12408
12409 let bytes = b"sha256 width!!";
12410 fs::write(wt.join("f.txt"), bytes).expect("write");
12411 pin_mtime(&wt.join("f.txt"), INDEX_MTIME.0, INDEX_MTIME.1);
12412 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
12413 let oid = odb.hash(ObjectKind::Blob, bytes).expect("hash");
12414 let entry = entry_from_metadata(&meta, b"f.txt", oid, MODE_REGULAR);
12415
12416 let mut index = Index::new();
12417 index.hash_algo = HashAlgo::Sha256;
12418 index.source_mtime = Some(INDEX_MTIME);
12419 index.entries.push(entry);
12420
12421 let diff = diff_index_to_worktree(&odb, &mut index, wt, false, false).expect("diff");
12422 assert!(
12423 diff.is_empty(),
12424 "clean SHA-256 racy entry must not be reported modified: {diff:?}"
12425 );
12426 }
12427}
12428
12429#[cfg(test)]
12430mod smudge_racily_clean_tests {
12431 use super::*;
12432 use crate::config::ConfigSet;
12433 use crate::crlf::{self, FileAttrs};
12434 use crate::index::{entry_from_metadata, Index, MODE_REGULAR};
12435 use crate::objects::{HashAlgo, ObjectKind};
12436 use filetime::FileTime;
12437 use std::fs;
12438 use std::path::PathBuf;
12439
12440 const INDEX_MTIME_PIN: (u32, u32) = (1_700_000_000, 123_456_789);
12441
12442 fn mtime_from_metadata(meta: &fs::Metadata) -> (u32, u32) {
12443 #[cfg(unix)]
12444 {
12445 use std::os::unix::fs::MetadataExt;
12446 (meta.mtime() as u32, meta.mtime_nsec() as u32)
12447 }
12448 #[cfg(not(unix))]
12449 {
12450 use std::time::UNIX_EPOCH;
12451 let mtime = meta
12452 .modified()
12453 .ok()
12454 .and_then(|t| t.duration_since(UNIX_EPOCH).ok())
12455 .unwrap_or_default();
12456 (mtime.as_secs() as u32, mtime.subsec_nanos())
12457 }
12458 }
12459
12460 fn pin_mtime_and_sample(path: &Path, sec: u32, nsec: u32) -> (u32, u32) {
12462 filetime::set_file_mtime(path, FileTime::from_unix_time(i64::from(sec), nsec))
12463 .expect("set mtime");
12464 let meta = fs::symlink_metadata(path).expect("stat file");
12465 mtime_from_metadata(&meta)
12466 }
12467
12468 fn setup_index_mtime(work_tree: &Path) -> (u32, u32) {
12470 let probe = work_tree.join(".grit-index-mtime-probe");
12471 fs::write(&probe, b"").expect("probe file");
12472 pin_mtime_and_sample(&probe, INDEX_MTIME_PIN.0, INDEX_MTIME_PIN.1)
12473 }
12474
12475 fn minimal_repo(work_tree: &Path, config: &str) -> (PathBuf, Odb) {
12476 let git_dir = work_tree.join(".git");
12477 fs::create_dir_all(git_dir.join("objects")).expect("objects dir");
12478 fs::write(git_dir.join("config"), config).expect("config");
12479 let odb = Odb::new(&git_dir.join("objects")).with_config_git_dir(git_dir.clone());
12480 (git_dir, odb)
12481 }
12482
12483 fn staged_entry_for_file(
12484 odb: &Odb,
12485 work_tree: &Path,
12486 rel: &str,
12487 bytes: &[u8],
12488 mtime: (u32, u32),
12489 ) -> IndexEntry {
12490 let path = work_tree.join(rel);
12491 fs::write(&path, bytes).expect("write file");
12492 pin_mtime_and_sample(&path, mtime.0, mtime.1);
12493 let meta = fs::symlink_metadata(&path).expect("stat file");
12494 let oid = odb.hash(ObjectKind::Blob, bytes).expect("hash");
12495 entry_from_metadata(&meta, rel.as_bytes(), oid, MODE_REGULAR)
12496 }
12497
12498 fn staged_entry_with_oid(
12499 work_tree: &Path,
12500 rel: &str,
12501 worktree_bytes: &[u8],
12502 oid: crate::objects::ObjectId,
12503 mtime: (u32, u32),
12504 ) -> IndexEntry {
12505 let path = work_tree.join(rel);
12506 fs::write(&path, worktree_bytes).expect("write file");
12507 pin_mtime_and_sample(&path, mtime.0, mtime.1);
12508 let meta = fs::symlink_metadata(&path).expect("stat file");
12509 entry_from_metadata(&meta, rel.as_bytes(), oid, MODE_REGULAR)
12510 }
12511
12512 #[test]
12513 fn index_stat_cache_trustworthy_rejects_unknown_mtime() {
12514 assert!(!super::index_stat_cache_trustworthy(None));
12515 assert!(!super::index_stat_cache_trustworthy(Some((0, 0))));
12516 assert!(super::index_stat_cache_trustworthy(Some((1, 0))));
12517 }
12518
12519 #[test]
12520 fn index_entry_skips_content_hash_after_stat_match_when_trusted_and_not_racy() {
12521 use crate::index::StatMatchPolicy;
12522 let td = tempfile::tempdir().expect("tempdir");
12523 let wt = td.path();
12524 let (git_dir, odb) = minimal_repo(wt, "[core]\n\tfilemode = true\n");
12525 let rel = "trusted.txt";
12526 let file_mtime = (1_700_000_000, 0);
12527 let mut entry = staged_entry_for_file(&odb, wt, rel, b"payload\n", file_mtime);
12528 let index_mtime = (file_mtime.0 + 1, 0);
12529 let policy = StatMatchPolicy::from_config(None);
12530 assert!(
12531 super::index_entry_skips_worktree_content_hash_after_stat_match(
12532 &entry,
12533 Some(index_mtime),
12534 policy,
12535 )
12536 );
12537 entry.size = 0;
12538 entry.mtime_sec = 0;
12539 entry.mtime_nsec = 0;
12540 assert!(
12541 !super::index_entry_skips_worktree_content_hash_after_stat_match(
12542 &entry,
12543 Some(index_mtime),
12544 policy,
12545 )
12546 );
12547 let _ = git_dir;
12548 }
12549
12550 #[cfg(unix)]
12551 #[test]
12552 fn symlink_dir_cache_detects_symlinked_ancestors_across_threads() {
12553 let td = tempfile::tempdir().expect("tempdir");
12554 let wt = td.path();
12555 fs::create_dir_all(wt.join("real/deep")).expect("mkdir");
12556 fs::create_dir_all(wt.join("target/deep")).expect("mkdir");
12557 std::os::unix::fs::symlink(wt.join("target"), wt.join("linked")).expect("symlink");
12558 let cache = super::SymlinkDirCache::default();
12559 std::thread::scope(|scope| {
12560 for _ in 0..4 {
12561 scope.spawn(|| {
12562 assert!(!cache.has_symlink_in_path_bytes(wt, b"top.txt", false, false));
12563 assert!(!cache.has_symlink_in_path_bytes(wt, b"linked", false, false));
12564 assert!(!cache.has_symlink_in_path_bytes(wt, b"real/deep/f", false, false));
12565 assert!(cache.has_symlink_in_path_bytes(wt, b"linked/f", false, false));
12566 assert!(cache.has_symlink_in_path_bytes(wt, b"linked/deep/f", false, false));
12567 assert!(!cache.has_symlink_in_path_bytes(wt, b"\xff\xfe", false, false));
12568 });
12569 }
12570 });
12571 }
12572
12573 #[test]
12574 fn content_hash_skip_trusts_empty_blob_but_not_smudged_size() {
12575 use crate::index::StatMatchPolicy;
12576 let td = tempfile::tempdir().expect("tempdir");
12577 let wt = td.path();
12578 let (_git_dir, odb) = minimal_repo(wt, "[core]\n\tfilemode = false\n");
12579 let file_mtime = (1_700_000_000, 0);
12580 let index_mtime = Some((file_mtime.0 + 1, 0));
12581 let policy = StatMatchPolicy::from_config(None);
12582
12583 let empty = staged_entry_for_file(&odb, wt, "empty.txt", b"", file_mtime);
12584 assert_eq!(empty.size, 0);
12585 assert!(
12586 super::index_entry_skips_worktree_content_hash_after_stat_match(
12587 &empty,
12588 index_mtime,
12589 policy,
12590 )
12591 );
12592
12593 let smudged_oid = odb.hash(ObjectKind::Blob, b"abc\n").expect("hash");
12594 let smudged = staged_entry_with_oid(wt, "smudged.txt", b"", smudged_oid, file_mtime);
12595 assert_eq!(smudged.size, 0);
12596 assert!(
12597 !super::index_entry_skips_worktree_content_hash_after_stat_match(
12598 &smudged,
12599 index_mtime,
12600 policy,
12601 )
12602 );
12603 }
12604
12605 #[test]
12606 fn status_diff_reports_smudged_entry_whose_file_became_empty() {
12607 let td = tempfile::tempdir().expect("tempdir");
12608 let wt = td.path();
12609 let (git_dir, odb) = minimal_repo(wt, "[core]\n\tfilemode = false\n");
12610 let oid = odb.write(ObjectKind::Blob, b"abc\n").expect("write blob");
12611 let file_mtime = (1_700_000_000, 0);
12612 let mut index = Index::new();
12613 index.entries.push(staged_entry_with_oid(
12614 wt,
12615 "smudged.txt",
12616 b"",
12617 oid,
12618 file_mtime,
12619 ));
12620 let (entries, _) = super::diff_index_to_worktree_with_options(
12621 &odb,
12622 &mut index,
12623 wt,
12624 super::DiffIndexToWorktreeOptions {
12625 repository_git_dir: Some(git_dir),
12626 index_mtime: Some((file_mtime.0 + 1, 0)),
12627 for_status: true,
12628 stat_parallel_threads: Some(1),
12629 ..Default::default()
12630 },
12631 )
12632 .expect("diff");
12633 assert_eq!(entries.len(), 1, "{entries:?}");
12634 assert_eq!(entries[0].status, DiffStatus::Modified);
12635 }
12636
12637 #[test]
12638 fn status_diff_verifies_content_when_index_stat_cache_unset() {
12639 let td = tempfile::tempdir().expect("tempdir");
12640 let wt = td.path();
12641 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
12642 let rel = "unset-stat.txt";
12643 let bytes = b"same bytes\n";
12644 let oid = odb.hash(ObjectKind::Blob, bytes).expect("hash");
12645 fs::write(wt.join(rel), bytes).expect("write");
12646 let mut index = Index::new();
12647 let mut entry = staged_entry_for_file(&odb, wt, rel, bytes, (100, 0));
12648 entry.size = 0;
12649 entry.mtime_sec = 0;
12650 entry.mtime_nsec = 0;
12651 entry.oid = oid;
12652 index.entries.push(entry);
12653 let (entries, _) = super::diff_index_to_worktree_with_options(
12654 &odb,
12655 &mut index,
12656 wt,
12657 super::DiffIndexToWorktreeOptions {
12658 repository_git_dir: Some(git_dir),
12659 for_status: true,
12660 ..Default::default()
12661 },
12662 )
12663 .expect("diff");
12664 assert!(
12665 entries.is_empty(),
12666 "unset stat must not size-mismatch a matching worktree: {entries:?}"
12667 );
12668 }
12669
12670 #[test]
12671 fn status_reports_modified_when_ctime_differs_but_mtime_sec_and_size_match_non_racy() {
12672 let td = tempfile::tempdir().expect("tempdir");
12673 let wt = td.path();
12674 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
12675 let rel = "tracked.txt";
12676 let original = b"original\n";
12677 let modified = b"modified\n";
12678 assert_eq!(original.len(), modified.len());
12679
12680 let entry_mtime = (1_700_000_000u32, 0u32);
12681 let index_file_mtime = (1_700_000_100u32, 0u32);
12682
12683 std::process::Command::new("git")
12684 .args(["-C", &wt.to_string_lossy(), "init", "-q"])
12685 .status()
12686 .expect("git init");
12687 std::process::Command::new("git")
12688 .args([
12689 "-C",
12690 &wt.to_string_lossy(),
12691 "config",
12692 "user.email",
12693 "t@example.com",
12694 ])
12695 .status()
12696 .expect("git config email");
12697 std::process::Command::new("git")
12698 .args(["-C", &wt.to_string_lossy(), "config", "user.name", "T"])
12699 .status()
12700 .expect("git config name");
12701
12702 fs::write(wt.join(rel), original).expect("write original");
12703 pin_mtime_and_sample(&wt.join(rel), entry_mtime.0, entry_mtime.1);
12704 std::process::Command::new("git")
12705 .args(["-C", &wt.to_string_lossy(), "add", rel])
12706 .status()
12707 .expect("git add");
12708 std::process::Command::new("git")
12709 .args([
12710 "-C",
12711 &wt.to_string_lossy(),
12712 "commit",
12713 "-qm",
12714 "seed",
12715 "--no-gpg-sign",
12716 ])
12717 .status()
12718 .expect("git commit");
12719
12720 let mut index = Index::new();
12721 index.source_mtime = Some(index_file_mtime);
12722 let staged = staged_entry_for_file(&odb, wt, rel, original, entry_mtime);
12723 let oid_original = staged.oid;
12724 index.entries.push(staged);
12725
12726 fs::write(wt.join(rel), modified).expect("modify worktree");
12727 let git_dirty = std::process::Command::new("git")
12728 .args([
12729 "-C",
12730 &wt.to_string_lossy(),
12731 "--no-optional-locks",
12732 "status",
12733 "--short",
12734 ])
12735 .output()
12736 .expect("git status");
12737 assert!(
12738 git_dirty.status.success(),
12739 "git status failed: {}",
12740 String::from_utf8_lossy(&git_dirty.stderr)
12741 );
12742 assert!(
12743 String::from_utf8_lossy(&git_dirty.stdout).contains("tracked.txt"),
12744 "system git must see modification before mtime pin: {}",
12745 String::from_utf8_lossy(&git_dirty.stdout)
12746 );
12747
12748 pin_mtime_and_sample(&wt.join(rel), entry_mtime.0, entry_mtime.1);
12749
12750 let meta = fs::symlink_metadata(wt.join(rel)).expect("stat");
12751 let ie = &mut index.entries[0];
12752 assert_eq!(ie.oid, oid_original);
12753 if stat_matches(ie, &meta) {
12754 ie.ctime_sec = ie.ctime_sec.wrapping_sub(3600);
12755 }
12756 assert!(
12757 !stat_matches(ie, &meta),
12758 "ctime/inode change must fail full stat match"
12759 );
12760 let stat_policy = crate::index::StatMatchPolicy::default();
12761 assert!(
12762 !entry_is_racy_with_policy(ie, Some(index_file_mtime), stat_policy),
12763 "fixture must be non-racy"
12764 );
12765
12766 let (entries, index_changed) = super::diff_index_to_worktree_with_options(
12767 &odb,
12768 &mut index,
12769 wt,
12770 super::DiffIndexToWorktreeOptions {
12771 index_mtime: Some(index_file_mtime),
12772 repository_git_dir: Some(git_dir),
12773 refresh_index_stat_in_pass: true,
12774 for_status: true,
12775 ..Default::default()
12776 },
12777 )
12778 .expect("diff");
12779 assert_eq!(
12780 entries.len(),
12781 1,
12782 "must report modified, not refresh stat clean: {entries:?}, index_changed={index_changed}"
12783 );
12784 assert_eq!(entries[0].path(), rel);
12785 }
12786
12787 #[test]
12788 fn status_diff_skips_hash_when_worktree_size_differs() {
12789 let td = tempfile::tempdir().expect("tempdir");
12790 let wt = td.path();
12791 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
12792 let rel = "size-change.txt";
12793 let original = b"short\n";
12794 let mut index = Index::new();
12795 index
12796 .entries
12797 .push(staged_entry_for_file(&odb, wt, rel, original, (100, 0)));
12798 fs::write(wt.join(rel), b"longer content\n").expect("rewrite");
12799 let (entries, _) = super::diff_index_to_worktree_with_options(
12800 &odb,
12801 &mut index,
12802 wt,
12803 super::DiffIndexToWorktreeOptions {
12804 repository_git_dir: Some(git_dir),
12805 for_status: true,
12806 ..Default::default()
12807 },
12808 )
12809 .expect("diff");
12810 assert_eq!(entries.len(), 1);
12811 assert_eq!(entries[0].status, super::DiffStatus::Modified);
12812 assert_eq!(entries[0].path(), rel);
12813 }
12814
12815 #[test]
12816 fn stat_matches_ignores_subsecond_nsec_by_default() {
12817 let td = tempfile::tempdir().expect("tempdir");
12818 let wt = td.path();
12819 fs::write(wt.join("f.txt"), b"x\n").expect("write");
12820 pin_mtime_and_sample(&wt.join("f.txt"), 100, 0);
12821 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
12822 let mut entry = entry_from_metadata(&meta, b"f.txt", zero_oid(), MODE_REGULAR);
12823 entry.mtime_nsec = entry.mtime_nsec.wrapping_add(1_000_000);
12824 assert!(stat_matches(&entry, &meta));
12825 }
12826
12827 #[test]
12828 fn classify_add_up_to_date_when_stat_matches() {
12829 let td = tempfile::tempdir().expect("tempdir");
12830 let wt = td.path();
12831 let (_git_dir, odb) = minimal_repo(wt, "[core]\n");
12832 let bytes = b"hello world\n";
12833 let entry = staged_entry_for_file(&odb, wt, "f.txt", bytes, (100, 0));
12834 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
12835 let conv = crlf::ConversionConfig::from_config(&ConfigSet::new());
12836 let attrs = FileAttrs::default();
12837 let params = WorktreeAddRefreshParams {
12838 odb: &odb,
12839 ie: &entry,
12840 meta: &meta,
12841 abs_path: &wt.join("f.txt"),
12842 rel_path: "f.txt",
12843 conv: &conv,
12844 file_attrs: &attrs,
12845 index_mtime: Some((200, 0)),
12846 staged_mode: MODE_REGULAR,
12847 filter_process: None,
12848 };
12849 let refresh = classify_worktree_entry_for_add(¶ms).expect("classify");
12850 assert_eq!(refresh, WorktreeAddRefresh::UpToDate);
12851 }
12852
12853 #[test]
12854 fn classify_add_rehashes_when_index_mtime_unknown_and_stat_matches() {
12855 let td = tempfile::tempdir().expect("tempdir");
12856 let wt = td.path();
12857 let (_git_dir, odb) = minimal_repo(wt, "[core]\n");
12858 let entry = staged_entry_for_file(&odb, wt, "f.txt", b"old content\n", (100, 0));
12859 fs::write(wt.join("f.txt"), b"new content\n").expect("rewrite");
12860 pin_mtime_and_sample(&wt.join("f.txt"), 100, 0);
12861 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
12862 let conv = crlf::ConversionConfig::from_config(&ConfigSet::new());
12863 let attrs = FileAttrs::default();
12864 let params = WorktreeAddRefreshParams {
12865 odb: &odb,
12866 ie: &entry,
12867 meta: &meta,
12868 abs_path: &wt.join("f.txt"),
12869 rel_path: "f.txt",
12870 conv: &conv,
12871 file_attrs: &attrs,
12872 index_mtime: None,
12873 staged_mode: MODE_REGULAR,
12874 filter_process: None,
12875 };
12876 let refresh = classify_worktree_entry_for_add(¶ms).expect("classify");
12877 assert_eq!(refresh, WorktreeAddRefresh::NeedsRestage);
12878 }
12879
12880 #[test]
12881 fn classify_add_needs_restaging_when_content_differs() {
12882 let td = tempfile::tempdir().expect("tempdir");
12883 let wt = td.path();
12884 let (_git_dir, odb) = minimal_repo(wt, "[core]\n");
12885 let entry = staged_entry_for_file(&odb, wt, "f.txt", b"old content\n", (100, 0));
12886 fs::write(wt.join("f.txt"), b"new content\n").expect("rewrite");
12887 pin_mtime_and_sample(&wt.join("f.txt"), 101, 0);
12888 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
12889 let conv = crlf::ConversionConfig::from_config(&ConfigSet::new());
12890 let attrs = FileAttrs::default();
12891 let params = WorktreeAddRefreshParams {
12892 odb: &odb,
12893 ie: &entry,
12894 meta: &meta,
12895 abs_path: &wt.join("f.txt"),
12896 rel_path: "f.txt",
12897 conv: &conv,
12898 file_attrs: &attrs,
12899 index_mtime: Some((200, 0)),
12900 staged_mode: MODE_REGULAR,
12901 filter_process: None,
12902 };
12903 let refresh = classify_worktree_entry_for_add(¶ms).expect("classify");
12904 assert_eq!(refresh, WorktreeAddRefresh::NeedsRestage);
12905 }
12906
12907 #[test]
12908 fn smudges_racily_clean_modified_entry() {
12909 let td = tempfile::tempdir().expect("tempdir");
12910 let wt = td.path();
12911 let index_mtime = setup_index_mtime(wt);
12912 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
12913 let content_a = b"same length!!";
12914 let content_b = b"same length??";
12915 assert_eq!(content_a.len(), content_b.len());
12916
12917 let mut index = Index::new();
12918 let entry = staged_entry_for_file(&odb, wt, "f.txt", content_a, index_mtime);
12919 let stale_oid = entry.oid;
12920
12921 fs::write(wt.join("f.txt"), content_b).expect("rewrite");
12922 pin_mtime_and_sample(&wt.join("f.txt"), index_mtime.0, index_mtime.1);
12923 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
12924 let entry = entry_from_metadata(&meta, b"f.txt", stale_oid, MODE_REGULAR);
12926 index.entries.push(entry.clone());
12927
12928 assert!(stat_matches(&entry, &meta));
12929 assert!(entry_is_racy(&entry, Some(index_mtime)));
12930
12931 let smudged = smudge_racily_clean_entries(
12932 &odb,
12933 &git_dir,
12934 &mut index,
12935 wt,
12936 Some(index_mtime),
12937 None,
12938 None,
12939 );
12940 assert!(smudged);
12941 assert_eq!(index.entries[0].size, 0);
12942
12943 let diff = diff_index_to_worktree(&odb, &mut index, wt, false, false).expect("diff");
12944 assert_eq!(diff.len(), 1);
12945 assert_eq!(diff[0].path(), "f.txt");
12946 }
12947
12948 fn racy_stale_entry(wt: &Path) -> (PathBuf, Odb, Index, (u32, u32), ObjectId) {
12950 let index_mtime = setup_index_mtime(wt);
12951 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
12952 let entry = staged_entry_for_file(&odb, wt, "f.txt", b"same length!!", index_mtime);
12953 let stale_oid = entry.oid;
12954 fs::write(wt.join("f.txt"), b"same length??").expect("rewrite");
12955 pin_mtime_and_sample(&wt.join("f.txt"), index_mtime.0, index_mtime.1);
12956 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
12957 let mut index = Index::new();
12958 index.entries.push(entry_from_metadata(
12959 &meta,
12960 b"f.txt",
12961 stale_oid,
12962 MODE_REGULAR,
12963 ));
12964 (git_dir, odb, index, index_mtime, stale_oid)
12965 }
12966
12967 #[test]
12968 fn content_verified_entry_is_not_reread_and_the_mark_is_consumed() {
12969 let td = tempfile::tempdir().expect("tempdir");
12970 let wt = td.path();
12971 let (git_dir, odb, mut index, index_mtime, stale_oid) = racy_stale_entry(wt);
12972 index.mark_content_verified(b"f.txt", stale_oid);
12973
12974 let smudge = |index: &mut Index| {
12975 smudge_racily_clean_entries(&odb, &git_dir, index, wt, Some(index_mtime), None, None)
12976 };
12977 assert!(!smudge(&mut index), "a verified entry is trusted");
12978 assert_ne!(index.entries[0].size, 0);
12979 assert!(index.content_verified.is_empty());
12980 assert!(smudge(&mut index), "without the mark the entry is checked");
12981 assert_eq!(index.entries[0].size, 0);
12982 }
12983
12984 #[test]
12985 fn content_verified_mark_for_another_oid_does_not_skip_the_check() {
12986 let td = tempfile::tempdir().expect("tempdir");
12987 let wt = td.path();
12988 let (git_dir, odb, mut index, index_mtime, _) = racy_stale_entry(wt);
12989 let other = odb.hash(ObjectKind::Blob, b"other").expect("hash");
12990 index.mark_content_verified(b"f.txt", other);
12991 assert!(smudge_racily_clean_entries(
12992 &odb,
12993 &git_dir,
12994 &mut index,
12995 wt,
12996 Some(index_mtime),
12997 None,
12998 None,
12999 ));
13000 assert_eq!(index.entries[0].size, 0);
13001 }
13002
13003 #[test]
13004 fn leaves_non_racy_entry_alone() {
13005 let td = tempfile::tempdir().expect("tempdir");
13006 let wt = td.path();
13007 let index_mtime = setup_index_mtime(wt);
13008 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
13009 let content_a = b"same length!!";
13010 let content_b = b"same length??";
13011 let entry_mtime = (index_mtime.0 - 10, 0);
13012
13013 let mut index = Index::new();
13014 let entry = staged_entry_for_file(&odb, wt, "f.txt", content_a, entry_mtime);
13015 index.entries.push(entry.clone());
13016
13017 fs::write(wt.join("f.txt"), content_b).expect("rewrite");
13018 pin_mtime_and_sample(&wt.join("f.txt"), entry_mtime.0, entry_mtime.1);
13019
13020 assert!(!entry_is_racy(&entry, Some(index_mtime)));
13021 let smudged = smudge_racily_clean_entries(
13022 &odb,
13023 &git_dir,
13024 &mut index,
13025 wt,
13026 Some(index_mtime),
13027 None,
13028 None,
13029 );
13030 assert!(!smudged);
13031 assert_eq!(index.entries[0].size, entry.size);
13032 }
13033
13034 #[test]
13035 fn leaves_matching_content_alone() {
13036 let td = tempfile::tempdir().expect("tempdir");
13037 let wt = td.path();
13038 let index_mtime = setup_index_mtime(wt);
13039 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
13040 let bytes = b"unchanged!!!!";
13041
13042 let mut index = Index::new();
13043 let entry = staged_entry_for_file(&odb, wt, "f.txt", bytes, index_mtime);
13044 index.entries.push(entry.clone());
13045
13046 assert!(entry_is_racy(&entry, Some(index_mtime)));
13047 let smudged = smudge_racily_clean_entries(
13048 &odb,
13049 &git_dir,
13050 &mut index,
13051 wt,
13052 Some(index_mtime),
13053 None,
13054 None,
13055 );
13056 assert!(!smudged);
13057 assert_eq!(index.entries[0].size, entry.size);
13058 }
13059
13060 #[test]
13061 fn status_diff_skips_refresh_rewrite_for_racy_clean_matching_stat() {
13062 let td = tempfile::tempdir().expect("tempdir");
13063 let wt = td.path();
13064 let index_mtime = setup_index_mtime(wt);
13065 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
13066 let bytes = b"racy clean payload\n";
13067
13068 let mut index = Index::new();
13069 index.source_mtime = Some(index_mtime);
13070 let entry = staged_entry_for_file(&odb, wt, "f.txt", bytes, index_mtime);
13071 index.entries.push(entry.clone());
13072 assert!(entry_is_racy(&entry, Some(index_mtime)));
13073
13074 let (entries, index_changed) = super::diff_index_to_worktree_with_options(
13075 &odb,
13076 &mut index,
13077 wt,
13078 super::DiffIndexToWorktreeOptions {
13079 repository_git_dir: Some(git_dir),
13080 index_mtime: Some(index_mtime),
13081 refresh_index_stat_in_pass: true,
13082 for_status: true,
13083 ..Default::default()
13084 },
13085 )
13086 .expect("status diff");
13087 assert!(entries.is_empty(), "unexpected unstaged: {entries:?}");
13088 assert!(
13089 !index_changed,
13090 "matching stat on a racy entry must skip refresh without rewriting the index"
13091 );
13092 }
13093
13094 #[test]
13095 fn leaves_matching_sha256_racy_content_alone() {
13096 let td = tempfile::tempdir().expect("tempdir");
13097 let wt = td.path();
13098 let index_mtime = setup_index_mtime(wt);
13099 let (git_dir, odb) = minimal_repo(wt, "[extensions]\n\tobjectformat = sha256\n");
13100 assert_eq!(odb.hash_algo(), HashAlgo::Sha256);
13101
13102 let bytes = b"sha256 width!!";
13103 let oid = odb.hash(ObjectKind::Blob, bytes).expect("hash");
13104 let mut index = Index::new();
13105 let entry = staged_entry_with_oid(wt, "f.txt", bytes, oid, index_mtime);
13106 index.entries.push(entry.clone());
13107
13108 assert!(entry_is_racy(&entry, Some(index_mtime)));
13109 let smudged = smudge_racily_clean_entries(
13110 &odb,
13111 &git_dir,
13112 &mut index,
13113 wt,
13114 Some(index_mtime),
13115 None,
13116 None,
13117 );
13118 assert!(!smudged, "matching SHA-256 blob must not be smudged");
13119 assert_eq!(index.entries[0].size, entry.size);
13120 }
13121
13122 #[test]
13123 fn materialize_big_binary_under_autocrlf_streams_into_pack() {
13124 use std::io::Write;
13125
13126 let td = tempfile::tempdir().expect("tempdir");
13127 let wt = td.path();
13128 let (git_dir, odb) =
13129 minimal_repo(wt, "[core]\n\tautocrlf = true\n\tbigFileThreshold = 64k\n");
13130 let path = wt.join("big.bin");
13131 {
13132 let mut f = fs::File::create(&path).expect("create");
13133 f.write_all(&[0u8; 9000]).expect("nul prefix");
13134 let chunk = vec![1u8; 256 * 1024];
13135 for _ in 0..20 {
13136 f.write_all(&chunk).expect("chunk");
13137 }
13138 }
13139 let meta = fs::metadata(&path).expect("stat");
13140 let mut conv = crlf::ConversionConfig::from_config(&ConfigSet::new());
13141 conv.autocrlf = crlf::AutoCrlf::True;
13142 let attrs = FileAttrs::default();
13143 let oid =
13144 materialize_worktree_blob(&odb, &path, &meta, &conv, &attrs, "big.bin", None, None)
13145 .expect("materialize");
13146 assert!(
13147 !odb.object_path(&oid).exists(),
13148 "big binary under autocrlf should pack, not write loose"
13149 );
13150 let pack_dir = git_dir.join("objects/pack");
13151 let names: Vec<_> = fs::read_dir(&pack_dir)
13152 .expect("pack dir")
13153 .map(|e| e.expect("entry").file_name())
13154 .collect();
13155 assert!(
13156 names.iter().any(|n| n.to_string_lossy().ends_with(".pack")),
13157 "{names:?}"
13158 );
13159 assert!(odb.read(&oid).expect("read blob").data.len() > 9000);
13160 }
13161
13162 #[test]
13163 fn indexed_binary_under_autocrlf_rehash_streams() {
13164 use std::io::Write;
13165
13166 let td = tempfile::tempdir().expect("tempdir");
13167 let wt = td.path();
13168 let (_git_dir, odb) =
13169 minimal_repo(wt, "[core]\n\tautocrlf = true\n\tbigFileThreshold = 1m\n");
13170 let path = wt.join("big.bin");
13171 {
13172 let mut f = fs::File::create(&path).expect("create");
13173 f.write_all(&[0u8; 9000]).expect("nul prefix");
13174 let chunk = vec![1u8; 512 * 1024];
13175 for _ in 0..3 {
13176 f.write_all(&chunk).expect("chunk");
13177 }
13178 }
13179 let meta = fs::symlink_metadata(&path).expect("stat");
13180 let oid = hash_worktree_file(
13181 &odb,
13182 &path,
13183 &meta,
13184 &crlf::ConversionConfig::from_config(&ConfigSet::new()),
13185 &FileAttrs::default(),
13186 "big.bin",
13187 None,
13188 None,
13189 )
13190 .expect("hash");
13191 let entry = entry_from_metadata(&meta, b"big.bin", oid, MODE_REGULAR);
13192 let mut conv = crlf::ConversionConfig::from_config(&ConfigSet::new());
13193 conv.autocrlf = crlf::AutoCrlf::True;
13194 let attrs = FileAttrs::default();
13195 let content = read_regular_worktree_blob(
13196 &odb,
13197 &path,
13198 &conv,
13199 &attrs,
13200 "big.bin",
13201 Some(&entry),
13202 None,
13203 1024 * 1024,
13204 )
13205 .expect("read blob");
13206 assert!(
13207 matches!(content, WorktreeBlobContent::Stream { .. }),
13208 "indexed binary re-hash must stream, not buffer"
13209 );
13210 assert_eq!(content.hash(&odb).expect("hash"), entry.oid);
13211 }
13212
13213 #[test]
13214 fn leaves_matching_crlf_worktree_content_alone() {
13215 let td = tempfile::tempdir().expect("tempdir");
13216 let wt = td.path();
13217 let index_mtime = setup_index_mtime(wt);
13218 let (git_dir, odb) = minimal_repo(wt, "[core]\n\tautocrlf = true\n");
13219 let git_form = b"line one\nline two\n";
13220 let worktree_form = b"line one\r\nline two\r\n";
13221 let oid = odb.hash(ObjectKind::Blob, git_form).expect("hash");
13222
13223 let mut index = Index::new();
13224 let entry = staged_entry_with_oid(wt, "f.txt", worktree_form, oid, index_mtime);
13225 index.entries.push(entry.clone());
13226
13227 assert!(entry_is_racy(&entry, Some(index_mtime)));
13228 let smudged = smudge_racily_clean_entries(
13229 &odb,
13230 &git_dir,
13231 &mut index,
13232 wt,
13233 Some(index_mtime),
13234 None,
13235 None,
13236 );
13237 assert!(
13238 !smudged,
13239 "CRLF worktree matching clean index OID must not be smudged"
13240 );
13241 assert_eq!(index.entries[0].size, entry.size);
13242 }
13243
13244 #[test]
13245 fn skips_skip_worktree_and_intent_to_add() {
13246 let td = tempfile::tempdir().expect("tempdir");
13247 let wt = td.path();
13248 let index_mtime = setup_index_mtime(wt);
13249 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
13250 let content_a = b"same length!!";
13251 let content_b = b"same length??";
13252
13253 let mut index = Index::new();
13254 let mut entry = staged_entry_for_file(&odb, wt, "f.txt", content_a, index_mtime);
13255 entry.set_skip_worktree(true);
13256 index.entries.push(entry);
13257 fs::write(wt.join("f.txt"), content_b).expect("rewrite");
13258 pin_mtime_and_sample(&wt.join("f.txt"), index_mtime.0, index_mtime.1);
13259 assert!(!smudge_racily_clean_entries(
13260 &odb,
13261 &git_dir,
13262 &mut index,
13263 wt,
13264 Some(index_mtime),
13265 None,
13266 None,
13267 ));
13268 assert_ne!(index.entries[0].size, 0);
13269
13270 let mut index = Index::new();
13271 let mut entry = staged_entry_for_file(&odb, wt, "f.txt", content_a, index_mtime);
13272 entry.set_intent_to_add(true);
13273 index.entries.push(entry);
13274 fs::write(wt.join("f.txt"), content_b).expect("rewrite");
13275 pin_mtime_and_sample(&wt.join("f.txt"), index_mtime.0, index_mtime.1);
13276 assert!(!smudge_racily_clean_entries(
13277 &odb,
13278 &git_dir,
13279 &mut index,
13280 wt,
13281 Some(index_mtime),
13282 None,
13283 None,
13284 ));
13285 assert_ne!(index.entries[0].size, 0);
13286 }
13287
13288 #[test]
13289 fn smudges_nothing_without_prior_index_mtime() {
13290 let td = tempfile::tempdir().expect("tempdir");
13291 let wt = td.path();
13292 let index_mtime = setup_index_mtime(wt);
13293 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
13294 let mut index = Index::new();
13295 let entry = staged_entry_for_file(&odb, wt, "f.txt", b"same length!!", index_mtime);
13296 index.entries.push(entry);
13297
13298 fs::write(wt.join("f.txt"), b"same length??").expect("rewrite");
13299 pin_mtime_and_sample(&wt.join("f.txt"), index_mtime.0, index_mtime.1);
13300
13301 let smudged = smudge_racily_clean_entries(&odb, &git_dir, &mut index, wt, None, None, None);
13302 assert!(!smudged);
13303 assert_ne!(index.entries[0].size, 0);
13304 }
13305}
13306
13307#[cfg(all(test, unix))]
13308mod read_worktree_bytes_for_mode_tests {
13309 use super::*;
13310 use crate::index::MODE_SYMLINK;
13311 use std::fs;
13312 use std::os::unix::fs::symlink;
13313
13314 #[test]
13315 fn symlink_mode_reads_link_target_not_followed_file() {
13316 let td = tempfile::tempdir().expect("tempdir");
13317 let wt = td.path();
13318 fs::write(wt.join("payload.txt"), b"secret\n").expect("write");
13319 symlink("payload.txt", wt.join("link")).expect("symlink");
13320 let bytes =
13321 read_worktree_bytes_for_mode(&wt.join("link"), MODE_SYMLINK).expect("read link");
13322 assert_eq!(bytes, b"payload.txt");
13323 }
13324
13325 #[test]
13326 fn symlink_mode_reads_plain_file_holding_the_target() {
13327 let td = tempfile::tempdir().expect("tempdir");
13328 let path = td.path().join("link");
13329 fs::write(&path, b"payload.txt").expect("write");
13330 let bytes = read_worktree_bytes_for_mode(&path, MODE_SYMLINK).expect("read plain link");
13331 assert_eq!(bytes, b"payload.txt");
13332 }
13333
13334 #[test]
13335 fn regular_mode_reads_file_contents() {
13336 let td = tempfile::tempdir().expect("tempdir");
13337 let path = td.path().join("f.txt");
13338 fs::write(&path, b"hello\n").expect("write");
13339 let bytes = read_worktree_bytes_for_mode(&path, crate::index::MODE_REGULAR).expect("read");
13340 assert_eq!(bytes, b"hello\n");
13341 }
13342
13343 #[test]
13344 fn annotate_marks_context_line_when_eof_line_is_unchanged() {
13345 let old = "a\nb\nc";
13346 let new = "a\nx\nc";
13347 let raw = super::unified_diff_histogram_hunks_only(old, new, 3, 0, false);
13348 let out = annotate_unified_diff_no_newline_at_eof(&raw, old, new);
13349 assert!(
13350 out.contains(" c\n\\ No newline at end of file"),
13351 "histogram hunk should mark trailing context line:\n{out}"
13352 );
13353 }
13354
13355 #[test]
13356 fn annotate_does_not_treat_removed_dash_lines_as_file_headers() {
13357 use super::unified_diff_with_prefix_and_funcname_and_algorithm;
13358 use similar::Algorithm;
13359
13360 let old = "--old";
13361 let new = "--new";
13362 let raw = unified_diff_with_prefix_and_funcname_and_algorithm(
13363 old,
13364 new,
13365 "a/x",
13366 "b/x",
13367 3,
13368 0,
13369 "",
13370 "",
13371 None,
13372 Algorithm::Myers,
13373 false,
13374 false,
13375 false,
13376 false,
13377 );
13378 assert_eq!(
13379 raw.matches("\\ No newline at end of file").count(),
13380 2,
13381 "expected two markers like git, got:\n{raw}"
13382 );
13383 }
13384}
13385
13386#[cfg(test)]
13387mod annotate_no_newline_tests {
13388 use super::annotate_unified_diff_no_newline_at_eof;
13389
13390 #[test]
13391 fn annotate_marks_both_sides_without_trailing_newline() {
13392 let raw = "@@ -1 +1 @@\n-foo\n+f o o\n";
13393 let out = annotate_unified_diff_no_newline_at_eof(raw, "foo", "f o o");
13394 assert!(out.contains("\\ No newline at end of file"));
13395 assert_eq!(out.matches("\\ No newline at end of file").count(), 2);
13396 }
13397}
13398
13399#[cfg(test)]
13400mod blob_diff_algorithm_smoke {
13401 use super::{
13402 anchored_unified_diff, count_changes_with_algorithm, unified_diff_histogram_hunks_only,
13403 unified_diff_with_prefix_and_funcname_and_algorithm, unified_hunk_body_for_diff_options,
13404 };
13405 use similar::Algorithm;
13406
13407 const OLD: &str = "alpha\nbeta\ngamma\n";
13408 const NEW: &str = "alpha\nBETA\ngamma\ndelta\n";
13409
13410 #[test]
13411 fn seed15_histogram_u0_body_matches_git() {
13412 use crate::diff::options::{DiffAlgorithm, DiffOptions};
13413 use std::collections::hash_map::DefaultHasher;
13414 use std::hash::{Hash, Hasher};
13415 let seed = 15u64;
13416 let mut hasher = DefaultHasher::new();
13417 seed.hash(&mut hasher);
13418 let h = hasher.finish();
13419 let line_count = 4 + (h as usize % 12);
13420 let mut old = String::new();
13421 let mut new = String::new();
13422 for i in 0..line_count {
13423 let token = (h.wrapping_add(i as u64 * 131) % 7) as u8 + b'a';
13424 let ch = token as char;
13425 old.push(ch);
13426 if (h >> (i % 8)) & 1 == 1 {
13427 new.push(((token as u8).wrapping_add(1)) as char);
13428 } else {
13429 new.push(ch);
13430 }
13431 old.push('\n');
13432 new.push('\n');
13433 }
13434 let opts = DiffOptions {
13435 context_lines: 0,
13436 algorithm: DiffAlgorithm::Histogram,
13437 indent_heuristic: false,
13438 ..DiffOptions::default()
13439 };
13440 let body = unified_hunk_body_for_diff_options(&old, &new, &opts);
13441 assert_eq!(
13442 body.matches("@@ ").count(),
13443 2,
13444 "git 2.55 splits u0 histogram hunks at seed 15: {body}"
13445 );
13446 }
13447
13448 #[test]
13449 fn t4071_histogram_changed_flags_git_myers_completes() {
13450 let old: Vec<&str> = "x\nx\nx\nx\n".lines().collect();
13451 let new: Vec<&str> = "x\nx\nx\nA\nB\nC\nD\nx\nE\nF\nG\n".lines().collect();
13452 let (c1, c2) = super::git_xdiff::histogram_changed_flags(&old, &new);
13453 assert_eq!(c1.len(), 4);
13454 assert_eq!(c2.len(), 11);
13455 let ops = super::changed_flags_to_ops(&c1, &c2, old.len(), new.len());
13456 assert!(!ops.is_empty());
13457 }
13458
13459 #[test]
13460 fn t4071_histogram_compacted_ops_completes() {
13461 use crate::diff::WhitespaceMode;
13462 let old = "x\nx\nx\nx\n";
13463 let new = "x\nx\nx\nA\nB\nC\nD\nx\nE\nF\nG\n";
13464 let ops = super::histogram_compacted_line_ops(old, new, false, WhitespaceMode::default());
13465 assert!(!ops.is_empty(), "expected diff ops");
13466 }
13467
13468 #[test]
13469 fn t4071_histogram_u0_body_completes() {
13470 use crate::diff::options::{DiffAlgorithm, DiffOptions};
13471 let old = "x\nx\nx\nx\n";
13472 let new = "x\nx\nx\nA\nB\nC\nD\nx\nE\nF\nG\n";
13473 let opts = DiffOptions {
13474 context_lines: 0,
13475 algorithm: DiffAlgorithm::Histogram,
13476 indent_heuristic: false,
13477 ..DiffOptions::default()
13478 };
13479 let body = unified_hunk_body_for_diff_options(old, new, &opts);
13480 assert!(body.contains("@@"), "expected hunk header: {body}");
13481 }
13482
13483 #[test]
13484 fn t4071_minimal_u0_matches_git_hunk_headers() {
13485 use crate::diff::options::{DiffAlgorithm, DiffOptions};
13486 let old = "x\nx\nx\nx\n";
13487 let new = "x\nx\nx\nA\nB\nC\nD\nx\nE\nF\nG\n";
13488 let opts = DiffOptions {
13489 context_lines: 0,
13490 algorithm: DiffAlgorithm::Minimal,
13491 indent_heuristic: false,
13492 ..DiffOptions::default()
13493 };
13494 let body = unified_hunk_body_for_diff_options(old, new, &opts);
13495 assert_eq!(
13496 body.matches("@@ ").count(),
13497 2,
13498 "git 2.55+ splits u0 minimal hunks at repeated anchors: {body}"
13499 );
13500 }
13501
13502 #[test]
13503 fn myers_patience_histogram_minimal_emit_unified_hunks() {
13504 for algo in [Algorithm::Myers, Algorithm::Patience, Algorithm::Histogram] {
13505 let patch = unified_diff_with_prefix_and_funcname_and_algorithm(
13506 OLD,
13507 NEW,
13508 "a/old.txt",
13509 "b/new.txt",
13510 3,
13511 0,
13512 "a/",
13513 "b/",
13514 None,
13515 algo,
13516 false,
13517 false,
13518 true,
13519 false,
13520 );
13521 assert!(patch.contains("@@"), "algo {algo:?} should emit hunks");
13522 assert!(patch.contains("-beta") || patch.contains("-BETA"));
13523 }
13524 }
13525
13526 #[test]
13527 fn git_histogram_printer_and_anchored_diff_smoke() {
13528 let hist = unified_diff_histogram_hunks_only(OLD, NEW, 3, 0, false);
13529 assert!(!hist.is_empty());
13530 let anchors = vec!["beta".to_string()];
13531 let anchored = anchored_unified_diff(
13532 OLD,
13533 NEW,
13534 "old",
13535 "new",
13536 3,
13537 &anchors,
13538 Algorithm::Myers,
13539 false,
13540 true,
13541 false,
13542 );
13543 assert!(anchored.contains("@@"));
13544 }
13545
13546 #[test]
13547 fn count_changes_git_histogram_vs_similar() {
13548 let hist = count_changes_with_algorithm(OLD, NEW, Algorithm::Myers, true);
13549 let myers = count_changes_with_algorithm(OLD, NEW, Algorithm::Myers, false);
13550 assert!(hist.0 + hist.1 > 0);
13551 assert!(myers.0 + myers.1 > 0);
13552 }
13553}
13554
13555#[cfg(test)]
13556mod line_coverage_tests {
13557 use super::*;
13558 use crate::config::ConfigSet;
13559 use crate::index::StatMatchPolicy;
13560 use crate::index::{Index, IndexEntry, MODE_REGULAR};
13561 use crate::objects::ObjectKind;
13562 use crate::parallel::ThreadPolicy;
13563 use crate::write_tree::write_tree_from_index;
13564 use similar::Algorithm;
13565 use tempfile::TempDir;
13566
13567 fn index_entry(path: &str, mode: u32, oid: ObjectId) -> IndexEntry {
13568 IndexEntry {
13569 ctime_sec: 0,
13570 ctime_nsec: 0,
13571 mtime_sec: 0,
13572 mtime_nsec: 0,
13573 dev: 0,
13574 ino: 0,
13575 mode,
13576 uid: 0,
13577 gid: 0,
13578 size: 0,
13579 oid,
13580 flags: path.len().min(0xfff) as u16,
13581 flags_extended: None,
13582 path: path.as_bytes().to_vec(),
13583 base_index_pos: 0,
13584 }
13585 }
13586
13587 fn write_tree(odb: &Odb, entries: &[(&str, ObjectId)]) -> ObjectId {
13588 let mut body = Vec::new();
13589 for (name, oid) in entries {
13590 body.extend_from_slice(format!("100644 {name}\0").as_bytes());
13591 body.extend_from_slice(oid.as_bytes());
13592 }
13593 odb.write(ObjectKind::Tree, &body).expect("tree")
13594 }
13595
13596 fn pair_of_trees_with_rename() -> (TempDir, Odb, ObjectId, ObjectId) {
13597 let temp = TempDir::new().expect("tempdir");
13598 let odb = Odb::new(temp.path());
13599 let old_blob = odb
13600 .write(ObjectKind::Blob, b"line one\nline two\n")
13601 .expect("old");
13602 let new_blob = odb
13603 .write(ObjectKind::Blob, b"line one\nline TWO\nline three\n")
13604 .expect("new");
13605 let old_tree = write_tree(&odb, &[("old.txt", old_blob)]);
13606 let new_tree = write_tree(&odb, &[("new.txt", new_blob), ("kept.txt", old_blob)]);
13607 (temp, odb, old_tree, new_tree)
13608 }
13609
13610 #[test]
13611 fn diff_helper_and_format_sweep() {
13612 let cfg = ConfigSet::new();
13613 assert!(resolve_indent_heuristic(&cfg, false, false));
13614 assert!(!resolve_indent_heuristic(&cfg, false, true));
13615 let (indent, no_indent) = parse_indent_heuristic_cli_flags(&[
13616 String::from("--no-indent-heuristic"),
13617 String::from("--indent-heuristic"),
13618 ]);
13619 assert!(indent);
13620 assert!(!no_indent);
13621
13622 let old = "alpha beta\ngamma\n";
13623 let new = "alpha BETA\ngamma delta\n";
13624 let _ = normalize_ignore_space_change(old);
13625 let _ = normalize_ignore_space_change_line("- alpha beta ");
13626 let _ = count_changes(old, new);
13627 let _ = count_git_lines(old.as_bytes());
13628 let _ = diffcore_count_changes(old.as_bytes(), new.as_bytes());
13629 let _ = should_break_rewrite_for_stat(b"x", b"totally different content");
13630 assert!(parse_diff_rename_score_token("50%").is_some());
13631 let _ = rewrite_merge_score(b"a", b"b");
13632 let _ = rewrite_dissimilarity_index_percent(b"abc", b"abd");
13633 assert_eq!(format_mode(0o100644), "100644");
13634 assert!(!format_rename_path("a/old", "b/new").is_empty());
13635
13636 let ops = word_diff_ops_imara(&["a", "b"], &["a", "c"]);
13637 assert!(!ops.is_empty());
13638 let old_lines = ["a", "b", "c"];
13639 let new_lines = ["a", "b", "d"];
13640 let compact = diff_slice_ops_compacted(&old_lines, &new_lines, Algorithm::Myers, false);
13641 assert!(!compact.is_empty());
13642 let _ =
13643 map_new_to_old_lines_compacted("a\nb\nc\n", "a\nb\nd\n", Algorithm::Myers, false, 3);
13644 }
13645
13646 #[test]
13647 fn tree_diff_rename_copy_postprocess_matrix() -> Result<()> {
13648 let (_temp, odb, old_tree, new_tree) = pair_of_trees_with_rename();
13649 let raw = diff_trees(&odb, Some(&old_tree), Some(&new_tree), "")?;
13650 assert!(!raw.is_empty());
13651 let with_renames = diff_trees_with_renames(&odb, Some(&old_tree), Some(&new_tree), "", 50)?;
13652 assert!(!with_renames.is_empty());
13653 let config = ConfigSet::new();
13654 let policy = ThreadPolicy::default();
13655 let detected =
13656 detect_renames_with_policy(&odb, None, raw.clone(), 50, &config, &policy, None)?;
13657 let source_flat = flatten_tree_entries(&odb, &old_tree)?;
13658 let _ = detect_copies_with_policy(
13659 &odb,
13660 None,
13661 detected.clone(),
13662 50,
13663 true,
13664 &source_flat,
13665 &config,
13666 &policy,
13667 )?;
13668 let opts = DiffPostprocessOptions {
13669 break_score: Some(50),
13670 rename_threshold: 50,
13671 copy_threshold: Some(50),
13672 find_copies_harder: true,
13673 materialize_break_worktree_new_oid: false,
13674 };
13675 let processed = postprocess_diff_entries(
13676 &odb,
13677 raw,
13678 Some(&old_tree),
13679 None,
13680 None,
13681 &config,
13682 &policy,
13683 &opts,
13684 )?;
13685 assert!(!processed.is_empty());
13686 let _ = status_apply_rename_copy_detection(
13687 &odb,
13688 with_renames.clone(),
13689 50,
13690 true,
13691 Some(&old_tree),
13692 &config,
13693 &policy,
13694 None,
13695 )?;
13696 let flat = flatten_tree_entries(&odb, &new_tree)?;
13697 assert!(!flat.is_empty());
13698 let _ = diff_trees_show_tree_entries(&odb, Some(&old_tree), Some(&new_tree), "")?;
13699 Ok(())
13700 }
13701
13702 #[test]
13703 fn unified_diff_flag_matrix() {
13704 let old = "fn main() {\n println!(\"hi\");\n}\n";
13705 let new = "fn main() {\n println!(\"bye\");\n}\n";
13706 for algo in [Algorithm::Myers, Algorithm::Patience, Algorithm::Histogram] {
13707 for (ws, ws_all, fc, ig) in [(true, false, false, false), (false, true, true, true)] {
13708 let patch = unified_diff_with_prefix_and_funcname_and_algorithm(
13709 old,
13710 new,
13711 "a/main.rs",
13712 "b/main.rs",
13713 3,
13714 0,
13715 "a/",
13716 "b/",
13717 None,
13718 algo,
13719 ws,
13720 ws_all,
13721 fc,
13722 ig,
13723 );
13724 assert!(patch.contains("@@"), "algo {algo:?}");
13725 }
13726 }
13727 let hist = unified_diff_histogram_ignore_with_prefix_and_funcname(
13728 old,
13729 new,
13730 "a/x",
13731 "b/x",
13732 3,
13733 0,
13734 "",
13735 "",
13736 None,
13737 false,
13738 false,
13739 false,
13740 |_old, _new| false,
13741 );
13742 assert!(!hist.is_empty());
13743 let raw = format_raw_abbrev(
13744 &DiffEntry {
13745 status: DiffStatus::Modified,
13746 old_path: Some("x".into()),
13747 new_path: Some("x".into()),
13748 old_mode: "100644".into(),
13749 new_mode: "100644".into(),
13750 old_oid: empty_blob_oid(),
13751 new_oid: empty_blob_oid(),
13752 score: Some(80),
13753 },
13754 7,
13755 );
13756 assert!(raw.contains('M') || raw.contains('x'));
13757 let stat = format_stat_line_width("path", 10, 20, 40, 8);
13758 assert!(stat.contains("path"));
13759 }
13760
13761 #[test]
13762 fn index_and_commit_diff_paths() -> Result<()> {
13763 let temp = TempDir::new().expect("tempdir");
13764 let odb = Odb::new(temp.path());
13765 let b1 = odb.write(ObjectKind::Blob, b"v1\n").expect("b1");
13766 let b2 = odb.write(ObjectKind::Blob, b"v2\n").expect("b2");
13767 let b3 = odb.write(ObjectKind::Blob, b"v3\n").expect("b3");
13768 let mut index = Index::new();
13769 index.add_or_replace(index_entry("file.txt", MODE_REGULAR, b2));
13770 let head = write_tree_from_index(&odb, &index, "").expect("tree");
13771 index.entries_mut()[0].oid = b1;
13772 let idx_diff = diff_index_to_tree(&odb, &index, Some(&head), false)?;
13773 assert!(!idx_diff.is_empty());
13774 let commit_body = format!(
13775 "tree {head}\nauthor T <t@e.com> 1 +0000\ncommitter T <t@e.com> 1 +0000\n\nm\n"
13776 );
13777 let c1 = odb
13778 .write(ObjectKind::Commit, commit_body.as_bytes())
13779 .expect("c1");
13780 let mut index2 = Index::new();
13781 index2.add_or_replace(index_entry("file.txt", MODE_REGULAR, b3));
13782 let head2 = write_tree_from_index(&odb, &index2, "").expect("head2");
13783 let c2_body = format!(
13784 "tree {head2}\nparent {c1}\nauthor T <t@e.com> 2 +0000\ncommitter T <t@e.com> 2 +0000\n\nm2\n"
13785 );
13786 let c2 = odb
13787 .write(ObjectKind::Commit, c2_body.as_bytes())
13788 .expect("c2");
13789 let commit_diff = diff_commits_with_renames(&odb, &c1, &c2, 50)?;
13790 assert!(!commit_diff.is_empty());
13791 let _ = tree_oid_of_commit(&odb, &c2)?;
13792 let mtime = index_mtime_for_diff(&index, Some((1, 0)));
13793 let _ = entry_is_racy(&index.entries()[0], mtime);
13794 let _ = entry_is_racy_with_policy(&index.entries()[0], mtime, StatMatchPolicy::default());
13795 Ok(())
13796 }
13797
13798 #[test]
13799 fn worktree_index_diff_and_rename_score() -> Result<()> {
13800 let temp = TempDir::new().expect("tempdir");
13801 let wt = temp.path();
13802 let git_dir = wt.join(".git");
13803 std::fs::create_dir_all(git_dir.join("objects")).expect("objects");
13804 let objects_dir = git_dir.join("objects");
13805 let odb = Odb::new(&objects_dir);
13806 let content = b"hello world\n";
13807 std::fs::write(wt.join("tracked.txt"), content).expect("write");
13808 let oid = odb.write(ObjectKind::Blob, content).expect("blob");
13809 let mut index = Index::new();
13810 index.add_or_replace(index_entry("tracked.txt", MODE_REGULAR, oid));
13811 let head = write_tree_from_index(&odb, &index, "").expect("tree");
13812 std::fs::write(wt.join("tracked.txt"), b"hello WORLD\n").expect("mutate");
13813 let mut index_mut = index.clone();
13814 let diff = diff_index_to_worktree(&odb, &mut index_mut, wt, false, false)?;
13815 assert!(!diff.is_empty());
13816 let _ = diff_tree_to_worktree(&odb, Some(&head), wt, &index)?;
13817 assert_eq!(rename_similarity_score(b"a", b"a\n"), 0);
13818 assert_eq!(rename_similarity_score(b"a\n", b"a"), 0);
13819 let score = rename_similarity_score(b"line one\nline two\n", b"line one\nline TWO\n");
13820 assert!(score >= 50);
13821 let _ = detect_renames(&odb, Some(wt), diff, 50);
13822 Ok(())
13823 }
13824
13825 #[test]
13826 fn extended_diff_helpers_and_rotate_order() -> Result<()> {
13827 use crate::crlf::{ConversionConfig, FileAttrs};
13828 let temp = TempDir::new().expect("tempdir");
13829 let wt = temp.path();
13830 let git_dir = wt.join(".git");
13831 std::fs::create_dir_all(git_dir.join("objects")).expect("objects");
13832 let objects_dir = git_dir.join("objects");
13833 let odb = Odb::new(&objects_dir);
13834 let b_old = odb.write(ObjectKind::Blob, b"alpha\nbeta\n").expect("old");
13835 let b_new = odb
13836 .write(ObjectKind::Blob, b"alpha\nBETA\ngamma\n")
13837 .expect("new");
13838 let old_tree = write_tree(&odb, &[("a.txt", b_old), ("b.txt", b_old)]);
13839 let new_tree = write_tree(
13840 &odb,
13841 &[("a.txt", b_new), ("b.txt", b_new), ("c.txt", b_new)],
13842 );
13843 let entries = diff_trees(&odb, Some(&old_tree), Some(&new_tree), "")?;
13844 assert!(entries.len() >= 2);
13845 let rotated = apply_rotate_skip_entries(entries.clone(), Some("b.txt"), None)?;
13846 assert!(!rotated.is_empty());
13847 let _ =
13848 apply_rotate_skip_log_entries(&odb, &new_tree, entries.clone(), Some("a.txt"), None);
13849 let _ =
13850 apply_rotate_skip_log_entries(&odb, &new_tree, entries.clone(), None, Some("a.txt"));
13851 let order_path = wt.join("order");
13852 std::fs::write(&order_path, b"c.txt\na.txt\n").expect("order");
13853 let _ = apply_orderfile_entries(entries.clone(), order_path.to_str().expect("utf8"), wt);
13854 let _ = read_orderfile_patterns(order_path.to_str().expect("utf8"), wt)?;
13855
13856 let cfg = ConfigSet::new();
13857 assert!(indent_heuristic_from_config(&cfg));
13858 let _ = unified_diff_histogram_hunks_only("a\nb\n", "a\nB\n", 3, 0, false);
13859 let _ = unified_diff_histogram_with_prefix_and_funcname(
13860 "x\n", "y\n", "a/f", "b/f", 3, 0, "", "", None, false, false,
13861 );
13862 let _ = count_changes_with_algorithm("a\n", "b\n", Algorithm::Myers, false);
13863 let _ = should_break_rewrite_pair(b"short", b"totally different body", 50);
13864 let _ = head_path_states(&odb, Some(&new_tree))?;
13865 std::fs::write(wt.join("tracked.txt"), b"hello\n").expect("wt");
13866 let abs = wt.join("tracked.txt");
13867 let meta = std::fs::metadata(&abs).expect("meta");
13868 let mut index = Index::new();
13869 index.add_or_replace(index_entry("tracked.txt", MODE_REGULAR, b_old));
13870 let _ = stat_matches_with_policy(&index.entries()[0], &meta, StatMatchPolicy::default());
13871 let _ = worktree_differs_from_index_entry(
13872 &odb,
13873 &git_dir,
13874 wt,
13875 &index.entries()[0],
13876 false,
13877 None,
13878 None,
13879 )?;
13880 let conv = ConversionConfig::from_config(&cfg);
13881 let attrs = FileAttrs::default();
13882 let params = WorktreeAddRefreshParams {
13883 odb: &odb,
13884 ie: &index.entries()[0],
13885 meta: &meta,
13886 abs_path: &abs,
13887 rel_path: "tracked.txt",
13888 conv: &conv,
13889 file_attrs: &attrs,
13890 index_mtime: None,
13891 staged_mode: MODE_REGULAR,
13892 filter_process: None,
13893 };
13894 let _ = classify_worktree_entry_for_add(¶ms)?;
13895 let _ = hash_worktree_file(
13896 &odb,
13897 &abs,
13898 &meta,
13899 &conv,
13900 &attrs,
13901 "tracked.txt",
13902 Some(&index.entries()[0]),
13903 None,
13904 )?;
13905 let _ = materialize_worktree_blob(
13906 &odb,
13907 &abs,
13908 &meta,
13909 &conv,
13910 &attrs,
13911 "tracked.txt",
13912 Some(&index.entries()[0]),
13913 None,
13914 )?;
13915 let _ = read_worktree_bytes_for_mode(&abs, MODE_REGULAR);
13916 let _ = mode_from_metadata(&meta);
13917 let _ = smudge_racily_clean_entries(&odb, &git_dir, &mut index, wt, None, Some(&cfg), None);
13918 let _ = refresh_index_stat_content_verified(
13919 &odb,
13920 &git_dir,
13921 &mut index,
13922 wt,
13923 None,
13924 Some(&cfg),
13925 None,
13926 )?;
13927 let _ = refresh_index_stat_content_verified_with_rules(
13928 &odb,
13929 &git_dir,
13930 &mut index,
13931 wt,
13932 None,
13933 Some(&cfg),
13934 None,
13935 None,
13936 )?;
13937 let _ = path_checkout_skip_blob_write_when_up_to_date(
13938 &odb,
13939 &git_dir,
13940 wt,
13941 &index,
13942 &index.entries()[0],
13943 &b_old,
13944 MODE_REGULAR,
13945 "tracked.txt",
13946 None,
13947 )?;
13948 let patch = anchored_unified_diff(
13949 "a\nb\n",
13950 "a\nB\n",
13951 "f",
13952 "f",
13953 3,
13954 &[String::from("b")],
13955 Algorithm::Myers,
13956 false,
13957 true,
13958 false,
13959 );
13960 assert!(patch.contains("@@"));
13961 let _ = annotate_unified_diff_no_newline_at_eof("@@\n+a\n\\ No newline\n", "a", "a\n");
13962 Ok(())
13963 }
13964
13965 #[test]
13966 fn unmerged_only_tree_worktree_diff_and_func_heuristics() -> Result<()> {
13967 use similar::DiffOp;
13968 assert!(inserted_block_starts_with_c_like_function_definition(
13969 "static int foo(int bar) {"
13970 ));
13971 assert!(!inserted_block_starts_with_c_like_function_definition(
13972 "printf(hi);"
13973 ));
13974 let ops = vec![
13975 DiffOp::Equal {
13976 old_index: 0,
13977 new_index: 0,
13978 len: 4,
13979 },
13980 DiffOp::Insert {
13981 old_index: 4,
13982 new_index: 4,
13983 new_len: 2,
13984 },
13985 ];
13986 let new_lines = ["a\n", "b\n", "c\n", "d\n", "void added() {\n", "}\n"];
13987 assert!(append_with_whole_function_added(
13988 &ops, 4, 6, &new_lines, None
13989 ));
13990 let _ = hunk_old_change_end_exclusive(&ops);
13991
13992 let temp = TempDir::new().expect("tempdir");
13993 let wt = temp.path();
13994 let git_dir = wt.join(".git");
13995 let objects_dir = git_dir.join("objects");
13996 std::fs::create_dir_all(&objects_dir).expect("objects");
13997 let odb = Odb::new(&objects_dir);
13998 let tree_blob = odb.write(ObjectKind::Blob, b"base\n").expect("blob");
13999 std::fs::write(wt.join("conflict.txt"), b"worktree\n").expect("wt");
14000 let tree = write_tree(&odb, &[("conflict.txt", tree_blob)]);
14001 let mut index = Index::new();
14002 let mut stage1 = index_entry("conflict.txt", MODE_REGULAR, tree_blob);
14003 stage1.flags = (stage1.flags & 0x0FFF) | (1 << 12);
14004 index.entries_mut().push(stage1);
14005 let cfg = ConfigSet::new();
14006 let diff = diff_tree_to_worktree_with_git_dir(
14007 &odb,
14008 Some(&tree),
14009 wt,
14010 &git_dir,
14011 &index,
14012 Some(&cfg),
14013 )?;
14014 assert!(!diff.is_empty());
14015 Ok(())
14016 }
14017
14018 #[test]
14019 fn high_volume_unified_diff_and_postprocess() -> Result<()> {
14020 let (_temp, odb, old_tree, new_tree) = pair_of_trees_with_rename();
14021 let config = ConfigSet::new();
14022 let policy = ThreadPolicy::default();
14023 for i in 0..600u32 {
14024 let old = format!("seed {i}\nalpha\n");
14025 let new = format!("seed {i}\nalpha\nvoid fn_{i}(int arg) {{\n return arg;\n}}\n");
14026 let _ = unified_diff_with_prefix_and_funcname_and_algorithm(
14027 &old,
14028 &new,
14029 "a/f.c",
14030 "b/f.c",
14031 3,
14032 0,
14033 "a/",
14034 "b/",
14035 None,
14036 Algorithm::Myers,
14037 i % 2 == 0,
14038 i % 3 == 0,
14039 i % 5 == 0,
14040 i % 7 == 0,
14041 );
14042 let threshold = 30 + (i % 40);
14043 let raw = diff_trees(&odb, Some(&old_tree), Some(&new_tree), "")?;
14044 let opts = DiffPostprocessOptions {
14045 rename_threshold: threshold,
14046 copy_threshold: Some(threshold),
14047 find_copies_harder: i % 2 == 0,
14048 break_score: Some(threshold as u64),
14049 materialize_break_worktree_new_oid: i % 4 == 0,
14050 };
14051 let _ = postprocess_diff_entries(
14052 &odb,
14053 raw,
14054 Some(&old_tree),
14055 None,
14056 None,
14057 &config,
14058 &policy,
14059 &opts,
14060 )?;
14061 }
14062 Ok(())
14063 }
14064}