1use crate::git_path::CanonicalizeExt;
24use std::borrow::Cow;
25use std::collections::HashSet;
26use std::fs;
27#[cfg(unix)]
28use std::os::unix::fs::MetadataExt;
29use std::path::{Path, PathBuf};
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::{
40 parse_commit, CommitData, FileMode, ObjectId, ObjectKind, TreeEntry, TreeRefIter,
41};
42use crate::odb::Odb;
43use crate::parallel::ThreadPolicy;
44use crate::repo::Repository;
45use crate::userdiff::FuncnameMatcher;
46use crate::worktree_rules::WorktreeRules;
47
48use super::rename_lines::RenameLineTable;
49use super::unified_emit::{
50 emit_unified_body_from_edits, line_ending_only_edits, EditSpan, LineSide, UnifiedEmitOptions,
51};
52
53fn line_compare_keys<'a>(
59 text: &'a str,
60 side: &LineSide<'a>,
61 ws: super::WhitespaceMode,
62 normalized: &'a mut Vec<Cow<'a, str>>,
63) -> Vec<&'a str> {
64 if ws.normalizes_line_compare() {
65 normalized.extend(
67 side.lines
68 .iter()
69 .map(|line| ws.normalize_line_for_compare(line.body)),
70 );
71 return normalized.iter().map(Cow::as_ref).collect();
72 }
73 let mut keys = Vec::with_capacity(side.len());
74 let mut rest = text;
75 while !rest.is_empty() {
76 let end = memchr::memchr(b'\n', rest.as_bytes()).map_or(rest.len(), |nl| nl + 1);
77 let (line, tail) = rest.split_at(end);
78 keys.push(line);
79 rest = tail;
80 }
81 debug_assert_eq!(keys.len(), side.len());
82 keys
83}
84
85#[path = "../structured_diff.rs"]
86pub mod structured;
87
88pub(crate) fn imara_unified_hunk_slices(body: &str) -> Vec<&str> {
89 let mut starts: Vec<usize> = Vec::new();
90 if body.starts_with("@@") {
91 starts.push(0);
92 }
93 for (idx, _) in body.match_indices("\n@@ ") {
94 starts.push(idx + 1);
95 }
96 starts.push(body.len());
97 starts.windows(2).map(|w| &body[w[0]..w[1]]).collect()
98}
99
100pub(crate) fn histogram_compacted_line_ops(
102 old_content: &str,
103 new_content: &str,
104 indent_heuristic: bool,
105 ws: super::WhitespaceMode,
106) -> Vec<similar::DiffOp> {
107 let before = LineSide::from_text(old_content);
108 let after = LineSide::from_text(new_content);
109 let old_lines: Vec<&str> = before.lines.iter().map(|l| l.body).collect();
110 let new_lines: Vec<&str> = after.lines.iter().map(|l| l.body).collect();
111 let old_len = old_lines.len();
112 let new_len = new_lines.len();
113 let (mut old_normalized, mut new_normalized) = (Vec::new(), Vec::new());
114 let old_compare = line_compare_keys(old_content, &before, ws, &mut old_normalized);
115 let new_compare = line_compare_keys(new_content, &after, ws, &mut new_normalized);
116
117 let (changed1, changed2) = git_xdiff::histogram_changed_flags(&old_compare, &new_compare);
118
119 if !changed1.iter().any(|c| *c) && !changed2.iter().any(|c| *c) {
120 return Vec::new();
121 }
122 let raw = changed_flags_to_ops(&changed1, &changed2, old_len, new_len);
123 diff_indent_heuristic::apply_change_compact_to_ops_with_ws_histogram(
124 &raw,
125 &old_lines,
126 &new_lines,
127 indent_heuristic,
128 ws,
129 true,
130 )
131}
132
133pub(crate) fn histogram_unified_body_raw(
134 old_content: &str,
135 new_content: &str,
136 context_lines: usize,
137 inter_hunk_context: usize,
138 indent_heuristic: bool,
139 ws: super::WhitespaceMode,
140) -> String {
141 let before_side = LineSide::from_text(old_content);
142 let after_side = LineSide::from_text(new_content);
143 let ops = histogram_compacted_line_ops(old_content, new_content, indent_heuristic, ws);
144 let mut edits = diff_ops_to_edit_spans(&ops);
145 if edits.is_empty() {
146 if old_content != new_content && !ws.normalizes_line_compare() {
147 edits = line_ending_only_edits(&before_side, &after_side);
148 }
149 if edits.is_empty() {
150 return String::new();
151 }
152 }
153 emit_unified_body_from_edits(
154 &before_side,
155 &after_side,
156 &edits,
157 UnifiedEmitOptions {
158 context_lines,
159 inter_hunk_context,
160 function_context: false,
161 },
162 None,
163 )
164}
165
166#[allow(clippy::too_many_arguments)]
182fn diff_ops_to_ignorable_edit_spans<F>(
183 ops: &[similar::DiffOp],
184 old_lines: &[&str],
185 new_lines: &[&str],
186 is_ignorable_change: F,
187) -> Vec<EditSpan>
188where
189 F: Fn(&[&str], &[&str]) -> bool,
190{
191 use similar::DiffOp;
192 let mut out = Vec::new();
193 for op in ops {
194 match *op {
195 DiffOp::Equal { .. } => {}
196 DiffOp::Delete {
197 old_index,
198 old_len,
199 new_index,
200 } => {
201 let removed = &old_lines[old_index..old_index + old_len];
202 out.push(EditSpan {
203 before_start: old_index as i64,
204 before_len: old_len as i64,
205 after_start: new_index as i64,
206 after_len: 0,
207 skip_in_output: is_ignorable_change(removed, &[]),
208 });
209 }
210 DiffOp::Insert {
211 old_index,
212 new_index,
213 new_len,
214 } => {
215 let added = &new_lines[new_index..new_index + new_len];
216 out.push(EditSpan {
217 before_start: old_index as i64,
218 before_len: 0,
219 after_start: new_index as i64,
220 after_len: new_len as i64,
221 skip_in_output: is_ignorable_change(&[], added),
222 });
223 }
224 DiffOp::Replace {
225 old_index,
226 old_len,
227 new_index,
228 new_len,
229 } => {
230 let removed = &old_lines[old_index..old_index + old_len];
231 let added = &new_lines[new_index..new_index + new_len];
232 out.push(EditSpan {
233 before_start: old_index as i64,
234 before_len: old_len as i64,
235 after_start: new_index as i64,
236 after_len: new_len as i64,
237 skip_in_output: is_ignorable_change(removed, added),
238 });
239 }
240 }
241 }
242 out
243}
244
245#[allow(clippy::too_many_arguments)]
246pub(crate) fn histogram_unified_body_ignore_fc<F>(
247 old_content: &str,
248 new_content: &str,
249 context_lines: usize,
250 inter_hunk_context: usize,
251 function_context: bool,
252 funcname_matcher: Option<&FuncnameMatcher>,
253 indent_heuristic: bool,
254 ws: super::WhitespaceMode,
255 is_ignorable_change: F,
256) -> String
257where
258 F: Fn(&[&str], &[&str]) -> bool,
259{
260 let before = LineSide::from_text(old_content);
261 let after = LineSide::from_text(new_content);
262 let old_lines: Vec<&str> = before.lines.iter().map(|l| l.body).collect();
263 let new_lines: Vec<&str> = after.lines.iter().map(|l| l.body).collect();
264 let ops = histogram_compacted_line_ops(old_content, new_content, indent_heuristic, ws);
265 if ops.is_empty() {
266 return String::new();
267 }
268 let edits = diff_ops_to_ignorable_edit_spans(&ops, &old_lines, &new_lines, is_ignorable_change);
269 emit_unified_body_from_edits(
270 &before,
271 &after,
272 &edits,
273 UnifiedEmitOptions {
274 context_lines,
275 inter_hunk_context,
276 function_context,
277 },
278 funcname_matcher,
279 )
280}
281
282pub(crate) fn git_xdiff_compacted_line_ops(
283 old_compare: &[&str],
284 new_compare: &[&str],
285 old_lines: &[&str],
286 new_lines: &[&str],
287 indent_heuristic: bool,
288 need_min: bool,
289) -> Vec<similar::DiffOp> {
290 use crate::line_interner::LineInterner;
291
292 let mut interner = LineInterner::with_capacity(old_compare.len() + new_compare.len());
293 let ids1 = interner.intern_all(old_compare);
294 let ids2 = interner.intern_all(new_compare);
295 let (changed1, changed2) = git_xdiff::changed_flags(&ids1, &ids2, need_min);
296 diff_indent_heuristic::apply_change_compact_to_masks(
297 changed1,
298 changed2,
299 old_lines,
300 new_lines,
301 indent_heuristic,
302 )
303}
304
305fn diff_ops_to_edit_spans(ops: &[similar::DiffOp]) -> Vec<EditSpan> {
306 use similar::DiffOp;
307 let mut out = Vec::new();
308 for op in ops {
309 match *op {
310 DiffOp::Equal { .. } => {}
311 DiffOp::Delete {
312 old_index,
313 old_len,
314 new_index,
315 } => out.push(EditSpan {
316 before_start: old_index as i64,
317 before_len: old_len as i64,
318 after_start: new_index as i64,
319 after_len: 0,
320 skip_in_output: false,
321 }),
322 DiffOp::Insert {
323 old_index,
324 new_index,
325 new_len,
326 } => out.push(EditSpan {
327 before_start: old_index as i64,
328 before_len: 0,
329 after_start: new_index as i64,
330 after_len: new_len as i64,
331 skip_in_output: false,
332 }),
333 DiffOp::Replace {
334 old_index,
335 old_len,
336 new_index,
337 new_len,
338 } => out.push(EditSpan {
339 before_start: old_index as i64,
340 before_len: old_len as i64,
341 after_start: new_index as i64,
342 after_len: new_len as i64,
343 skip_in_output: false,
344 }),
345 }
346 }
347 out
348}
349
350fn git_xdiff_unified_body_raw(
352 old_content: &str,
353 new_content: &str,
354 context_lines: usize,
355 inter_hunk_context: usize,
356 indent_heuristic: bool,
357 need_min: bool,
358 ws: super::WhitespaceMode,
359) -> String {
360 git_xdiff_unified_body_raw_with_ignorable(
361 old_content,
362 new_content,
363 context_lines,
364 inter_hunk_context,
365 indent_heuristic,
366 need_min,
367 ws,
368 None::<fn(&[&str], &[&str]) -> bool>,
369 )
370}
371
372#[allow(clippy::too_many_arguments)]
373fn git_xdiff_unified_body_raw_with_ignorable<F>(
374 old_content: &str,
375 new_content: &str,
376 context_lines: usize,
377 inter_hunk_context: usize,
378 indent_heuristic: bool,
379 need_min: bool,
380 ws: super::WhitespaceMode,
381 is_ignorable_change: Option<F>,
382) -> String
383where
384 F: Fn(&[&str], &[&str]) -> bool,
385{
386 let before = LineSide::from_text(old_content);
387 let after = LineSide::from_text(new_content);
388 if before.is_empty() && after.is_empty() {
389 return String::new();
390 }
391 let old_refs: Vec<&str> = before.lines.iter().map(|l| l.body).collect();
392 let new_refs: Vec<&str> = after.lines.iter().map(|l| l.body).collect();
393 let (mut old_normalized, mut new_normalized) = (Vec::new(), Vec::new());
394 let old_compare = line_compare_keys(old_content, &before, ws, &mut old_normalized);
395 let new_compare = line_compare_keys(new_content, &after, ws, &mut new_normalized);
396 let ops = git_xdiff_compacted_line_ops(
397 &old_compare,
398 &new_compare,
399 &old_refs,
400 &new_refs,
401 indent_heuristic,
402 need_min,
403 );
404 let mut edits = if let Some(is_ignorable) = is_ignorable_change {
405 diff_ops_to_ignorable_edit_spans(&ops, &old_refs, &new_refs, is_ignorable)
406 } else {
407 diff_ops_to_edit_spans(&ops)
408 };
409 if edits.is_empty() {
410 if old_content != new_content && !ws.normalizes_line_compare() {
411 edits = line_ending_only_edits(&before, &after);
412 }
413 if edits.is_empty() {
414 return String::new();
415 }
416 }
417 emit_unified_body_from_edits(
418 &before,
419 &after,
420 &edits,
421 UnifiedEmitOptions {
422 context_lines,
423 inter_hunk_context,
424 function_context: false,
425 },
426 None,
427 )
428}
429
430#[allow(clippy::too_many_arguments)]
431fn git_patience_unified_body_raw_with_ignorable<F>(
432 old_content: &str,
433 new_content: &str,
434 context_lines: usize,
435 inter_hunk_context: usize,
436 indent_heuristic: bool,
437 ws: super::WhitespaceMode,
438 is_ignorable_change: Option<F>,
439) -> String
440where
441 F: Fn(&[&str], &[&str]) -> bool,
442{
443 let before = LineSide::from_text(old_content);
444 let after = LineSide::from_text(new_content);
445 if before.is_empty() && after.is_empty() {
446 return String::new();
447 }
448 let old_refs: Vec<&str> = before.lines.iter().map(|l| l.body).collect();
449 let new_refs: Vec<&str> = after.lines.iter().map(|l| l.body).collect();
450 let (mut old_normalized, mut new_normalized) = (Vec::new(), Vec::new());
451 let old_compare = line_compare_keys(old_content, &before, ws, &mut old_normalized);
452 let new_compare = line_compare_keys(new_content, &after, ws, &mut new_normalized);
453 let (changed1, changed2) = git_xdiff::patience_changed_flags(&old_compare, &new_compare);
454 let raw = changed_flags_to_ops(&changed1, &changed2, old_refs.len(), new_refs.len());
455 let ops = if ws.normalizes_line_compare() {
456 diff_indent_heuristic::apply_change_compact_to_ops_with_ws(
457 &raw,
458 &old_refs,
459 &new_refs,
460 indent_heuristic,
461 ws,
462 )
463 } else {
464 diff_indent_heuristic::apply_change_compact_to_ops(
465 &raw,
466 &old_refs,
467 &new_refs,
468 indent_heuristic,
469 )
470 };
471 let mut edits = if let Some(is_ignorable) = is_ignorable_change {
472 diff_ops_to_ignorable_edit_spans(&ops, &old_refs, &new_refs, is_ignorable)
473 } else {
474 diff_ops_to_edit_spans(&ops)
475 };
476 if edits.is_empty() {
477 if old_content != new_content && !ws.normalizes_line_compare() {
478 edits = line_ending_only_edits(&before, &after);
479 }
480 if edits.is_empty() {
481 return String::new();
482 }
483 }
484 emit_unified_body_from_edits(
485 &before,
486 &after,
487 &edits,
488 UnifiedEmitOptions {
489 context_lines,
490 inter_hunk_context,
491 function_context: false,
492 },
493 None,
494 )
495}
496
497fn blank_line_ignorable(ws: super::WhitespaceMode) -> impl Fn(&[&str], &[&str]) -> bool + Copy {
498 move |removed: &[&str], added: &[&str]| {
499 ws.ignore_blank_lines
500 && removed.iter().all(|l| crate::ws::ws_blank_line(l))
501 && added.iter().all(|l| crate::ws::ws_blank_line(l))
502 }
503}
504
505pub(crate) fn unified_hunk_body_for_diff_options(
507 old_content: &str,
508 new_content: &str,
509 options: &crate::diff::options::DiffOptions,
510) -> String {
511 use crate::diff::options::DiffAlgorithm;
512
513 let ws = options.whitespace;
514 if ws.is_active() {
515 let blank_only = blank_line_ignorable(ws);
516 if ws.normalizes_line_compare() {
517 let ignorable = ws.ignore_blank_lines.then_some(blank_only);
518 return match options.algorithm {
519 DiffAlgorithm::Myers => git_xdiff_unified_body_raw_with_ignorable(
520 old_content,
521 new_content,
522 options.context_lines,
523 options.inter_hunk_context,
524 options.indent_heuristic,
525 false,
526 ws,
527 ignorable,
528 ),
529 DiffAlgorithm::Minimal => git_xdiff_unified_body_raw_with_ignorable(
530 old_content,
531 new_content,
532 options.context_lines,
533 options.inter_hunk_context,
534 options.indent_heuristic,
535 true,
536 ws,
537 ignorable,
538 ),
539 DiffAlgorithm::Patience => git_patience_unified_body_raw_with_ignorable(
540 old_content,
541 new_content,
542 options.context_lines,
543 options.inter_hunk_context,
544 options.indent_heuristic,
545 ws,
546 ignorable,
547 ),
548 DiffAlgorithm::Histogram => {
549 if ws.ignore_blank_lines {
550 histogram_unified_body_ignore_fc(
551 old_content,
552 new_content,
553 options.context_lines,
554 options.inter_hunk_context,
555 false,
556 None,
557 options.indent_heuristic,
558 ws,
559 blank_only,
560 )
561 } else {
562 histogram_unified_body_raw(
563 old_content,
564 new_content,
565 options.context_lines,
566 options.inter_hunk_context,
567 options.indent_heuristic,
568 ws,
569 )
570 }
571 }
572 };
573 }
574 if ws.ignore_blank_lines {
575 return match options.algorithm {
576 DiffAlgorithm::Myers => git_xdiff_unified_body_raw_with_ignorable(
577 old_content,
578 new_content,
579 options.context_lines,
580 options.inter_hunk_context,
581 options.indent_heuristic,
582 false,
583 ws,
584 Some(blank_only),
585 ),
586 DiffAlgorithm::Minimal => git_xdiff_unified_body_raw_with_ignorable(
587 old_content,
588 new_content,
589 options.context_lines,
590 options.inter_hunk_context,
591 options.indent_heuristic,
592 true,
593 ws,
594 Some(blank_only),
595 ),
596 DiffAlgorithm::Patience => git_patience_unified_body_raw_with_ignorable(
597 old_content,
598 new_content,
599 options.context_lines,
600 options.inter_hunk_context,
601 options.indent_heuristic,
602 ws,
603 Some(blank_only),
604 ),
605 DiffAlgorithm::Histogram => histogram_unified_body_ignore_fc(
606 old_content,
607 new_content,
608 options.context_lines,
609 options.inter_hunk_context,
610 false,
611 None,
612 options.indent_heuristic,
613 ws,
614 blank_only,
615 ),
616 };
617 }
618 }
619 match options.algorithm {
620 DiffAlgorithm::Myers => git_xdiff_unified_body_raw(
621 old_content,
622 new_content,
623 options.context_lines,
624 options.inter_hunk_context,
625 options.indent_heuristic,
626 false,
627 ws,
628 ),
629 DiffAlgorithm::Minimal => git_xdiff_unified_body_raw(
630 old_content,
631 new_content,
632 options.context_lines,
633 options.inter_hunk_context,
634 options.indent_heuristic,
635 true,
636 ws,
637 ),
638 DiffAlgorithm::Patience => git_patience_unified_body_raw(
639 old_content,
640 new_content,
641 options.context_lines,
642 options.inter_hunk_context,
643 options.indent_heuristic,
644 ),
645 DiffAlgorithm::Histogram => histogram_unified_body_raw(
646 old_content,
647 new_content,
648 options.context_lines,
649 options.inter_hunk_context,
650 options.indent_heuristic,
651 ws,
652 ),
653 }
654}
655
656fn git_patience_unified_body_raw(
658 old_content: &str,
659 new_content: &str,
660 context_lines: usize,
661 inter_hunk_context: usize,
662 indent_heuristic: bool,
663) -> String {
664 let before = LineSide::from_text(old_content);
665 let after = LineSide::from_text(new_content);
666 if before.is_empty() && after.is_empty() {
667 return String::new();
668 }
669 let old_refs: Vec<&str> = before.lines.iter().map(|l| l.body).collect();
670 let new_refs: Vec<&str> = after.lines.iter().map(|l| l.body).collect();
671 let ws = super::WhitespaceMode::default();
672 let (mut old_normalized, mut new_normalized) = (Vec::new(), Vec::new());
673 let old_compare = line_compare_keys(old_content, &before, ws, &mut old_normalized);
674 let new_compare = line_compare_keys(new_content, &after, ws, &mut new_normalized);
675 let (changed1, changed2) = git_xdiff::patience_changed_flags(&old_compare, &new_compare);
676 let raw = changed_flags_to_ops(&changed1, &changed2, old_refs.len(), new_refs.len());
677 let ops = diff_indent_heuristic::apply_change_compact_to_ops(
678 &raw,
679 &old_refs,
680 &new_refs,
681 indent_heuristic,
682 );
683 let mut edits = diff_ops_to_edit_spans(&ops);
684 if edits.is_empty() {
685 if old_content != new_content {
686 edits = line_ending_only_edits(&before, &after);
687 }
688 if edits.is_empty() {
689 return String::new();
690 }
691 }
692 emit_unified_body_from_edits(
693 &before,
694 &after,
695 &edits,
696 UnifiedEmitOptions {
697 context_lines,
698 inter_hunk_context,
699 function_context: false,
700 },
701 None,
702 )
703}
704
705pub(crate) fn unified_hunk_header_counts(
707 header_line: &str,
708) -> Option<(usize, usize, usize, usize)> {
709 let rest = header_line.strip_prefix("@@ ")?;
710 let (old_part, rest2) = rest.split_once(" +")?;
711 let old_tok = old_part.strip_prefix('-')?.split(' ').next()?;
712 let new_tok = rest2.split(" @@").next()?.split(' ').next()?;
713
714 fn parse_side(spec: &str) -> (usize, usize) {
715 if let Some((start, count)) = spec.split_once(',') {
716 (start.parse().unwrap_or(0), count.parse().unwrap_or(0))
717 } else {
718 (spec.parse().unwrap_or(0), 1)
719 }
720 }
721
722 let (os, ol) = parse_side(old_tok);
723 let (ns, nl) = parse_side(new_tok);
724 Some((os, ol, ns, nl))
725}
726
727#[allow(clippy::too_many_arguments)]
729pub(crate) fn write_unified_diff_from_diff_options<W: std::io::Write>(
730 out: &mut W,
731 old_content: &str,
732 new_content: &str,
733 old_path: &str,
734 new_path: &str,
735 src_prefix: &str,
736 dst_prefix: &str,
737 funcname_matcher: Option<&FuncnameMatcher>,
738 options: &crate::diff::options::DiffOptions,
739 quote_path_fully: bool,
740) -> Result<()> {
741 use crate::quote_path::format_diff_path_with_prefix;
742
743 if old_path == "/dev/null" {
744 out.write_all(b"--- /dev/null\n")?;
745 } else if src_prefix.is_empty() {
746 writeln!(out, "--- {old_path}")?;
747 } else {
748 out.write_all(b"--- ")?;
749 out.write_all(
750 format_diff_path_with_prefix(src_prefix, old_path, quote_path_fully).as_bytes(),
751 )?;
752 out.write_all(b"\n")?;
753 }
754 if new_path == "/dev/null" {
755 out.write_all(b"+++ /dev/null\n")?;
756 } else if dst_prefix.is_empty() {
757 writeln!(out, "+++ {new_path}")?;
758 } else {
759 out.write_all(b"+++ ")?;
760 out.write_all(
761 format_diff_path_with_prefix(dst_prefix, new_path, quote_path_fully).as_bytes(),
762 )?;
763 out.write_all(b"\n")?;
764 }
765
766 let body = unified_hunk_body_for_diff_options(old_content, new_content, options);
767 if body.is_empty() {
768 return Ok(());
769 }
770 let old_lines: Vec<&str> = old_content.lines().collect();
771 for hunk_str in imara_unified_hunk_slices(&body) {
772 if hunk_str.is_empty() {
773 continue;
774 }
775 if let Some(first_newline) = hunk_str.find('\n') {
776 let header_line = &hunk_str[..first_newline];
777 let rest = &hunk_str[first_newline..];
778 if let Some(func_ctx) =
779 extract_function_context(header_line, &old_lines, funcname_matcher, Some(old_path))
780 {
781 out.write_all(header_line.as_bytes())?;
782 out.write_all(b" ")?;
783 out.write_all(func_ctx.as_bytes())?;
784 out.write_all(rest.as_bytes())?;
785 } else {
786 out.write_all(hunk_str.as_bytes())?;
787 }
788 } else {
789 out.write_all(hunk_str.as_bytes())?;
790 }
791 }
792 Ok(())
793}
794
795#[expect(clippy::too_many_arguments)]
796fn unified_diff_git_myers_with_prefix_and_funcname(
797 old_content: &str,
798 new_content: &str,
799 old_path: &str,
800 new_path: &str,
801 context_lines: usize,
802 inter_hunk_context: usize,
803 src_prefix: &str,
804 dst_prefix: &str,
805 funcname_matcher: Option<&FuncnameMatcher>,
806 indent_heuristic: bool,
807 quote_path_fully: bool,
808) -> String {
809 use crate::quote_path::format_diff_path_with_prefix;
810
811 let body = git_xdiff_unified_body_raw(
812 old_content,
813 new_content,
814 context_lines,
815 inter_hunk_context,
816 indent_heuristic,
817 false,
818 super::WhitespaceMode::default(),
819 );
820
821 let mut output = String::new();
822 if old_path == "/dev/null" {
823 output.push_str("--- /dev/null\n");
824 } else if src_prefix.is_empty() {
825 output.push_str(&format!("--- {old_path}\n"));
826 } else {
827 output.push_str("--- ");
828 output.push_str(&format_diff_path_with_prefix(
829 src_prefix,
830 old_path,
831 quote_path_fully,
832 ));
833 output.push('\n');
834 }
835 if new_path == "/dev/null" {
836 output.push_str("+++ /dev/null\n");
837 } else if dst_prefix.is_empty() {
838 output.push_str(&format!("+++ {new_path}\n"));
839 } else {
840 output.push_str("+++ ");
841 output.push_str(&format_diff_path_with_prefix(
842 dst_prefix,
843 new_path,
844 quote_path_fully,
845 ));
846 output.push('\n');
847 }
848
849 let old_lines: Vec<&str> = old_content.lines().collect();
850 for hunk_str in imara_unified_hunk_slices(&body) {
851 if hunk_str.is_empty() {
852 continue;
853 }
854 if let Some(first_newline) = hunk_str.find('\n') {
855 let header_line = &hunk_str[..first_newline];
856 let rest = &hunk_str[first_newline..];
857 if let Some(func_ctx) =
858 extract_function_context(header_line, &old_lines, funcname_matcher, Some(old_path))
859 {
860 output.push_str(header_line);
861 output.push(' ');
862 output.push_str(&func_ctx);
863 output.push_str(rest);
864 } else {
865 output.push_str(hunk_str);
866 }
867 } else {
868 output.push_str(hunk_str);
869 }
870 }
871
872 annotate_unified_diff_no_newline_at_eof(&output, old_content, new_content)
873}
874
875#[expect(clippy::too_many_arguments)]
876fn unified_diff_git_minimal_with_prefix_and_funcname(
877 old_content: &str,
878 new_content: &str,
879 old_path: &str,
880 new_path: &str,
881 context_lines: usize,
882 inter_hunk_context: usize,
883 src_prefix: &str,
884 dst_prefix: &str,
885 funcname_matcher: Option<&FuncnameMatcher>,
886 indent_heuristic: bool,
887 quote_path_fully: bool,
888) -> String {
889 use crate::quote_path::format_diff_path_with_prefix;
890
891 let body = git_xdiff_unified_body_raw(
892 old_content,
893 new_content,
894 context_lines,
895 inter_hunk_context,
896 indent_heuristic,
897 true,
898 super::WhitespaceMode::default(),
899 );
900
901 let mut output = String::new();
902 if old_path == "/dev/null" {
903 output.push_str("--- /dev/null\n");
904 } else if src_prefix.is_empty() {
905 output.push_str(&format!("--- {old_path}\n"));
906 } else {
907 output.push_str("--- ");
908 output.push_str(&format_diff_path_with_prefix(
909 src_prefix,
910 old_path,
911 quote_path_fully,
912 ));
913 output.push('\n');
914 }
915 if new_path == "/dev/null" {
916 output.push_str("+++ /dev/null\n");
917 } else if dst_prefix.is_empty() {
918 output.push_str(&format!("+++ {new_path}\n"));
919 } else {
920 output.push_str("+++ ");
921 output.push_str(&format_diff_path_with_prefix(
922 dst_prefix,
923 new_path,
924 quote_path_fully,
925 ));
926 output.push('\n');
927 }
928
929 let old_lines: Vec<&str> = old_content.lines().collect();
930 for hunk_str in imara_unified_hunk_slices(&body) {
931 if hunk_str.is_empty() {
932 continue;
933 }
934 if let Some(first_newline) = hunk_str.find('\n') {
935 let header_line = &hunk_str[..first_newline];
936 let rest = &hunk_str[first_newline..];
937 if let Some(func_ctx) =
938 extract_function_context(header_line, &old_lines, funcname_matcher, Some(old_path))
939 {
940 output.push_str(header_line);
941 output.push(' ');
942 output.push_str(&func_ctx);
943 output.push_str(rest);
944 } else {
945 output.push_str(hunk_str);
946 }
947 } else {
948 output.push_str(hunk_str);
949 }
950 }
951
952 annotate_unified_diff_no_newline_at_eof(&output, old_content, new_content)
953}
954
955#[expect(clippy::too_many_arguments)]
956fn unified_diff_git_patience_with_prefix_and_funcname(
957 old_content: &str,
958 new_content: &str,
959 old_path: &str,
960 new_path: &str,
961 context_lines: usize,
962 inter_hunk_context: usize,
963 src_prefix: &str,
964 dst_prefix: &str,
965 funcname_matcher: Option<&FuncnameMatcher>,
966 indent_heuristic: bool,
967 quote_path_fully: bool,
968) -> String {
969 use crate::quote_path::format_diff_path_with_prefix;
970
971 let body = git_patience_unified_body_raw(
972 old_content,
973 new_content,
974 context_lines,
975 inter_hunk_context,
976 indent_heuristic,
977 );
978
979 let mut output = String::new();
980 if old_path == "/dev/null" {
981 output.push_str("--- /dev/null\n");
982 } else if src_prefix.is_empty() {
983 output.push_str(&format!("--- {old_path}\n"));
984 } else {
985 output.push_str("--- ");
986 output.push_str(&format_diff_path_with_prefix(
987 src_prefix,
988 old_path,
989 quote_path_fully,
990 ));
991 output.push('\n');
992 }
993 if new_path == "/dev/null" {
994 output.push_str("+++ /dev/null\n");
995 } else if dst_prefix.is_empty() {
996 output.push_str(&format!("+++ {new_path}\n"));
997 } else {
998 output.push_str("+++ ");
999 output.push_str(&format_diff_path_with_prefix(
1000 dst_prefix,
1001 new_path,
1002 quote_path_fully,
1003 ));
1004 output.push('\n');
1005 }
1006
1007 let old_lines: Vec<&str> = old_content.lines().collect();
1008 for hunk_str in imara_unified_hunk_slices(&body) {
1009 if hunk_str.is_empty() {
1010 continue;
1011 }
1012 if let Some(first_newline) = hunk_str.find('\n') {
1013 let header_line = &hunk_str[..first_newline];
1014 let rest = &hunk_str[first_newline..];
1015 if let Some(func_ctx) =
1016 extract_function_context(header_line, &old_lines, funcname_matcher, Some(old_path))
1017 {
1018 output.push_str(header_line);
1019 output.push(' ');
1020 output.push_str(&func_ctx);
1021 output.push_str(rest);
1022 } else {
1023 output.push_str(hunk_str);
1024 }
1025 } else {
1026 output.push_str(hunk_str);
1027 }
1028 }
1029
1030 annotate_unified_diff_no_newline_at_eof(&output, old_content, new_content)
1031}
1032
1033#[must_use]
1037pub fn unified_diff_histogram_hunks_only(
1038 old_content: &str,
1039 new_content: &str,
1040 context_lines: usize,
1041 inter_hunk_context: usize,
1042 indent_heuristic: bool,
1043) -> String {
1044 histogram_unified_body_raw(
1045 old_content,
1046 new_content,
1047 context_lines,
1048 inter_hunk_context,
1049 indent_heuristic,
1050 super::WhitespaceMode::default(),
1051 )
1052}
1053#[expect(clippy::too_many_arguments)]
1054#[must_use]
1056pub fn unified_diff_histogram_with_prefix_and_funcname(
1057 old_content: &str,
1058 new_content: &str,
1059 old_path: &str,
1060 new_path: &str,
1061 context_lines: usize,
1062 inter_hunk_context: usize,
1063 src_prefix: &str,
1064 dst_prefix: &str,
1065 funcname_matcher: Option<&FuncnameMatcher>,
1066 indent_heuristic: bool,
1067 quote_path_fully: bool,
1068) -> String {
1069 unified_diff_histogram_with_prefix_and_funcname_indent(
1070 old_content,
1071 new_content,
1072 old_path,
1073 new_path,
1074 context_lines,
1075 inter_hunk_context,
1076 src_prefix,
1077 dst_prefix,
1078 funcname_matcher,
1079 indent_heuristic,
1080 quote_path_fully,
1081 )
1082}
1083
1084#[expect(clippy::too_many_arguments)]
1085fn unified_diff_histogram_with_prefix_and_funcname_indent(
1086 old_content: &str,
1087 new_content: &str,
1088 old_path: &str,
1089 new_path: &str,
1090 context_lines: usize,
1091 inter_hunk_context: usize,
1092 src_prefix: &str,
1093 dst_prefix: &str,
1094 funcname_matcher: Option<&FuncnameMatcher>,
1095 indent_heuristic: bool,
1096 quote_path_fully: bool,
1097) -> String {
1098 use crate::quote_path::format_diff_path_with_prefix;
1099
1100 let body = histogram_unified_body_raw(
1101 old_content,
1102 new_content,
1103 context_lines,
1104 inter_hunk_context,
1105 indent_heuristic,
1106 super::WhitespaceMode::default(),
1107 );
1108
1109 let mut output = String::new();
1110 if old_path == "/dev/null" {
1111 output.push_str("--- /dev/null\n");
1112 } else if src_prefix.is_empty() {
1113 output.push_str(&format!("--- {old_path}\n"));
1114 } else {
1115 output.push_str("--- ");
1116 output.push_str(&format_diff_path_with_prefix(
1117 src_prefix,
1118 old_path,
1119 quote_path_fully,
1120 ));
1121 output.push('\n');
1122 }
1123 if new_path == "/dev/null" {
1124 output.push_str("+++ /dev/null\n");
1125 } else if dst_prefix.is_empty() {
1126 output.push_str(&format!("+++ {new_path}\n"));
1127 } else {
1128 output.push_str("+++ ");
1129 output.push_str(&format_diff_path_with_prefix(
1130 dst_prefix,
1131 new_path,
1132 quote_path_fully,
1133 ));
1134 output.push('\n');
1135 }
1136
1137 let old_lines: Vec<&str> = old_content.lines().collect();
1138 for hunk_str in imara_unified_hunk_slices(&body) {
1139 if hunk_str.is_empty() {
1140 continue;
1141 }
1142 if let Some(first_newline) = hunk_str.find('\n') {
1143 let header_line = &hunk_str[..first_newline];
1144 let rest = &hunk_str[first_newline..];
1145 if let Some(func_ctx) =
1146 extract_function_context(header_line, &old_lines, funcname_matcher, Some(old_path))
1147 {
1148 output.push_str(header_line);
1149 output.push(' ');
1150 output.push_str(&func_ctx);
1151 output.push_str(rest);
1152 } else {
1153 output.push_str(hunk_str);
1154 }
1155 } else {
1156 output.push_str(hunk_str);
1157 }
1158 }
1159
1160 annotate_unified_diff_no_newline_at_eof(&output, old_content, new_content)
1161}
1162
1163#[allow(clippy::too_many_arguments)]
1173pub fn unified_diff_histogram_ignore_with_prefix_and_funcname<F>(
1174 old_content: &str,
1175 new_content: &str,
1176 old_path: &str,
1177 new_path: &str,
1178 context_lines: usize,
1179 inter_hunk_context: usize,
1180 src_prefix: &str,
1181 dst_prefix: &str,
1182 funcname_matcher: Option<&FuncnameMatcher>,
1183 quote_path_fully: bool,
1184 function_context: bool,
1185 indent_heuristic: bool,
1186 is_ignorable_change: F,
1187) -> String
1188where
1189 F: Fn(&[&str], &[&str]) -> bool,
1190{
1191 use crate::quote_path::format_diff_path_with_prefix;
1192
1193 let body = histogram_unified_body_ignore_fc(
1194 old_content,
1195 new_content,
1196 context_lines,
1197 inter_hunk_context,
1198 function_context,
1199 funcname_matcher,
1200 indent_heuristic,
1201 super::WhitespaceMode::default(),
1202 is_ignorable_change,
1203 );
1204 if body.is_empty() {
1205 return String::new();
1206 }
1207
1208 let mut output = String::new();
1209 if old_path == "/dev/null" {
1210 output.push_str("--- /dev/null\n");
1211 } else if src_prefix.is_empty() {
1212 output.push_str(&format!("--- {old_path}\n"));
1213 } else {
1214 output.push_str("--- ");
1215 output.push_str(&format_diff_path_with_prefix(
1216 src_prefix,
1217 old_path,
1218 quote_path_fully,
1219 ));
1220 output.push('\n');
1221 }
1222 if new_path == "/dev/null" {
1223 output.push_str("+++ /dev/null\n");
1224 } else if dst_prefix.is_empty() {
1225 output.push_str(&format!("+++ {new_path}\n"));
1226 } else {
1227 output.push_str("+++ ");
1228 output.push_str(&format_diff_path_with_prefix(
1229 dst_prefix,
1230 new_path,
1231 quote_path_fully,
1232 ));
1233 output.push('\n');
1234 }
1235
1236 let old_lines: Vec<&str> = old_content.lines().collect();
1237 for hunk_str in imara_unified_hunk_slices(&body) {
1238 if hunk_str.is_empty() {
1239 continue;
1240 }
1241 if let Some(first_newline) = hunk_str.find('\n') {
1242 let header_line = &hunk_str[..first_newline];
1243 let rest = &hunk_str[first_newline..];
1244 if let Some(func_ctx) =
1245 extract_function_context(header_line, &old_lines, funcname_matcher, Some(old_path))
1246 {
1247 output.push_str(header_line);
1248 output.push(' ');
1249 output.push_str(&func_ctx);
1250 output.push_str(rest);
1251 } else {
1252 output.push_str(hunk_str);
1253 }
1254 } else {
1255 output.push_str(hunk_str);
1256 }
1257 }
1258
1259 output
1260}
1261
1262#[must_use]
1264pub fn indent_heuristic_from_config(config: &ConfigSet) -> bool {
1265 match config.get_bool("diff.indentHeuristic") {
1266 Some(Ok(b)) => b,
1267 Some(Err(_)) | None => true,
1268 }
1269}
1270
1271#[must_use]
1273pub fn resolve_indent_heuristic(
1274 config: &ConfigSet,
1275 cli_indent_heuristic: bool,
1276 cli_no_indent_heuristic: bool,
1277) -> bool {
1278 if cli_no_indent_heuristic {
1279 false
1280 } else if cli_indent_heuristic {
1281 true
1282 } else {
1283 indent_heuristic_from_config(config)
1284 }
1285}
1286
1287mod git_xdiff {
1297 const XDL_MAX_COST_MIN: i64 = 256;
1298 const XDL_HEUR_MIN_COST: i64 = 256;
1299 const XDL_SNAKE_CNT: i64 = 20;
1300 const XDL_K_HEUR: i64 = 4;
1301 const XDL_LINE_MAX: i64 = i64::MAX;
1302 const XDL_KPDIS_RUN: i64 = 4;
1303 const XDL_MAX_EQLIMIT: i64 = 1024;
1304 const XDL_SIMSCAN_WINDOW: i64 = 100;
1305
1306 const DISCARD: u8 = 0;
1308 const KEEP: u8 = 1;
1309 const INVESTIGATE: u8 = 2;
1310
1311 fn bogosqrt(mut n: i64) -> i64 {
1313 let mut i: i64 = 1;
1314 while n > 0 {
1315 i <<= 1;
1316 n >>= 2;
1317 }
1318 i
1319 }
1320
1321 struct XdAlgoEnv {
1322 mxcost: i64,
1323 snake_cnt: i64,
1324 heur_min: i64,
1325 }
1326
1327 struct XdpSplit {
1328 i1: i64,
1329 i2: i64,
1330 min_lo: bool,
1331 min_hi: bool,
1332 }
1333
1334 struct XdFile<'a> {
1337 ids: &'a [u32],
1338 changed: Vec<bool>,
1339 reference_index: Vec<usize>,
1340 dstart: i64,
1341 dend: i64,
1342 nreff: i64,
1343 }
1344
1345 #[inline]
1347 fn get_id(xdf: &XdFile<'_>, idx: i64) -> u32 {
1348 xdf.ids[xdf.reference_index[idx as usize]]
1349 }
1350
1351 #[allow(clippy::too_many_arguments)]
1354 fn xdl_split(
1355 xdf1: &XdFile<'_>,
1356 off1: i64,
1357 lim1: i64,
1358 xdf2: &XdFile<'_>,
1359 off2: i64,
1360 lim2: i64,
1361 kvdf: &mut [i64],
1362 kvdb: &mut [i64],
1363 koff: i64,
1364 need_min: bool,
1365 spl: &mut XdpSplit,
1366 xenv: &XdAlgoEnv,
1367 ) -> i64 {
1368 let dmin = off1 - lim2;
1369 let dmax = lim1 - off2;
1370 let fmid = off1 - off2;
1371 let bmid = lim1 - lim2;
1372 let odd = ((fmid - bmid) & 1) != 0;
1373 let mut fmin = fmid;
1374 let mut fmax = fmid;
1375 let mut bmin = bmid;
1376 let mut bmax = bmid;
1377
1378 let kf = |k: i64| -> usize { (k + koff) as usize };
1379
1380 kvdf[kf(fmid)] = off1;
1381 kvdb[kf(bmid)] = lim1;
1382
1383 let mut ec: i64 = 1;
1384 loop {
1385 let mut got_snake = false;
1386
1387 if fmin > dmin {
1388 fmin -= 1;
1389 kvdf[kf(fmin - 1)] = -1;
1390 } else {
1391 fmin += 1;
1392 }
1393 if fmax < dmax {
1394 fmax += 1;
1395 kvdf[kf(fmax + 1)] = -1;
1396 } else {
1397 fmax -= 1;
1398 }
1399
1400 let mut d = fmax;
1401 while d >= fmin {
1402 let mut i1 = if kvdf[kf(d - 1)] >= kvdf[kf(d + 1)] {
1403 kvdf[kf(d - 1)] + 1
1404 } else {
1405 kvdf[kf(d + 1)]
1406 };
1407 let prev1 = i1;
1408 let mut i2 = i1 - d;
1409 while i1 < lim1 && i2 < lim2 && get_id(xdf1, i1) == get_id(xdf2, i2) {
1410 i1 += 1;
1411 i2 += 1;
1412 }
1413 if i1 - prev1 > xenv.snake_cnt {
1414 got_snake = true;
1415 }
1416 kvdf[kf(d)] = i1;
1417 if odd && bmin <= d && d <= bmax && kvdb[kf(d)] <= i1 {
1418 spl.i1 = i1;
1419 spl.i2 = i2;
1420 spl.min_lo = true;
1421 spl.min_hi = true;
1422 return ec;
1423 }
1424 d -= 2;
1425 }
1426
1427 if bmin > dmin {
1428 bmin -= 1;
1429 kvdb[kf(bmin - 1)] = XDL_LINE_MAX;
1430 } else {
1431 bmin += 1;
1432 }
1433 if bmax < dmax {
1434 bmax += 1;
1435 kvdb[kf(bmax + 1)] = XDL_LINE_MAX;
1436 } else {
1437 bmax -= 1;
1438 }
1439
1440 let mut d = bmax;
1441 while d >= bmin {
1442 let mut i1 = if kvdb[kf(d - 1)] < kvdb[kf(d + 1)] {
1443 kvdb[kf(d - 1)]
1444 } else {
1445 kvdb[kf(d + 1)] - 1
1446 };
1447 let prev1 = i1;
1448 let mut i2 = i1 - d;
1449 while i1 > off1 && i2 > off2 && get_id(xdf1, i1 - 1) == get_id(xdf2, i2 - 1) {
1450 i1 -= 1;
1451 i2 -= 1;
1452 }
1453 if prev1 - i1 > xenv.snake_cnt {
1454 got_snake = true;
1455 }
1456 kvdb[kf(d)] = i1;
1457 if !odd && fmin <= d && d <= fmax && i1 <= kvdf[kf(d)] {
1458 spl.i1 = i1;
1459 spl.i2 = i2;
1460 spl.min_lo = true;
1461 spl.min_hi = true;
1462 return ec;
1463 }
1464 d -= 2;
1465 }
1466
1467 if need_min {
1468 ec += 1;
1469 continue;
1470 }
1471
1472 if got_snake && ec > xenv.heur_min {
1473 let mut best = 0i64;
1474 let mut d = fmax;
1475 while d >= fmin {
1476 let dd = if d > fmid { d - fmid } else { fmid - d };
1477 let i1 = kvdf[kf(d)];
1478 let i2 = i1 - d;
1479 let v = (i1 - off1) + (i2 - off2) - dd;
1480 if v > XDL_K_HEUR * ec
1481 && v > best
1482 && off1 + xenv.snake_cnt <= i1
1483 && i1 < lim1
1484 && off2 + xenv.snake_cnt <= i2
1485 && i2 < lim2
1486 {
1487 let mut k = 1i64;
1488 while get_id(xdf1, i1 - k) == get_id(xdf2, i2 - k) {
1489 if k == xenv.snake_cnt {
1490 best = v;
1491 spl.i1 = i1;
1492 spl.i2 = i2;
1493 break;
1494 }
1495 k += 1;
1496 }
1497 }
1498 d -= 2;
1499 }
1500 if best > 0 {
1501 spl.min_lo = true;
1502 spl.min_hi = false;
1503 return ec;
1504 }
1505
1506 let mut best = 0i64;
1507 let mut d = bmax;
1508 while d >= bmin {
1509 let dd = if d > bmid { d - bmid } else { bmid - d };
1510 let i1 = kvdb[kf(d)];
1511 let i2 = i1 - d;
1512 let v = (lim1 - i1) + (lim2 - i2) - dd;
1513 if v > XDL_K_HEUR * ec
1514 && v > best
1515 && off1 < i1
1516 && i1 <= lim1 - xenv.snake_cnt
1517 && off2 < i2
1518 && i2 <= lim2 - xenv.snake_cnt
1519 {
1520 let mut k = 0i64;
1521 while get_id(xdf1, i1 + k) == get_id(xdf2, i2 + k) {
1522 if k == xenv.snake_cnt - 1 {
1523 best = v;
1524 spl.i1 = i1;
1525 spl.i2 = i2;
1526 break;
1527 }
1528 k += 1;
1529 }
1530 }
1531 d -= 2;
1532 }
1533 if best > 0 {
1534 spl.min_lo = false;
1535 spl.min_hi = true;
1536 return ec;
1537 }
1538 }
1539
1540 if ec >= xenv.mxcost {
1541 let mut fbest = -1i64;
1542 let mut fbest1 = -1i64;
1543 let mut d = fmax;
1544 while d >= fmin {
1545 let mut i1 = kvdf[kf(d)].min(lim1);
1546 let mut i2 = i1 - d;
1547 if lim2 < i2 {
1548 i1 = lim2 + d;
1549 i2 = lim2;
1550 }
1551 if fbest < i1 + i2 {
1552 fbest = i1 + i2;
1553 fbest1 = i1;
1554 }
1555 d -= 2;
1556 }
1557
1558 let mut bbest = XDL_LINE_MAX;
1559 let mut bbest1 = XDL_LINE_MAX;
1560 let mut d = bmax;
1561 while d >= bmin {
1562 let mut i1 = off1.max(kvdb[kf(d)]);
1563 let mut i2 = i1 - d;
1564 if i2 < off2 {
1565 i1 = off2 + d;
1566 i2 = off2;
1567 }
1568 if i1 + i2 < bbest {
1569 bbest = i1 + i2;
1570 bbest1 = i1;
1571 }
1572 d -= 2;
1573 }
1574
1575 if (lim1 + lim2) - bbest < fbest - (off1 + off2) {
1576 spl.i1 = fbest1;
1577 spl.i2 = fbest - fbest1;
1578 spl.min_lo = true;
1579 spl.min_hi = false;
1580 } else {
1581 spl.i1 = bbest1;
1582 spl.i2 = bbest - bbest1;
1583 spl.min_lo = false;
1584 spl.min_hi = true;
1585 }
1586 return ec;
1587 }
1588
1589 ec += 1;
1590 }
1591 }
1592
1593 #[allow(clippy::too_many_arguments)]
1595 fn xdl_recs_cmp(
1596 xdf1: &mut XdFile<'_>,
1597 mut off1: i64,
1598 mut lim1: i64,
1599 xdf2: &mut XdFile<'_>,
1600 mut off2: i64,
1601 mut lim2: i64,
1602 kvdf: &mut [i64],
1603 kvdb: &mut [i64],
1604 koff: i64,
1605 need_min: bool,
1606 xenv: &XdAlgoEnv,
1607 ) {
1608 while off1 < lim1 && off2 < lim2 && get_id(xdf1, off1) == get_id(xdf2, off2) {
1609 off1 += 1;
1610 off2 += 1;
1611 }
1612 while off1 < lim1 && off2 < lim2 && get_id(xdf1, lim1 - 1) == get_id(xdf2, lim2 - 1) {
1613 lim1 -= 1;
1614 lim2 -= 1;
1615 }
1616
1617 if off1 == lim1 {
1618 while off2 < lim2 {
1619 let r = xdf2.reference_index[off2 as usize];
1620 xdf2.changed[r] = true;
1621 off2 += 1;
1622 }
1623 } else if off2 == lim2 {
1624 while off1 < lim1 {
1625 let r = xdf1.reference_index[off1 as usize];
1626 xdf1.changed[r] = true;
1627 off1 += 1;
1628 }
1629 } else {
1630 let mut spl = XdpSplit {
1631 i1: 0,
1632 i2: 0,
1633 min_lo: false,
1634 min_hi: false,
1635 };
1636 xdl_split(
1637 xdf1, off1, lim1, xdf2, off2, lim2, kvdf, kvdb, koff, need_min, &mut spl, xenv,
1638 );
1639 xdl_recs_cmp(
1640 xdf1, off1, spl.i1, xdf2, off2, spl.i2, kvdf, kvdb, koff, spl.min_lo, xenv,
1641 );
1642 xdl_recs_cmp(
1643 xdf1, spl.i1, lim1, xdf2, spl.i2, lim2, kvdf, kvdb, koff, spl.min_hi, xenv,
1644 );
1645 }
1646 }
1647
1648 fn clean_mmatch(action: &[u8], i: i64, mut s: i64, mut e: i64) -> bool {
1650 if i - s > XDL_SIMSCAN_WINDOW {
1651 s = i - XDL_SIMSCAN_WINDOW;
1652 }
1653 if e - i > XDL_SIMSCAN_WINDOW {
1654 e = i + XDL_SIMSCAN_WINDOW;
1655 }
1656
1657 let mut rdis0 = 0i64;
1658 let mut rpdis0 = 1i64;
1659 let mut r = 1i64;
1660 while i - r >= s {
1661 match action[(i - r) as usize] {
1662 DISCARD => rdis0 += 1,
1663 INVESTIGATE => rpdis0 += 1,
1664 _ => break, }
1666 r += 1;
1667 }
1668 if rdis0 == 0 {
1669 return false;
1670 }
1671 let mut rdis1 = 0i64;
1672 let mut rpdis1 = 1i64;
1673 let mut r = 1i64;
1674 while i + r <= e {
1675 match action[(i + r) as usize] {
1676 DISCARD => rdis1 += 1,
1677 INVESTIGATE => rpdis1 += 1,
1678 _ => break, }
1680 r += 1;
1681 }
1682 if rdis1 == 0 {
1683 return false;
1684 }
1685 rdis1 += rdis0;
1686 rpdis1 += rpdis0;
1687 rpdis1 * XDL_KPDIS_RUN < (rpdis1 + rdis1)
1688 }
1689
1690 pub fn changed_flags(ids1: &[u32], ids2: &[u32], need_min: bool) -> (Vec<bool>, Vec<bool>) {
1696 changed_flags_with_shared(ids1, ids2, &[], 0, need_min)
1697 }
1698
1699 pub fn changed_flags_with_shared(
1706 ids1: &[u32],
1707 ids2: &[u32],
1708 shared_counts: &[u32],
1709 shared_records: usize,
1710 need_min: bool,
1711 ) -> (Vec<bool>, Vec<bool>) {
1712 let nrec1 = ids1.len();
1713 let nrec2 = ids2.len();
1714
1715 let nclasses = ids1
1717 .iter()
1718 .chain(ids2)
1719 .max()
1720 .map_or(0, |&max| max as usize + 1);
1721 let mut count1 = vec![0i64; nclasses];
1722 let mut count2 = vec![0i64; nclasses];
1723 for &id in ids1 {
1724 count1[id as usize] += 1;
1725 }
1726 for &id in ids2 {
1727 count2[id as usize] += 1;
1728 }
1729 for (id, &shared) in shared_counts.iter().enumerate().take(nclasses) {
1730 count1[id] += i64::from(shared);
1731 count2[id] += i64::from(shared);
1732 }
1733
1734 let mut xdf1 = XdFile {
1735 ids: ids1,
1736 changed: vec![false; nrec1],
1737 reference_index: Vec::new(),
1738 dstart: 0,
1739 dend: nrec1 as i64 - 1,
1740 nreff: 0,
1741 };
1742 let mut xdf2 = XdFile {
1743 ids: ids2,
1744 changed: vec![false; nrec2],
1745 reference_index: Vec::new(),
1746 dstart: 0,
1747 dend: nrec2 as i64 - 1,
1748 nreff: 0,
1749 };
1750
1751 let lim = nrec1.min(nrec2) as i64;
1753 let mut i = 0i64;
1754 while i < lim && ids1[i as usize] == ids2[i as usize] {
1755 i += 1;
1756 }
1757 xdf1.dstart = i;
1758 xdf2.dstart = i;
1759 let mut j = 0i64;
1760 let rem = lim - i;
1761 while j < rem && ids1[nrec1 - 1 - j as usize] == ids2[nrec2 - 1 - j as usize] {
1762 j += 1;
1763 }
1764 xdf1.dend = nrec1 as i64 - j - 1;
1765 xdf2.dend = nrec2 as i64 - j - 1;
1766
1767 let mut action1 = vec![0u8; nrec1 + 1];
1769 let mut action2 = vec![0u8; nrec2 + 1];
1770
1771 let mlim1 = if need_min {
1772 i64::MAX
1773 } else {
1774 let mut mlim = bogosqrt((nrec1 + shared_records) as i64);
1775 if mlim > XDL_MAX_EQLIMIT {
1776 mlim = XDL_MAX_EQLIMIT;
1777 }
1778 mlim
1779 };
1780 let mut idx = xdf1.dstart;
1781 while idx <= xdf1.dend {
1782 let id = ids1[idx as usize];
1783 let nm = count2[id as usize];
1784 action1[idx as usize] = if nm == 0 {
1785 DISCARD
1786 } else if nm >= mlim1 {
1787 INVESTIGATE
1788 } else {
1789 KEEP
1790 };
1791 idx += 1;
1792 }
1793
1794 let mlim2 = if need_min {
1795 i64::MAX
1796 } else {
1797 let mut mlim = bogosqrt((nrec2 + shared_records) as i64);
1798 if mlim > XDL_MAX_EQLIMIT {
1799 mlim = XDL_MAX_EQLIMIT;
1800 }
1801 mlim
1802 };
1803 let mut idx = xdf2.dstart;
1804 while idx <= xdf2.dend {
1805 let id = ids2[idx as usize];
1806 let nm = count1[id as usize];
1807 action2[idx as usize] = if nm == 0 {
1808 DISCARD
1809 } else if nm >= mlim2 {
1810 INVESTIGATE
1811 } else {
1812 KEEP
1813 };
1814 idx += 1;
1815 }
1816
1817 let mut idx = xdf1.dstart;
1818 while idx <= xdf1.dend {
1819 let a = action1[idx as usize];
1820 if a == KEEP
1821 || (a == INVESTIGATE && !clean_mmatch(&action1, idx, xdf1.dstart, xdf1.dend))
1822 {
1823 xdf1.reference_index.push(idx as usize);
1824 } else {
1825 xdf1.changed[idx as usize] = true;
1826 }
1827 idx += 1;
1828 }
1829 xdf1.nreff = xdf1.reference_index.len() as i64;
1830
1831 let mut idx = xdf2.dstart;
1832 while idx <= xdf2.dend {
1833 let a = action2[idx as usize];
1834 if a == KEEP
1835 || (a == INVESTIGATE && !clean_mmatch(&action2, idx, xdf2.dstart, xdf2.dend))
1836 {
1837 xdf2.reference_index.push(idx as usize);
1838 } else {
1839 xdf2.changed[idx as usize] = true;
1840 }
1841 idx += 1;
1842 }
1843 xdf2.nreff = xdf2.reference_index.len() as i64;
1844
1845 let ndiags = xdf1.nreff + xdf2.nreff + 3;
1847 let kvd_len = (2 * ndiags + 2) as usize;
1848 let mut kvd = vec![0i64; kvd_len];
1849 let koff = xdf2.nreff + 1;
1851 let (kvdf_slice, kvdb_slice) = kvd.split_at_mut(ndiags as usize);
1852 let xenv = XdAlgoEnv {
1855 mxcost: bogosqrt(ndiags).max(XDL_MAX_COST_MIN),
1856 snake_cnt: XDL_SNAKE_CNT,
1857 heur_min: XDL_HEUR_MIN_COST,
1858 };
1859
1860 let nreff1 = xdf1.nreff;
1861 let nreff2 = xdf2.nreff;
1862 xdl_recs_cmp(
1863 &mut xdf1, 0, nreff1, &mut xdf2, 0, nreff2, kvdf_slice, kvdb_slice, koff, need_min,
1864 &xenv,
1865 );
1866
1867 (xdf1.changed, xdf2.changed)
1868 }
1869
1870 pub fn patience_changed_flags(lines1: &[&str], lines2: &[&str]) -> (Vec<bool>, Vec<bool>) {
1872 let mut changed1 = vec![false; lines1.len()];
1873 let mut changed2 = vec![false; lines2.len()];
1874 patience_recurse(
1875 lines1,
1876 lines2,
1877 &mut changed1,
1878 &mut changed2,
1879 0,
1880 lines1.len(),
1881 0,
1882 lines2.len(),
1883 );
1884 (changed1, changed2)
1885 }
1886
1887 #[allow(clippy::too_many_arguments)]
1888 fn patience_recurse(
1889 lines1: &[&str],
1890 lines2: &[&str],
1891 changed1: &mut [bool],
1892 changed2: &mut [bool],
1893 o_start: usize,
1894 o_end: usize,
1895 n_start: usize,
1896 n_end: usize,
1897 ) {
1898 if o_start >= o_end && n_start >= n_end {
1899 return;
1900 }
1901 if o_start >= o_end {
1902 changed2[n_start..n_end].fill(true);
1903 return;
1904 }
1905 if n_start >= n_end {
1906 changed1[o_start..o_end].fill(true);
1907 return;
1908 }
1909
1910 let anchors = patience_anchors(lines1, lines2, o_start, o_end, n_start, n_end);
1911 if anchors.is_empty() {
1912 merge_myers_region(
1913 lines1, lines2, changed1, changed2, o_start, o_end, n_start, n_end,
1914 );
1915 return;
1916 }
1917
1918 let mut prev_o = o_start;
1919 let mut prev_n = n_start;
1920 for (ao, an) in anchors {
1921 patience_recurse(lines1, lines2, changed1, changed2, prev_o, ao, prev_n, an);
1922 prev_o = ao + 1;
1923 prev_n = an + 1;
1924 }
1925 patience_recurse(
1926 lines1, lines2, changed1, changed2, prev_o, o_end, prev_n, n_end,
1927 );
1928 }
1929
1930 #[allow(clippy::too_many_arguments)]
1931 fn patience_anchors(
1932 lines1: &[&str],
1933 lines2: &[&str],
1934 o_start: usize,
1935 o_end: usize,
1936 n_start: usize,
1937 n_end: usize,
1938 ) -> Vec<(usize, usize)> {
1939 use std::collections::HashMap;
1940
1941 let old_slice = &lines1[o_start..o_end];
1942 let new_slice = &lines2[n_start..n_end];
1943 let mut old_pos: HashMap<&str, Vec<usize>> = HashMap::new();
1944 let mut new_pos: HashMap<&str, Vec<usize>> = HashMap::new();
1945 for (i, line) in old_slice.iter().enumerate() {
1946 old_pos.entry(*line).or_default().push(o_start + i);
1947 }
1948 for (i, line) in new_slice.iter().enumerate() {
1949 new_pos.entry(*line).or_default().push(n_start + i);
1950 }
1951
1952 let mut pairs: Vec<(usize, usize)> = Vec::new();
1953 for (line, o_indices) in &old_pos {
1954 if o_indices.len() != 1 {
1955 continue;
1956 }
1957 if let Some(n_indices) = new_pos.get(line) {
1958 if n_indices.len() == 1 {
1959 pairs.push((o_indices[0], n_indices[0]));
1960 }
1961 }
1962 }
1963 pairs.sort_by_key(|p| p.0);
1964 longest_increasing_by_second(&pairs)
1965 }
1966
1967 fn longest_increasing_by_second(pairs: &[(usize, usize)]) -> Vec<(usize, usize)> {
1968 if pairs.is_empty() {
1969 return Vec::new();
1970 }
1971 let n = pairs.len();
1972 let mut tails = vec![0usize; n];
1973 let mut prev = vec![usize::MAX; n];
1974 let mut len = 0usize;
1975 for i in 0..n {
1976 let v = pairs[i].1;
1977 let mut lo = 0usize;
1978 let mut hi = len;
1979 while lo < hi {
1980 let mid = (lo + hi) / 2;
1981 if pairs[tails[mid]].1 < v {
1982 lo = mid + 1;
1983 } else {
1984 hi = mid;
1985 }
1986 }
1987 if lo > 0 {
1988 prev[i] = tails[lo - 1];
1989 }
1990 tails[lo] = i;
1991 if lo == len {
1992 len += 1;
1993 }
1994 }
1995 let mut out = Vec::with_capacity(len);
1996 let mut k = tails[len.saturating_sub(1)];
1997 while k != usize::MAX {
1998 out.push(pairs[k]);
1999 k = prev[k];
2000 }
2001 out.reverse();
2002 out
2003 }
2004
2005 pub(crate) fn histogram_changed_flags(
2006 lines1: &[&str],
2007 lines2: &[&str],
2008 ) -> (Vec<bool>, Vec<bool>) {
2009 crate::diff::git_histogram::histogram_changed_flags(
2010 lines1,
2011 lines2,
2012 |changed1, changed2, o_start, o_end, n_start, n_end| {
2013 merge_myers_region(
2014 lines1, lines2, changed1, changed2, o_start, o_end, n_start, n_end,
2015 );
2016 },
2017 )
2018 }
2019
2020 #[allow(clippy::too_many_arguments)]
2021 fn merge_myers_region(
2022 lines1: &[&str],
2023 lines2: &[&str],
2024 changed1: &mut [bool],
2025 changed2: &mut [bool],
2026 o_start: usize,
2027 o_end: usize,
2028 n_start: usize,
2029 n_end: usize,
2030 ) {
2031 use crate::line_interner::LineInterner;
2032
2033 let old_slice = &lines1[o_start..o_end];
2034 let new_slice = &lines2[n_start..n_end];
2035 if old_slice.is_empty() && new_slice.is_empty() {
2036 return;
2037 }
2038 let mut interner = LineInterner::with_capacity(old_slice.len() + new_slice.len());
2039 let ids1 = interner.intern_all(old_slice);
2040 let ids2 = interner.intern_all(new_slice);
2041 let (sub1, sub2) = changed_flags(&ids1, &ids2, false);
2042 for (i, ch) in sub1.iter().enumerate() {
2043 if *ch {
2044 changed1[o_start + i] = true;
2045 }
2046 }
2047 for (j, ch) in sub2.iter().enumerate() {
2048 if *ch {
2049 changed2[n_start + j] = true;
2050 }
2051 }
2052 }
2053}
2054
2055pub(crate) fn myers_rediff_changed_masks_for_region<'a>(
2057 old_lines: &[&'a str],
2058 new_lines: &[&'a str],
2059 ws: super::WhitespaceMode,
2060) -> (Vec<bool>, Vec<bool>) {
2061 use crate::line_interner::LineInterner;
2062
2063 if old_lines.is_empty() && new_lines.is_empty() {
2064 return (Vec::new(), Vec::new());
2065 }
2066 let normalize = |lines: &[&'a str]| -> Vec<Cow<'a, str>> {
2067 lines
2068 .iter()
2069 .map(|line| ws.normalize_line_for_compare(line))
2070 .collect()
2071 };
2072 let (old_normalized, new_normalized) = (normalize(old_lines), normalize(new_lines));
2073 let old_keys: Vec<&str> = old_normalized.iter().map(Cow::as_ref).collect();
2074 let new_keys: Vec<&str> = new_normalized.iter().map(Cow::as_ref).collect();
2075 let mut interner = LineInterner::with_capacity(old_keys.len() + new_keys.len());
2076 let ids1 = interner.intern_all(&old_keys);
2077 let ids2 = interner.intern_all(&new_keys);
2078 git_xdiff::changed_flags(&ids1, &ids2, false)
2079}
2080
2081fn changed_flags_to_ops(
2083 changed1: &[bool],
2084 changed2: &[bool],
2085 old_len: usize,
2086 new_len: usize,
2087) -> Vec<similar::DiffOp> {
2088 use similar::DiffOp;
2089 let mut ops: Vec<DiffOp> = Vec::new();
2090 let mut i = 0usize;
2091 let mut j = 0usize;
2092 while i < old_len || j < new_len {
2093 let del = i < old_len && changed1[i];
2094 let ins = j < new_len && changed2[j];
2095 if !del && !ins {
2096 if i < old_len && j < new_len {
2097 let start_i = i;
2099 let start_j = j;
2100 while i < old_len && j < new_len && !changed1[i] && !changed2[j] {
2101 i += 1;
2102 j += 1;
2103 }
2104 ops.push(DiffOp::Equal {
2105 old_index: start_i,
2106 new_index: start_j,
2107 len: i - start_i,
2108 });
2109 } else if i < old_len {
2110 let start_i = i;
2111 while i < old_len {
2112 i += 1;
2113 }
2114 ops.push(DiffOp::Delete {
2115 old_index: start_i,
2116 old_len: i - start_i,
2117 new_index: j,
2118 });
2119 } else if j < new_len {
2120 let start_j = j;
2121 while j < new_len {
2122 j += 1;
2123 }
2124 ops.push(DiffOp::Insert {
2125 old_index: i,
2126 new_index: start_j,
2127 new_len: j - start_j,
2128 });
2129 }
2130 } else {
2131 let start_i = i;
2133 let start_j = j;
2134 while i < old_len && changed1[i] {
2135 i += 1;
2136 }
2137 while j < new_len && changed2[j] {
2138 j += 1;
2139 }
2140 let dlen = i - start_i;
2141 let ilen = j - start_j;
2142 if dlen > 0 && ilen > 0 {
2143 ops.push(DiffOp::Replace {
2144 old_index: start_i,
2145 old_len: dlen,
2146 new_index: start_j,
2147 new_len: ilen,
2148 });
2149 } else if dlen > 0 {
2150 ops.push(DiffOp::Delete {
2151 old_index: start_i,
2152 old_len: dlen,
2153 new_index: start_j,
2154 });
2155 } else if ilen > 0 {
2156 ops.push(DiffOp::Insert {
2157 old_index: start_i,
2158 new_index: start_j,
2159 new_len: ilen,
2160 });
2161 }
2162 }
2163 }
2164 ops
2165}
2166
2167#[must_use]
2169pub fn diff_slice_ops_compacted(
2170 old_lines: &[&str],
2171 new_lines: &[&str],
2172 algorithm: similar::Algorithm,
2173 indent_heuristic: bool,
2174) -> Vec<similar::DiffOp> {
2175 diff_indent_heuristic::diff_slice_ops_compacted(
2176 old_lines,
2177 new_lines,
2178 algorithm,
2179 indent_heuristic,
2180 )
2181}
2182
2183#[must_use]
2186#[allow(clippy::too_many_arguments)]
2190pub(crate) fn line_diff_ops_git_xdiff_compacted_ids(
2191 old_ids: &[u32],
2192 new_ids: &[u32],
2193 shared_counts: &[u32],
2194 shared_records: usize,
2195 old_lines: &[&str],
2196 new_lines: &[&str],
2197 indent_heuristic: bool,
2198 ws: super::WhitespaceMode,
2199) -> Vec<similar::DiffOp> {
2200 let (changed1, changed2) = git_xdiff::changed_flags_with_shared(
2201 old_ids,
2202 new_ids,
2203 shared_counts,
2204 shared_records,
2205 false,
2206 );
2207 let ops = changed_flags_to_ops(&changed1, &changed2, old_lines.len(), new_lines.len());
2208 diff_indent_heuristic::apply_change_compact_to_ops_with_ws(
2209 &ops,
2210 old_lines,
2211 new_lines,
2212 indent_heuristic,
2213 ws,
2214 )
2215}
2216
2217#[cfg(test)]
2219#[must_use]
2220pub(crate) fn xdiff_new_line_changed_flags(
2221 old_lines: &[&str],
2222 new_lines: &[&str],
2223 indent_heuristic: bool,
2224) -> Vec<bool> {
2225 let mut interner =
2226 crate::line_interner::LineInterner::with_capacity(old_lines.len() + new_lines.len());
2227 let ids1 = interner.intern_all(old_lines);
2228 let ids2 = interner.intern_all(new_lines);
2229 let (changed1, changed2) = git_xdiff::changed_flags(&ids1, &ids2, false);
2230 let ops = changed_flags_to_ops(&changed1, &changed2, old_lines.len(), new_lines.len());
2231 let compacted = diff_indent_heuristic::apply_change_compact_to_ops(
2232 &ops,
2233 old_lines,
2234 new_lines,
2235 indent_heuristic,
2236 );
2237 let mut flags = vec![false; new_lines.len()];
2238 for op in compacted {
2239 match op.tag() {
2240 similar::DiffTag::Insert | similar::DiffTag::Replace => {
2241 for idx in op.new_range() {
2242 if idx < flags.len() {
2243 flags[idx] = true;
2244 }
2245 }
2246 }
2247 similar::DiffTag::Equal | similar::DiffTag::Delete => {}
2248 }
2249 }
2250 flags
2251}
2252
2253#[must_use]
2255pub fn map_new_to_old_lines_compacted(
2256 old_joined: &str,
2257 new_joined: &str,
2258 algorithm: similar::Algorithm,
2259 indent_heuristic: bool,
2260 new_line_count: usize,
2261) -> Vec<Option<usize>> {
2262 let ops = diff_indent_heuristic::diff_lines_ops_compacted(
2263 old_joined,
2264 new_joined,
2265 algorithm,
2266 indent_heuristic,
2267 );
2268 diff_indent_heuristic::map_new_to_old_from_ops(&ops, new_line_count)
2269}
2270
2271#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2273#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
2274#[cfg_attr(feature = "serde-models", serde(rename_all = "snake_case"))]
2275pub enum DiffStatus {
2276 Added,
2278 Deleted,
2280 Modified,
2282 Renamed,
2284 Copied,
2286 TypeChanged,
2288 Unmerged,
2290}
2291
2292impl DiffStatus {
2293 #[must_use]
2295 pub fn letter(&self) -> char {
2296 match self {
2297 Self::Added => 'A',
2298 Self::Deleted => 'D',
2299 Self::Modified => 'M',
2300 Self::Renamed => 'R',
2301 Self::Copied => 'C',
2302 Self::TypeChanged => 'T',
2303 Self::Unmerged => 'U',
2304 }
2305 }
2306
2307 #[must_use]
2314 pub(crate) fn for_content_change(old_mode: u32, new_mode: u32) -> Self {
2315 if old_mode & 0o170000 != new_mode & 0o170000 {
2316 Self::TypeChanged
2317 } else {
2318 Self::Modified
2319 }
2320 }
2321}
2322
2323#[derive(Debug, Clone, PartialEq, Eq)]
2325#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
2326pub struct DiffEntry {
2327 pub status: DiffStatus,
2329 pub old_path: Option<String>,
2331 pub new_path: Option<String>,
2333 pub old_mode: FileMode,
2335 pub new_mode: FileMode,
2337 pub old_oid: ObjectId,
2339 pub new_oid: ObjectId,
2342 pub score: Option<u32>,
2344}
2345
2346impl DiffEntry {
2347 #[must_use]
2349 pub fn path(&self) -> &str {
2350 self.new_path
2351 .as_deref()
2352 .or(self.old_path.as_deref())
2353 .unwrap_or("")
2354 }
2355
2356 #[must_use]
2361 pub fn display_path(&self) -> String {
2362 match self.status {
2363 DiffStatus::Renamed | DiffStatus::Copied => {
2364 let old = self.old_path.as_deref().unwrap_or("");
2365 let new = self.new_path.as_deref().unwrap_or("");
2366 if old.is_empty() || new.is_empty() {
2367 self.path().to_owned()
2368 } else {
2369 format!("{old} -> {new}")
2370 }
2371 }
2372 _ => self.path().to_owned(),
2373 }
2374 }
2375}
2376
2377#[must_use]
2379pub fn zero_oid() -> ObjectId {
2380 ObjectId::from_bytes(&[0u8; 20]).unwrap_or_else(|_| {
2381 panic!("internal error: failed to create zero OID");
2383 })
2384}
2385
2386#[must_use]
2388pub fn empty_blob_oid() -> ObjectId {
2389 ObjectId::from_hex("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391").unwrap_or_else(|_| {
2390 panic!("internal error: failed to create empty blob OID");
2392 })
2393}
2394
2395pub fn diff_trees(
2410 odb: &Odb,
2411 old_tree_oid: Option<&ObjectId>,
2412 new_tree_oid: Option<&ObjectId>,
2413 prefix: &str,
2414) -> Result<Vec<DiffEntry>> {
2415 crate::trace::trace_span!("grit.diff.tree");
2416 diff_trees_opts(odb, old_tree_oid, new_tree_oid, prefix, false, None)
2417}
2418
2419pub(crate) fn diff_trees_with_index(
2422 odb: &Odb,
2423 index: &Index,
2424 old_tree_oid: Option<&ObjectId>,
2425 new_tree_oid: Option<&ObjectId>,
2426 prefix: &str,
2427 trust_index_for_old_tree: bool,
2428) -> Result<Vec<DiffEntry>> {
2429 let hint = (trust_index_for_old_tree
2430 && old_tree_oid.is_some()
2431 && index.entries_sorted
2432 && !index.entries.iter().any(|e| e.stage() != 0))
2433 .then_some(DiffTreesIndexHint { index });
2434 diff_trees_opts(
2435 odb,
2436 old_tree_oid,
2437 new_tree_oid,
2438 prefix,
2439 false,
2440 hint.as_ref(),
2441 )
2442}
2443
2444#[derive(Clone, Copy)]
2445struct DiffTreesIndexHint<'a> {
2446 index: &'a Index,
2447}
2448
2449pub fn diff_tree_path_change_count(
2457 odb: &Odb,
2458 old_tree_oid: Option<&ObjectId>,
2459 new_tree_oid: Option<&ObjectId>,
2460 prefix: &str,
2461) -> Result<usize> {
2462 if old_tree_oid == new_tree_oid {
2463 return Ok(0);
2464 }
2465 let old_entries = match old_tree_oid {
2466 Some(oid) => read_tree(odb, oid)?,
2467 None => Vec::new(),
2468 };
2469 let new_entries = match new_tree_oid {
2470 Some(oid) => read_tree(odb, oid)?,
2471 None => Vec::new(),
2472 };
2473 let mut count = 0usize;
2474 diff_tree_entries_count(odb, &old_entries, &new_entries, prefix, false, &mut count)?;
2475 Ok(count)
2476}
2477
2478pub fn diff_trees_with_renames(
2487 odb: &Odb,
2488 old_tree_oid: Option<&ObjectId>,
2489 new_tree_oid: Option<&ObjectId>,
2490 prefix: &str,
2491 rename_threshold: u32,
2492) -> Result<Vec<DiffEntry>> {
2493 let config = ConfigSet::new();
2494 let policy = ThreadPolicy::default();
2495 diff_trees_with_renames_and_policy(
2496 odb,
2497 old_tree_oid,
2498 new_tree_oid,
2499 prefix,
2500 rename_threshold,
2501 &config,
2502 &policy,
2503 )
2504}
2505
2506pub fn diff_trees_with_renames_and_policy(
2512 odb: &Odb,
2513 old_tree_oid: Option<&ObjectId>,
2514 new_tree_oid: Option<&ObjectId>,
2515 prefix: &str,
2516 rename_threshold: u32,
2517 config: &ConfigSet,
2518 thread_policy: &ThreadPolicy,
2519) -> Result<Vec<DiffEntry>> {
2520 let mut entries = diff_trees(odb, old_tree_oid, new_tree_oid, prefix)?;
2521 if rename_threshold <= 100 {
2522 entries = detect_renames_with_policy(
2523 odb,
2524 None,
2525 entries,
2526 rename_threshold,
2527 config,
2528 thread_policy,
2529 None,
2530 )?;
2531 }
2532 Ok(entries)
2533}
2534
2535pub fn tree_oid_of_commit(odb: &Odb, commit_oid: &ObjectId) -> Result<ObjectId> {
2541 let obj = odb.read(commit_oid)?;
2542 if obj.kind != ObjectKind::Commit {
2543 return Err(Error::InvalidRef(format!(
2544 "invalid ref: {commit_oid} is not a commit"
2545 )));
2546 }
2547 let commit = parse_commit(&obj.data)?;
2548 Ok(commit.tree)
2549}
2550
2551pub fn tree_oid_of_commit_in_repo(
2557 repo: &crate::repo::Repository,
2558 commit_oid: &ObjectId,
2559) -> Result<ObjectId> {
2560 if let Some(chain) = repo.caches().commit_graph_chain(repo.odb().objects_dir()) {
2561 if let Some(tree) = chain.graph_commit_tree(commit_oid) {
2562 return Ok(tree);
2563 }
2564 }
2565 tree_oid_of_commit(repo.odb(), commit_oid)
2566}
2567
2568pub fn diff_commits(odb: &Odb, base: &ObjectId, tip: &ObjectId) -> Result<Vec<DiffEntry>> {
2574 let base_tree = tree_oid_of_commit(odb, base)?;
2575 let tip_tree = tree_oid_of_commit(odb, tip)?;
2576 diff_trees(odb, Some(&base_tree), Some(&tip_tree), "")
2577}
2578
2579pub fn diff_commits_with_renames(
2588 odb: &Odb,
2589 base: &ObjectId,
2590 tip: &ObjectId,
2591 rename_threshold: u32,
2592) -> Result<Vec<DiffEntry>> {
2593 let config = ConfigSet::new();
2594 let policy = ThreadPolicy::default();
2595 diff_commits_with_renames_and_policy(odb, base, tip, rename_threshold, &config, &policy)
2596}
2597
2598pub fn diff_commits_with_renames_and_policy(
2604 odb: &Odb,
2605 base: &ObjectId,
2606 tip: &ObjectId,
2607 rename_threshold: u32,
2608 config: &ConfigSet,
2609 thread_policy: &ThreadPolicy,
2610) -> Result<Vec<DiffEntry>> {
2611 let base_tree = tree_oid_of_commit(odb, base)?;
2612 let tip_tree = tree_oid_of_commit(odb, tip)?;
2613 diff_trees_with_renames_and_policy(
2614 odb,
2615 Some(&base_tree),
2616 Some(&tip_tree),
2617 "",
2618 rename_threshold,
2619 config,
2620 thread_policy,
2621 )
2622}
2623
2624pub fn diff_trees_show_tree_entries(
2631 odb: &Odb,
2632 old_tree_oid: Option<&ObjectId>,
2633 new_tree_oid: Option<&ObjectId>,
2634 prefix: &str,
2635) -> Result<Vec<DiffEntry>> {
2636 diff_trees_opts(odb, old_tree_oid, new_tree_oid, prefix, true, None)
2637}
2638
2639fn diff_subtrees_in_place(
2640 odb: &Odb,
2641 old_tree_oid: Option<&ObjectId>,
2642 new_tree_oid: Option<&ObjectId>,
2643 prefix: &str,
2644 show_trees: bool,
2645 index_hint: Option<&DiffTreesIndexHint<'_>>,
2646 result: &mut Vec<DiffEntry>,
2647) -> Result<()> {
2648 if !show_trees {
2649 if let (Some(a), Some(b)) = (old_tree_oid, new_tree_oid) {
2650 if a == b {
2651 return Ok(());
2652 }
2653 }
2654 }
2655 let old_entries = old_tree_oid
2658 .map(|oid| odb.cached_tree_entries(oid))
2659 .transpose()?;
2660 let new_entries = new_tree_oid
2661 .map(|oid| odb.cached_tree_entries(oid))
2662 .transpose()?;
2663 diff_tree_entries_opts(
2664 odb,
2665 old_entries
2666 .as_deref()
2667 .map(Vec::as_slice)
2668 .unwrap_or_default(),
2669 new_entries
2670 .as_deref()
2671 .map(Vec::as_slice)
2672 .unwrap_or_default(),
2673 prefix,
2674 show_trees,
2675 index_hint,
2676 result,
2677 )
2678}
2679
2680fn diff_trees_opts(
2681 odb: &Odb,
2682 old_tree_oid: Option<&ObjectId>,
2683 new_tree_oid: Option<&ObjectId>,
2684 prefix: &str,
2685 show_trees: bool,
2686 index_hint: Option<&DiffTreesIndexHint<'_>>,
2687) -> Result<Vec<DiffEntry>> {
2688 let mut result = Vec::new();
2689 diff_subtrees_in_place(
2690 odb,
2691 old_tree_oid,
2692 new_tree_oid,
2693 prefix,
2694 show_trees,
2695 index_hint,
2696 &mut result,
2697 )?;
2698 Ok(result)
2699}
2700
2701#[cfg(test)]
2702use std::cell::RefCell;
2703#[cfg(test)]
2704use std::rc::Rc;
2705#[cfg(test)]
2706use std::sync::atomic::{AtomicUsize, Ordering};
2707
2708#[cfg(test)]
2709thread_local! {
2710 static DIFF_TREE_READ_HOOK: RefCell<Option<Rc<AtomicUsize>>> = const { RefCell::new(None) };
2711}
2712
2713#[cfg(test)]
2715pub(crate) fn set_diff_tree_read_hook_for_tests(hook: Option<Rc<AtomicUsize>>) {
2716 DIFF_TREE_READ_HOOK.with(|cell| *cell.borrow_mut() = hook);
2717}
2718
2719fn read_tree_from_odb(odb: &Odb, oid: &ObjectId) -> Result<Vec<TreeEntry>> {
2721 #[cfg(test)]
2722 if let Some(counter) = DIFF_TREE_READ_HOOK.with(|cell| cell.borrow().clone()) {
2723 counter.fetch_add(1, Ordering::Relaxed);
2724 }
2725 Ok((*odb.cached_tree_entries(oid)?).clone())
2726}
2727
2728fn read_tree(odb: &Odb, oid: &ObjectId) -> Result<Vec<TreeEntry>> {
2730 read_tree_from_odb(odb, oid)
2731}
2732
2733fn diff_tree_entries_count(
2734 odb: &Odb,
2735 old: &[TreeEntry],
2736 new: &[TreeEntry],
2737 prefix: &str,
2738 show_trees: bool,
2739 count: &mut usize,
2740) -> Result<()> {
2741 let mut oi = 0;
2742 let mut ni = 0;
2743
2744 while oi < old.len() || ni < new.len() {
2745 match (old.get(oi), new.get(ni)) {
2746 (Some(o), Some(n)) => {
2747 let cmp = crate::objects::tree_entry_cmp(
2748 &o.name,
2749 is_tree_mode(o.mode),
2750 &n.name,
2751 is_tree_mode(n.mode),
2752 );
2753 match cmp {
2754 std::cmp::Ordering::Less => {
2755 emit_deleted_count(odb, o, prefix, show_trees, count)?;
2756 oi += 1;
2757 }
2758 std::cmp::Ordering::Greater => {
2759 emit_added_count(odb, n, prefix, show_trees, count)?;
2760 ni += 1;
2761 }
2762 std::cmp::Ordering::Equal => {
2763 if o.oid != n.oid || o.mode != n.mode {
2764 let path = crate::repo_relpath::format_path_stored(prefix, &o.name);
2765 if is_tree_mode(o.mode) && is_tree_mode(n.mode) {
2766 if show_trees {
2767 *count += 1;
2768 }
2769 *count += diff_tree_path_change_count(
2770 odb,
2771 Some(&o.oid),
2772 Some(&n.oid),
2773 &path,
2774 )?;
2775 } else if is_tree_mode(o.mode) != is_tree_mode(n.mode) {
2776 emit_deleted_count(odb, o, prefix, show_trees, count)?;
2777 emit_added_count(odb, n, prefix, show_trees, count)?;
2778 } else {
2779 *count += 1;
2780 }
2781 }
2782 oi += 1;
2783 ni += 1;
2784 }
2785 }
2786 }
2787 (Some(o), None) => {
2788 emit_deleted_count(odb, o, prefix, show_trees, count)?;
2789 oi += 1;
2790 }
2791 (None, Some(n)) => {
2792 emit_added_count(odb, n, prefix, show_trees, count)?;
2793 ni += 1;
2794 }
2795 (None, None) => break,
2796 }
2797 }
2798
2799 Ok(())
2800}
2801
2802fn emit_deleted_count(
2803 odb: &Odb,
2804 entry: &TreeEntry,
2805 prefix: &str,
2806 show_trees: bool,
2807 count: &mut usize,
2808) -> Result<()> {
2809 let path = crate::repo_relpath::format_path_stored(prefix, &entry.name);
2810 if is_tree_mode(entry.mode) {
2811 if show_trees {
2812 *count += 1;
2813 }
2814 *count += diff_tree_path_change_count(odb, Some(&entry.oid), None, &path)?;
2815 } else {
2816 *count += 1;
2817 }
2818 Ok(())
2819}
2820
2821fn emit_added_count(
2822 odb: &Odb,
2823 entry: &TreeEntry,
2824 prefix: &str,
2825 show_trees: bool,
2826 count: &mut usize,
2827) -> Result<()> {
2828 let path = crate::repo_relpath::format_path_stored(prefix, &entry.name);
2829 if is_tree_mode(entry.mode) {
2830 if show_trees {
2831 *count += 1;
2832 }
2833 *count += diff_tree_path_change_count(odb, None, Some(&entry.oid), &path)?;
2834 } else {
2835 *count += 1;
2836 }
2837 Ok(())
2838}
2839
2840fn diff_tree_entries_opts(
2842 odb: &Odb,
2843 old: &[TreeEntry],
2844 new: &[TreeEntry],
2845 prefix: &str,
2846 show_trees: bool,
2847 index_hint: Option<&DiffTreesIndexHint<'_>>,
2848 result: &mut Vec<DiffEntry>,
2849) -> Result<()> {
2850 let mut oi = 0;
2851 let mut ni = 0;
2852
2853 while oi < old.len() || ni < new.len() {
2854 match (old.get(oi), new.get(ni)) {
2855 (Some(o), Some(n)) => {
2856 let cmp = crate::objects::tree_entry_cmp(
2857 &o.name,
2858 is_tree_mode(o.mode),
2859 &n.name,
2860 is_tree_mode(n.mode),
2861 );
2862 match cmp {
2863 std::cmp::Ordering::Less => {
2864 emit_deleted_opts(odb, o, prefix, show_trees, index_hint, result)?;
2866 oi += 1;
2867 }
2868 std::cmp::Ordering::Greater => {
2869 emit_added_opts(odb, n, prefix, show_trees, result)?;
2871 ni += 1;
2872 }
2873 std::cmp::Ordering::Equal => {
2874 if o.oid != n.oid || o.mode != n.mode {
2876 let path = crate::repo_relpath::format_path_stored(prefix, &o.name);
2877 if is_tree_mode(o.mode) && is_tree_mode(n.mode) {
2878 if show_trees {
2880 result.push(DiffEntry {
2881 status: DiffStatus::Modified,
2882 old_path: Some(path.clone()),
2883 new_path: Some(path.clone()),
2884 old_mode: FileMode::new(o.mode),
2885 new_mode: FileMode::new(n.mode),
2886 old_oid: o.oid,
2887 new_oid: n.oid,
2888 score: None,
2889 });
2890 }
2891 diff_subtrees_in_place(
2892 odb,
2893 Some(&o.oid),
2894 Some(&n.oid),
2895 &path,
2896 show_trees,
2897 index_hint,
2898 result,
2899 )?;
2900 } else if is_tree_mode(o.mode) && !is_tree_mode(n.mode) {
2901 emit_deleted_opts(odb, o, prefix, show_trees, index_hint, result)?;
2903 emit_added_opts(odb, n, prefix, show_trees, result)?;
2904 } else if !is_tree_mode(o.mode) && is_tree_mode(n.mode) {
2905 emit_deleted_opts(odb, o, prefix, show_trees, index_hint, result)?;
2907 emit_added_opts(odb, n, prefix, show_trees, result)?;
2908 } else {
2909 result.push(DiffEntry {
2910 status: DiffStatus::for_content_change(o.mode, n.mode),
2911 old_path: Some(path.clone()),
2912 new_path: Some(path),
2913 old_mode: FileMode::new(o.mode),
2914 new_mode: FileMode::new(n.mode),
2915 old_oid: o.oid,
2916 new_oid: n.oid,
2917 score: None,
2918 });
2919 }
2920 }
2921 oi += 1;
2922 ni += 1;
2923 }
2924 }
2925 }
2926 (Some(o), None) => {
2927 emit_deleted_opts(odb, o, prefix, show_trees, index_hint, result)?;
2928 oi += 1;
2929 }
2930 (None, Some(n)) => {
2931 emit_added_opts(odb, n, prefix, show_trees, result)?;
2932 ni += 1;
2933 }
2934 (None, None) => break,
2935 }
2936 }
2937
2938 Ok(())
2939}
2940
2941fn emit_deleted_tree_from_index(index: &Index, dir_prefix: &str, result: &mut Vec<DiffEntry>) {
2942 let scan_prefix: Vec<u8> = if dir_prefix.is_empty() {
2943 Vec::new()
2944 } else {
2945 format!("{dir_prefix}/").into_bytes()
2946 };
2947 if index.entries_sorted {
2948 let start = if scan_prefix.is_empty() {
2949 0
2950 } else {
2951 index
2952 .entries
2953 .partition_point(|e| e.path.as_slice() < scan_prefix.as_slice())
2954 };
2955 for ie in index.entries[start..].iter() {
2956 if ie.stage() != 0 {
2957 continue;
2958 }
2959 if !scan_prefix.is_empty() && !ie.path.starts_with(&scan_prefix) {
2960 break;
2961 }
2962 if scan_prefix.is_empty() && ie.path.contains(&b'/') {
2963 continue;
2964 }
2965 let path = String::from_utf8_lossy(&ie.path).into_owned();
2966 result.push(DiffEntry {
2967 status: DiffStatus::Deleted,
2968 old_path: Some(path),
2969 new_path: None,
2970 old_mode: FileMode::new(ie.mode),
2971 new_mode: FileMode::ABSENT,
2972 old_oid: ie.oid,
2973 new_oid: zero_oid(),
2974 score: None,
2975 });
2976 }
2977 return;
2978 }
2979 for ie in &index.entries {
2980 if ie.stage() != 0 {
2981 continue;
2982 }
2983 let Ok(path) = std::str::from_utf8(&ie.path) else {
2984 continue;
2985 };
2986 let under = if dir_prefix.is_empty() {
2987 !path.contains('/')
2988 } else if path == dir_prefix {
2989 false
2990 } else {
2991 path.strip_prefix(dir_prefix)
2992 .is_some_and(|rest| rest.starts_with('/'))
2993 };
2994 if !under {
2995 continue;
2996 }
2997 result.push(DiffEntry {
2998 status: DiffStatus::Deleted,
2999 old_path: Some(path.to_owned()),
3000 new_path: None,
3001 old_mode: FileMode::new(ie.mode),
3002 new_mode: FileMode::ABSENT,
3003 old_oid: ie.oid,
3004 new_oid: zero_oid(),
3005 score: None,
3006 });
3007 }
3008}
3009
3010fn emit_deleted_opts(
3011 odb: &Odb,
3012 entry: &TreeEntry,
3013 prefix: &str,
3014 show_trees: bool,
3015 index_hint: Option<&DiffTreesIndexHint<'_>>,
3016 result: &mut Vec<DiffEntry>,
3017) -> Result<()> {
3018 let path = crate::repo_relpath::format_path_stored(prefix, &entry.name);
3019 if is_tree_mode(entry.mode) {
3020 if show_trees {
3021 result.push(DiffEntry {
3022 status: DiffStatus::Deleted,
3023 old_path: Some(path.clone()),
3024 new_path: None,
3025 old_mode: FileMode::new(entry.mode),
3026 new_mode: FileMode::ABSENT,
3027 old_oid: entry.oid,
3028 new_oid: zero_oid(),
3029 score: None,
3030 });
3031 }
3032 if let Some(hint) =
3033 index_hint.filter(|hint| !hint.index.has_sparse_directory_placeholders())
3034 {
3035 emit_deleted_tree_from_index(hint.index, &path, result);
3036 return Ok(());
3037 }
3038 diff_subtrees_in_place(odb, Some(&entry.oid), None, &path, show_trees, None, result)?;
3039 } else {
3040 result.push(DiffEntry {
3041 status: DiffStatus::Deleted,
3042 old_path: Some(path.clone()),
3043 new_path: None,
3044 old_mode: FileMode::new(entry.mode),
3045 new_mode: FileMode::ABSENT,
3046 old_oid: entry.oid,
3047 new_oid: zero_oid(),
3048 score: None,
3049 });
3050 }
3051 Ok(())
3052}
3053
3054fn emit_added_opts(
3055 odb: &Odb,
3056 entry: &TreeEntry,
3057 prefix: &str,
3058 show_trees: bool,
3059 result: &mut Vec<DiffEntry>,
3060) -> Result<()> {
3061 let path = crate::repo_relpath::format_path_stored(prefix, &entry.name);
3062 if is_tree_mode(entry.mode) {
3063 if show_trees {
3064 result.push(DiffEntry {
3065 status: DiffStatus::Added,
3066 old_path: None,
3067 new_path: Some(path.clone()),
3068 old_mode: FileMode::ABSENT,
3069 new_mode: FileMode::new(entry.mode),
3070 old_oid: zero_oid(),
3071 new_oid: entry.oid,
3072 score: None,
3073 });
3074 }
3075 diff_subtrees_in_place(odb, None, Some(&entry.oid), &path, show_trees, None, result)?;
3076 } else {
3077 result.push(DiffEntry {
3078 status: DiffStatus::Added,
3079 old_path: None,
3080 new_path: Some(path),
3081 old_mode: FileMode::ABSENT,
3082 new_mode: FileMode::new(entry.mode),
3083 old_oid: zero_oid(),
3084 new_oid: entry.oid,
3085 score: None,
3086 });
3087 }
3088 Ok(())
3089}
3090
3091pub fn diff_index_to_tree(
3111 odb: &Odb,
3112 index: &Index,
3113 tree_oid: Option<&ObjectId>,
3114 ignore_submodules: bool,
3115) -> Result<Vec<DiffEntry>> {
3116 match index_cache_tree_walk(index, tree_oid) {
3117 CacheTreeWalk::Fast {
3118 head_tree,
3119 cache_root,
3120 } => {
3121 diff_index_to_tree_with_cache_tree(odb, index, head_tree, cache_root, ignore_submodules)
3122 }
3123 CacheTreeWalk::Flatten => {
3124 diff_index_to_tree_flatten(odb, index, tree_oid, ignore_submodules)
3125 }
3126 }
3127}
3128
3129pub fn diff_index_to_tree_with_renames(
3138 odb: &Odb,
3139 index: &Index,
3140 tree_oid: Option<&ObjectId>,
3141 ignore_submodules: bool,
3142 rename_threshold: u32,
3143 config: &ConfigSet,
3144 thread_policy: &ThreadPolicy,
3145) -> Result<Vec<DiffEntry>> {
3146 let entries = diff_index_to_tree(odb, index, tree_oid, ignore_submodules)?;
3147 apply_rename_detection_to_diff_entries(
3148 odb,
3149 entries,
3150 None,
3151 rename_threshold,
3152 config,
3153 thread_policy,
3154 )
3155}
3156
3157pub fn apply_rename_detection_to_diff_entries(
3166 odb: &Odb,
3167 entries: Vec<DiffEntry>,
3168 work_root: Option<&Path>,
3169 rename_threshold: u32,
3170 config: &ConfigSet,
3171 thread_policy: &ThreadPolicy,
3172) -> Result<Vec<DiffEntry>> {
3173 if entries.is_empty() || rename_threshold >= 100 {
3174 return Ok(entries);
3175 }
3176 if work_root.is_some() {
3177 let opts = DiffPostprocessOptions {
3178 rename_threshold,
3179 ..DiffPostprocessOptions::default()
3180 };
3181 postprocess_diff_entries(
3182 odb,
3183 entries,
3184 None,
3185 work_root,
3186 None,
3187 config,
3188 thread_policy,
3189 &opts,
3190 )
3191 } else {
3192 detect_renames_with_policy(
3193 odb,
3194 None,
3195 entries,
3196 rename_threshold,
3197 config,
3198 thread_policy,
3199 None,
3200 )
3201 }
3202}
3203
3204pub fn append_untracked_worktree_added_for_rename_detection(
3216 repo: &Repository,
3217 index: &Index,
3218 rules: &WorktreeRules,
3219 mut entries: Vec<DiffEntry>,
3220) -> Result<(Vec<DiffEntry>, HashSet<String>)> {
3221 let mut synthetic_new_paths = HashSet::new();
3222 if !entries.iter().any(|e| e.status == DiffStatus::Deleted) {
3223 return Ok((entries, synthetic_new_paths));
3224 }
3225 let mut existing_new: HashSet<String> =
3226 entries.iter().filter_map(|e| e.new_path.clone()).collect();
3227 let untracked = list_untracked_worktree_files_for_rename(repo, index, rules)?;
3228 for path in untracked {
3229 if existing_new.contains(&path) {
3230 continue;
3231 }
3232 existing_new.insert(path.clone());
3233 synthetic_new_paths.insert(path.clone());
3234 entries.push(DiffEntry {
3235 status: DiffStatus::Added,
3236 old_path: None,
3237 new_path: Some(path),
3238 old_mode: FileMode::ABSENT,
3239 new_mode: FileMode::REGULAR,
3240 old_oid: zero_oid(),
3241 new_oid: zero_oid(),
3242 score: None,
3243 });
3244 }
3245 Ok((entries, synthetic_new_paths))
3246}
3247
3248#[must_use]
3250pub fn drop_unused_synthetic_rename_candidates(
3251 entries: Vec<DiffEntry>,
3252 synthetic_new_paths: &HashSet<String>,
3253) -> Vec<DiffEntry> {
3254 if synthetic_new_paths.is_empty() {
3255 return entries;
3256 }
3257 entries
3258 .into_iter()
3259 .filter(|e| {
3260 !(e.status == DiffStatus::Added
3261 && e.new_path
3262 .as_ref()
3263 .is_some_and(|p| synthetic_new_paths.contains(p)))
3264 })
3265 .collect()
3266}
3267
3268fn list_untracked_worktree_files_for_rename(
3269 repo: &Repository,
3270 index: &Index,
3271 rules: &WorktreeRules,
3272) -> Result<Vec<String>> {
3273 let work_tree = repo.work_tree().ok_or(Error::RequiresWorkTree)?;
3274 let mut out = Vec::new();
3275 walk_untracked_worktree_files(repo, index, work_tree, "", rules, &mut out)?;
3276 out.sort();
3277 Ok(out)
3278}
3279
3280fn walk_untracked_worktree_files(
3281 repo: &Repository,
3282 index: &Index,
3283 dir: &Path,
3284 rel_prefix: &str,
3285 rules: &WorktreeRules,
3286 out: &mut Vec<String>,
3287) -> Result<()> {
3288 let read = match fs::read_dir(dir) {
3289 Ok(r) => r,
3290 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
3291 Err(e) => return Err(Error::Io(e)),
3292 };
3293 for entry in read {
3294 let entry = entry.map_err(Error::Io)?;
3295 let name = entry.file_name();
3296 let name = name.to_string_lossy();
3297 if rel_prefix.is_empty() && name == ".git" {
3298 continue;
3299 }
3300 let rel_path = if rel_prefix.is_empty() {
3301 name.to_string()
3302 } else {
3303 format!("{rel_prefix}/{name}")
3304 };
3305 let file_type = entry.file_type().map_err(Error::Io)?;
3306 if file_type.is_dir() {
3307 let (ignored, _) = rules
3308 .ignore_mut()
3309 .check_path(repo, Some(index), &rel_path, true)?;
3310 if ignored {
3311 continue;
3312 }
3313 walk_untracked_worktree_files(
3314 repo,
3315 index,
3316 &dir.join(name.as_ref()),
3317 &rel_path,
3318 rules,
3319 out,
3320 )?;
3321 } else if file_type.is_file() || file_type.is_symlink() {
3322 if index.get(rel_path.as_bytes(), 0).is_some() {
3323 continue;
3324 }
3325 let (ignored, _) =
3326 rules
3327 .ignore_mut()
3328 .check_path(repo, Some(index), &rel_path, false)?;
3329 if !ignored {
3330 out.push(rel_path);
3331 }
3332 }
3333 }
3334 Ok(())
3335}
3336
3337pub fn diff_index_to_worktree_with_renames(
3347 repo: &Repository,
3348 index: &mut Index,
3349 work_tree: &Path,
3350 options: DiffIndexToWorktreeOptions,
3351 rename_threshold: u32,
3352) -> Result<(Vec<DiffEntry>, bool)> {
3353 let config = repo.config()?.as_ref().clone();
3354 let thread_policy = repo.thread_policy();
3355 let odb = repo.odb();
3356 let rules_for_append = options.worktree_rules.clone();
3357 let (mut entries, changed) =
3358 diff_index_to_worktree_with_options(odb, index, work_tree, options)?;
3359 let mut synthetic_new_paths = HashSet::new();
3360 if rename_threshold < 100 {
3361 if let Some(rules_arc) = rules_for_append {
3362 let rules = rules_arc
3363 .lock()
3364 .map_err(|e| Error::Message(format!("worktree rules lock poisoned: {e}")))?;
3365 (entries, synthetic_new_paths) =
3366 append_untracked_worktree_added_for_rename_detection(repo, index, &rules, entries)?;
3367 } else {
3368 let rules = WorktreeRules::from_repository(repo, index)?;
3369 (entries, synthetic_new_paths) =
3370 append_untracked_worktree_added_for_rename_detection(repo, index, &rules, entries)?;
3371 }
3372 }
3373 let entries = apply_rename_detection_to_diff_entries(
3374 odb,
3375 entries,
3376 Some(work_tree),
3377 rename_threshold,
3378 &config,
3379 &thread_policy,
3380 )?;
3381 let entries = drop_unused_synthetic_rename_candidates(entries, &synthetic_new_paths);
3382 Ok((entries, changed))
3383}
3384
3385enum CacheTreeWalk<'a> {
3386 Fast {
3387 head_tree: &'a ObjectId,
3388 cache_root: &'a CacheTreeNode,
3389 },
3390 Flatten,
3391}
3392
3393fn index_cache_tree_walk<'a>(
3396 index: &'a Index,
3397 tree_oid: Option<&'a ObjectId>,
3398) -> CacheTreeWalk<'a> {
3399 let Some(head_tree) = tree_oid else {
3400 return CacheTreeWalk::Flatten;
3401 };
3402 let Some(cache_root) = index.cache_tree.as_ref() else {
3403 return CacheTreeWalk::Flatten;
3404 };
3405 if index.entries.iter().any(|e| e.stage() != 0) {
3406 return CacheTreeWalk::Flatten;
3407 }
3408 CacheTreeWalk::Fast {
3409 head_tree,
3410 cache_root,
3411 }
3412}
3413
3414fn diff_index_to_tree_flatten(
3416 odb: &Odb,
3417 index: &Index,
3418 tree_oid: Option<&ObjectId>,
3419 ignore_submodules: bool,
3420) -> Result<Vec<DiffEntry>> {
3421 let tree_entries = match tree_oid {
3423 Some(oid) => flatten_tree(odb, oid, "")?,
3424 None => Vec::new(),
3425 };
3426
3427 let mut tree_map: std::collections::BTreeMap<&str, &FlatEntry> =
3429 std::collections::BTreeMap::new();
3430 for entry in &tree_entries {
3431 tree_map.insert(&entry.path, entry);
3432 }
3433
3434 let mut result = Vec::new();
3435 let mut stage0_paths = std::collections::BTreeSet::new();
3436 let mut unmerged_modes: std::collections::BTreeMap<String, (u8, u32)> =
3437 std::collections::BTreeMap::new();
3438
3439 for ie in &index.entries {
3441 let path = String::from_utf8_lossy(&ie.path).to_string();
3442 if ie.stage() == 0 && ie.intent_to_add() {
3443 continue;
3446 }
3447 if ie.stage() != 0 {
3448 let rank = match ie.stage() {
3449 2 => 0u8,
3450 3 => 1u8,
3451 1 => 2u8,
3452 _ => 3u8,
3453 };
3454 match unmerged_modes.get(&path) {
3455 Some((existing_rank, _)) if *existing_rank <= rank => {}
3456 _ => {
3457 unmerged_modes.insert(path, (rank, ie.mode));
3458 }
3459 }
3460 continue;
3461 }
3462 if ignore_submodules && ie.mode == 0o160000 {
3463 let _ = tree_map.remove(path.as_str());
3464 stage0_paths.insert(path.clone());
3465 continue;
3466 }
3467 stage0_paths.insert(path.clone());
3468 match tree_map.remove(path.as_str()) {
3469 Some(te) => {
3470 if te.oid != ie.oid || te.mode != ie.mode {
3472 result.push(DiffEntry {
3473 status: DiffStatus::for_content_change(te.mode, ie.mode),
3474 old_path: Some(path.clone()),
3475 new_path: Some(path),
3476 old_mode: FileMode::new(te.mode),
3477 new_mode: FileMode::new(ie.mode),
3478 old_oid: te.oid,
3479 new_oid: ie.oid,
3480 score: None,
3481 });
3482 }
3483 }
3484 None => {
3485 result.push(DiffEntry {
3487 status: DiffStatus::Added,
3488 old_path: None,
3489 new_path: Some(path),
3490 old_mode: FileMode::ABSENT,
3491 new_mode: FileMode::new(ie.mode),
3492 old_oid: zero_oid(),
3493 new_oid: ie.oid,
3494 score: None,
3495 });
3496 }
3497 }
3498 }
3499
3500 diff_index_to_tree_finish_unmerged_and_deleted(
3501 ignore_submodules,
3502 &mut tree_map,
3503 &unmerged_modes,
3504 &stage0_paths,
3505 &mut result,
3506 );
3507
3508 sort_entries_by_path(&mut result);
3509 Ok(result)
3510}
3511
3512fn diff_index_to_tree_finish_unmerged_and_deleted(
3513 ignore_submodules: bool,
3514 tree_map: &mut std::collections::BTreeMap<&str, &FlatEntry>,
3515 unmerged_modes: &std::collections::BTreeMap<String, (u8, u32)>,
3516 stage0_paths: &std::collections::BTreeSet<String>,
3517 result: &mut Vec<DiffEntry>,
3518) {
3519 for (path, (_, mode)) in unmerged_modes {
3520 if stage0_paths.contains(path) {
3521 continue;
3522 }
3523 tree_map.remove(path.as_str());
3524 result.push(DiffEntry {
3525 status: DiffStatus::Unmerged,
3526 old_path: Some(path.clone()),
3527 new_path: Some(path.clone()),
3528 old_mode: FileMode::ABSENT,
3529 new_mode: FileMode::new(*mode),
3530 old_oid: zero_oid(),
3531 new_oid: zero_oid(),
3532 score: None,
3533 });
3534 }
3535
3536 for (path, te) in std::mem::take(tree_map) {
3538 if ignore_submodules && te.mode == 0o160000 {
3539 continue;
3540 }
3541 result.push(DiffEntry {
3542 status: DiffStatus::Deleted,
3543 old_path: Some(path.to_owned()),
3544 new_path: None,
3545 old_mode: FileMode::new(te.mode),
3546 new_mode: FileMode::ABSENT,
3547 old_oid: te.oid,
3548 new_oid: zero_oid(),
3549 score: None,
3550 });
3551 }
3552}
3553
3554fn diff_index_to_tree_with_cache_tree(
3557 odb: &Odb,
3558 index: &Index,
3559 head_tree: &ObjectId,
3560 cache_root: &CacheTreeNode,
3561 ignore_submodules: bool,
3562) -> Result<Vec<DiffEntry>> {
3563 let mut result = Vec::new();
3564 let mut idx_cursor = 0usize;
3565
3566 let mut walk = DiffIndexTreeWalk {
3567 odb,
3568 index,
3569 idx_cursor: &mut idx_cursor,
3570 ignore_submodules,
3571 result: &mut result,
3572 abort_to_flatten: false,
3573 };
3574 diff_index_tree_at(&mut walk, head_tree, Some(cache_root), b"")?;
3575 if walk.abort_to_flatten {
3576 return diff_index_to_tree_flatten(odb, index, Some(head_tree), ignore_submodules);
3577 }
3578
3579 while idx_cursor < index.entries.len() {
3581 let ie = &index.entries[idx_cursor];
3582 idx_cursor += 1;
3583 let path = String::from_utf8_lossy(&ie.path).to_string();
3584 if ie.stage() == 0 && ie.intent_to_add() {
3585 continue;
3586 }
3587 if ie.stage() != 0 {
3588 continue;
3589 }
3590 if ignore_submodules && ie.mode == 0o160000 {
3591 continue;
3592 }
3593 result.push(DiffEntry {
3594 status: DiffStatus::Added,
3595 old_path: None,
3596 new_path: Some(path),
3597 old_mode: FileMode::ABSENT,
3598 new_mode: FileMode::new(ie.mode),
3599 old_oid: zero_oid(),
3600 new_oid: ie.oid,
3601 score: None,
3602 });
3603 }
3604
3605 sort_entries_by_path(&mut result);
3606 Ok(result)
3607}
3608
3609enum IndexLevelKind<'a> {
3610 Blob {
3611 entry: &'a IndexEntry,
3612 name: &'a [u8],
3613 },
3614 Tree {
3615 name: &'a [u8],
3616 },
3617}
3618
3619fn index_relative_in_dir<'a>(path: &'a [u8], dir_prefix: &[u8]) -> Option<&'a [u8]> {
3622 if dir_prefix.is_empty() {
3623 return Some(path);
3624 }
3625 let rest = path.strip_prefix(dir_prefix)?;
3626 match rest.split_first() {
3627 None => Some(rest),
3628 Some((b'/', tail)) => Some(tail),
3629 Some(_) => None,
3630 }
3631}
3632
3633fn join_index_path(dir_prefix: &[u8], name: &[u8]) -> Vec<u8> {
3634 if dir_prefix.is_empty() {
3635 return name.to_vec();
3636 }
3637 let mut path = Vec::with_capacity(dir_prefix.len() + 1 + name.len());
3638 path.extend_from_slice(dir_prefix);
3639 path.push(b'/');
3640 path.extend_from_slice(name);
3641 path
3642}
3643
3644fn peek_index_level_entry<'a>(
3645 index: &'a Index,
3646 cursor: &mut usize,
3647 dir_prefix: &[u8],
3648 ignore_submodules: bool,
3649) -> Option<IndexLevelKind<'a>> {
3650 while let Some(ie) = index.entries.get(*cursor) {
3651 let rel = index_relative_in_dir(&ie.path, dir_prefix)?;
3652 if ie.stage() != 0 || ie.intent_to_add() || rel.is_empty() {
3653 *cursor += 1;
3654 continue;
3655 }
3656 if let Some(slash) = rel.iter().position(|&b| b == b'/') {
3657 return Some(IndexLevelKind::Tree {
3658 name: &rel[..slash],
3659 });
3660 }
3661 if ignore_submodules && ie.mode == 0o160000 {
3662 *cursor += 1;
3663 continue;
3664 }
3665 return Some(IndexLevelKind::Blob {
3666 entry: ie,
3667 name: rel,
3668 });
3669 }
3670 None
3671}
3672
3673fn skip_index_subtree(index: &Index, cursor: &mut usize, dir_prefix: &[u8]) {
3674 while index
3675 .entries
3676 .get(*cursor)
3677 .is_some_and(|ie| index_relative_in_dir(&ie.path, dir_prefix).is_some())
3678 {
3679 *cursor += 1;
3680 }
3681}
3682
3683const CACHE_CHILD_LINEAR_SCAN_MAX: usize = 16;
3685
3686struct CacheChildren<'a> {
3692 children: &'a [CacheTreeNode],
3693 sorted: Vec<&'a CacheTreeNode>,
3694}
3695
3696impl<'a> CacheChildren<'a> {
3697 fn new(node: Option<&'a CacheTreeNode>) -> Self {
3698 let children = node.map_or(&[][..], |n| n.children.as_slice());
3699 let sorted = if children.len() > CACHE_CHILD_LINEAR_SCAN_MAX {
3700 let mut sorted: Vec<&CacheTreeNode> = children.iter().collect();
3701 sorted.sort_by(|a, b| a.name.cmp(&b.name));
3702 sorted
3703 } else {
3704 Vec::new()
3705 };
3706 Self { children, sorted }
3707 }
3708
3709 fn get(&self, name: &[u8]) -> Option<&'a CacheTreeNode> {
3710 if self.sorted.is_empty() {
3711 return self.children.iter().find(|c| c.name == name);
3712 }
3713 let pos = self.sorted.partition_point(|c| c.name.as_slice() < name);
3714 self.sorted.get(pos).copied().filter(|c| c.name == name)
3715 }
3716}
3717
3718struct DiffIndexTreeWalk<'a, 'b> {
3719 odb: &'a Odb,
3720 index: &'a Index,
3721 idx_cursor: &'b mut usize,
3722 ignore_submodules: bool,
3723 result: &'b mut Vec<DiffEntry>,
3724 abort_to_flatten: bool,
3726}
3727
3728fn tree_entries_have_duplicate_names(entries: &[TreeEntry]) -> bool {
3729 let mut seen = std::collections::BTreeSet::<&[u8]>::new();
3730 for entry in entries {
3731 if !seen.insert(entry.name.as_slice()) {
3732 return true;
3733 }
3734 }
3735 false
3736}
3737
3738fn diff_index_tree_at(
3740 walk: &mut DiffIndexTreeWalk<'_, '_>,
3741 tree_oid: &ObjectId,
3742 cache_node: Option<&CacheTreeNode>,
3743 dir_prefix: &[u8],
3744) -> Result<()> {
3745 let tree_entries = read_tree(walk.odb, tree_oid)?;
3746 if tree_entries_have_duplicate_names(&tree_entries) {
3747 walk.abort_to_flatten = true;
3748 return Ok(());
3749 }
3750 let cache_children = CacheChildren::new(cache_node);
3751 let mut ti = 0usize;
3752
3753 loop {
3754 let te = tree_entries.get(ti);
3755 let ie_kind = peek_index_level_entry(
3756 walk.index,
3757 walk.idx_cursor,
3758 dir_prefix,
3759 walk.ignore_submodules,
3760 );
3761
3762 match (te, ie_kind) {
3763 (Some(t_entry), Some(ie)) => {
3764 let t_name = &t_entry.name;
3765 let (i_name, i_is_tree) = match &ie {
3766 IndexLevelKind::Blob { name, .. } => (*name, false),
3767 IndexLevelKind::Tree { name } => (*name, true),
3768 };
3769 let t_is_tree = is_tree_mode(t_entry.mode);
3770 let cmp = crate::objects::tree_entry_cmp(t_name, t_is_tree, i_name, i_is_tree);
3771 match cmp {
3772 std::cmp::Ordering::Less => {
3773 emit_index_tree_deleted(
3774 walk.odb,
3775 t_entry,
3776 dir_prefix,
3777 walk.ignore_submodules,
3778 walk.result,
3779 )?;
3780 ti += 1;
3781 }
3782 std::cmp::Ordering::Greater => {
3783 emit_index_tree_added(walk, dir_prefix)?;
3784 }
3785 std::cmp::Ordering::Equal => {
3786 if t_is_tree && i_is_tree {
3787 let full_path = join_index_path(dir_prefix, t_name);
3788 let child_cache = cache_children.get(t_name);
3789 if child_cache
3790 .is_some_and(|c| c.is_valid() && c.oid == Some(t_entry.oid))
3791 {
3792 skip_index_subtree(walk.index, walk.idx_cursor, &full_path);
3793 ti += 1;
3794 continue;
3795 }
3796 diff_index_tree_at(walk, &t_entry.oid, child_cache, &full_path)?;
3797 } else if t_is_tree != i_is_tree {
3798 emit_index_tree_deleted(
3799 walk.odb,
3800 t_entry,
3801 dir_prefix,
3802 walk.ignore_submodules,
3803 walk.result,
3804 )?;
3805 emit_index_tree_added(walk, dir_prefix)?;
3806 } else {
3807 let IndexLevelKind::Blob { entry: ie, .. } = ie else {
3808 ti += 1;
3809 continue;
3810 };
3811 *walk.idx_cursor += 1;
3812 if t_entry.oid != ie.oid || t_entry.mode != ie.mode {
3813 let path = String::from_utf8_lossy(&ie.path).into_owned();
3814 walk.result.push(DiffEntry {
3815 status: DiffStatus::for_content_change(t_entry.mode, ie.mode),
3816 old_path: Some(path.clone()),
3817 new_path: Some(path),
3818 old_mode: FileMode::new(t_entry.mode),
3819 new_mode: FileMode::new(ie.mode),
3820 old_oid: t_entry.oid,
3821 new_oid: ie.oid,
3822 score: None,
3823 });
3824 }
3825 }
3826 ti += 1;
3827 }
3828 }
3829 }
3830 (Some(t_entry), None) => {
3831 emit_index_tree_deleted(
3832 walk.odb,
3833 t_entry,
3834 dir_prefix,
3835 walk.ignore_submodules,
3836 walk.result,
3837 )?;
3838 ti += 1;
3839 }
3840 (None, Some(_)) => {
3841 emit_index_tree_added(walk, dir_prefix)?;
3842 }
3843 (None, None) => break,
3844 }
3845 }
3846
3847 Ok(())
3848}
3849
3850fn emit_index_tree_deleted(
3851 odb: &Odb,
3852 entry: &TreeEntry,
3853 dir_prefix: &[u8],
3854 ignore_submodules: bool,
3855 result: &mut Vec<DiffEntry>,
3856) -> Result<()> {
3857 if ignore_submodules && entry.mode == 0o160000 {
3858 return Ok(());
3859 }
3860 let path = String::from_utf8_lossy(&join_index_path(dir_prefix, &entry.name)).into_owned();
3861 if is_tree_mode(entry.mode) {
3862 for fe in flatten_tree(odb, &entry.oid, &path)? {
3863 if ignore_submodules && fe.mode == 0o160000 {
3864 continue;
3865 }
3866 result.push(DiffEntry {
3867 status: DiffStatus::Deleted,
3868 old_path: Some(fe.path.clone()),
3869 new_path: None,
3870 old_mode: FileMode::new(fe.mode),
3871 new_mode: FileMode::ABSENT,
3872 old_oid: fe.oid,
3873 new_oid: zero_oid(),
3874 score: None,
3875 });
3876 }
3877 } else {
3878 result.push(DiffEntry {
3879 status: DiffStatus::Deleted,
3880 old_path: Some(path),
3881 new_path: None,
3882 old_mode: FileMode::new(entry.mode),
3883 new_mode: FileMode::ABSENT,
3884 old_oid: entry.oid,
3885 new_oid: zero_oid(),
3886 score: None,
3887 });
3888 }
3889 Ok(())
3890}
3891
3892fn emit_index_tree_added(walk: &mut DiffIndexTreeWalk<'_, '_>, dir_prefix: &[u8]) -> Result<()> {
3893 let ie_kind = peek_index_level_entry(
3894 walk.index,
3895 walk.idx_cursor,
3896 dir_prefix,
3897 walk.ignore_submodules,
3898 )
3899 .ok_or_else(|| Error::IndexError("missing index entry during tree walk".to_owned()))?;
3900 match ie_kind {
3901 IndexLevelKind::Blob { entry: ie, .. } => {
3902 let path = String::from_utf8_lossy(&ie.path).into_owned();
3903 *walk.idx_cursor += 1;
3904 walk.result.push(DiffEntry {
3905 status: DiffStatus::Added,
3906 old_path: None,
3907 new_path: Some(path),
3908 old_mode: FileMode::ABSENT,
3909 new_mode: FileMode::new(ie.mode),
3910 old_oid: zero_oid(),
3911 new_oid: ie.oid,
3912 score: None,
3913 });
3914 }
3915 IndexLevelKind::Tree { name } => {
3916 let full_path = join_index_path(dir_prefix, name);
3917 let start = *walk.idx_cursor;
3918 skip_index_subtree(walk.index, walk.idx_cursor, &full_path);
3919 for ie in &walk.index.entries[start..*walk.idx_cursor] {
3920 if ie.stage() != 0 || ie.intent_to_add() {
3921 continue;
3922 }
3923 if walk.ignore_submodules && ie.mode == 0o160000 {
3924 continue;
3925 }
3926 let path = String::from_utf8_lossy(&ie.path).into_owned();
3927 walk.result.push(DiffEntry {
3928 status: DiffStatus::Added,
3929 old_path: None,
3930 new_path: Some(path),
3931 old_mode: FileMode::ABSENT,
3932 new_mode: FileMode::new(ie.mode),
3933 old_oid: zero_oid(),
3934 new_oid: ie.oid,
3935 score: None,
3936 });
3937 }
3938 }
3939 }
3940 Ok(())
3941}
3942
3943pub fn diff_index_to_worktree(
3968 odb: &Odb,
3969 index: &mut Index,
3970 work_tree: &Path,
3971 ignore_submodule_untracked: bool,
3972 simplify_gitlinks: bool,
3973) -> Result<Vec<DiffEntry>> {
3974 diff_index_to_worktree_with_options(
3975 odb,
3976 index,
3977 work_tree,
3978 DiffIndexToWorktreeOptions {
3979 ignore_submodule_untracked,
3980 simplify_gitlinks,
3981 ..DiffIndexToWorktreeOptions::default()
3982 },
3983 )
3984 .map(|(entries, _)| entries)
3985}
3986
3987#[derive(Clone)]
3989pub struct DiffIndexToWorktreeOptions {
3990 pub index_mtime: Option<(u32, u32)>,
3996 pub ignore_submodule_untracked: bool,
3998 pub simplify_gitlinks: bool,
4000 pub error_on_broken_gitlinks: bool,
4003 pub repository_git_dir: Option<std::path::PathBuf>,
4007 pub refresh_index_stat_in_pass: bool,
4010 pub config: Option<std::sync::Arc<crate::config::ConfigSet>>,
4012 pub stat_parallel_threads: Option<usize>,
4016 pub thread_policy: Option<crate::parallel::ThreadPolicy>,
4018 pub worktree_rules:
4020 Option<std::sync::Arc<std::sync::Mutex<crate::worktree_rules::WorktreeRules>>>,
4021 pub for_status: bool,
4023 pub refresh_fsmonitor: bool,
4025 pub command_runner: Option<std::sync::Arc<dyn crate::command_runner::CommandRunner>>,
4027 pub fsmonitor_query_boundary: Option<crate::fsmonitor::FsmonitorQueryBoundary>,
4029 pub restrict_to_paths: Option<std::sync::Arc<std::collections::BTreeSet<String>>>,
4036 pub hash_blobs_without_write: bool,
4038 pub cancel_watch: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
4041}
4042
4043impl Default for DiffIndexToWorktreeOptions {
4044 fn default() -> Self {
4045 Self {
4046 index_mtime: None,
4047 ignore_submodule_untracked: false,
4048 simplify_gitlinks: false,
4049 error_on_broken_gitlinks: false,
4050 repository_git_dir: None,
4051 refresh_index_stat_in_pass: false,
4052 config: None,
4053 stat_parallel_threads: None,
4054 thread_policy: None,
4055 worktree_rules: None,
4056 for_status: false,
4057 refresh_fsmonitor: true,
4058 command_runner: None,
4059 fsmonitor_query_boundary: None,
4060 restrict_to_paths: None,
4061 hash_blobs_without_write: false,
4062 cancel_watch: None,
4063 }
4064 }
4065}
4066
4067#[inline]
4068fn poll_diff_cancel_watch(
4069 watch: Option<&std::sync::Arc<std::sync::atomic::AtomicBool>>,
4070) -> crate::error::Result<()> {
4071 if let Some(flag) = watch {
4072 crate::progress::poll_watch(flag)?;
4073 }
4074 Ok(())
4075}
4076
4077struct StagingHashJob {
4078 entry_index: usize,
4079 rel_path: String,
4080 file_path: PathBuf,
4081 entry: IndexEntry,
4082 worktree_mode: u32,
4084 file_attrs: crate::crlf::FileAttrs,
4085}
4086
4087#[allow(clippy::too_many_arguments)]
4088fn parallel_process_blob_worktree_jobs(
4089 odb: &Odb,
4090 jobs: &[StagingHashJob],
4091 thread_policy: &crate::parallel::ThreadPolicy,
4092 config: &crate::config::ConfigSet,
4093 materialize_dirty_blobs: bool,
4094 hash_blobs_without_write: bool,
4095 conv: &crate::crlf::ConversionConfig,
4096 filter_process: Option<&crate::filter_process::FilterProcessState>,
4097 result: &mut Vec<DiffEntry>,
4098) -> Result<()> {
4099 use crate::parallel::{try_par_map, ParallelHashError, ParallelOp};
4100
4101 if jobs.is_empty() {
4102 return Ok(());
4103 }
4104 let mut par_opts = thread_policy.resolve(ParallelOp::IndexRefresh, config);
4105 par_opts.min_items = 0;
4106 par_opts.min_total_bytes = None;
4107 let est_bytes = jobs.len().saturating_mul(8192);
4108 let store_dirty_blobs = (materialize_dirty_blobs && !hash_blobs_without_write)
4109 .then_some(crate::odb::BULK_NEW_LOOSE_WRITE);
4110 let stream_from = if store_dirty_blobs.is_some() {
4111 odb.big_file_threshold()?
4112 } else {
4113 hash_stream_threshold(odb)?
4114 };
4115
4116 let rows = try_par_map::<StagingHashJob, Option<(usize, DiffEntry)>, Error, _>(
4117 jobs,
4118 par_opts,
4119 est_bytes,
4120 |job| {
4121 let modified = |new_oid| {
4122 Ok(Some((
4123 job.entry_index,
4124 worktree_content_change_entry(
4125 &job.entry,
4126 &job.rel_path,
4127 job.worktree_mode,
4128 new_oid,
4129 ),
4130 )))
4131 };
4132 let content = match read_regular_worktree_blob(
4133 odb,
4134 &job.file_path,
4135 conv,
4136 &job.file_attrs,
4137 &job.rel_path,
4138 Some(&job.entry),
4139 filter_process,
4140 stream_from,
4141 ) {
4142 Ok(content) => content,
4143 Err(err) if unreadable_worktree_blob_is_modified(&err, materialize_dirty_blobs) => {
4144 return modified(zero_oid());
4145 }
4146 Err(err) => return Err(err),
4147 };
4148 let eff_oid = content.object_id(odb, store_dirty_blobs)?;
4149 if eff_oid != job.entry.oid || job.worktree_mode != job.entry.mode {
4150 let new_oid = if eff_oid != job.entry.oid && !materialize_dirty_blobs {
4151 zero_oid()
4152 } else {
4153 eff_oid
4154 };
4155 modified(new_oid)
4156 } else {
4157 Ok(None)
4158 }
4159 },
4160 )
4161 .map_err(|e| match e {
4162 ParallelHashError::Task(err) => err,
4163 })?;
4164
4165 let mut rows: Vec<(usize, DiffEntry)> = rows.into_iter().flatten().collect();
4166 rows.sort_by_key(|(idx, _)| *idx);
4167 result.extend(rows.into_iter().map(|(_, d)| d));
4168 Ok(())
4169}
4170
4171fn worktree_content_change_entry(
4173 ie: &IndexEntry,
4174 path: &str,
4175 worktree_mode: u32,
4176 new_oid: ObjectId,
4177) -> DiffEntry {
4178 DiffEntry {
4179 status: DiffStatus::for_content_change(ie.mode, worktree_mode),
4180 old_path: Some(path.to_owned()),
4181 new_path: Some(path.to_owned()),
4182 old_mode: FileMode::new(ie.mode),
4183 new_mode: FileMode::new(worktree_mode),
4184 old_oid: ie.oid,
4185 new_oid,
4186 score: None,
4187 }
4188}
4189
4190fn unreadable_worktree_blob_is_modified(err: &Error, materialize_dirty_blobs: bool) -> bool {
4197 !materialize_dirty_blobs
4198 && matches!(
4199 err,
4200 Error::WorktreePathIo {
4201 operation: crate::worktree::WorktreePathOperation::Read,
4202 ..
4203 }
4204 )
4205}
4206
4207#[must_use]
4209fn try_push_status_modified_on_size_mismatch(
4210 ie: &IndexEntry,
4211 path_str_ref: &str,
4212 meta: &fs::Metadata,
4213 worktree_mode: u32,
4214 for_status: bool,
4215 result: &mut Vec<DiffEntry>,
4216) -> bool {
4217 if ie.stat_cache_unset() {
4218 return false;
4219 }
4220 if !for_status || crate::index::munge_index_size(meta.len()) == ie.size {
4221 return false;
4222 }
4223 result.push(worktree_content_change_entry(
4224 ie,
4225 path_str_ref,
4226 worktree_mode,
4227 ie.oid,
4228 ));
4229 true
4230}
4231
4232fn try_push_status_modified_on_size_mismatch_stat(
4233 ie: &IndexEntry,
4234 path_str_ref: &str,
4235 stat: &crate::worktree_scan::BlobDiskStat,
4236 worktree_mode: u32,
4237 for_status: bool,
4238 result: &mut Vec<DiffEntry>,
4239) -> bool {
4240 if ie.stat_cache_unset() {
4241 return false;
4242 }
4243 if !for_status || stat.worktree_size() == ie.size {
4244 return false;
4245 }
4246 result.push(worktree_content_change_entry(
4247 ie,
4248 path_str_ref,
4249 worktree_mode,
4250 ie.oid,
4251 ));
4252 true
4253}
4254
4255#[allow(clippy::too_many_arguments)]
4256fn push_index_blob_worktree_diff(
4257 odb: &Odb,
4258 ie: &IndexEntry,
4259 path_str_ref: &str,
4260 file_path: &Path,
4261 meta: fs::Metadata,
4262 index_mtime: Option<(u32, u32)>,
4263 mode_policy: crate::index::WorktreeModePolicy,
4264 stat_policy: crate::index::StatMatchPolicy,
4265 file_attrs_for: &dyn Fn(&str) -> crate::crlf::FileAttrs,
4266 conv: &crate::crlf::ConversionConfig,
4267 materialize_dirty_blobs: bool,
4268 hash_blobs_without_write: bool,
4269 for_status: bool,
4270 filter_process: Option<&crate::filter_process::FilterProcessState>,
4271 result: &mut Vec<DiffEntry>,
4272) -> Result<()> {
4273 if meta.is_dir() {
4274 if file_path.join(".git").exists() {
4275 let head = read_submodule_head_oid(file_path).unwrap_or_else(zero_oid);
4276 let path_owned = path_str_ref.to_owned();
4277 result.push(DiffEntry {
4278 status: DiffStatus::TypeChanged,
4279 old_path: Some(path_owned.clone()),
4280 new_path: Some(path_owned),
4281 old_mode: FileMode::new(ie.mode),
4282 new_mode: FileMode::new(0o160000),
4283 old_oid: ie.oid,
4284 new_oid: head,
4285 score: None,
4286 });
4287 return Ok(());
4288 }
4289 result.push(DiffEntry {
4290 status: DiffStatus::Deleted,
4291 old_path: Some(path_str_ref.to_owned()),
4292 new_path: None,
4293 old_mode: FileMode::new(ie.mode),
4294 new_mode: FileMode::ABSENT,
4295 old_oid: ie.oid,
4296 new_oid: zero_oid(),
4297 score: None,
4298 });
4299 return Ok(());
4300 }
4301
4302 let worktree_mode =
4303 mode_policy.index_mode_for_worktree(mode_from_metadata(&meta), Some(ie.mode));
4304 let stat_same = stat_matches_with_policy(ie, &meta, stat_policy);
4305 if stat_same && worktree_mode != ie.mode {
4306 result.push(worktree_content_change_entry(
4307 ie,
4308 path_str_ref,
4309 worktree_mode,
4310 ie.oid,
4311 ));
4312 return Ok(());
4313 }
4314
4315 if stat_same
4316 && index_entry_skips_worktree_content_hash_after_stat_match(ie, index_mtime, stat_policy)
4317 {
4318 return Ok(());
4319 }
4320
4321 if try_push_status_modified_on_size_mismatch(
4322 ie,
4323 path_str_ref,
4324 &meta,
4325 worktree_mode,
4326 for_status,
4327 result,
4328 ) {
4329 return Ok(());
4330 }
4331
4332 let file_attrs = file_attrs_for(path_str_ref);
4333 let store = (materialize_dirty_blobs && !hash_blobs_without_write)
4334 .then_some(crate::odb::BULK_NEW_LOOSE_WRITE);
4335 let stream_from = if store.is_some() {
4336 odb.big_file_threshold()?
4337 } else {
4338 hash_stream_threshold(odb)?
4339 };
4340 let content = match worktree_blob_content_from(
4341 odb,
4342 file_path,
4343 &meta,
4344 conv,
4345 &file_attrs,
4346 path_str_ref,
4347 Some(ie),
4348 filter_process,
4349 stream_from,
4350 ) {
4351 Ok(content) => content,
4352 Err(err) if unreadable_worktree_blob_is_modified(&err, materialize_dirty_blobs) => {
4353 result.push(worktree_content_change_entry(
4354 ie,
4355 path_str_ref,
4356 worktree_mode,
4357 zero_oid(),
4358 ));
4359 return Ok(());
4360 }
4361 Err(err) => return Err(err),
4362 };
4363 let eff_oid = content.object_id(odb, store)?;
4364
4365 if eff_oid != ie.oid || worktree_mode != ie.mode {
4366 let new_oid = if eff_oid != ie.oid && !materialize_dirty_blobs {
4369 zero_oid()
4370 } else {
4371 eff_oid
4372 };
4373 result.push(worktree_content_change_entry(
4374 ie,
4375 path_str_ref,
4376 worktree_mode,
4377 new_oid,
4378 ));
4379 }
4380 Ok(())
4381}
4382
4383pub fn diff_index_to_worktree_with_options(
4393 odb: &Odb,
4394 index: &mut Index,
4395 work_tree: &Path,
4396 options: DiffIndexToWorktreeOptions,
4397) -> Result<(Vec<DiffEntry>, bool)> {
4398 let default_git_dir = work_tree.join(".git");
4399 let git_dir = options
4400 .repository_git_dir
4401 .clone()
4402 .unwrap_or(default_git_dir);
4403 diff_index_to_worktree_inner(odb, index, work_tree, &git_dir, options, false)
4404 .map(|(entries, changed, _)| (entries, changed))
4405}
4406
4407pub fn diff_index_to_worktree_for_staging(
4413 odb: &Odb,
4414 repository_git_dir: &Path,
4415 index: &mut Index,
4416 work_tree: &Path,
4417 options: DiffIndexToWorktreeOptions,
4418) -> Result<(
4419 Vec<DiffEntry>,
4420 bool,
4421 std::collections::BTreeMap<String, crate::index::IndexStatFields>,
4422)> {
4423 diff_index_to_worktree_inner(odb, index, work_tree, repository_git_dir, options, true)
4424}
4425
4426#[allow(clippy::too_many_arguments)]
4428fn worktree_diff_pass1_index_entry(
4429 odb: &Odb,
4430 work_tree: &Path,
4431 ie: &IndexEntry,
4432 path_str_ref: &str,
4433 simplify_gitlinks: bool,
4434 ignore_submodule_untracked: bool,
4435 error_on_broken_gitlinks: bool,
4436 materialize_dirty_blobs: bool,
4437 filter_fp: Option<&crate::filter_process::FilterProcessState>,
4438 conv: &crate::crlf::ConversionConfig,
4439 file_attrs_for: &dyn Fn(&str) -> crate::crlf::FileAttrs,
4440 precompose_unicode: bool,
4441 ignorecase: bool,
4442 result: &mut Vec<DiffEntry>,
4443) -> Result<()> {
4444 if ie.skip_worktree() || ie.assume_unchanged() {
4445 return Ok(());
4446 }
4447 let is_intent_to_add = ie.intent_to_add();
4448
4449 if ie.mode == 0o160000 {
4450 let sub_dir =
4451 index_entry_worktree_abs_bytes(work_tree, &ie.path, precompose_unicode, ignorecase);
4452 match fs::symlink_metadata(&sub_dir) {
4453 Err(e)
4454 if e.kind() == std::io::ErrorKind::NotFound
4455 || e.raw_os_error() == Some(20) =>
4456 {
4457 if !simplify_gitlinks {
4458 let path_owned = path_str_ref.to_owned();
4459 result.push(DiffEntry {
4460 status: DiffStatus::Deleted,
4461 old_path: Some(path_owned.clone()),
4462 new_path: Some(path_owned),
4463 old_mode: FileMode::new(ie.mode),
4464 new_mode: FileMode::ABSENT,
4465 old_oid: ie.oid,
4466 new_oid: zero_oid(),
4467 score: None,
4468 });
4469 }
4470 return Ok(());
4471 }
4472 Err(e) => return Err(Error::Io(e)),
4473 Ok(meta) if !meta.is_dir() => {
4474 let worktree_mode = mode_from_metadata(&meta);
4475 let file_attrs = file_attrs_for(path_str_ref);
4476 let worktree_oid = worktree_file_oid(
4477 odb,
4478 &sub_dir,
4479 &meta,
4480 conv,
4481 &file_attrs,
4482 path_str_ref,
4483 None,
4484 materialize_dirty_blobs,
4485 filter_fp,
4486 )?;
4487 let path_owned = path_str_ref.to_owned();
4488 result.push(DiffEntry {
4489 status: DiffStatus::TypeChanged,
4490 old_path: Some(path_owned.clone()),
4491 new_path: Some(path_owned),
4492 old_mode: FileMode::new(ie.mode),
4493 new_mode: FileMode::new(worktree_mode),
4494 old_oid: ie.oid,
4495 new_oid: worktree_oid,
4496 score: None,
4497 });
4498 return Ok(());
4499 }
4500 Ok(_) => {}
4501 }
4502 let sub_head_oid = read_submodule_head_oid(&sub_dir);
4503 let ref_matches = if let Some(oid) = sub_head_oid {
4504 oid == ie.oid
4505 } else {
4506 let is_placeholder = submodule_worktree_is_unpopulated_placeholder(&sub_dir);
4507 if error_on_broken_gitlinks
4508 && !is_placeholder
4509 && submodule_embedded_git_dir(&sub_dir).is_some()
4510 {
4511 return Err(Error::Config(
4512 format!("could not read submodule HEAD for '{path_str_ref}'").into(),
4513 ));
4514 }
4515 is_placeholder
4516 };
4517 if simplify_gitlinks {
4518 if !ref_matches {
4519 let path_owned = path_str_ref.to_owned();
4520 let new_oid = sub_head_oid.unwrap_or_else(zero_oid);
4521 result.push(DiffEntry {
4522 status: DiffStatus::Modified,
4523 old_path: Some(path_owned.clone()),
4524 new_path: Some(path_owned),
4525 old_mode: FileMode::new(ie.mode),
4526 new_mode: FileMode::new(ie.mode),
4527 old_oid: ie.oid,
4528 new_oid,
4529 score: None,
4530 });
4531 }
4532 return Ok(());
4533 }
4534 if sub_head_oid.is_some() && submodule_head_object_broken(&sub_dir) {
4535 return Err(Error::Config(
4536 format!("'git status --porcelain=2' failed in submodule {path_str_ref}").into(),
4537 ));
4538 }
4539 let mut flags = submodule_porcelain_flags(work_tree, path_str_ref, ie.oid);
4540 if ignore_submodule_untracked {
4541 flags.untracked = false;
4542 }
4543 let inner_dirty = flags.modified || flags.untracked;
4544 if !ref_matches || inner_dirty {
4545 let path_owned = path_str_ref.to_owned();
4546 let new_oid = if !ref_matches {
4547 sub_head_oid.unwrap_or_else(zero_oid)
4548 } else {
4549 zero_oid()
4550 };
4551 result.push(DiffEntry {
4552 status: DiffStatus::Modified,
4553 old_path: Some(path_owned.clone()),
4554 new_path: Some(path_owned),
4555 old_mode: FileMode::new(ie.mode),
4556 new_mode: FileMode::new(ie.mode),
4557 old_oid: ie.oid,
4558 new_oid,
4559 score: None,
4560 });
4561 }
4562 return Ok(());
4563 }
4564
4565 let file_path =
4566 index_entry_worktree_abs_bytes(work_tree, &ie.path, precompose_unicode, ignorecase);
4567 let path_match = crate::repo_relpath::as_utf8_for_matching(path_str_ref);
4568
4569 if is_intent_to_add {
4570 match fs::symlink_metadata(&file_path) {
4571 Ok(meta) => {
4572 let file_attrs = file_attrs_for(path_match.as_ref());
4573 let worktree_oid = worktree_file_oid(
4574 odb,
4575 &file_path,
4576 &meta,
4577 conv,
4578 &file_attrs,
4579 path_match.as_ref(),
4580 None,
4581 materialize_dirty_blobs,
4582 filter_fp,
4583 )?;
4584 let worktree_mode = mode_from_metadata(&meta);
4585 result.push(DiffEntry {
4586 status: DiffStatus::Added,
4587 old_path: None,
4588 new_path: Some(path_str_ref.to_owned()),
4589 old_mode: FileMode::ABSENT,
4590 new_mode: FileMode::new(worktree_mode),
4591 old_oid: zero_oid(),
4592 new_oid: worktree_oid,
4593 score: None,
4594 });
4595 }
4596 Err(e)
4597 if e.kind() == std::io::ErrorKind::NotFound
4598 || e.raw_os_error() == Some(20) =>
4599 {
4600 result.push(DiffEntry {
4601 status: DiffStatus::Deleted,
4602 old_path: Some(path_str_ref.to_owned()),
4603 new_path: None,
4604 old_mode: FileMode::new(ie.mode),
4605 new_mode: FileMode::ABSENT,
4606 old_oid: ie.oid,
4607 new_oid: zero_oid(),
4608 score: None,
4609 });
4610 }
4611 Err(e) => return Err(Error::Io(e)),
4612 }
4613 }
4614 Ok(())
4615}
4616
4617#[inline]
4618fn index_entry_needs_worktree_diff_pass1(ie: &IndexEntry) -> bool {
4619 ie.mode == crate::index::MODE_GITLINK || ie.intent_to_add()
4620}
4621
4622fn diff_index_to_worktree_inner(
4623 odb: &Odb,
4624 index: &mut Index,
4625 work_tree: &Path,
4626 repository_git_dir: &Path,
4627 options: DiffIndexToWorktreeOptions,
4628 materialize_dirty_blobs: bool,
4629) -> Result<(
4630 Vec<DiffEntry>,
4631 bool,
4632 std::collections::BTreeMap<String, crate::index::IndexStatFields>,
4633)> {
4634 use crate::config::ConfigSet;
4635 use crate::crlf;
4636 use std::collections::BTreeMap;
4637
4638 let mut staging_stat_cache = BTreeMap::new();
4639 let cancel_watch = options.cancel_watch.as_ref();
4640 poll_diff_cancel_watch(cancel_watch)?;
4641 let index_mtime = index_mtime_for_diff(index, options.index_mtime);
4642
4643 let ignore_submodule_untracked = options.ignore_submodule_untracked;
4644 let simplify_gitlinks = options.simplify_gitlinks;
4645
4646 let rules_lock = options
4647 .worktree_rules
4648 .as_ref()
4649 .map(|shared| {
4650 shared
4651 .lock()
4652 .map_err(|e| Error::Message(format!("worktree rules lock poisoned: {e}")))
4653 })
4654 .transpose()?;
4655 let config = if let Some(rules) = rules_lock.as_ref() {
4656 rules.config().clone()
4657 } else if let Some(c) = options.config.as_ref() {
4658 c.as_ref().clone()
4659 } else {
4660 ConfigSet::load(
4661 &crate::environment::Environment::empty(),
4662 Some(repository_git_dir),
4663 true,
4664 )
4665 .unwrap_or_else(|_| ConfigSet::new())
4666 };
4667 let conv = rules_lock
4668 .as_ref()
4669 .map(|r| r.conversion().clone())
4670 .unwrap_or_else(|| crlf::ConversionConfig::from_config(&config));
4671 let legacy_attrs = if rules_lock.is_none() {
4672 Some(crlf::load_gitattributes_parsed(work_tree))
4673 } else {
4674 None
4675 };
4676 let file_attrs_for = |path: &str| -> crlf::FileAttrs {
4677 if let Some(rules) = rules_lock.as_ref() {
4678 rules.file_attrs(path, false)
4679 } else if let Some(parsed) = legacy_attrs.as_ref() {
4680 crlf::file_attrs_from_gitattributes(&parsed.rules, &parsed.macros, path, false, &config)
4681 } else {
4682 crlf::FileAttrs::default()
4683 }
4684 };
4685 let filter_fp = rules_lock.as_ref().map(|r| r.filter_process());
4686 let precompose_unicode = config
4687 .get_bool("core.precomposeunicode")
4688 .and_then(|r| r.ok())
4689 .unwrap_or(false);
4690 let mode_policy = crate::index::WorktreeModePolicy::from_config(&config);
4691 let stat_policy = crate::index::StatMatchPolicy::from_config(Some(&config));
4692 let ignorecase = config
4693 .get_bool("core.ignorecase")
4694 .and_then(|r| r.ok())
4695 .unwrap_or(false);
4696 let hash_blobs_without_write = options.hash_blobs_without_write;
4697
4698 let mut result = Vec::new();
4699 let refresh_index_stat_in_pass = options.refresh_index_stat_in_pass;
4700 let mut index_stat_refresh_changed = false;
4701 let mut honor_fsmonitor_valid = false;
4702 if options.refresh_fsmonitor {
4703 let default_runner = crate::command_runner::system_command_runner();
4704 let runner = options
4705 .command_runner
4706 .as_deref()
4707 .unwrap_or(default_runner.as_ref());
4708 let boundary = options.fsmonitor_query_boundary.unwrap_or_else(|| {
4709 crate::fsmonitor::FsmonitorQueryBoundary(
4710 crate::environment::wall_clock_unix_timestamp_nanos(),
4711 )
4712 });
4713 let refresh_opts = crate::fsmonitor::FsmonitorRefreshOptions::with_query_boundary(boundary);
4714 let refresh = crate::fsmonitor::refresh_index(
4715 index,
4716 repository_git_dir,
4717 work_tree,
4718 &config,
4719 Some(runner),
4720 &refresh_opts,
4721 )?;
4722 if refresh.index_changed {
4723 index_stat_refresh_changed = true;
4724 }
4725 honor_fsmonitor_valid = refresh.honor_valid_bits;
4726 }
4727 let use_fsmonitor_stat_skip = honor_fsmonitor_valid;
4728 let mut unmerged_base: std::collections::BTreeMap<String, (u8, IndexEntry)> =
4729 std::collections::BTreeMap::new();
4730
4731 let dir_symlinks = SymlinkDirCache::default();
4735
4736 let error_on_broken_gitlinks = options.error_on_broken_gitlinks;
4737 if options.restrict_to_paths.is_none() {
4738 for ie in &index.entries {
4739 poll_diff_cancel_watch(cancel_watch)?;
4740 if ie.stage() != 0 {
4741 let path = String::from_utf8_lossy(&ie.path).to_string();
4742 let rank = match ie.stage() {
4743 2 => 0u8,
4744 3 => 1u8,
4745 1 => 2u8,
4746 _ => 3u8,
4747 };
4748 match unmerged_base.get(&path) {
4749 Some((existing_rank, _)) if *existing_rank <= rank => {}
4750 _ => {
4751 unmerged_base.insert(path, (rank, ie.clone()));
4752 }
4753 }
4754 }
4755 }
4756 }
4757
4758 if let Some(limit) = options.restrict_to_paths.as_ref() {
4759 for path in limit.iter() {
4760 let Some(ie) = index.get(path.as_bytes(), 0) else {
4761 continue;
4762 };
4763 if !index_entry_needs_worktree_diff_pass1(ie) {
4764 continue;
4765 }
4766 worktree_diff_pass1_index_entry(
4767 odb,
4768 work_tree,
4769 ie,
4770 path.as_str(),
4771 simplify_gitlinks,
4772 ignore_submodule_untracked,
4773 error_on_broken_gitlinks,
4774 materialize_dirty_blobs,
4775 filter_fp,
4776 &conv,
4777 &file_attrs_for,
4778 precompose_unicode,
4779 ignorecase,
4780 &mut result,
4781 )?;
4782 }
4783 } else {
4784 for ie in &index.entries {
4785 poll_diff_cancel_watch(cancel_watch)?;
4786 if ie.stage() != 0 {
4787 continue;
4788 }
4789 if !index_entry_needs_worktree_diff_pass1(ie) {
4790 continue;
4791 }
4792 let path_stored = crate::repo_relpath::storage_string(&ie.path);
4793 worktree_diff_pass1_index_entry(
4794 odb,
4795 work_tree,
4796 ie,
4797 &path_stored,
4798 simplify_gitlinks,
4799 ignore_submodule_untracked,
4800 error_on_broken_gitlinks,
4801 materialize_dirty_blobs,
4802 filter_fp,
4803 &conv,
4804 &file_attrs_for,
4805 precompose_unicode,
4806 ignorecase,
4807 &mut result,
4808 )?;
4809 }
4810 }
4811
4812 use crate::worktree_scan::{
4813 collect_blob_lookup_batches_with_literal_paths, group_blob_entries_by_dir,
4814 stat_scan_thread_count, visit_blob_lookup_batches, BlobDiskLookup, BlobScanSparseMode,
4815 WorktreeBlobScanOptions, PARALLEL_STAT_MIN_ENTRIES,
4816 };
4817 let preload_index = config
4818 .get_bool("core.preloadIndex")
4819 .and_then(|r| r.ok())
4820 .unwrap_or(true);
4821 let blob_scan = WorktreeBlobScanOptions {
4822 preload_index,
4823 stat_parallel_threads: options.stat_parallel_threads,
4824 };
4825 let blob_dirs = if let Some(limit) = options.restrict_to_paths.as_ref() {
4826 crate::worktree_scan::group_blob_entries_by_dir_for_paths(index, limit)
4827 } else {
4828 group_blob_entries_by_dir(index, honor_fsmonitor_valid)
4829 };
4830 let work_tree_owned = work_tree.to_path_buf();
4831 let dir_abs = move |dir: &[u8]| {
4832 if dir.is_empty() {
4833 work_tree_owned.clone()
4834 } else {
4835 index_entry_worktree_abs_bytes(&work_tree_owned, dir, precompose_unicode, ignorecase)
4836 }
4837 };
4838 let work_tree_for_files = work_tree.to_path_buf();
4839 let file_abs = move |rel: &[u8]| {
4840 index_entry_worktree_abs_bytes(&work_tree_for_files, rel, precompose_unicode, ignorecase)
4841 };
4842 let blob_count = blob_dirs.len();
4843 let thread_policy = options.thread_policy.unwrap_or_default();
4844 let hash_threads = stat_scan_thread_count(&thread_policy, &config, blob_scan, blob_count);
4845 let parallel_blob_work = blob_count >= PARALLEL_STAT_MIN_ENTRIES
4846 && hash_threads > 1
4847 && (materialize_dirty_blobs || options.for_status);
4848 let mut staging_hash_jobs: Vec<StagingHashJob> = Vec::new();
4849 let mut refresh_content_verified = vec![false; index.entries.len()];
4851 let sparse = if parallel_blob_work {
4856 BlobScanSparseMode::StatusDiff {
4857 stat_policy,
4858 index_mtime,
4859 mode_policy,
4860 for_status: options.for_status,
4861 refresh_index_stat_in_pass,
4862 parallel_blob_work,
4863 honor_fsmonitor_stat_skip: use_fsmonitor_stat_skip,
4864 }
4865 } else {
4866 BlobScanSparseMode::Dense
4867 };
4868 let blob_stat_batches = collect_blob_lookup_batches_with_literal_paths(
4869 index,
4870 &blob_dirs,
4871 blob_scan,
4872 &thread_policy,
4873 &config,
4874 dir_abs,
4875 file_abs,
4876 !precompose_unicode && !ignorecase,
4877 |rel_path| {
4878 dir_symlinks.has_symlink_in_path_bytes(
4879 work_tree,
4880 rel_path,
4881 precompose_unicode,
4882 ignorecase,
4883 )
4884 },
4885 sparse,
4886 )?;
4887 let refresh_checks = if refresh_index_stat_in_pass {
4888 let par_opts =
4889 refresh_hash_parallel_options(&thread_policy, &config, options.stat_parallel_threads);
4890 refresh_content_checks(
4891 odb,
4892 index,
4893 &blob_stat_batches,
4894 index_mtime,
4895 stat_policy,
4896 &|ie, stat| {
4897 !(use_fsmonitor_stat_skip && crate::fsmonitor::entry_skips_worktree_stat(ie, index))
4898 && worktree_diff_refreshes_entry(
4899 ie,
4900 stat,
4901 index_mtime,
4902 stat_policy,
4903 options.for_status,
4904 )
4905 },
4906 &WorktreeHashContext {
4907 work_tree,
4908 precompose_unicode,
4909 ignorecase,
4910 conv: &conv,
4911 file_attrs_for: &file_attrs_for,
4912 filter_process: filter_fp,
4913 },
4914 par_opts,
4915 )
4916 } else {
4917 Vec::new()
4918 };
4919 poll_diff_cancel_watch(cancel_watch)?;
4920 visit_blob_lookup_batches(blob_stat_batches, |entry_index, lookup| {
4921 poll_diff_cancel_watch(cancel_watch)?;
4922 let path_owned = crate::repo_relpath::storage_string(&index.entries[entry_index].path);
4923 let path_str_ref = path_owned.as_str();
4924 if use_fsmonitor_stat_skip {
4925 let ie_skip = &index.entries[entry_index];
4926 if crate::fsmonitor::entry_skips_worktree_stat(ie_skip, index) {
4927 return Ok(());
4928 }
4929 }
4930 match lookup {
4931 BlobDiskLookup::Missing => {
4932 let ie = &index.entries[entry_index];
4933 result.push(DiffEntry {
4934 status: DiffStatus::Deleted,
4935 old_path: Some(path_str_ref.to_owned()),
4936 new_path: None,
4937 old_mode: FileMode::new(ie.mode),
4938 new_mode: FileMode::ABSENT,
4939 old_oid: ie.oid,
4940 new_oid: zero_oid(),
4941 score: None,
4942 });
4943 }
4944 BlobDiskLookup::Io(e) => return Err(Error::Io(e)),
4945 BlobDiskLookup::Present(stat) => {
4946 if materialize_dirty_blobs {
4947 staging_stat_cache.insert(path_str_ref.to_owned(), stat.fields);
4948 }
4949 if refresh_index_stat_in_pass
4950 && worktree_diff_refreshes_entry(
4951 &index.entries[entry_index],
4952 &stat,
4953 index_mtime,
4954 stat_policy,
4955 options.for_status,
4956 )
4957 {
4958 let refresh = refresh_index_blob_stat_from_scan(
4959 &mut index.entries[entry_index],
4960 &stat,
4961 index_mtime,
4962 stat_policy,
4963 refresh_checks[entry_index].unwrap_or(false),
4964 );
4965 if refresh.index_changed {
4966 index_stat_refresh_changed = true;
4967 }
4968 if refresh.content_verified {
4969 refresh_content_verified[entry_index] = true;
4970 let path = index.entries[entry_index].path.clone();
4971 let oid = index.entries[entry_index].oid;
4972 index.mark_content_verified(&path, oid);
4973 }
4974 if index.fsmonitor_last_update.is_some()
4975 && stat_matches_blob_stat(&index.entries[entry_index], &stat, stat_policy)
4976 && !refresh.index_changed
4977 {
4978 index.entries[entry_index].set_fsmonitor_valid(true);
4979 }
4980 }
4981 let ie = &index.entries[entry_index];
4982 let worktree_mode =
4983 mode_policy.index_mode_for_worktree(stat.git_mode, Some(ie.mode));
4984 if parallel_blob_work && !stat.is_dir && stat.git_mode != crate::index::MODE_SYMLINK
4988 {
4989 let stat_same = stat_matches_blob_stat(ie, &stat, stat_policy);
4990 if stat_same
4991 && worktree_mode == ie.mode
4992 && index_entry_skips_worktree_content_hash_after_stat_match(
4993 ie,
4994 index_mtime,
4995 stat_policy,
4996 )
4997 {
4998 return Ok(());
4999 }
5000 if stat_same && worktree_mode != ie.mode {
5001 result.push(worktree_content_change_entry(
5002 ie,
5003 path_str_ref,
5004 worktree_mode,
5005 ie.oid,
5006 ));
5007 } else if try_push_status_modified_on_size_mismatch_stat(
5008 ie,
5009 path_str_ref,
5010 &stat,
5011 worktree_mode,
5012 options.for_status,
5013 &mut result,
5014 ) {
5015 } else if !refresh_content_verified[entry_index] {
5016 let file_path = index_entry_worktree_abs_bytes(
5017 work_tree,
5018 &index.entries[entry_index].path,
5019 precompose_unicode,
5020 ignorecase,
5021 );
5022 staging_hash_jobs.push(StagingHashJob {
5023 entry_index,
5024 rel_path: path_str_ref.to_owned(),
5025 file_path,
5026 entry: ie.clone(),
5027 worktree_mode,
5028 file_attrs: file_attrs_for(path_str_ref),
5029 });
5030 }
5031 } else {
5032 let same_file_type = (worktree_mode == crate::index::MODE_SYMLINK)
5036 == (ie.mode == crate::index::MODE_SYMLINK);
5037 if !stat.is_dir
5038 && worktree_mode == ie.mode
5039 && ((refresh_content_verified[entry_index] && same_file_type)
5040 || (stat_matches_blob_stat(ie, &stat, stat_policy)
5041 && index_entry_skips_worktree_content_hash_after_stat_match(
5042 ie,
5043 index_mtime,
5044 stat_policy,
5045 )))
5046 {
5047 return Ok(());
5048 }
5049 let file_path = index_entry_worktree_abs_bytes(
5050 work_tree,
5051 &index.entries[entry_index].path,
5052 precompose_unicode,
5053 ignorecase,
5054 );
5055 let meta = fs::symlink_metadata(&file_path).map_err(Error::Io)?;
5056 push_index_blob_worktree_diff(
5057 odb,
5058 ie,
5059 path_str_ref,
5060 &file_path,
5061 meta,
5062 index_mtime,
5063 mode_policy,
5064 stat_policy,
5065 &file_attrs_for,
5066 &conv,
5067 materialize_dirty_blobs,
5068 hash_blobs_without_write,
5069 options.for_status,
5070 filter_fp,
5071 &mut result,
5072 )?;
5073 }
5074 }
5075 }
5076 Ok(())
5077 })?;
5078 if parallel_blob_work && !staging_hash_jobs.is_empty() {
5079 parallel_process_blob_worktree_jobs(
5080 odb,
5081 &staging_hash_jobs,
5082 &thread_policy,
5083 &config,
5084 materialize_dirty_blobs,
5085 hash_blobs_without_write,
5086 &conv,
5087 filter_fp,
5088 &mut result,
5089 )?;
5090 }
5091
5092 for (path, (_, base_entry)) in unmerged_base {
5093 let file_path = index_entry_worktree_abs(work_tree, &path, precompose_unicode, ignorecase);
5094 let wt_meta = match fs::symlink_metadata(&file_path) {
5095 Ok(meta) => Some(meta),
5096 Err(e)
5097 if e.kind() == std::io::ErrorKind::NotFound
5098 || e.raw_os_error() == Some(20) =>
5099 {
5100 None
5101 }
5102 Err(e) => return Err(Error::Io(e)),
5103 };
5104
5105 let new_mode = wt_meta.as_ref().map_or_else(
5106 || FileMode::ABSENT,
5107 |meta| FileMode::new(mode_from_metadata(meta)),
5108 );
5109 result.push(DiffEntry {
5110 status: DiffStatus::Unmerged,
5111 old_path: Some(path.clone()),
5112 new_path: Some(path.clone()),
5113 old_mode: FileMode::ABSENT,
5114 new_mode,
5115 old_oid: zero_oid(),
5116 new_oid: zero_oid(),
5117 score: None,
5118 });
5119
5120 if !options.for_status {
5121 if let Some(meta) = wt_meta {
5122 let file_attrs = file_attrs_for(&path);
5123 let wt_oid = worktree_file_oid(
5124 odb,
5125 &file_path,
5126 &meta,
5127 &conv,
5128 &file_attrs,
5129 &path,
5130 Some(&base_entry),
5131 materialize_dirty_blobs,
5132 filter_fp,
5133 )?;
5134 let wt_mode = mode_from_metadata(&meta);
5135 if wt_oid != base_entry.oid || wt_mode != base_entry.mode {
5136 result.push(DiffEntry {
5137 status: DiffStatus::for_content_change(base_entry.mode, wt_mode),
5138 old_path: Some(path.clone()),
5139 new_path: Some(path),
5140 old_mode: FileMode::new(base_entry.mode),
5141 new_mode: FileMode::new(wt_mode),
5142 old_oid: base_entry.oid,
5143 new_oid: wt_oid,
5144 score: None,
5145 });
5146 }
5147 }
5148 }
5149 }
5150
5151 Ok((result, index_stat_refresh_changed, staging_stat_cache))
5152}
5153
5154#[derive(Default)]
5157pub(crate) struct SymlinkDirCache {
5158 dirs: std::sync::Mutex<hashbrown::HashMap<Vec<u8>, bool>>,
5160}
5161
5162pub(crate) fn index_entry_worktree_abs(
5163 work_tree: &Path,
5164 index_relpath: &str,
5165 precompose_unicode: bool,
5166 ignorecase: bool,
5167) -> PathBuf {
5168 index_entry_worktree_abs_bytes(
5169 work_tree,
5170 index_relpath.as_bytes(),
5171 precompose_unicode,
5172 ignorecase,
5173 )
5174}
5175
5176pub(crate) fn index_entry_worktree_abs_bytes(
5177 work_tree: &Path,
5178 index_relpath: &[u8],
5179 precompose_unicode: bool,
5180 ignorecase: bool,
5181) -> PathBuf {
5182 if precompose_unicode || ignorecase {
5183 if let Ok(s) = std::str::from_utf8(index_relpath) {
5184 if precompose_unicode {
5185 return crate::unicode_normalization::resolve_worktree_path_for_staging(
5186 work_tree, s, true,
5187 )
5188 .abs;
5189 }
5190 return crate::path_icase::worktree_path_for_index_entry(work_tree, s, ignorecase);
5191 }
5192 }
5193 crate::index::worktree_path_from_index_rel(work_tree, index_relpath)
5194}
5195
5196#[must_use]
5201pub fn index_mtime_for_diff(index: &Index, explicit: Option<(u32, u32)>) -> Option<(u32, u32)> {
5202 explicit.or(index.source_mtime)
5203}
5204
5205impl SymlinkDirCache {
5206 pub(crate) fn has_symlink_in_path_bytes(
5211 &self,
5212 work_tree: &Path,
5213 rel_path: &[u8],
5214 precompose_unicode: bool,
5215 ignorecase: bool,
5216 ) -> bool {
5217 let Some(dir_len) = rel_path.iter().rposition(|&b| b == b'/') else {
5218 return false;
5219 };
5220 let prefix_ends = rel_path[..dir_len]
5221 .iter()
5222 .enumerate()
5223 .filter_map(|(i, &b)| (b == b'/').then_some(i))
5224 .chain(std::iter::once(dir_len));
5225 for end in prefix_ends {
5226 let prefix = &rel_path[..end];
5227 if prefix.is_empty() || prefix.ends_with(b"/") {
5228 continue;
5229 }
5230 let cached = self
5231 .dirs
5232 .lock()
5233 .unwrap_or_else(|e| e.into_inner())
5234 .get(prefix)
5235 .copied();
5236 let is_symlink = cached.unwrap_or_else(|| {
5237 let abs = index_entry_worktree_abs_bytes(
5238 work_tree,
5239 prefix,
5240 precompose_unicode,
5241 ignorecase,
5242 );
5243 let probed = crate::worktree_scan::path_is_symlink(&abs);
5244 self.dirs
5245 .lock()
5246 .unwrap_or_else(|e| e.into_inner())
5247 .insert(prefix.to_vec(), probed);
5248 probed
5249 });
5250 if is_symlink {
5251 return true;
5252 }
5253 }
5254 false
5255 }
5256}
5257
5258#[must_use]
5264pub fn entry_is_racy(ie: &IndexEntry, index_mtime: Option<(u32, u32)>) -> bool {
5265 entry_is_racy_with_policy(ie, index_mtime, crate::index::StatMatchPolicy::default())
5266}
5267
5268#[must_use]
5270pub fn entry_is_racy_with_policy(
5271 ie: &IndexEntry,
5272 index_mtime: Option<(u32, u32)>,
5273 policy: crate::index::StatMatchPolicy,
5274) -> bool {
5275 let Some((index_mtime_sec, index_mtime_nsec)) = index_mtime else {
5276 return false;
5277 };
5278 if index_mtime_sec == 0 {
5279 return false;
5280 }
5281 if policy.use_nanosec {
5282 index_mtime_sec < ie.mtime_sec
5283 || (index_mtime_sec == ie.mtime_sec && index_mtime_nsec <= ie.mtime_nsec)
5284 } else {
5285 index_mtime_sec <= ie.mtime_sec
5286 }
5287}
5288
5289pub fn smudge_racily_clean_entries(
5306 odb: &Odb,
5307 git_dir: &Path,
5308 index: &mut Index,
5309 work_tree: &Path,
5310 index_mtime: Option<(u32, u32)>,
5311 config: Option<&ConfigSet>,
5312 filter_process: Option<&crate::filter_process::FilterProcessState>,
5313) -> bool {
5314 use crate::crlf;
5315 use crate::index::{MODE_EXECUTABLE, MODE_REGULAR, MODE_SYMLINK};
5316
5317 let Some(index_mtime) = index_mtime else {
5318 return false;
5319 };
5320
5321 let config = config.cloned().unwrap_or_else(|| {
5322 ConfigSet::load(
5323 &crate::environment::Environment::empty(),
5324 Some(git_dir),
5325 true,
5326 )
5327 .unwrap_or_default()
5328 });
5329 let conv = crlf::ConversionConfig::from_config(&config);
5330 let attrs = crlf::load_gitattributes(work_tree);
5331 let verified = std::mem::take(&mut index.content_verified);
5332
5333 let mut changed = false;
5334 for ie in &mut index.entries {
5335 if ie.stage() != 0 || ie.skip_worktree() || ie.assume_unchanged() || ie.intent_to_add() {
5336 continue;
5337 }
5338 if ie.mode != MODE_REGULAR && ie.mode != MODE_EXECUTABLE && ie.mode != MODE_SYMLINK {
5339 continue;
5340 }
5341 if ie.size == 0 {
5342 continue;
5343 }
5344 if !entry_is_racy(ie, Some(index_mtime)) {
5345 continue;
5346 }
5347 if verified.get(&ie.path) == Some(&ie.oid) {
5348 continue;
5349 }
5350 let Ok(path) = std::str::from_utf8(&ie.path) else {
5351 continue;
5352 };
5353 let abs = work_tree.join(path);
5354 let Ok(meta) = fs::symlink_metadata(&abs) else {
5355 continue;
5356 };
5357 if !stat_matches(ie, &meta) {
5358 continue;
5359 }
5360 let file_attrs = crlf::get_file_attrs(&attrs, path, false, &config);
5361 let content_matches = hash_worktree_file(
5362 odb,
5363 &abs,
5364 &meta,
5365 &conv,
5366 &file_attrs,
5367 path,
5368 Some(ie),
5369 filter_process,
5370 )
5371 .map(|oid| oid == ie.oid)
5372 .unwrap_or(false);
5373 if content_matches {
5374 continue;
5375 }
5376 ie.size = 0;
5377 changed = true;
5378 }
5379 changed
5380}
5381pub fn worktree_differs_from_index_entry(
5389 odb: &Odb,
5390 repository_git_dir: &Path,
5391 work_tree: &Path,
5392 ie: &IndexEntry,
5393 ignore_submodule_untracked: bool,
5394 config: Option<&ConfigSet>,
5395 filter_process: Option<&crate::filter_process::FilterProcessState>,
5396) -> Result<bool> {
5397 WorktreeEntryComparer::new(repository_git_dir, work_tree, config).differs(
5398 odb,
5399 ie,
5400 ignore_submodule_untracked,
5401 filter_process,
5402 )
5403}
5404
5405pub(crate) struct WorktreeEntryComparer<'a> {
5409 work_tree: &'a Path,
5410 config: ConfigSet,
5411 mode_policy: crate::index::WorktreeModePolicy,
5412 precompose_unicode: bool,
5413 ignorecase: bool,
5414 conv: std::cell::OnceCell<crate::crlf::ConversionConfig>,
5415 attrs: std::cell::OnceCell<Vec<crate::crlf::AttrRule>>,
5416}
5417
5418impl<'a> WorktreeEntryComparer<'a> {
5419 pub(crate) fn new(
5421 repository_git_dir: &Path,
5422 work_tree: &'a Path,
5423 config: Option<&ConfigSet>,
5424 ) -> Self {
5425 let config = config.cloned().unwrap_or_else(|| {
5426 ConfigSet::load(
5427 &crate::environment::Environment::empty(),
5428 Some(repository_git_dir),
5429 true,
5430 )
5431 .unwrap_or_else(|_| ConfigSet::new())
5432 });
5433 let flag =
5434 |key: &str, default: bool| config.get_bool(key).and_then(|r| r.ok()).unwrap_or(default);
5435 Self {
5436 work_tree,
5437 mode_policy: crate::index::WorktreeModePolicy::from_config(&config),
5438 precompose_unicode: flag("core.precomposeunicode", false),
5439 ignorecase: flag("core.ignorecase", false),
5440 config,
5441 conv: std::cell::OnceCell::new(),
5442 attrs: std::cell::OnceCell::new(),
5443 }
5444 }
5445
5446 pub(crate) fn differs(
5448 &self,
5449 odb: &Odb,
5450 ie: &IndexEntry,
5451 ignore_submodule_untracked: bool,
5452 filter_process: Option<&crate::filter_process::FilterProcessState>,
5453 ) -> Result<bool> {
5454 use crate::crlf;
5455
5456 let work_tree = self.work_tree;
5457 let path_stored = crate::repo_relpath::storage_string(&ie.path);
5458 let path_str_ref = crate::repo_relpath::as_utf8_for_matching(&path_stored);
5459 let file_path = index_entry_worktree_abs_bytes(
5460 work_tree,
5461 &ie.path,
5462 self.precompose_unicode,
5463 self.ignorecase,
5464 );
5465
5466 if ie.mode == 0o160000 {
5467 let sub_head_oid = read_submodule_head(&file_path);
5468 let ref_matches = match sub_head_oid {
5469 Some(oid) => oid == ie.oid,
5470 None => submodule_worktree_is_unpopulated_placeholder(&file_path),
5471 };
5472 let mut flags = submodule_porcelain_flags(work_tree, path_str_ref.as_ref(), ie.oid);
5473 if ignore_submodule_untracked {
5474 flags.untracked = false;
5475 }
5476 return Ok(!ref_matches || flags.modified || flags.untracked);
5477 }
5478
5479 let meta = match fs::symlink_metadata(&file_path) {
5480 Ok(m) => m,
5481 Err(e)
5482 if e.kind() == std::io::ErrorKind::NotFound
5483 || e.raw_os_error() == Some(20) =>
5484 {
5485 return Ok(true);
5486 }
5487 Err(e) => return Err(Error::Io(e)),
5488 };
5489
5490 if meta.is_dir() {
5491 return Ok(true);
5492 }
5493
5494 if !self
5495 .mode_policy
5496 .worktree_mode_matches(mode_from_metadata(&meta), ie.mode)
5497 {
5498 return Ok(true);
5499 }
5500
5501 let conv = self
5502 .conv
5503 .get_or_init(|| crlf::ConversionConfig::from_config(&self.config));
5504 let attrs = self
5505 .attrs
5506 .get_or_init(|| crlf::load_gitattributes(work_tree));
5507 let file_attrs = crlf::get_file_attrs(attrs, path_str_ref.as_ref(), false, &self.config);
5508 let worktree_oid = hash_worktree_file(
5509 odb,
5510 &file_path,
5511 &meta,
5512 conv,
5513 &file_attrs,
5514 path_str_ref.as_ref(),
5515 Some(ie),
5516 filter_process,
5517 )?;
5518
5519 let mut eff_oid = worktree_oid;
5520 if eff_oid != ie.oid {
5521 if let Ok(raw) = fs::read(&file_path) {
5522 let raw_oid = odb.hash(ObjectKind::Blob, &raw)?;
5523 if raw_oid == ie.oid {
5524 eff_oid = ie.oid;
5525 }
5526 }
5527 }
5528
5529 Ok(eff_oid != ie.oid)
5530 }
5531}
5532
5533pub struct WorktreeAddRefreshParams<'a> {
5535 pub odb: &'a Odb,
5537 pub ie: &'a IndexEntry,
5539 pub meta: &'a fs::Metadata,
5541 pub abs_path: &'a Path,
5543 pub rel_path: &'a str,
5545 pub conv: &'a crate::crlf::ConversionConfig,
5547 pub file_attrs: &'a crate::crlf::FileAttrs,
5549 pub index_mtime: Option<(u32, u32)>,
5551 pub staged_mode: u32,
5554 pub filter_process: Option<&'a crate::filter_process::FilterProcessState>,
5556}
5557
5558#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5560pub enum WorktreeAddRefresh {
5561 UpToDate,
5563 ModeOnly {
5565 mode: u32,
5567 },
5568 StatOnly,
5570 NeedsRestage,
5572}
5573
5574#[must_use]
5576pub(crate) fn index_stat_cache_trustworthy(index_mtime: Option<(u32, u32)>) -> bool {
5577 match index_mtime {
5578 None => false,
5579 Some((0, _)) => false,
5580 Some(_) => true,
5581 }
5582}
5583
5584#[must_use]
5593pub(crate) fn index_entry_skips_worktree_content_hash_after_stat_match(
5594 ie: &IndexEntry,
5595 index_mtime: Option<(u32, u32)>,
5596 stat_policy: crate::index::StatMatchPolicy,
5597) -> bool {
5598 if ie.stat_cache_unset() || (ie.size == 0 && !ie.oid.is_empty_blob()) {
5599 return false;
5600 }
5601 if !index_stat_cache_trustworthy(index_mtime) {
5602 return false;
5603 }
5604 !entry_is_racy_with_policy(ie, index_mtime, stat_policy)
5605}
5606
5607pub fn classify_worktree_entry_for_add(
5621 params: &WorktreeAddRefreshParams<'_>,
5622) -> Result<WorktreeAddRefresh> {
5623 use crate::index::{MODE_EXECUTABLE, MODE_REGULAR, MODE_SYMLINK};
5624
5625 let ie = params.ie;
5626 let meta = params.meta;
5627 let staged_mode = params.staged_mode;
5628
5629 if ie.intent_to_add() {
5630 return Ok(WorktreeAddRefresh::NeedsRestage);
5631 }
5632
5633 let racy = entry_is_racy(ie, params.index_mtime);
5634 let stat_same = stat_matches(ie, meta);
5635 let trust_stat = index_stat_cache_trustworthy(params.index_mtime);
5636
5637 if ie.mode == MODE_SYMLINK {
5638 if !meta.file_type().is_symlink() && staged_mode != MODE_SYMLINK {
5641 return Ok(WorktreeAddRefresh::NeedsRestage);
5642 }
5643 if stat_same && !racy && trust_stat && staged_mode == ie.mode {
5644 return Ok(WorktreeAddRefresh::UpToDate);
5645 }
5646 if stat_same && !racy && trust_stat && staged_mode != ie.mode {
5647 return Ok(WorktreeAddRefresh::ModeOnly { mode: staged_mode });
5648 }
5649 return worktree_effective_oid_matches_index(params);
5650 }
5651
5652 if ie.mode != MODE_REGULAR && ie.mode != MODE_EXECUTABLE {
5653 return Ok(WorktreeAddRefresh::NeedsRestage);
5654 }
5655
5656 if meta.file_type().is_symlink() || meta.is_dir() {
5657 return Ok(WorktreeAddRefresh::NeedsRestage);
5658 }
5659
5660 if stat_same && !racy && trust_stat {
5661 if staged_mode == ie.mode {
5662 return Ok(WorktreeAddRefresh::UpToDate);
5663 }
5664 return Ok(WorktreeAddRefresh::ModeOnly { mode: staged_mode });
5665 }
5666
5667 worktree_effective_oid_matches_index(params)
5668}
5669
5670fn worktree_effective_oid_matches_index(
5671 params: &WorktreeAddRefreshParams<'_>,
5672) -> Result<WorktreeAddRefresh> {
5673 let WorktreeAddRefreshParams {
5674 odb,
5675 ie,
5676 meta,
5677 abs_path,
5678 rel_path,
5679 conv,
5680 file_attrs,
5681 staged_mode,
5682 ..
5683 } = params;
5684 let worktree_oid = hash_worktree_file(
5685 odb,
5686 abs_path,
5687 meta,
5688 conv,
5689 file_attrs,
5690 rel_path,
5691 Some(ie),
5692 params.filter_process,
5693 )?;
5694 let mut eff_oid = worktree_oid;
5695 if eff_oid != ie.oid {
5696 if let Ok(raw) = fs::read(abs_path) {
5697 let raw_oid = odb.hash(ObjectKind::Blob, &raw)?;
5698 if raw_oid == ie.oid {
5699 eff_oid = ie.oid;
5700 }
5701 }
5702 }
5703 if eff_oid != ie.oid {
5704 return Ok(WorktreeAddRefresh::NeedsRestage);
5705 }
5706 if *staged_mode != ie.mode {
5707 return Ok(WorktreeAddRefresh::ModeOnly { mode: *staged_mode });
5708 }
5709 if stat_matches(ie, meta) {
5710 Ok(WorktreeAddRefresh::UpToDate)
5711 } else {
5712 Ok(WorktreeAddRefresh::StatOnly)
5713 }
5714}
5715
5716#[must_use]
5718pub fn stat_matches(ie: &IndexEntry, meta: &fs::Metadata) -> bool {
5719 stat_matches_with_policy(ie, meta, crate::index::StatMatchPolicy::default())
5720}
5721
5722pub(crate) fn stat_matches_blob_stat(
5724 ie: &IndexEntry,
5725 stat: &crate::worktree_scan::BlobDiskStat,
5726 policy: crate::index::StatMatchPolicy,
5727) -> bool {
5728 stat.fields.matches_index_entry(ie, policy)
5729}
5730
5731#[must_use]
5733pub fn stat_matches_with_policy(
5734 ie: &IndexEntry,
5735 meta: &fs::Metadata,
5736 policy: crate::index::StatMatchPolicy,
5737) -> bool {
5738 if crate::index::munge_index_size(meta.len()) != ie.size {
5739 return false;
5740 }
5741 #[cfg(unix)]
5742 {
5743 use std::os::unix::fs::MetadataExt;
5744 if meta.mtime() as u32 != ie.mtime_sec {
5745 return false;
5746 }
5747 if policy.use_nanosec && policy.check_stat && meta.mtime_nsec() as u32 != ie.mtime_nsec {
5748 return false;
5749 }
5750 if policy.trust_ctime && policy.check_stat {
5751 if meta.ctime() as u32 != ie.ctime_sec {
5752 return false;
5753 }
5754 if policy.use_nanosec && meta.ctime_nsec() as u32 != ie.ctime_nsec {
5755 return false;
5756 }
5757 }
5758 if policy.check_stat && meta.ino() as u32 != ie.ino {
5759 return false;
5760 }
5761 }
5762 #[cfg(not(unix))]
5763 {
5764 let ((ctime_sec, ctime_nsec), (mtime_sec, mtime_nsec)) = crate::index::metadata_times(meta);
5765 if mtime_sec != ie.mtime_sec {
5766 return false;
5767 }
5768 if policy.use_nanosec && policy.check_stat && mtime_nsec != ie.mtime_nsec {
5769 return false;
5770 }
5771 if policy.trust_ctime && policy.check_stat {
5772 if ctime_sec != ie.ctime_sec {
5773 return false;
5774 }
5775 if policy.use_nanosec && ctime_nsec != ie.ctime_nsec {
5776 return false;
5777 }
5778 }
5779 }
5780 true
5781}
5782
5783pub fn refresh_index_stat_content_verified(
5806 odb: &Odb,
5807 git_dir: &Path,
5808 index: &mut Index,
5809 work_tree: &Path,
5810 index_mtime: Option<(u32, u32)>,
5811 config: Option<&ConfigSet>,
5812 _stat_parallel_threads: Option<usize>,
5813) -> Result<bool> {
5814 let _ = _stat_parallel_threads;
5815 refresh_index_stat_content_verified_with_rules(
5816 odb,
5817 git_dir,
5818 index,
5819 work_tree,
5820 index_mtime,
5821 config,
5822 _stat_parallel_threads,
5823 None,
5824 )
5825}
5826
5827#[allow(clippy::too_many_arguments)]
5835pub fn refresh_index_stat_content_verified_with_rules(
5836 odb: &Odb,
5837 git_dir: &Path,
5838 index: &mut Index,
5839 work_tree: &Path,
5840 index_mtime: Option<(u32, u32)>,
5841 config: Option<&ConfigSet>,
5842 stat_parallel_threads: Option<usize>,
5843 worktree_rules: Option<&std::sync::Arc<std::sync::Mutex<crate::worktree_rules::WorktreeRules>>>,
5844) -> Result<bool> {
5845 use crate::config::ConfigSet;
5846 use crate::crlf;
5847 use crate::worktree_scan::{
5848 collect_blob_lookup_batches, group_blob_entries_by_dir, visit_blob_lookup_batches,
5849 BlobDiskLookup, BlobScanSparseMode, WorktreeBlobScanOptions,
5850 };
5851
5852 let index_mtime = index_mtime_for_diff(index, index_mtime);
5853 let rules_lock = worktree_rules
5854 .map(|shared| {
5855 shared
5856 .lock()
5857 .map_err(|e| Error::Message(format!("worktree rules lock poisoned: {e}")))
5858 })
5859 .transpose()?;
5860 let config = rules_lock
5861 .as_ref()
5862 .map(|r| r.config().clone())
5863 .unwrap_or_else(|| {
5864 config.cloned().unwrap_or_else(|| {
5865 ConfigSet::load(
5866 &crate::environment::Environment::empty(),
5867 Some(git_dir),
5868 true,
5869 )
5870 .unwrap_or_default()
5871 })
5872 });
5873 let preload_index = config
5874 .get_bool("core.preloadIndex")
5875 .and_then(|r| r.ok())
5876 .unwrap_or(true);
5877 let blob_scan = WorktreeBlobScanOptions {
5878 preload_index,
5879 stat_parallel_threads,
5880 };
5881 let conv = rules_lock
5882 .as_ref()
5883 .map(|r| r.conversion().clone())
5884 .unwrap_or_else(|| crlf::ConversionConfig::from_config(&config));
5885 let legacy_attrs = if rules_lock.is_none() {
5886 Some(crlf::load_gitattributes_parsed(work_tree))
5887 } else {
5888 None
5889 };
5890 let file_attrs_for = |path: &str| -> crlf::FileAttrs {
5891 if let Some(rules) = rules_lock.as_ref() {
5892 rules.file_attrs(path, false)
5893 } else if let Some(parsed) = legacy_attrs.as_ref() {
5894 crlf::file_attrs_from_gitattributes(&parsed.rules, &parsed.macros, path, false, &config)
5895 } else {
5896 crlf::FileAttrs::default()
5897 }
5898 };
5899 let precompose_unicode = config
5900 .get_bool("core.precomposeunicode")
5901 .and_then(|r| r.ok())
5902 .unwrap_or(false);
5903 let ignorecase = config
5904 .get_bool("core.ignorecase")
5905 .and_then(|r| r.ok())
5906 .unwrap_or(false);
5907 let stat_policy = crate::index::StatMatchPolicy::from_config(Some(&config));
5908
5909 let blob_dirs = group_blob_entries_by_dir(index, false);
5910 let work_tree_owned = work_tree.to_path_buf();
5911 let dir_abs = move |dir: &[u8]| {
5912 if dir.is_empty() {
5913 work_tree_owned.clone()
5914 } else {
5915 index_entry_worktree_abs_bytes(&work_tree_owned, dir, precompose_unicode, ignorecase)
5916 }
5917 };
5918 let work_tree_for_files = work_tree.to_path_buf();
5919 let file_abs = move |rel: &[u8]| {
5920 index_entry_worktree_abs_bytes(&work_tree_for_files, rel, precompose_unicode, ignorecase)
5921 };
5922 let dir_symlinks = SymlinkDirCache::default();
5923 let mut changed = false;
5924 let filter_fp = rules_lock.as_ref().map(|r| r.filter_process());
5925 let thread_policy = crate::parallel::ThreadPolicy::default();
5926 let refresh_batches = collect_blob_lookup_batches(
5928 index,
5929 &blob_dirs,
5930 blob_scan,
5931 &thread_policy,
5932 &config,
5933 dir_abs,
5934 file_abs,
5935 |rel_path| {
5936 dir_symlinks.has_symlink_in_path_bytes(
5937 work_tree,
5938 rel_path,
5939 precompose_unicode,
5940 ignorecase,
5941 )
5942 },
5943 BlobScanSparseMode::StatRefresh {
5944 stat_policy,
5945 index_mtime,
5946 },
5947 )?;
5948 let par_opts = refresh_hash_parallel_options(&thread_policy, &config, stat_parallel_threads);
5949 let refresh_checks = refresh_content_checks(
5950 odb,
5951 index,
5952 &refresh_batches,
5953 index_mtime,
5954 stat_policy,
5955 &|_, _| true,
5956 &WorktreeHashContext {
5957 work_tree,
5958 precompose_unicode,
5959 ignorecase,
5960 conv: &conv,
5961 file_attrs_for: &file_attrs_for,
5962 filter_process: filter_fp,
5963 },
5964 par_opts,
5965 );
5966 visit_blob_lookup_batches(refresh_batches, |entry_index, lookup| {
5967 let BlobDiskLookup::Present(stat) = lookup else {
5968 return Ok(());
5969 };
5970 let refresh = refresh_index_blob_stat_from_scan(
5971 &mut index.entries[entry_index],
5972 &stat,
5973 index_mtime,
5974 stat_policy,
5975 refresh_checks[entry_index].unwrap_or(false),
5976 );
5977 if refresh.index_changed {
5978 changed = true;
5979 }
5980 if refresh.content_verified {
5981 let path = index.entries[entry_index].path.clone();
5982 let oid = index.entries[entry_index].oid;
5983 index.mark_content_verified(&path, oid);
5984 }
5985 Ok(())
5986 })?;
5987 Ok(changed)
5988}
5989
5990struct BlobStatRefreshOutcome {
5992 index_changed: bool,
5994 content_verified: bool,
5996}
5997
5998fn refresh_index_entry_eligible(ie: &IndexEntry) -> bool {
6000 use crate::index::{MODE_EXECUTABLE, MODE_REGULAR, MODE_SYMLINK};
6001 ie.stage() == 0
6002 && !ie.skip_worktree()
6003 && !ie.assume_unchanged()
6004 && !ie.intent_to_add()
6005 && matches!(ie.mode, MODE_REGULAR | MODE_EXECUTABLE | MODE_SYMLINK)
6006}
6007
6008fn refresh_needs_content_check(
6011 ie: &IndexEntry,
6012 stat: &crate::worktree_scan::BlobDiskStat,
6013 index_mtime: Option<(u32, u32)>,
6014 stat_policy: crate::index::StatMatchPolicy,
6015) -> bool {
6016 refresh_index_entry_eligible(ie)
6017 && !stat.is_dir
6018 && (!stat_matches_blob_stat(ie, stat, stat_policy)
6019 || entry_is_racy_with_policy(ie, index_mtime, stat_policy))
6020}
6021
6022pub(crate) fn worktree_diff_refreshes_entry(
6027 ie: &IndexEntry,
6028 stat: &crate::worktree_scan::BlobDiskStat,
6029 index_mtime: Option<(u32, u32)>,
6030 stat_policy: crate::index::StatMatchPolicy,
6031 for_status: bool,
6032) -> bool {
6033 if !for_status {
6034 return true;
6035 }
6036 if stat_matches_blob_stat(ie, stat, stat_policy) {
6037 return entry_is_racy_with_policy(ie, index_mtime, stat_policy);
6038 }
6039 stat.worktree_size() == ie.size || ie.stat_cache_unset()
6040}
6041
6042fn refresh_hash_parallel_options(
6049 thread_policy: &crate::parallel::ThreadPolicy,
6050 config: &ConfigSet,
6051 stat_parallel_threads: Option<usize>,
6052) -> crate::parallel::ParallelOptions {
6053 let policy = crate::parallel::ThreadPolicy {
6054 min_items_override: thread_policy.min_items_override.or(Some(0)),
6055 min_total_bytes_override: thread_policy.min_total_bytes_override.or(Some(0)),
6056 ..*thread_policy
6057 };
6058 let mut par_opts = policy.resolve(crate::parallel::ParallelOp::IndexRefresh, config);
6059 if let Some(cap) = stat_parallel_threads {
6060 par_opts.threads = par_opts
6061 .threads
6062 .min(std::num::NonZeroUsize::new(cap).unwrap_or(std::num::NonZeroUsize::MIN));
6063 }
6064 par_opts.min_items = 0;
6065 par_opts.min_total_bytes = None;
6066 par_opts
6067}
6068
6069struct WorktreeHashContext<'a> {
6071 work_tree: &'a Path,
6072 precompose_unicode: bool,
6073 ignorecase: bool,
6074 conv: &'a crate::crlf::ConversionConfig,
6075 file_attrs_for: &'a dyn Fn(&str) -> crate::crlf::FileAttrs,
6076 filter_process: Option<&'a crate::filter_process::FilterProcessState>,
6077}
6078
6079#[allow(clippy::too_many_arguments)]
6089fn refresh_content_checks(
6090 odb: &Odb,
6091 index: &Index,
6092 batches: &[Vec<(usize, crate::worktree_scan::BlobDiskLookup)>],
6093 index_mtime: Option<(u32, u32)>,
6094 stat_policy: crate::index::StatMatchPolicy,
6095 include: &dyn Fn(&IndexEntry, &crate::worktree_scan::BlobDiskStat) -> bool,
6096 ctx: &WorktreeHashContext<'_>,
6097 par_opts: crate::parallel::ParallelOptions,
6098) -> Vec<Option<bool>> {
6099 use crate::worktree_scan::BlobDiskLookup;
6100
6101 struct ContentCheck {
6103 entry_index: usize,
6104 worktree_mode: u32,
6105 file_attrs: crate::crlf::FileAttrs,
6106 }
6107
6108 let mut jobs = Vec::new();
6109 let mut total_bytes = 0usize;
6110 for (entry_index, lookup) in batches.iter().flatten() {
6111 let BlobDiskLookup::Present(stat) = lookup else {
6112 continue;
6113 };
6114 let ie = &index.entries[*entry_index];
6115 if !refresh_needs_content_check(ie, stat, index_mtime, stat_policy) || !include(ie, stat) {
6116 continue;
6117 }
6118 total_bytes = total_bytes.saturating_add(ie.size as usize);
6119 jobs.push(ContentCheck {
6120 entry_index: *entry_index,
6121 worktree_mode: stat.git_mode,
6122 file_attrs: (ctx.file_attrs_for)(&crate::repo_relpath::storage_string(&ie.path)),
6123 });
6124 }
6125 let mut checks = vec![None; index.entries.len()];
6126 if jobs.is_empty() {
6127 return checks;
6128 }
6129 let (work_tree, precompose_unicode, ignorecase) =
6130 (ctx.work_tree, ctx.precompose_unicode, ctx.ignorecase);
6131 let (conv, filter_process) = (ctx.conv, ctx.filter_process);
6132 let entries = index.entries.as_slice();
6133 let matches = if par_opts.threads.get() > 1 && jobs.len() > 1 {
6134 crate::parallel::try_par_map_force::<_, _, std::convert::Infallible, _>(
6135 &jobs,
6136 par_opts,
6137 |job| {
6138 let ie = &entries[job.entry_index];
6139 let file_path = index_entry_worktree_abs_bytes(
6140 work_tree,
6141 &ie.path,
6142 precompose_unicode,
6143 ignorecase,
6144 );
6145 Ok(hash_worktree_file_with_mode(
6146 odb,
6147 &file_path,
6148 job.worktree_mode,
6149 conv,
6150 &job.file_attrs,
6151 &crate::repo_relpath::storage_string(&ie.path),
6152 Some(ie),
6153 filter_process,
6154 )
6155 .is_ok_and(|oid| oid == ie.oid))
6156 },
6157 )
6158 .unwrap_or_else(|e| match e {
6159 crate::parallel::ParallelHashError::Task(infallible) => match infallible {},
6160 })
6161 } else {
6162 jobs.iter()
6163 .map(|job| {
6164 let ie = &entries[job.entry_index];
6165 let file_path = index_entry_worktree_abs_bytes(
6166 work_tree,
6167 &ie.path,
6168 precompose_unicode,
6169 ignorecase,
6170 );
6171 hash_worktree_file_with_mode(
6172 odb,
6173 &file_path,
6174 job.worktree_mode,
6175 conv,
6176 &job.file_attrs,
6177 &crate::repo_relpath::storage_string(&ie.path),
6178 Some(ie),
6179 filter_process,
6180 )
6181 .is_ok_and(|oid| oid == ie.oid)
6182 })
6183 .collect()
6184 };
6185 for (job, matched) in jobs.iter().zip(matches) {
6186 checks[job.entry_index] = Some(matched);
6187 }
6188 checks
6189}
6190
6191fn refresh_index_blob_stat_from_scan(
6197 ie: &mut IndexEntry,
6198 stat: &crate::worktree_scan::BlobDiskStat,
6199 index_mtime: Option<(u32, u32)>,
6200 stat_policy: crate::index::StatMatchPolicy,
6201 content_matches: bool,
6202) -> BlobStatRefreshOutcome {
6203 const UNCHANGED: BlobStatRefreshOutcome = BlobStatRefreshOutcome {
6204 index_changed: false,
6205 content_verified: false,
6206 };
6207 if !refresh_index_entry_eligible(ie) || stat.is_dir {
6208 return UNCHANGED;
6209 }
6210 let cached = crate::index::IndexStatFields::of_entry(ie);
6211 let stat_same = cached.equivalent_to(&stat.fields, stat_policy);
6212 let stat_matches = stat_matches_blob_stat(ie, stat, stat_policy);
6213 if !stat_matches || entry_is_racy_with_policy(ie, index_mtime, stat_policy) {
6214 if !content_matches {
6215 if stat_matches {
6216 invalidate_index_stat_cache(ie);
6217 return BlobStatRefreshOutcome {
6218 index_changed: true,
6219 content_verified: false,
6220 };
6221 }
6222 return UNCHANGED;
6223 }
6224 if !stat_same {
6225 stat.fields.store_in(ie);
6226 }
6227 return BlobStatRefreshOutcome {
6228 index_changed: !stat_same,
6229 content_verified: true,
6230 };
6231 }
6232 if stat_same {
6233 return BlobStatRefreshOutcome {
6234 index_changed: false,
6235 content_verified: true,
6236 };
6237 }
6238 stat.fields.store_in(ie);
6239 BlobStatRefreshOutcome {
6240 index_changed: true,
6241 content_verified: false,
6242 }
6243}
6244
6245#[allow(clippy::too_many_arguments)] pub fn path_checkout_skip_blob_write_when_up_to_date(
6258 odb: &Odb,
6259 git_dir: &Path,
6260 work_tree: &Path,
6261 index: &Index,
6262 entry: &IndexEntry,
6263 expected_oid: &ObjectId,
6264 expected_mode: u32,
6265 rel_path: &str,
6266 filter_process: Option<&crate::filter_process::FilterProcessState>,
6267) -> Result<bool> {
6268 path_checkout_skip_blob_write_when_up_to_date_with_config(
6269 odb,
6270 git_dir,
6271 work_tree,
6272 index,
6273 entry,
6274 expected_oid,
6275 expected_mode,
6276 rel_path,
6277 filter_process,
6278 None,
6279 )
6280}
6281
6282#[allow(clippy::too_many_arguments)]
6284pub(crate) fn path_checkout_skip_blob_write_when_up_to_date_with_config(
6285 odb: &Odb,
6286 git_dir: &Path,
6287 work_tree: &Path,
6288 index: &Index,
6289 entry: &IndexEntry,
6290 expected_oid: &ObjectId,
6291 expected_mode: u32,
6292 rel_path: &str,
6293 filter_process: Option<&crate::filter_process::FilterProcessState>,
6294 preloaded_config: Option<&crate::config::ConfigSet>,
6295) -> Result<bool> {
6296 use crate::config::ConfigSet;
6297 use crate::crlf;
6298 use crate::index::{MODE_EXECUTABLE, MODE_REGULAR};
6299
6300 if entry.oid != *expected_oid || entry.mode != expected_mode {
6301 return Ok(false);
6302 }
6303 if expected_mode != MODE_REGULAR && expected_mode != MODE_EXECUTABLE {
6304 return Ok(false);
6305 }
6306 let abs_path = crate::repo_relpath::worktree_abs(work_tree, rel_path);
6307 let meta = match fs::symlink_metadata(&abs_path) {
6308 Ok(m) => m,
6309 Err(_) => return Ok(false),
6310 };
6311 if !meta.is_file() || !stat_matches(entry, &meta) {
6312 return Ok(false);
6313 }
6314 let owned_config;
6315 let config = match preloaded_config {
6316 Some(cfg) => cfg,
6317 None => {
6318 owned_config = ConfigSet::load(
6319 &crate::environment::Environment::empty(),
6320 Some(git_dir),
6321 true,
6322 )
6323 .unwrap_or_default();
6324 &owned_config
6325 }
6326 };
6327 if !crate::index::WorktreeModePolicy::from_config(config)
6328 .worktree_mode_matches(mode_from_metadata(&meta), expected_mode)
6329 {
6330 return Ok(false);
6331 }
6332 let index_mtime = index_mtime_for_diff(index, None);
6333 if !entry_is_racy(entry, index_mtime) {
6334 return Ok(true);
6335 }
6336 let conv = crlf::ConversionConfig::from_config(config);
6337 let attrs = crlf::load_gitattributes(work_tree);
6338 let file_attrs = crlf::get_file_attrs(&attrs, rel_path, false, config);
6339 let wt_oid = hash_worktree_file(
6340 odb,
6341 &abs_path,
6342 &meta,
6343 &conv,
6344 &file_attrs,
6345 rel_path,
6346 Some(entry),
6347 filter_process,
6348 )?;
6349 Ok(wt_oid == *expected_oid)
6350}
6351
6352fn invalidate_index_stat_cache(ie: &mut IndexEntry) {
6354 ie.ctime_sec = 0;
6355 ie.ctime_nsec = 0;
6356 ie.mtime_sec = 0;
6357 ie.mtime_nsec = 0;
6358 ie.dev = 0;
6359 ie.ino = 0;
6360 ie.size = 0;
6361}
6362
6363pub fn read_worktree_bytes_for_mode(path: &Path, mode: u32) -> std::io::Result<Vec<u8>> {
6374 if mode == MODE_SYMLINK && fs::symlink_metadata(path)?.file_type().is_symlink() {
6375 let target = fs::read_link(path)?;
6376 return Ok(symlink_target_bytes(&target));
6377 }
6378 fs::read(path)
6379}
6380
6381pub fn read_worktree_side_bytes_for_diff(
6391 odb: &Odb,
6392 work_tree: &Path,
6393 rel_path: &str,
6394 mode: u32,
6395 config: &crate::config::ConfigSet,
6396 index_entry: Option<&IndexEntry>,
6397 worktree_rules: Option<&crate::worktree_rules::WorktreeRules>,
6398) -> Result<Vec<u8>> {
6399 let file_path = crate::repo_relpath::worktree_abs(work_tree, rel_path);
6400 let rel_match = crate::repo_relpath::as_utf8_for_matching(rel_path);
6401 let meta = fs::symlink_metadata(&file_path).map_err(|source| {
6402 crate::worktree::map_worktree_io(
6403 rel_match.as_ref(),
6404 crate::worktree::WorktreePathOperation::Metadata,
6405 source,
6406 )
6407 })?;
6408 if mode == MODE_SYMLINK && meta.file_type().is_symlink() {
6409 let target = fs::read_link(&file_path).map_err(|source| {
6410 crate::worktree::map_worktree_io(
6411 rel_match.as_ref(),
6412 crate::worktree::WorktreePathOperation::Read,
6413 source,
6414 )
6415 })?;
6416 return Ok(symlink_target_bytes(&target));
6417 }
6418 if let Some(rules) = worktree_rules {
6419 return worktree_blob_bytes(
6420 odb,
6421 &file_path,
6422 &meta,
6423 rules.conversion(),
6424 &rules.file_attrs(rel_match.as_ref(), meta.is_dir()),
6425 rel_match.as_ref(),
6426 index_entry,
6427 Some(rules.filter_process()),
6428 );
6429 }
6430 let conv = crate::crlf::ConversionConfig::from_config(config);
6431 let attrs = crate::crlf::load_gitattributes(work_tree);
6432 let file_attrs = crate::crlf::get_file_attrs(&attrs, rel_match.as_ref(), meta.is_dir(), config);
6433 worktree_blob_bytes(
6434 odb,
6435 &file_path,
6436 &meta,
6437 &conv,
6438 &file_attrs,
6439 rel_match.as_ref(),
6440 index_entry,
6441 None,
6442 )
6443}
6444
6445pub(crate) fn symlink_target_bytes(target: &Path) -> Vec<u8> {
6451 #[cfg(unix)]
6452 {
6453 use std::os::unix::ffi::OsStrExt as _;
6454 target.as_os_str().as_bytes().to_vec()
6455 }
6456 #[cfg(not(unix))]
6457 {
6458 target.as_os_str().to_string_lossy().as_bytes().to_vec()
6459 }
6460}
6461
6462pub(crate) enum WorktreeBlobContent {
6469 Bytes(Vec<u8>),
6471 Stream {
6473 file: fs::File,
6475 size: u64,
6477 },
6478}
6479
6480impl WorktreeBlobContent {
6481 pub(crate) fn hash(self, odb: &Odb) -> Result<ObjectId> {
6483 match self {
6484 Self::Bytes(data) => odb.hash(ObjectKind::Blob, &data),
6485 Self::Stream { mut file, size } => odb.hash_stream(ObjectKind::Blob, size, &mut file),
6486 }
6487 }
6488
6489 pub(crate) fn store(self, odb: &Odb, options: crate::odb::WriteOptions) -> Result<ObjectId> {
6491 match self {
6492 Self::Bytes(data) => odb.write_with_options(ObjectKind::Blob, &data, options),
6493 Self::Stream { mut file, size } => {
6494 odb.write_stream(ObjectKind::Blob, size, &mut file, options)
6495 }
6496 }
6497 }
6498
6499 fn object_id(self, odb: &Odb, store: Option<crate::odb::WriteOptions>) -> Result<ObjectId> {
6501 match store {
6502 Some(options) => self.store(odb, options),
6503 None => self.hash(odb),
6504 }
6505 }
6506}
6507
6508#[allow(clippy::too_many_arguments)]
6510fn read_regular_worktree_bytes(
6511 odb: &Odb,
6512 mut file: fs::File,
6513 size: u64,
6514 conv: &crate::crlf::ConversionConfig,
6515 file_attrs: &crate::crlf::FileAttrs,
6516 rel_path: &str,
6517 index_entry: Option<&IndexEntry>,
6518 filter_process: Option<&crate::filter_process::FilterProcessState>,
6519) -> Result<Vec<u8>> {
6520 let needs_index_blob =
6521 index_entry.is_some_and(|_| crate::crlf::clean_uses_autocrlf_index_guard(file_attrs, conv));
6522 let prior_blob: Option<Vec<u8>> = if needs_index_blob {
6523 index_entry
6524 .filter(|e| e.oid != zero_oid())
6525 .and_then(|e| odb.read(&e.oid).ok().map(|o| o.data))
6526 } else {
6527 None
6528 };
6529 let mut raw = Vec::with_capacity(usize::try_from(size).unwrap_or_default());
6530 std::io::Read::read_to_end(&mut file, &mut raw).map_err(|source| {
6531 crate::worktree::map_worktree_io(
6532 rel_path,
6533 crate::worktree::WorktreePathOperation::Read,
6534 source,
6535 )
6536 })?;
6537 #[cfg(test)]
6538 odb.hot_path_metrics().record_blob_content_read();
6539 let opts = crate::crlf::ConvertToGitOpts {
6540 index_blob: prior_blob.as_deref(),
6541 renormalize: false,
6542 check_safecrlf: false,
6543 filter_process,
6544 command_runner: None,
6545 };
6546 Ok(crate::crlf::convert_to_git_owned(
6547 raw, rel_path, conv, file_attrs, opts,
6548 )?)
6549}
6550
6551fn open_with_size(path: &Path, rel_path: &str) -> Result<(fs::File, u64)> {
6552 let file = fs::File::open(path).map_err(|source| {
6553 crate::worktree::map_worktree_io(
6554 rel_path,
6555 crate::worktree::WorktreePathOperation::Read,
6556 source,
6557 )
6558 })?;
6559 let size = file
6560 .metadata()
6561 .map_err(|source| {
6562 crate::worktree::map_worktree_io(
6563 rel_path,
6564 crate::worktree::WorktreePathOperation::Metadata,
6565 source,
6566 )
6567 })?
6568 .len();
6569 Ok((file, size))
6570}
6571
6572#[allow(clippy::too_many_arguments)]
6575fn read_regular_worktree_blob(
6576 odb: &Odb,
6577 path: &Path,
6578 conv: &crate::crlf::ConversionConfig,
6579 file_attrs: &crate::crlf::FileAttrs,
6580 rel_path: &str,
6581 index_entry: Option<&IndexEntry>,
6582 filter_process: Option<&crate::filter_process::FilterProcessState>,
6583 stream_from: u64,
6584) -> Result<WorktreeBlobContent> {
6585 let (mut file, size) = open_with_size(path, rel_path)?;
6586 if size >= stream_from
6587 && crate::crlf::worktree_allows_raw_byte_stream(
6588 conv,
6589 file_attrs,
6590 index_entry,
6591 &mut file,
6592 size,
6593 )?
6594 {
6595 #[cfg(test)]
6596 odb.hot_path_metrics().record_blob_content_read();
6597 return Ok(WorktreeBlobContent::Stream { file, size });
6598 }
6599 read_regular_worktree_bytes(
6600 odb,
6601 file,
6602 size,
6603 conv,
6604 file_attrs,
6605 rel_path,
6606 index_entry,
6607 filter_process,
6608 )
6609 .map(WorktreeBlobContent::Bytes)
6610}
6611
6612#[allow(clippy::too_many_arguments)]
6615pub(crate) fn worktree_blob_content(
6616 odb: &Odb,
6617 path: &Path,
6618 meta: &fs::Metadata,
6619 conv: &crate::crlf::ConversionConfig,
6620 file_attrs: &crate::crlf::FileAttrs,
6621 rel_path: &str,
6622 index_entry: Option<&IndexEntry>,
6623 filter_process: Option<&crate::filter_process::FilterProcessState>,
6624) -> Result<WorktreeBlobContent> {
6625 worktree_blob_content_from(
6626 odb,
6627 path,
6628 meta,
6629 conv,
6630 file_attrs,
6631 rel_path,
6632 index_entry,
6633 filter_process,
6634 hash_stream_threshold(odb)?,
6635 )
6636}
6637
6638#[allow(clippy::too_many_arguments)]
6640fn worktree_blob_content_from(
6641 odb: &Odb,
6642 path: &Path,
6643 meta: &fs::Metadata,
6644 conv: &crate::crlf::ConversionConfig,
6645 file_attrs: &crate::crlf::FileAttrs,
6646 rel_path: &str,
6647 index_entry: Option<&IndexEntry>,
6648 filter_process: Option<&crate::filter_process::FilterProcessState>,
6649 stream_from: u64,
6650) -> Result<WorktreeBlobContent> {
6651 if meta.file_type().is_symlink() {
6652 let target = fs::read_link(path).map_err(|source| {
6653 crate::worktree::map_worktree_io(
6654 rel_path,
6655 crate::worktree::WorktreePathOperation::Read,
6656 source,
6657 )
6658 })?;
6659 return Ok(WorktreeBlobContent::Bytes(symlink_target_bytes(&target)));
6660 }
6661 if meta.is_dir() {
6662 return Ok(WorktreeBlobContent::Bytes(Vec::new()));
6663 }
6664 read_regular_worktree_blob(
6665 odb,
6666 path,
6667 conv,
6668 file_attrs,
6669 rel_path,
6670 index_entry,
6671 filter_process,
6672 stream_from,
6673 )
6674}
6675
6676#[allow(clippy::too_many_arguments)]
6679pub(crate) fn worktree_blob_bytes(
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) -> Result<Vec<u8>> {
6689 if meta.file_type().is_symlink() {
6690 let target = fs::read_link(path).map_err(|source| {
6691 crate::worktree::map_worktree_io(
6692 rel_path,
6693 crate::worktree::WorktreePathOperation::Read,
6694 source,
6695 )
6696 })?;
6697 return Ok(symlink_target_bytes(&target));
6698 }
6699 if meta.is_dir() {
6700 return Ok(Vec::new());
6701 }
6702 let (file, size) = open_with_size(path, rel_path)?;
6703 read_regular_worktree_bytes(
6704 odb,
6705 file,
6706 size,
6707 conv,
6708 file_attrs,
6709 rel_path,
6710 index_entry,
6711 filter_process,
6712 )
6713}
6714
6715#[allow(clippy::too_many_arguments)]
6716fn worktree_file_oid(
6717 odb: &Odb,
6718 path: &Path,
6719 meta: &fs::Metadata,
6720 conv: &crate::crlf::ConversionConfig,
6721 file_attrs: &crate::crlf::FileAttrs,
6722 rel_path: &str,
6723 index_entry: Option<&IndexEntry>,
6724 materialize: bool,
6725 filter_process: Option<&crate::filter_process::FilterProcessState>,
6726) -> Result<ObjectId> {
6727 let stream_from = if materialize {
6728 odb.big_file_threshold()?
6729 } else {
6730 hash_stream_threshold(odb)?
6731 };
6732 worktree_blob_content_from(
6733 odb,
6734 path,
6735 meta,
6736 conv,
6737 file_attrs,
6738 rel_path,
6739 index_entry,
6740 filter_process,
6741 stream_from,
6742 )?
6743 .object_id(odb, materialize.then_some(crate::odb::BULK_NEW_LOOSE_WRITE))
6744}
6745
6746#[allow(clippy::too_many_arguments)]
6757pub fn hash_worktree_file(
6758 odb: &Odb,
6759 path: &Path,
6760 meta: &fs::Metadata,
6761 conv: &crate::crlf::ConversionConfig,
6762 file_attrs: &crate::crlf::FileAttrs,
6763 rel_path: &str,
6764 index_entry: Option<&IndexEntry>,
6765 filter_process: Option<&crate::filter_process::FilterProcessState>,
6766) -> Result<ObjectId> {
6767 worktree_file_oid(
6768 odb,
6769 path,
6770 meta,
6771 conv,
6772 file_attrs,
6773 rel_path,
6774 index_entry,
6775 false,
6776 filter_process,
6777 )
6778}
6779
6780const STREAMED_HASH_MIN_SIZE: u64 = 1024 * 1024;
6783
6784fn hash_stream_threshold(odb: &Odb) -> Result<u64> {
6786 Ok(odb.big_file_threshold()?.min(STREAMED_HASH_MIN_SIZE))
6787}
6788
6789#[allow(clippy::too_many_arguments)]
6793pub(crate) fn hash_worktree_file_with_mode(
6794 odb: &Odb,
6795 path: &Path,
6796 worktree_mode: u32,
6797 conv: &crate::crlf::ConversionConfig,
6798 file_attrs: &crate::crlf::FileAttrs,
6799 rel_path: &str,
6800 index_entry: Option<&IndexEntry>,
6801 filter_process: Option<&crate::filter_process::FilterProcessState>,
6802) -> Result<ObjectId> {
6803 if worktree_mode == MODE_SYMLINK {
6804 let data = read_worktree_bytes_for_mode(path, worktree_mode)?;
6805 return odb.hash(ObjectKind::Blob, &data);
6806 }
6807 read_regular_worktree_blob(
6808 odb,
6809 path,
6810 conv,
6811 file_attrs,
6812 rel_path,
6813 index_entry,
6814 filter_process,
6815 hash_stream_threshold(odb)?,
6816 )?
6817 .hash(odb)
6818}
6819
6820#[allow(clippy::too_many_arguments)]
6828pub fn materialize_worktree_blob(
6829 odb: &Odb,
6830 path: &Path,
6831 meta: &fs::Metadata,
6832 conv: &crate::crlf::ConversionConfig,
6833 file_attrs: &crate::crlf::FileAttrs,
6834 rel_path: &str,
6835 index_entry: Option<&IndexEntry>,
6836 filter_process: Option<&crate::filter_process::FilterProcessState>,
6837) -> Result<ObjectId> {
6838 worktree_file_oid(
6839 odb,
6840 path,
6841 meta,
6842 conv,
6843 file_attrs,
6844 rel_path,
6845 index_entry,
6846 true,
6847 filter_process,
6848 )
6849}
6850
6851pub fn mode_from_metadata(meta: &fs::Metadata) -> u32 {
6853 if meta.file_type().is_symlink() {
6854 0o120000
6855 } else {
6856 #[cfg(unix)]
6857 {
6858 if meta.mode() & 0o111 != 0 {
6859 return 0o100755;
6860 }
6861 }
6862 0o100644
6863 }
6864}
6865
6866pub fn diff_tree_to_worktree(
6883 odb: &Odb,
6884 tree_oid: Option<&ObjectId>,
6885 work_tree: &Path,
6886 index: &Index,
6887) -> Result<Vec<DiffEntry>> {
6888 diff_tree_to_worktree_with_git_dir(
6889 odb,
6890 tree_oid,
6891 work_tree,
6892 &work_tree.join(".git"),
6893 index,
6894 None,
6895 )
6896}
6897
6898pub fn diff_tree_to_worktree_with_git_dir(
6902 odb: &Odb,
6903 tree_oid: Option<&ObjectId>,
6904 work_tree: &Path,
6905 repository_git_dir: &Path,
6906 index: &Index,
6907 config: Option<&ConfigSet>,
6908) -> Result<Vec<DiffEntry>> {
6909 diff_tree_to_worktree_with_git_dir_and_rules(
6910 odb,
6911 tree_oid,
6912 work_tree,
6913 repository_git_dir,
6914 index,
6915 config,
6916 None,
6917 )
6918}
6919
6920pub fn diff_tree_to_worktree_with_git_dir_and_rules(
6922 odb: &Odb,
6923 tree_oid: Option<&ObjectId>,
6924 work_tree: &Path,
6925 repository_git_dir: &Path,
6926 index: &Index,
6927 config: Option<&ConfigSet>,
6928 worktree_rules: Option<&crate::worktree_rules::WorktreeRules>,
6929) -> Result<Vec<DiffEntry>> {
6930 use crate::config::ConfigSet;
6931 use crate::crlf;
6932
6933 if worktree_rules.is_none() {
6934 if let Some(tree_oid) = tree_oid {
6935 if index.cache_tree_root.as_ref() == Some(tree_oid)
6936 && crate::write_tree::cache_tree_fully_valid(odb, index.cache_tree.as_ref())
6937 {
6938 let mut idx = index.clone();
6939 let options = DiffIndexToWorktreeOptions {
6940 repository_git_dir: Some(repository_git_dir.to_path_buf()),
6941 refresh_index_stat_in_pass: false,
6942 for_status: false,
6946 config: config.cloned().map(std::sync::Arc::new),
6947 ..DiffIndexToWorktreeOptions::default()
6948 };
6949 let (entries, _, _) = diff_index_to_worktree_inner(
6950 odb,
6951 &mut idx,
6952 work_tree,
6953 repository_git_dir,
6954 options,
6955 false,
6956 )?;
6957 return Ok(entries);
6958 }
6959 }
6960 }
6961
6962 let config = worktree_rules
6963 .map(|r| r.config().clone())
6964 .unwrap_or_else(|| {
6965 config.cloned().unwrap_or_else(|| {
6966 ConfigSet::load(
6967 &crate::environment::Environment::empty(),
6968 Some(repository_git_dir),
6969 true,
6970 )
6971 .unwrap_or_else(|_| ConfigSet::new())
6972 })
6973 });
6974 let conv = worktree_rules
6975 .map(|r| r.conversion().clone())
6976 .unwrap_or_else(|| crlf::ConversionConfig::from_config(&config));
6977 let legacy_attrs = if worktree_rules.is_none() {
6978 Some(crlf::load_gitattributes_parsed(work_tree))
6979 } else {
6980 None
6981 };
6982 let file_attrs_for = |path: &str| -> crlf::FileAttrs {
6983 if let Some(rules) = worktree_rules {
6984 rules.file_attrs(path, false)
6985 } else if let Some(parsed) = legacy_attrs.as_ref() {
6986 crlf::file_attrs_from_gitattributes(&parsed.rules, &parsed.macros, path, false, &config)
6987 } else {
6988 crlf::FileAttrs::default()
6989 }
6990 };
6991 let filter_fp = worktree_rules.map(|r| r.filter_process());
6992
6993 let tree_flat = match tree_oid {
6995 Some(oid) => flatten_tree(odb, oid, "")?,
6996 None => Vec::new(),
6997 };
6998 let tree_map: std::collections::BTreeMap<String, &FlatEntry> =
6999 tree_flat.iter().map(|e| (e.path.clone(), e)).collect();
7000
7001 let mut index_entries: std::collections::BTreeMap<&[u8], &IndexEntry> =
7003 std::collections::BTreeMap::new();
7004 let mut index_paths: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
7005 let mut stage0_paths: std::collections::BTreeSet<Vec<u8>> = std::collections::BTreeSet::new();
7006 for ie in &index.entries {
7007 if ie.stage() != 0 {
7008 continue;
7009 }
7010 let path = String::from_utf8_lossy(&ie.path).to_string();
7011 index_entries.insert(&ie.path, ie);
7012 index_paths.insert(path);
7013 stage0_paths.insert(ie.path.clone());
7014 }
7015
7016 let mut unmerged_only_paths: std::collections::BTreeSet<String> =
7019 std::collections::BTreeSet::new();
7020 for ie in &index.entries {
7021 if !(1..=3).contains(&ie.stage()) {
7022 continue;
7023 }
7024 if stage0_paths.contains(&ie.path) {
7025 continue;
7026 }
7027 unmerged_only_paths.insert(String::from_utf8_lossy(&ie.path).into_owned());
7028 }
7029
7030 let mut all_paths: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
7032 all_paths.extend(tree_map.keys().cloned());
7033 all_paths.extend(index_paths.iter().cloned());
7034 all_paths.extend(unmerged_only_paths.iter().cloned());
7035
7036 let index_mtime = index_mtime_for_diff(index, None);
7037 let stat_policy = crate::index::StatMatchPolicy::from_config(Some(&config));
7038 let mode_policy = crate::index::WorktreeModePolicy::from_config(&config);
7039 let mut result = Vec::new();
7040
7041 for path in &all_paths {
7042 if index_entries
7043 .get(path.as_bytes())
7044 .is_some_and(|ie| ie.skip_worktree())
7045 {
7046 continue;
7049 }
7050 if index_entries
7051 .get(path.as_bytes())
7052 .is_some_and(|ie| ie.assume_unchanged())
7053 {
7054 continue;
7055 }
7056
7057 let tree_entry = tree_map.get(path.as_str());
7058
7059 let is_gitlink = tree_entry.is_some_and(|te| te.mode == 0o160000)
7061 || index_entries
7062 .get(path.as_bytes())
7063 .is_some_and(|ie| ie.mode == 0o160000);
7064 if is_gitlink {
7065 let sub_dir = work_tree.join(path);
7066 let index_gitlink_oid = index_entries
7067 .get(path.as_bytes())
7068 .filter(|ie| ie.mode == 0o160000)
7069 .map(|ie| ie.oid);
7070 match (tree_entry, index_gitlink_oid) {
7071 (Some(te), _) => {
7072 let sub_head = read_submodule_head_oid(&sub_dir);
7073 if sub_head.is_none() && !sub_dir.exists() {
7077 result.push(DiffEntry {
7078 status: DiffStatus::Deleted,
7079 old_path: Some(path.clone()),
7080 new_path: Some(path.clone()),
7081 old_mode: FileMode::new(te.mode),
7082 new_mode: FileMode::ABSENT,
7083 old_oid: te.oid,
7084 new_oid: zero_oid(),
7085 score: None,
7086 });
7087 continue;
7088 }
7089 let ref_matches = if let Some(head) = sub_head {
7090 head == te.oid
7091 } else {
7092 submodule_worktree_is_unpopulated_placeholder(&sub_dir)
7093 };
7094 let index_matches_tree = index_gitlink_oid.is_some_and(|oid| oid == te.oid);
7095 let sub_flags = submodule_porcelain_flags(work_tree, path, te.oid);
7098 let dirty_while_aligned =
7099 index_matches_tree && ref_matches && sub_flags.modified;
7100 if !ref_matches || dirty_while_aligned {
7101 let new_oid = if !ref_matches {
7105 sub_head.unwrap_or_else(zero_oid)
7106 } else {
7107 zero_oid()
7108 };
7109 result.push(DiffEntry {
7110 status: DiffStatus::Modified,
7111 old_path: Some(path.clone()),
7112 new_path: Some(path.clone()),
7113 old_mode: FileMode::new(te.mode),
7114 new_mode: FileMode::new(te.mode),
7115 old_oid: te.oid,
7116 new_oid,
7117 score: None,
7118 });
7119 }
7120 }
7121 (None, Some(idx_oid)) => {
7122 let new_oid = read_submodule_head_oid(&sub_dir).unwrap_or(idx_oid);
7127 result.push(DiffEntry {
7128 status: DiffStatus::Added,
7129 old_path: Some(path.clone()),
7130 new_path: Some(path.clone()),
7131 old_mode: FileMode::ABSENT,
7132 new_mode: FileMode::new(0o160000),
7133 old_oid: zero_oid(),
7134 new_oid,
7135 score: None,
7136 });
7137 }
7138 (None, None) => {}
7139 }
7140 continue;
7141 }
7142
7143 let file_path = work_tree.join(path);
7144
7145 let wt_meta = match fs::symlink_metadata(&file_path) {
7146 Ok(m) => Some(m),
7147 Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
7148 Err(e) => return Err(Error::Io(e)),
7149 };
7150
7151 if unmerged_only_paths.contains(path) {
7152 if let (Some(te), Some(meta)) = (tree_entry, wt_meta.as_ref()) {
7153 let file_attrs = file_attrs_for(path);
7154 let wt_oid = hash_worktree_file(
7155 odb,
7156 &file_path,
7157 meta,
7158 &conv,
7159 &file_attrs,
7160 path,
7161 None,
7162 filter_fp,
7163 )?;
7164 let wt_mode =
7165 mode_policy.index_mode_for_worktree(mode_from_metadata(meta), Some(te.mode));
7166 if wt_oid != te.oid || wt_mode != te.mode {
7167 result.push(DiffEntry {
7168 status: DiffStatus::for_content_change(te.mode, wt_mode),
7169 old_path: Some(path.clone()),
7170 new_path: Some(path.clone()),
7171 old_mode: FileMode::new(te.mode),
7172 new_mode: FileMode::new(wt_mode),
7173 old_oid: te.oid,
7174 new_oid: wt_oid,
7175 score: None,
7176 });
7177 }
7178 }
7179 continue;
7180 }
7181
7182 match (tree_entry, wt_meta) {
7183 (Some(te), Some(ref meta)) => {
7184 let Some(ie) = index_entries.get(path.as_bytes()) else {
7185 continue;
7186 };
7187 let wt_mode =
7188 mode_policy.index_mode_for_worktree(mode_from_metadata(meta), Some(ie.mode));
7189
7190 let index_matches_tree = ie.oid == te.oid && ie.mode == te.mode;
7191
7192 let stat_trusted = stat_matches_with_policy(ie, meta, stat_policy)
7194 && !entry_is_racy_with_policy(ie, index_mtime, stat_policy);
7195 if index_matches_tree && stat_trusted && wt_mode == te.mode {
7196 continue;
7197 }
7198
7199 let file_attrs = file_attrs_for(path);
7200 let idx_ent = index_entries.get(path.as_bytes()).copied();
7201
7202 if ie.oid == te.oid && ie.mode != te.mode && stat_trusted && wt_mode == ie.mode {
7207 result.push(DiffEntry {
7208 status: DiffStatus::for_content_change(te.mode, ie.mode),
7209 old_path: Some(path.clone()),
7210 new_path: Some(path.clone()),
7211 old_mode: FileMode::new(te.mode),
7212 new_mode: FileMode::new(ie.mode),
7213 old_oid: te.oid,
7214 new_oid: te.oid,
7215 score: None,
7216 });
7217 continue;
7218 }
7219
7220 if index_matches_tree {
7223 let wt_oid = hash_worktree_file(
7224 odb,
7225 &file_path,
7226 meta,
7227 &conv,
7228 &file_attrs,
7229 path,
7230 None,
7231 filter_fp,
7232 )?;
7233 if wt_oid != te.oid {
7234 result.push(DiffEntry {
7235 status: DiffStatus::for_content_change(te.mode, wt_mode),
7236 old_path: Some(path.clone()),
7237 new_path: Some(path.clone()),
7238 old_mode: FileMode::new(te.mode),
7239 new_mode: FileMode::new(wt_mode),
7240 old_oid: te.oid,
7241 new_oid: zero_oid(),
7244 score: None,
7245 });
7246 } else if wt_mode != te.mode {
7247 result.push(DiffEntry {
7248 status: DiffStatus::for_content_change(te.mode, wt_mode),
7249 old_path: Some(path.clone()),
7250 new_path: Some(path.clone()),
7251 old_mode: FileMode::new(te.mode),
7252 new_mode: FileMode::new(wt_mode),
7253 old_oid: te.oid,
7254 new_oid: te.oid,
7255 score: None,
7256 });
7257 }
7258 continue;
7259 }
7260
7261 let wt_oid = hash_worktree_file(
7263 odb,
7264 &file_path,
7265 meta,
7266 &conv,
7267 &file_attrs,
7268 path,
7269 idx_ent,
7270 filter_fp,
7271 )?;
7272 if wt_oid != te.oid || wt_mode != te.mode {
7273 let new_oid = if wt_oid != te.oid { zero_oid() } else { te.oid };
7274 result.push(DiffEntry {
7275 status: DiffStatus::for_content_change(te.mode, wt_mode),
7276 old_path: Some(path.clone()),
7277 new_path: Some(path.clone()),
7278 old_mode: FileMode::new(te.mode),
7279 new_mode: FileMode::new(wt_mode),
7280 old_oid: te.oid,
7281 new_oid,
7282 score: None,
7283 });
7284 }
7285 }
7286 (Some(te), None) => {
7287 result.push(DiffEntry {
7289 status: DiffStatus::Deleted,
7290 old_path: Some(path.clone()),
7291 new_path: None,
7292 old_mode: FileMode::new(te.mode),
7293 new_mode: FileMode::ABSENT,
7294 old_oid: te.oid,
7295 new_oid: zero_oid(),
7296 score: None,
7297 });
7298 }
7299 (None, Some(ref meta)) => {
7300 let file_attrs = file_attrs_for(path);
7302 let idx_ent = index_entries.get(path.as_bytes()).copied();
7303 let wt_oid = materialize_worktree_blob(
7304 odb,
7305 &file_path,
7306 meta,
7307 &conv,
7308 &file_attrs,
7309 path,
7310 idx_ent,
7311 filter_fp,
7312 )?;
7313 let wt_mode = mode_from_metadata(meta);
7314 result.push(DiffEntry {
7315 status: DiffStatus::Added,
7316 old_path: None,
7317 new_path: Some(path.clone()),
7318 old_mode: FileMode::ABSENT,
7319 new_mode: FileMode::new(wt_mode),
7320 old_oid: zero_oid(),
7321 new_oid: wt_oid,
7322 score: None,
7323 });
7324 }
7325 (None, None) => {
7326 }
7328 }
7329 }
7330
7331 sort_entries_by_path(&mut result);
7332 Ok(result)
7333}
7334
7335fn read_added_entry_bytes(
7338 odb: &Odb,
7339 entry: &DiffEntry,
7340 work_root: Option<&Path>,
7341) -> Option<Vec<u8>> {
7342 if entry.new_oid != zero_oid() {
7343 if let Ok(obj) = odb.read(&entry.new_oid) {
7344 return Some(obj.data);
7345 }
7346 }
7347 let path = entry.new_path.as_deref()?;
7348 let root = work_root?;
7349 fs::read(root.join(path)).ok()
7350}
7351
7352fn modified_as_copy_from_sources(
7353 odb: &Odb,
7354 work_root: Option<&Path>,
7355 e: &DiffEntry,
7356 threshold: u32,
7357 sources: &[(String, ObjectId, bool)],
7358 source_contents: &[Option<Vec<u8>>],
7359 source_tree_entries: &[(String, FileMode, ObjectId)],
7360) -> Result<Option<DiffEntry>> {
7361 fn regular_file_mode(mode: FileMode) -> bool {
7362 mode == FileMode::REGULAR || mode == FileMode::EXECUTABLE
7363 }
7364
7365 if e.status != DiffStatus::Modified || !regular_file_mode(e.new_mode) {
7366 return Ok(None);
7367 }
7368 let Some(new_data) = read_added_entry_bytes(odb, e, work_root) else {
7369 return Ok(None);
7370 };
7371 let new_oid_eff = if e.new_oid != zero_oid() {
7372 e.new_oid
7373 } else {
7374 odb.hash(ObjectKind::Blob, &new_data)?
7375 };
7376
7377 let mut best: Option<(usize, u32)> = None;
7378 for (si, (src_path, src_oid, is_deleted)) in sources.iter().enumerate() {
7379 if *is_deleted {
7380 continue;
7381 }
7382 if e.new_path.as_deref() == Some(src_path.as_str()) {
7383 continue;
7384 }
7385 let src_mode = source_tree_entries
7386 .iter()
7387 .find(|(p, _, _)| p == src_path)
7388 .map_or(FileMode::REGULAR, |(_, m, _)| *m);
7389 if !regular_file_mode(src_mode) {
7390 continue;
7391 }
7392
7393 let score = if *src_oid == new_oid_eff {
7394 100
7395 } else {
7396 match (&source_contents[si], Some(new_data.as_slice())) {
7397 (Some(old_data), Some(nd)) => compute_similarity(old_data, nd),
7398 _ => 0,
7399 }
7400 };
7401 if score >= threshold {
7402 let replace = match best {
7403 None => true,
7404 Some((_, s)) => score > s,
7405 };
7406 if replace {
7407 best = Some((si, score));
7408 }
7409 }
7410 }
7411
7412 let Some((si, score)) = best else {
7413 return Ok(None);
7414 };
7415 let (src_path, src_oid, _) = &sources[si];
7416 let src_mode = source_tree_entries
7417 .iter()
7418 .find(|(p, _, _)| p == src_path)
7419 .map_or(e.old_mode, |(_, m, _)| *m);
7420
7421 Ok(Some(DiffEntry {
7422 status: DiffStatus::Copied,
7423 old_path: Some(src_path.clone()),
7424 new_path: e.new_path.clone(),
7425 old_mode: src_mode,
7426 new_mode: e.new_mode,
7427 old_oid: *src_oid,
7428 new_oid: e.new_oid,
7429 score: Some(score),
7430 }))
7431}
7432
7433#[allow(clippy::expect_used)]
7445pub fn detect_renames(
7446 odb: &Odb,
7447 work_root: Option<&Path>,
7448 entries: Vec<DiffEntry>,
7449 threshold: u32,
7450) -> Vec<DiffEntry> {
7451 let config = ConfigSet::new();
7452 let policy = ThreadPolicy::default();
7453 detect_renames_with_policy(odb, work_root, entries, threshold, &config, &policy, None)
7454 .expect("rename detection")
7455}
7456
7457#[inline]
7458fn poll_rename_cancel(
7459 cancel_watch: Option<&std::sync::atomic::AtomicBool>,
7460) -> crate::error::Result<()> {
7461 if let Some(watch) = cancel_watch {
7462 crate::progress::poll_watch(watch)?;
7463 }
7464 Ok(())
7465}
7466
7467pub fn detect_renames_with_policy(
7478 odb: &Odb,
7479 work_root: Option<&Path>,
7480 entries: Vec<DiffEntry>,
7481 threshold: u32,
7482 config: &ConfigSet,
7483 thread_policy: &ThreadPolicy,
7484 cancel_watch: Option<&std::sync::atomic::AtomicBool>,
7485) -> Result<Vec<DiffEntry>> {
7486 poll_rename_cancel(cancel_watch)?;
7487 let has_deleted = entries.iter().any(|e| e.status == DiffStatus::Deleted);
7488 let has_added = entries.iter().any(|e| e.status == DiffStatus::Added);
7489 if (!has_deleted || !has_added) && entries_strictly_sorted_by_path(&entries) {
7490 return Ok(entries);
7492 }
7493 let mut deleted: Vec<DiffEntry> = Vec::new();
7495 let mut added: Vec<DiffEntry> = Vec::new();
7496 let mut others: Vec<DiffEntry> = Vec::new();
7497
7498 for entry in entries {
7499 match entry.status {
7500 DiffStatus::Deleted => deleted.push(entry),
7501 DiffStatus::Added => added.push(entry),
7502 _ => others.push(entry),
7503 }
7504 }
7505
7506 if deleted.is_empty() || added.is_empty() {
7507 let mut result = others;
7509 result.extend(deleted);
7510 result.extend(added);
7511 sort_entries_by_path(&mut result);
7512 return Ok(result);
7513 }
7514
7515 fn is_regularish_mode(mode: FileMode) -> bool {
7516 mode == FileMode::REGULAR || mode == FileMode::EXECUTABLE
7517 }
7518
7519 fn same_path_same_blob(del: &DiffEntry, add: &DiffEntry) -> bool {
7520 del.old_path == add.new_path && del.old_oid == add.new_oid && del.old_mode == add.new_mode
7521 }
7522
7523 fn sort_rename_scores(
7524 scores: &mut [(u32, usize, usize)],
7525 deleted: &[DiffEntry],
7526 added: &[DiffEntry],
7527 ) {
7528 scores.sort_by(|a, b| {
7529 let a_noop = same_path_same_blob(&deleted[a.1], &added[a.2]);
7530 let b_noop = same_path_same_blob(&deleted[b.1], &added[b.2]);
7531 let a_same = same_basename(&deleted[a.1], &added[a.2]);
7532 let b_same = same_basename(&deleted[b.1], &added[b.2]);
7533 a_noop
7534 .cmp(&b_noop)
7535 .then_with(|| b_same.cmp(&a_same))
7536 .then_with(|| b.0.cmp(&a.0))
7537 });
7538 }
7539
7540 fn apply_rename_scores(
7541 scores: &[(u32, usize, usize)],
7542 deleted: &[DiffEntry],
7543 added: &[DiffEntry],
7544 used_deleted: &mut [bool],
7545 used_added: &mut [bool],
7546 renames: &mut Vec<DiffEntry>,
7547 ) {
7548 for (score, di, ai) in scores {
7549 if used_deleted[*di] || used_added[*ai] {
7550 continue;
7551 }
7552 used_deleted[*di] = true;
7553 used_added[*ai] = true;
7554
7555 let del = &deleted[*di];
7556 let add = &added[*ai];
7557 if same_path_same_blob(del, add) {
7558 continue;
7559 }
7560
7561 renames.push(DiffEntry {
7562 status: DiffStatus::Renamed,
7563 old_path: del.old_path.clone(),
7564 new_path: add.new_path.clone(),
7565 old_mode: del.old_mode,
7566 new_mode: add.new_mode,
7567 old_oid: del.old_oid,
7568 new_oid: add.new_oid,
7569 score: Some(*score),
7570 });
7571 }
7572 }
7573
7574 let mut used_deleted = vec![false; deleted.len()];
7575 let mut used_added = vec![false; added.len()];
7576 let mut renames: Vec<DiffEntry> = Vec::new();
7577
7578 let mut exact_scores: Vec<(u32, usize, usize)> = Vec::new();
7580 for (di, del) in deleted.iter().enumerate() {
7581 poll_rename_cancel(cancel_watch)?;
7582 for (ai, add) in added.iter().enumerate() {
7583 if del.old_oid == add.new_oid {
7584 exact_scores.push((100, di, ai));
7585 }
7586 }
7587 }
7588 sort_rename_scores(&mut exact_scores, &deleted, &added);
7589 apply_rename_scores(
7590 &exact_scores,
7591 &deleted,
7592 &added,
7593 &mut used_deleted,
7594 &mut used_added,
7595 &mut renames,
7596 );
7597
7598 let remaining_deleted: Vec<usize> = used_deleted
7599 .iter()
7600 .enumerate()
7601 .filter_map(|(i, used)| (!*used).then_some(i))
7602 .collect();
7603 let remaining_added: Vec<usize> = used_added
7604 .iter()
7605 .enumerate()
7606 .filter_map(|(i, used)| (!*used).then_some(i))
7607 .collect();
7608
7609 let rename_limit = parse_rename_limit(config);
7610 if !remaining_deleted.is_empty()
7611 && !remaining_added.is_empty()
7612 && !inexact_matrix_exceeds_limit(
7613 remaining_added.len(),
7614 remaining_deleted.len(),
7615 rename_limit,
7616 )
7617 {
7618 let par_opts = diff_rename_parallel_options(config, thread_policy);
7619
7620 let deleted_sizes: Vec<Option<usize>> = remaining_deleted
7621 .iter()
7622 .map(|&di| blob_payload_size(odb, &deleted[di].old_oid))
7623 .collect();
7624 let added_sizes: Vec<Option<usize>> = remaining_added
7625 .iter()
7626 .map(|&ai| added_entry_payload_size(odb, &added[ai], work_root))
7627 .collect();
7628
7629 let mut load_deleted = vec![false; remaining_deleted.len()];
7630 let mut load_added = vec![false; remaining_added.len()];
7631 let mut candidate_pairs: Vec<(usize, usize)> = Vec::new();
7632 for (row, &di) in remaining_deleted.iter().enumerate() {
7633 poll_rename_cancel(cancel_watch)?;
7634 let del = &deleted[di];
7635 if !is_regularish_mode(del.old_mode) {
7636 continue;
7637 }
7638 let Some(ds) = deleted_sizes[row] else {
7639 continue;
7640 };
7641 for (col, &ai) in remaining_added.iter().enumerate() {
7642 let add = &added[ai];
7643 if !is_regularish_mode(add.new_mode) {
7644 continue;
7645 }
7646 let Some(asz) = added_sizes[col] else {
7647 continue;
7648 };
7649 if rename_pair_size_reachable(ds, asz, threshold) {
7650 load_deleted[row] = true;
7651 load_added[col] = true;
7652 candidate_pairs.push((row, col));
7653 }
7654 }
7655 }
7656
7657 if !candidate_pairs.is_empty() {
7658 let loads_needed = load_deleted.iter().filter(|&&b| b).count()
7659 + load_added.iter().filter(|&&b| b).count();
7660 let load_deleted_blob = |i: usize| -> Option<Vec<u8>> {
7661 if !load_deleted[i] {
7662 return None;
7663 }
7664 odb.read(&deleted[remaining_deleted[i]].old_oid)
7665 .ok()
7666 .map(|obj| normalize_bytes_for_rename(obj.data))
7667 };
7668 let load_added_blob = |i: usize| -> Option<Vec<u8>> {
7669 if !load_added[i] {
7670 return None;
7671 }
7672 read_added_entry_bytes(odb, &added[remaining_added[i]], work_root)
7673 .map(normalize_bytes_for_rename)
7674 };
7675 let (deleted_contents, added_contents) = if loads_needed <= 32 {
7676 (
7677 (0..remaining_deleted.len())
7678 .map(load_deleted_blob)
7679 .collect(),
7680 (0..remaining_added.len()).map(load_added_blob).collect(),
7681 )
7682 } else {
7683 (
7684 parallel_load_optional_blobs(
7685 odb,
7686 remaining_deleted.len(),
7687 par_opts,
7688 0,
7689 load_deleted_blob,
7690 ),
7691 parallel_load_optional_blobs(
7692 odb,
7693 remaining_added.len(),
7694 par_opts,
7695 0,
7696 load_added_blob,
7697 ),
7698 )
7699 };
7700
7701 let est_table_bytes: usize = deleted_contents
7702 .iter()
7703 .chain(added_contents.iter())
7704 .filter_map(|opt| opt.as_ref().map(|b| b.len()))
7705 .sum();
7706 let build_deleted_table = |row: usize| -> std::result::Result<
7707 Option<RenameLineTable>,
7708 std::convert::Infallible,
7709 > {
7710 Ok(deleted_contents[row]
7711 .as_ref()
7712 .map(|d| RenameLineTable::from_normalized_blob(d)))
7713 };
7714 let build_added_table = |col: usize| -> std::result::Result<
7715 Option<RenameLineTable>,
7716 std::convert::Infallible,
7717 > {
7718 Ok(added_contents[col]
7719 .as_ref()
7720 .map(|d| RenameLineTable::from_normalized_blob(d)))
7721 };
7722 let deleted_tables: Vec<Option<RenameLineTable>> = if loads_needed <= 32 {
7723 (0..remaining_deleted.len())
7724 .map(|row| build_deleted_table(row).unwrap())
7725 .collect()
7726 } else {
7727 parallel_map_indices(
7728 odb,
7729 remaining_deleted.len(),
7730 par_opts,
7731 est_table_bytes,
7732 build_deleted_table,
7733 )
7734 .expect("deleted rename line tables")
7735 };
7736 let added_tables: Vec<Option<RenameLineTable>> = if loads_needed <= 32 {
7737 (0..remaining_added.len())
7738 .map(|col| build_added_table(col).unwrap())
7739 .collect()
7740 } else {
7741 parallel_map_indices(
7742 odb,
7743 remaining_added.len(),
7744 par_opts,
7745 est_table_bytes,
7746 build_added_table,
7747 )
7748 .expect("added rename line tables")
7749 };
7750
7751 let mut inexact_scores: Vec<(u32, usize, usize)> = Vec::new();
7752 for (row, col) in candidate_pairs {
7753 poll_rename_cancel(cancel_watch)?;
7754 let di = remaining_deleted[row];
7755 let ai = remaining_added[col];
7756 let (Some(old_table), Some(new_table)) = (&deleted_tables[row], &added_tables[col])
7757 else {
7758 continue;
7759 };
7760 let score = RenameLineTable::similarity_score(old_table, new_table);
7761 if score >= threshold {
7762 inexact_scores.push((score, di, ai));
7763 }
7764 }
7765 sort_rename_scores(&mut inexact_scores, &deleted, &added);
7766 apply_rename_scores(
7767 &inexact_scores,
7768 &deleted,
7769 &added,
7770 &mut used_deleted,
7771 &mut used_added,
7772 &mut renames,
7773 );
7774 }
7775 }
7776
7777 let mut result = others;
7779 result.extend(renames);
7780 for (i, entry) in deleted.into_iter().enumerate() {
7781 if !used_deleted[i] {
7782 result.push(entry);
7783 }
7784 }
7785 for (i, entry) in added.into_iter().enumerate() {
7786 if !used_added[i] {
7787 result.push(entry);
7788 }
7789 }
7790
7791 result = prune_break_deletions_obviated_by_renames(result);
7792 sort_entries_by_path(&mut result);
7793 Ok(result)
7794}
7795
7796pub fn detect_copies(
7811 odb: &Odb,
7812 work_root: Option<&Path>,
7813 entries: Vec<DiffEntry>,
7814 threshold: u32,
7815 find_copies_harder: bool,
7816 source_tree_entries: &[(String, FileMode, ObjectId)],
7817) -> Result<Vec<DiffEntry>> {
7818 let config = ConfigSet::new();
7819 let policy = ThreadPolicy::default();
7820 detect_copies_with_policy(
7821 odb,
7822 work_root,
7823 entries,
7824 threshold,
7825 find_copies_harder,
7826 source_tree_entries,
7827 &config,
7828 &policy,
7829 )
7830}
7831
7832#[expect(clippy::too_many_arguments)]
7838pub fn detect_copies_with_policy(
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, FileMode, ObjectId)],
7845 config: &ConfigSet,
7846 thread_policy: &ThreadPolicy,
7847) -> Result<Vec<DiffEntry>> {
7848 use std::collections::{HashMap, HashSet};
7849
7850 let mut deleted: Vec<DiffEntry> = Vec::new();
7852 let mut added: Vec<DiffEntry> = Vec::new();
7853 let mut others: Vec<DiffEntry> = Vec::new();
7854
7855 for entry in entries {
7856 match entry.status {
7857 DiffStatus::Deleted => deleted.push(entry),
7858 DiffStatus::Added => added.push(entry),
7859 _ => others.push(entry),
7860 }
7861 }
7862
7863 let mut sources: Vec<(String, ObjectId, bool)> = Vec::new(); let mut deleted_source_idx: HashMap<String, usize> = HashMap::new();
7867
7868 for entry in &deleted {
7869 if let Some(ref path) = entry.old_path {
7870 deleted_source_idx.insert(path.clone(), sources.len());
7871 sources.push((path.clone(), entry.old_oid, true));
7872 }
7873 }
7874
7875 for entry in &others {
7878 if matches!(entry.status, DiffStatus::Modified | DiffStatus::TypeChanged) {
7879 if let Some(ref old_path) = entry.old_path {
7880 if !sources.iter().any(|(p, _, _)| p == old_path) {
7881 sources.push((old_path.clone(), entry.old_oid, false));
7882 }
7883 }
7884 }
7885 }
7886
7887 if find_copies_harder {
7889 for (path, _mode, oid) in source_tree_entries {
7890 if !sources.iter().any(|(p, _, _)| p == path) {
7891 sources.push((path.clone(), *oid, false));
7892 }
7893 }
7894 }
7895
7896 let mut source_modes: Vec<(String, FileMode, ObjectId)> = source_tree_entries.to_vec();
7899 for entry in deleted.iter().chain(&others) {
7900 if let Some(ref path) = entry.old_path {
7901 if !source_modes.iter().any(|(p, _, _)| p == path) {
7902 source_modes.push((path.clone(), entry.old_mode, entry.old_oid));
7903 }
7904 }
7905 }
7906
7907 if sources.is_empty() {
7908 let mut result = others;
7909 result.extend(deleted);
7910 result.extend(added);
7911 sort_entries_by_path(&mut result);
7912 return Ok(result);
7913 }
7914
7915 let par_opts = diff_rename_parallel_options(config, thread_policy);
7916 let rename_limit = parse_rename_limit(config);
7917
7918 fn apply_copy_score_assignments(
7919 scores: &[(u32, usize, usize)],
7920 sources: &[(String, ObjectId, bool)],
7921 added: &[DiffEntry],
7922 source_tree_entries: &[(String, FileMode, ObjectId)],
7923 used_added2: &mut [bool],
7924 renamed_deleted: &mut HashSet<usize>,
7925 result_entries: &mut Vec<DiffEntry>,
7926 ) {
7927 let mut used_added = vec![false; added.len()];
7928 let mut source_to_added: HashMap<usize, Vec<(usize, u32)>> = HashMap::new();
7929 for &(score, si, ai) in scores {
7930 if used_added[ai] {
7931 continue;
7932 }
7933 used_added[ai] = true;
7934 source_to_added.entry(si).or_default().push((ai, score));
7935 }
7936
7937 for (&si, assignments_for_src) in &source_to_added {
7938 let (_, _, is_deleted) = &sources[si];
7939 if *is_deleted && !assignments_for_src.is_empty() {
7940 let rename_ai = assignments_for_src
7941 .iter()
7942 .max_by_key(|(ai, _score)| added[*ai].path().to_string())
7943 .map(|(ai, _)| *ai);
7944
7945 for &(ai, score) in assignments_for_src {
7946 let (ref src_path, _, _) = sources[si];
7947 let add = &added[ai];
7948 let src_mode = source_tree_entries
7949 .iter()
7950 .find(|(p, _, _)| p == src_path)
7951 .map_or(add.old_mode, |(_, m, _)| *m);
7952
7953 let is_rename = Some(ai) == rename_ai;
7954 result_entries.push(DiffEntry {
7955 status: if is_rename {
7956 DiffStatus::Renamed
7957 } else {
7958 DiffStatus::Copied
7959 },
7960 old_path: Some(src_path.clone()),
7961 new_path: add.new_path.clone(),
7962 old_mode: src_mode,
7963 new_mode: add.new_mode,
7964 old_oid: sources[si].1,
7965 new_oid: add.new_oid,
7966 score: Some(score),
7967 });
7968 used_added2[ai] = true;
7969 }
7970 renamed_deleted.insert(si);
7971 } else {
7972 for &(ai, score) in assignments_for_src {
7973 let (ref src_path, _, _) = sources[si];
7974 let add = &added[ai];
7975 let src_mode = source_tree_entries
7976 .iter()
7977 .find(|(p, _, _)| p == src_path)
7978 .map_or(add.old_mode, |(_, m, _)| *m);
7979
7980 result_entries.push(DiffEntry {
7981 status: DiffStatus::Copied,
7982 old_path: Some(src_path.clone()),
7983 new_path: add.new_path.clone(),
7984 old_mode: src_mode,
7985 new_mode: add.new_mode,
7986 old_oid: sources[si].1,
7987 new_oid: add.new_oid,
7988 score: Some(score),
7989 });
7990 used_added2[ai] = true;
7991 }
7992 }
7993 }
7994 }
7995
7996 fn added_entry_oid(
7997 odb: &Odb,
7998 add: &DiffEntry,
7999 added_contents: &[Option<Vec<u8>>],
8000 ai: usize,
8001 ) -> Result<ObjectId> {
8002 if add.new_oid != zero_oid() {
8003 Ok(add.new_oid)
8004 } else if let Some(ref data) = added_contents[ai] {
8005 odb.hash(ObjectKind::Blob, data)
8006 } else {
8007 Ok(zero_oid())
8008 }
8009 }
8010
8011 let mut result_entries: Vec<DiffEntry> = Vec::new();
8012 let mut renamed_deleted: HashSet<usize> = HashSet::new();
8013 let mut used_added2 = vec![false; added.len()];
8014 let mut source_contents = vec![None; sources.len()];
8015
8016 if !added.is_empty() {
8017 let added_contents = parallel_load_optional_blobs(odb, added.len(), par_opts, 0, |ai| {
8018 read_added_entry_bytes(odb, &added[ai], work_root)
8019 });
8020
8021 let mut exact_scores: Vec<(u32, usize, usize)> = Vec::new();
8023 for (si, (src_path, src_oid, _)) in sources.iter().enumerate() {
8024 for (ai, add) in added.iter().enumerate() {
8025 if add.new_path.as_deref() == Some(src_path.as_str()) {
8026 continue;
8027 }
8028 let add_oid = added_entry_oid(odb, add, &added_contents, ai)?;
8029 if *src_oid == add_oid && add_oid != zero_oid() {
8030 exact_scores.push((100, si, ai));
8031 }
8032 }
8033 }
8034 exact_scores.sort_by_key(|a| std::cmp::Reverse(a.0));
8035 apply_copy_score_assignments(
8036 &exact_scores,
8037 &sources,
8038 &added,
8039 &source_modes,
8040 &mut used_added2,
8041 &mut renamed_deleted,
8042 &mut result_entries,
8043 );
8044
8045 let remaining_added: Vec<usize> = used_added2
8046 .iter()
8047 .enumerate()
8048 .filter_map(|(i, used)| (!*used).then_some(i))
8049 .collect();
8050 let remaining_source_count = sources
8051 .iter()
8052 .enumerate()
8053 .filter(|(si, (_, _, is_deleted))| !*is_deleted || !renamed_deleted.contains(si))
8054 .count();
8055
8056 let skip_inexact = remaining_added.is_empty()
8057 || inexact_matrix_exceeds_limit(
8058 remaining_added.len(),
8059 remaining_source_count,
8060 rename_limit,
8061 );
8062
8063 if !skip_inexact {
8064 source_contents = parallel_load_optional_blobs(odb, sources.len(), par_opts, 0, |si| {
8065 odb.read(&sources[si].1).ok().map(|obj| obj.data)
8066 });
8067
8068 let est_bytes: usize = source_contents
8069 .iter()
8070 .chain(added_contents.iter())
8071 .filter_map(|opt| opt.as_ref().map(|b| b.len()))
8072 .sum();
8073
8074 let source_tables: Vec<Option<RenameLineTable>> = parallel_map_indices(
8075 odb,
8076 sources.len(),
8077 par_opts,
8078 est_bytes,
8079 |si| -> std::result::Result<Option<RenameLineTable>, std::convert::Infallible> {
8080 Ok(source_contents[si]
8081 .as_ref()
8082 .map(|d| RenameLineTable::from_similarity_blob(d)))
8083 },
8084 )
8085 .expect("copy source line tables");
8086 let added_tables: Vec<Option<RenameLineTable>> = parallel_map_indices(
8087 odb,
8088 added.len(),
8089 par_opts,
8090 est_bytes,
8091 |ai| -> std::result::Result<Option<RenameLineTable>, std::convert::Infallible> {
8092 Ok(added_contents[ai]
8093 .as_ref()
8094 .map(|d| RenameLineTable::from_similarity_blob(d)))
8095 },
8096 )
8097 .expect("copy added line tables");
8098
8099 let score_rows: Vec<Vec<(u32, usize, usize)>> = parallel_map_indices(
8100 odb,
8101 sources.len(),
8102 par_opts,
8103 est_bytes,
8104 |si| -> std::result::Result<Vec<(u32, usize, usize)>, std::convert::Infallible> {
8105 if renamed_deleted.contains(&si) {
8106 return Ok(Vec::new());
8107 }
8108 let (src_path, _, _) = &sources[si];
8109 let mut row: Vec<(u32, usize, usize)> = Vec::new();
8110 for &ai in &remaining_added {
8111 let add = &added[ai];
8112 if add.new_path.as_deref() == Some(src_path.as_str()) {
8113 continue;
8114 }
8115 let score = match (&source_tables[si], &added_tables[ai]) {
8116 (Some(old_table), Some(new_table)) => {
8117 RenameLineTable::similarity_score(old_table, new_table)
8118 }
8119 _ => 0,
8120 };
8121 if score >= threshold {
8122 row.push((score, si, ai));
8123 }
8124 }
8125 Ok(row)
8126 },
8127 )
8128 .expect("copy score rows");
8129 let mut inexact_scores: Vec<(u32, usize, usize)> =
8130 score_rows.into_iter().flatten().collect();
8131 inexact_scores.sort_by_key(|a| std::cmp::Reverse(a.0));
8132 apply_copy_score_assignments(
8133 &inexact_scores,
8134 &sources,
8135 &added,
8136 &source_modes,
8137 &mut used_added2,
8138 &mut renamed_deleted,
8139 &mut result_entries,
8140 );
8141 }
8142 }
8143
8144 for entry in deleted.into_iter() {
8146 if let Some(ref path) = entry.old_path {
8147 if let Some(&si) = deleted_source_idx.get(path) {
8148 if renamed_deleted.contains(&si) {
8149 continue;
8151 }
8152 }
8153 }
8154 result_entries.push(entry);
8155 }
8156
8157 let mut result = others;
8158 result.extend(result_entries);
8159 for (i, entry) in added.into_iter().enumerate() {
8161 if !used_added2[i] {
8162 result.push(entry);
8163 }
8164 }
8165
8166 let mut final_result = Vec::with_capacity(result.len());
8167 for e in result {
8168 if let Some(c) = modified_as_copy_from_sources(
8169 odb,
8170 work_root,
8171 &e,
8172 threshold,
8173 &sources,
8174 &source_contents,
8175 &source_modes,
8176 )? {
8177 final_result.push(c);
8178 } else {
8179 final_result.push(e);
8180 }
8181 }
8182
8183 sort_entries_by_path(&mut final_result);
8184 Ok(final_result)
8185}
8186
8187#[derive(Debug, Clone, Copy)]
8189pub struct DiffPostprocessOptions {
8190 pub break_score: Option<u64>,
8193 pub rename_threshold: u32,
8195 pub copy_threshold: Option<u32>,
8197 pub find_copies_harder: bool,
8199 pub materialize_break_worktree_new_oid: bool,
8202}
8203
8204impl Default for DiffPostprocessOptions {
8205 fn default() -> Self {
8206 Self {
8207 break_score: None,
8208 rename_threshold: 100,
8209 copy_threshold: None,
8210 find_copies_harder: false,
8211 materialize_break_worktree_new_oid: false,
8212 }
8213 }
8214}
8215
8216#[derive(Debug, Clone, Copy)]
8219pub struct DiffPostprocessWorktree<'a> {
8220 pub repository_git_dir: &'a Path,
8222 pub index: &'a Index,
8224}
8225
8226fn blob_bytes(odb: &Odb, oid: &ObjectId) -> Result<Vec<u8>> {
8227 Ok(odb.read(oid)?.data)
8228}
8229
8230fn read_new_side_bytes(
8235 odb: &Odb,
8236 entry: &DiffEntry,
8237 work_root: Option<&Path>,
8238 worktree: Option<&DiffPostprocessWorktree<'_>>,
8239 config: &ConfigSet,
8240) -> Result<Vec<u8>> {
8241 if entry.new_oid != zero_oid() {
8242 return blob_bytes(odb, &entry.new_oid);
8243 }
8244 let path = entry.new_path.as_deref().ok_or_else(|| {
8245 Error::ObjectNotFound("modified entry missing new_path for worktree read".into())
8246 })?;
8247 let root = work_root.ok_or_else(|| {
8248 Error::ObjectNotFound(format!(
8249 "worktree bytes required for {} but no work_root was provided",
8250 path
8251 ))
8252 })?;
8253 if let Some(wctx) = worktree {
8254 let mode = entry.new_mode.bits();
8255 let index_entry = wctx.index.get(path.as_bytes(), 0);
8256 return read_worktree_side_bytes_for_diff(odb, root, path, mode, config, index_entry, None);
8257 }
8258 fs::read(root.join(path)).map_err(Error::Io)
8259}
8260
8261fn worktree_side_differs_after_normalize(
8263 odb: &Odb,
8264 entry: &DiffEntry,
8265 work_root: &Path,
8266 worktree: &DiffPostprocessWorktree<'_>,
8267 config: &ConfigSet,
8268) -> Result<bool> {
8269 let path = entry.new_path.as_deref().ok_or_else(|| {
8270 Error::ObjectNotFound("modified entry missing new_path for worktree read".into())
8271 })?;
8272 let raw = fs::read(work_root.join(path)).map_err(Error::Io)?;
8273 let canonical = read_new_side_bytes(odb, entry, Some(work_root), Some(worktree), config)?;
8274 Ok(raw != canonical)
8275}
8276
8277fn split_broken_modifications(
8278 odb: &Odb,
8279 work_root: Option<&Path>,
8280 entries: Vec<DiffEntry>,
8281 break_score: u64,
8282 worktree: Option<&DiffPostprocessWorktree<'_>>,
8283 config: &ConfigSet,
8284) -> Result<Vec<DiffEntry>> {
8285 let mut out = Vec::with_capacity(entries.len());
8286 for e in entries {
8287 if e.status != DiffStatus::Modified {
8288 out.push(e);
8289 continue;
8290 }
8291 let old = blob_bytes(odb, &e.old_oid)?;
8292 let new = read_new_side_bytes(odb, &e, work_root, worktree, config)?;
8293 if should_break_rewrite_pair(&old, &new, break_score) {
8294 let path = e.path().to_owned();
8295 out.push(DiffEntry {
8296 status: DiffStatus::Deleted,
8297 old_path: Some(path.clone()),
8298 new_path: None,
8299 old_mode: e.old_mode,
8300 new_mode: FileMode::ABSENT,
8301 old_oid: e.old_oid,
8302 new_oid: zero_oid(),
8303 score: None,
8304 });
8305 out.push(DiffEntry {
8306 status: DiffStatus::Added,
8307 old_path: None,
8308 new_path: Some(path),
8309 old_mode: FileMode::ABSENT,
8310 new_mode: e.new_mode,
8311 old_oid: zero_oid(),
8312 new_oid: e.new_oid,
8313 score: None,
8314 });
8315 } else {
8316 out.push(e);
8317 }
8318 }
8319 Ok(out)
8320}
8321
8322fn annotate_broken_modifications(
8323 odb: &Odb,
8324 work_root: Option<&Path>,
8325 entries: Vec<DiffEntry>,
8326 break_score: u64,
8327 worktree: Option<&DiffPostprocessWorktree<'_>>,
8328 config: &ConfigSet,
8329 materialize_worktree_oid: bool,
8330) -> Result<Vec<DiffEntry>> {
8331 let mut out = Vec::with_capacity(entries.len());
8332 for mut e in entries {
8333 if e.status == DiffStatus::Modified {
8334 let old = blob_bytes(odb, &e.old_oid)?;
8335 let new = read_new_side_bytes(odb, &e, work_root, worktree, config)?;
8336 if should_break_rewrite_pair(&old, &new, break_score) {
8337 let show_score = if e.new_oid != zero_oid() {
8341 true
8342 } else if materialize_worktree_oid {
8343 match (work_root, worktree) {
8344 (Some(root), Some(wctx)) => {
8345 worktree_side_differs_after_normalize(odb, &e, root, wctx, config)?
8346 }
8347 _ => false,
8348 }
8349 } else {
8350 false
8351 };
8352 if show_score {
8353 e.score = rewrite_dissimilarity_index_percent(&old, &new);
8354 }
8355 }
8356 }
8357 out.push(e);
8358 }
8359 Ok(out)
8360}
8361
8362fn materialize_break_worktree_new_oids(
8363 odb: &Odb,
8364 work_root: Option<&Path>,
8365 entries: &mut [DiffEntry],
8366 worktree: Option<&DiffPostprocessWorktree<'_>>,
8367 config: &ConfigSet,
8368) -> Result<()> {
8369 for e in entries {
8370 if e.new_oid != zero_oid() {
8371 continue;
8372 }
8373 if !matches!(e.status, DiffStatus::Modified | DiffStatus::Added) {
8374 continue;
8375 }
8376 let bytes = read_new_side_bytes(odb, e, work_root, worktree, config)?;
8377 e.new_oid = odb.hash(ObjectKind::Blob, &bytes)?;
8378 }
8379 Ok(())
8380}
8381
8382fn prune_break_deletions_obviated_by_renames(entries: Vec<DiffEntry>) -> Vec<DiffEntry> {
8385 use std::collections::HashSet;
8386 let rename_targets: HashSet<String> = entries
8387 .iter()
8388 .filter(|e| e.status == DiffStatus::Renamed)
8389 .filter_map(|e| e.new_path.clone())
8390 .collect();
8391 if rename_targets.is_empty() {
8392 return entries;
8393 }
8394 entries
8395 .into_iter()
8396 .filter(|e| {
8397 if e.status != DiffStatus::Deleted {
8398 return true;
8399 }
8400 e.old_path
8401 .as_ref()
8402 .is_none_or(|p| !rename_targets.contains(p))
8403 })
8404 .collect()
8405}
8406
8407#[allow(clippy::too_many_arguments)]
8417pub fn postprocess_diff_entries(
8418 odb: &Odb,
8419 entries: Vec<DiffEntry>,
8420 source_tree_oid: Option<&ObjectId>,
8421 work_root: Option<&Path>,
8422 worktree: Option<&DiffPostprocessWorktree<'_>>,
8423 config: &ConfigSet,
8424 thread_policy: &ThreadPolicy,
8425 opts: &DiffPostprocessOptions,
8426) -> Result<Vec<DiffEntry>> {
8427 let mut entries = entries;
8428 if let Some(break_score) = opts.break_score {
8429 let rename_or_copy = opts.rename_threshold < 100 || opts.copy_threshold.is_some();
8430 entries = if rename_or_copy {
8431 split_broken_modifications(odb, work_root, entries, break_score, worktree, config)?
8432 } else {
8433 annotate_broken_modifications(
8434 odb,
8435 work_root,
8436 entries,
8437 break_score,
8438 worktree,
8439 config,
8440 opts.materialize_break_worktree_new_oid,
8441 )?
8442 };
8443 if opts.materialize_break_worktree_new_oid {
8444 materialize_break_worktree_new_oids(odb, work_root, &mut entries, worktree, config)?;
8445 }
8446 }
8447
8448 let source_tree_entries = source_tree_oid
8449 .map(|t| flatten_tree_entries(odb, t))
8450 .transpose()?
8451 .unwrap_or_default();
8452
8453 if opts.copy_threshold.is_some() || opts.find_copies_harder {
8454 let threshold = opts.copy_threshold.unwrap_or(opts.rename_threshold.min(99));
8455 detect_copies_with_policy(
8456 odb,
8457 work_root,
8458 entries,
8459 threshold,
8460 opts.find_copies_harder,
8461 &source_tree_entries,
8462 config,
8463 thread_policy,
8464 )
8465 } else if opts.rename_threshold < 100 {
8466 detect_renames_with_policy(
8467 odb,
8468 work_root,
8469 entries,
8470 opts.rename_threshold,
8471 config,
8472 thread_policy,
8473 None,
8474 )
8475 } else {
8476 Ok(entries)
8477 }
8478}
8479
8480pub fn flatten_tree_entries(
8482 odb: &Odb,
8483 tree_oid: &ObjectId,
8484) -> Result<Vec<(String, FileMode, ObjectId)>> {
8485 fn walk(
8486 odb: &Odb,
8487 tree_oid: &ObjectId,
8488 prefix: &str,
8489 out: &mut Vec<(String, FileMode, ObjectId)>,
8490 ) -> Result<()> {
8491 let obj = odb.read(tree_oid)?;
8492 let oid_len = odb.hash_algo().len();
8493 for e in TreeRefIter::with_oid_len(&obj.data, oid_len) {
8494 let e = e?;
8495 let name = String::from_utf8_lossy(e.name);
8496 let path = if prefix.is_empty() {
8497 name.to_string()
8498 } else {
8499 format!("{prefix}/{name}")
8500 };
8501 if e.mode == 0o040000 {
8502 walk(odb, &e.oid, &path, out)?;
8503 } else {
8504 out.push((path, FileMode::new(e.mode), e.oid));
8505 }
8506 }
8507 Ok(())
8508 }
8509 let mut out = Vec::new();
8510 walk(odb, tree_oid, "", &mut out)?;
8511 Ok(out)
8512}
8513
8514#[allow(clippy::too_many_arguments)]
8524pub fn status_apply_rename_copy_detection(
8525 odb: &Odb,
8526 unstaged_raw: Vec<DiffEntry>,
8527 threshold: u32,
8528 copies: bool,
8529 head_tree: Option<&ObjectId>,
8530 config: &ConfigSet,
8531 thread_policy: &ThreadPolicy,
8532 cancel_watch: Option<&std::sync::atomic::AtomicBool>,
8533) -> Result<Vec<DiffEntry>> {
8534 if !copies {
8535 return detect_renames_with_policy(
8536 odb,
8537 None,
8538 unstaged_raw,
8539 threshold,
8540 config,
8541 thread_policy,
8542 cancel_watch,
8543 );
8544 }
8545 let source_tree_entries: Vec<(String, FileMode, ObjectId)> = match head_tree {
8546 Some(oid) => flatten_tree(odb, oid, "")?
8547 .into_iter()
8548 .map(|e| (e.path, FileMode::new(e.mode), e.oid))
8549 .collect(),
8550 None => Vec::new(),
8551 };
8552 detect_copies_with_policy(
8553 odb,
8554 None,
8555 unstaged_raw,
8556 threshold,
8557 false,
8558 &source_tree_entries,
8559 config,
8560 thread_policy,
8561 )
8562}
8563
8564pub fn format_rename_path(old: &str, new: &str) -> String {
8572 let ob = old.as_bytes();
8573 let nb = new.as_bytes();
8574
8575 let pfx = {
8577 let mut last_sep = 0usize;
8578 let min_len = ob.len().min(nb.len());
8579 for i in 0..min_len {
8580 if ob[i] != nb[i] {
8581 break;
8582 }
8583 if ob[i] == b'/' {
8584 last_sep = i + 1;
8585 }
8586 }
8587 last_sep
8588 };
8589
8590 let mut sfx = {
8592 let mut last_sep = 0usize;
8593 let min_len = ob.len().min(nb.len());
8594 for i in 0..min_len {
8595 let oi = ob.len() - 1 - i;
8596 let ni = nb.len() - 1 - i;
8597 if ob[oi] != nb[ni] {
8598 break;
8599 }
8600 if ob[oi] == b'/' {
8601 last_sep = i + 1;
8602 }
8603 }
8604 last_sep
8605 };
8606
8607 let mut sfx_at_old = ob.len() - sfx;
8609 let mut sfx_at_new = nb.len() - sfx;
8610
8611 while pfx > sfx_at_old && pfx > sfx_at_new && sfx > 0 {
8614 let suffix_bytes = &ob[sfx_at_old..];
8616 let mut new_sfx = 0;
8617 for (i, &b) in suffix_bytes.iter().enumerate().skip(1) {
8619 if b == b'/' {
8620 new_sfx = sfx - i;
8621 break;
8622 }
8623 }
8624 if new_sfx == 0 || new_sfx >= sfx {
8625 sfx_at_old = ob.len();
8626 sfx_at_new = nb.len();
8627 break;
8628 }
8629 sfx = new_sfx;
8630 sfx_at_old = ob.len() - sfx;
8631 sfx_at_new = nb.len() - sfx;
8632 }
8633
8634 let prefix = &old[..pfx];
8641 let suffix = &old[sfx_at_old..];
8642 let old_mid = if pfx <= sfx_at_old {
8643 &old[pfx..sfx_at_old]
8644 } else {
8645 ""
8646 };
8647 let new_mid = if pfx <= sfx_at_new {
8648 &new[pfx..sfx_at_new]
8649 } else {
8650 ""
8651 };
8652
8653 if prefix.is_empty() && suffix.is_empty() {
8654 return format!("{old} => {new}");
8655 }
8656
8657 format!("{prefix}{{{old_mid} => {new_mid}}}{suffix}")
8658}
8659
8660fn same_basename(del: &DiffEntry, add: &DiffEntry) -> bool {
8662 let old = del.old_path.as_deref().unwrap_or("");
8663 let new = add.new_path.as_deref().unwrap_or("");
8664 let old_base = old.rsplit('/').next().unwrap_or(old);
8665 let new_base = new.rsplit('/').next().unwrap_or(new);
8666 old_base == new_base && !old_base.is_empty()
8667}
8668
8669const RENAME_SCORE_MAX: u32 = 100;
8670
8671fn normalize_bytes_for_rename(data: Vec<u8>) -> Vec<u8> {
8673 if data.contains(&b'\r') {
8674 crate::crlf::crlf_to_lf(&data)
8675 } else {
8676 data
8677 }
8678}
8679
8680fn blob_payload_size(odb: &Odb, oid: &ObjectId) -> Option<usize> {
8682 if oid.is_zero() {
8683 return None;
8684 }
8685 odb.read_info(oid).ok().map(|info| info.size as usize)
8686}
8687
8688fn added_entry_payload_size(odb: &Odb, add: &DiffEntry, work_root: Option<&Path>) -> Option<usize> {
8690 if !add.new_oid.is_zero() {
8691 if let Some(size) = blob_payload_size(odb, &add.new_oid) {
8692 return Some(size);
8693 }
8694 }
8695 read_added_entry_bytes(odb, add, work_root).map(|data| data.len())
8696}
8697
8698fn rename_pair_size_reachable(src_size: usize, dst_size: usize, minimum_score: u32) -> bool {
8700 let max_size = src_size.max(dst_size);
8701 let min_size = src_size.min(dst_size);
8702 if max_size == 0 {
8703 return minimum_score <= RENAME_SCORE_MAX;
8704 }
8705 (max_size - min_size).saturating_mul(RENAME_SCORE_MAX as usize)
8706 <= max_size.saturating_mul((RENAME_SCORE_MAX - minimum_score) as usize)
8707}
8708
8709fn compute_similarity(old: &[u8], new: &[u8]) -> u32 {
8714 RenameLineTable::similarity_score(
8715 &RenameLineTable::from_similarity_blob(old),
8716 &RenameLineTable::from_similarity_blob(new),
8717 )
8718}
8719
8720#[must_use]
8724pub fn rename_similarity_score(old: &[u8], new: &[u8]) -> u32 {
8725 compute_similarity(old, new)
8726}
8727
8728pub fn format_raw(entry: &DiffEntry) -> String {
8734 let path = match entry.status {
8735 DiffStatus::Renamed | DiffStatus::Copied => {
8736 format!(
8737 "{}\t{}",
8738 entry.old_path.as_deref().unwrap_or(""),
8739 entry.new_path.as_deref().unwrap_or("")
8740 )
8741 }
8742 _ => entry.path().to_owned(),
8743 };
8744
8745 let status_str = match (entry.status, entry.score) {
8746 (DiffStatus::Renamed, Some(s)) => format!("R{:03}", s),
8747 (DiffStatus::Copied, Some(s)) => format!("C{:03}", s),
8748 (DiffStatus::Modified, Some(s)) => format!("M{s:03}"),
8749 (DiffStatus::TypeChanged, Some(s)) => format!("T{s:03}"),
8750 _ => entry.status.letter().to_string(),
8751 };
8752
8753 let (old_hex, new_hex) = raw_oid_hex_pair(&entry.old_oid, &entry.new_oid);
8754 format!(
8755 ":{} {} {} {} {}\t{}",
8756 entry.old_mode, entry.new_mode, old_hex, new_hex, status_str, path
8757 )
8758}
8759
8760fn raw_oid_hex_pair(old: &ObjectId, new: &ObjectId) -> (String, String) {
8768 let width = if !old.is_zero() {
8769 old.algo().hex_len()
8770 } else if !new.is_zero() {
8771 new.algo().hex_len()
8772 } else {
8773 old.algo().hex_len()
8774 };
8775 let render = |oid: &ObjectId| {
8776 if oid.is_zero() {
8777 "0".repeat(width)
8778 } else {
8779 oid.to_hex()
8780 }
8781 };
8782 (render(old), render(new))
8783}
8784
8785pub fn format_raw_abbrev(entry: &DiffEntry, abbrev_len: usize) -> String {
8787 let ellipsis = if crate::environment::Environment::empty()
8788 .var("GIT_PRINT_SHA1_ELLIPSIS")
8789 .as_deref()
8790 == Some("yes")
8791 {
8792 "..."
8793 } else {
8794 ""
8795 };
8796 let old_hex = format!("{}", entry.old_oid);
8797 let new_hex = format!("{}", entry.new_oid);
8798 let old_abbrev = &old_hex[..abbrev_len.min(old_hex.len())];
8799 let new_abbrev = &new_hex[..abbrev_len.min(new_hex.len())];
8800
8801 let path = match entry.status {
8803 DiffStatus::Renamed | DiffStatus::Copied => format!(
8804 "{}\t{}",
8805 entry.old_path.as_deref().unwrap_or(""),
8806 entry.new_path.as_deref().unwrap_or("")
8807 ),
8808 _ => entry.path().to_owned(),
8809 };
8810 let status_str = match (entry.status, entry.score) {
8811 (DiffStatus::Renamed, Some(s)) => format!("R{s:03}"),
8812 (DiffStatus::Copied, Some(s)) => format!("C{s:03}"),
8813 (DiffStatus::Modified, Some(s)) => format!("M{s:03}"),
8814 (DiffStatus::TypeChanged, Some(s)) => format!("T{s:03}"),
8815 _ => entry.status.letter().to_string(),
8816 };
8817
8818 format!(
8819 ":{} {} {}{} {}{} {}\t{}",
8820 entry.old_mode,
8821 entry.new_mode,
8822 old_abbrev,
8823 ellipsis,
8824 new_abbrev,
8825 ellipsis,
8826 status_str,
8827 path
8828 )
8829}
8830
8831pub fn unified_diff(
8846 old_content: &str,
8847 new_content: &str,
8848 old_path: &str,
8849 new_path: &str,
8850 context_lines: usize,
8851 indent_heuristic: bool,
8852 quote_path_fully: bool,
8853) -> String {
8854 unified_diff_with_prefix(
8855 old_content,
8856 new_content,
8857 old_path,
8858 new_path,
8859 context_lines,
8860 0,
8861 "a/",
8862 "b/",
8863 indent_heuristic,
8864 quote_path_fully,
8865 )
8866}
8867
8868#[allow(clippy::too_many_arguments)] pub fn unified_diff_with_prefix(
8874 old_content: &str,
8875 new_content: &str,
8876 old_path: &str,
8877 new_path: &str,
8878 context_lines: usize,
8879 inter_hunk_context: usize,
8880 src_prefix: &str,
8881 dst_prefix: &str,
8882 indent_heuristic: bool,
8883 quote_path_fully: bool,
8884) -> String {
8885 unified_diff_with_prefix_and_funcname(
8886 old_content,
8887 new_content,
8888 old_path,
8889 new_path,
8890 context_lines,
8891 inter_hunk_context,
8892 src_prefix,
8893 dst_prefix,
8894 None,
8895 indent_heuristic,
8896 quote_path_fully,
8897 )
8898}
8899
8900#[allow(clippy::too_many_arguments)]
8903pub fn unified_diff_with_prefix_and_funcname(
8904 old_content: &str,
8905 new_content: &str,
8906 old_path: &str,
8907 new_path: &str,
8908 context_lines: usize,
8909 inter_hunk_context: usize,
8910 src_prefix: &str,
8911 dst_prefix: &str,
8912 funcname_matcher: Option<&FuncnameMatcher>,
8913 indent_heuristic: bool,
8914 quote_path_fully: bool,
8915) -> String {
8916 unified_diff_with_prefix_and_funcname_and_algorithm(
8917 old_content,
8918 new_content,
8919 old_path,
8920 new_path,
8921 context_lines,
8922 inter_hunk_context,
8923 src_prefix,
8924 dst_prefix,
8925 funcname_matcher,
8926 similar::Algorithm::Myers,
8927 false,
8928 false,
8929 indent_heuristic,
8930 quote_path_fully,
8931 )
8932}
8933
8934#[allow(clippy::too_many_arguments)]
8940pub fn write_unified_diff_with_prefix_and_funcname_and_algorithm<W: std::io::Write>(
8941 out: &mut W,
8942 old_content: &str,
8943 new_content: &str,
8944 old_path: &str,
8945 new_path: &str,
8946 context_lines: usize,
8947 inter_hunk_context: usize,
8948 src_prefix: &str,
8949 dst_prefix: &str,
8950 funcname_matcher: Option<&FuncnameMatcher>,
8951 algorithm: similar::Algorithm,
8952 function_context: bool,
8953 use_git_histogram: bool,
8954 indent_heuristic: bool,
8955 quote_path_fully: bool,
8956) -> Result<()> {
8957 if function_context {
8958 let patch = unified_diff_with_function_context(
8959 old_content,
8960 new_content,
8961 old_path,
8962 new_path,
8963 context_lines,
8964 inter_hunk_context,
8965 src_prefix,
8966 dst_prefix,
8967 funcname_matcher,
8968 algorithm,
8969 indent_heuristic,
8970 quote_path_fully,
8971 );
8972 out.write_all(patch.as_bytes())?;
8973 return Ok(());
8974 }
8975
8976 if use_git_histogram {
8977 let patch = unified_diff_histogram_with_prefix_and_funcname_indent(
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 indent_heuristic,
8988 quote_path_fully,
8989 );
8990 out.write_all(patch.as_bytes())?;
8991 return Ok(());
8992 }
8993
8994 if algorithm == similar::Algorithm::Myers && !function_context {
8995 let patch = unified_diff_git_myers_with_prefix_and_funcname(
8996 old_content,
8997 new_content,
8998 old_path,
8999 new_path,
9000 context_lines,
9001 inter_hunk_context,
9002 src_prefix,
9003 dst_prefix,
9004 funcname_matcher,
9005 indent_heuristic,
9006 quote_path_fully,
9007 );
9008 out.write_all(patch.as_bytes())?;
9009 return Ok(());
9010 }
9011
9012 if algorithm == similar::Algorithm::Lcs && !function_context {
9013 let patch = unified_diff_git_minimal_with_prefix_and_funcname(
9014 old_content,
9015 new_content,
9016 old_path,
9017 new_path,
9018 context_lines,
9019 inter_hunk_context,
9020 src_prefix,
9021 dst_prefix,
9022 funcname_matcher,
9023 indent_heuristic,
9024 quote_path_fully,
9025 );
9026 out.write_all(patch.as_bytes())?;
9027 return Ok(());
9028 }
9029
9030 if algorithm == similar::Algorithm::Patience && !function_context {
9031 let patch = unified_diff_git_patience_with_prefix_and_funcname(
9032 old_content,
9033 new_content,
9034 old_path,
9035 new_path,
9036 context_lines,
9037 inter_hunk_context,
9038 src_prefix,
9039 dst_prefix,
9040 funcname_matcher,
9041 indent_heuristic,
9042 quote_path_fully,
9043 );
9044 out.write_all(patch.as_bytes())?;
9045 return Ok(());
9046 }
9047
9048 use crate::quote_path::format_diff_path_with_prefix;
9049 use similar::{udiff::UnifiedDiffHunk, TextDiff};
9050
9051 let diff = TextDiff::configure()
9052 .algorithm(algorithm)
9053 .diff_lines(old_content, new_content);
9054 let compacted_ops = diff_indent_heuristic::diff_lines_ops_compacted(
9055 old_content,
9056 new_content,
9057 algorithm,
9058 indent_heuristic,
9059 );
9060
9061 if old_path == "/dev/null" {
9062 out.write_all(b"--- /dev/null\n")?;
9063 } else if src_prefix.is_empty() {
9064 writeln!(out, "--- {old_path}")?;
9065 } else {
9066 out.write_all(b"--- ")?;
9067 out.write_all(
9068 format_diff_path_with_prefix(src_prefix, old_path, quote_path_fully).as_bytes(),
9069 )?;
9070 out.write_all(b"\n")?;
9071 }
9072 if new_path == "/dev/null" {
9073 out.write_all(b"+++ /dev/null\n")?;
9074 } else if dst_prefix.is_empty() {
9075 writeln!(out, "+++ {new_path}")?;
9076 } else {
9077 out.write_all(b"+++ ")?;
9078 out.write_all(
9079 format_diff_path_with_prefix(dst_prefix, new_path, quote_path_fully).as_bytes(),
9080 )?;
9081 out.write_all(b"\n")?;
9082 }
9083
9084 let old_lines: Vec<&str> = old_content.lines().collect();
9085 let max_common_gap = context_lines
9086 .saturating_mul(2)
9087 .saturating_add(inter_hunk_context);
9088 let op_groups = group_diff_ops_gap(compacted_ops, context_lines, max_common_gap);
9089
9090 let mut hunk_buf = String::new();
9091 let mut eof_annot = UnifiedDiffNoNewlineAnnotator::new(old_content, new_content);
9092 for ops in op_groups {
9093 if ops.is_empty() {
9094 continue;
9095 }
9096 let hunk = UnifiedDiffHunk::new(ops, &diff, true);
9097 hunk_buf.clear();
9098 use std::fmt::Write as FmtWrite;
9099 let _ = FmtWrite::write_fmt(&mut hunk_buf, format_args!("{hunk}"));
9100 let mut fragment = hunk_buf.clone();
9101 if let Some(first_newline) = hunk_buf.find('\n') {
9102 let header_line = &hunk_buf[..first_newline];
9103 let rest = &hunk_buf[first_newline..];
9104 if let Some(func_ctx) =
9105 extract_function_context(header_line, &old_lines, funcname_matcher, Some(old_path))
9106 {
9107 fragment.clear();
9108 fragment.push_str(header_line);
9109 fragment.push(' ');
9110 fragment.push_str(&func_ctx);
9111 fragment.push_str(rest);
9112 }
9113 }
9114 eof_annot.write_hunk(out, &fragment)?;
9115 }
9116 Ok(())
9117}
9118
9119#[allow(clippy::too_many_arguments)]
9125pub fn unified_diff_with_prefix_and_funcname_and_algorithm(
9126 old_content: &str,
9127 new_content: &str,
9128 old_path: &str,
9129 new_path: &str,
9130 context_lines: usize,
9131 inter_hunk_context: usize,
9132 src_prefix: &str,
9133 dst_prefix: &str,
9134 funcname_matcher: Option<&FuncnameMatcher>,
9135 algorithm: similar::Algorithm,
9136 function_context: bool,
9137 use_git_histogram: bool,
9138 indent_heuristic: bool,
9139 quote_path_fully: bool,
9140) -> String {
9141 let mut output = Vec::new();
9142 if write_unified_diff_with_prefix_and_funcname_and_algorithm(
9143 &mut output,
9144 old_content,
9145 new_content,
9146 old_path,
9147 new_path,
9148 context_lines,
9149 inter_hunk_context,
9150 src_prefix,
9151 dst_prefix,
9152 funcname_matcher,
9153 algorithm,
9154 function_context,
9155 use_git_histogram,
9156 indent_heuristic,
9157 quote_path_fully,
9158 )
9159 .is_err()
9160 {
9161 output.clear();
9162 }
9163 String::from_utf8_lossy(&output).into_owned()
9164}
9165
9166pub(crate) fn group_diff_ops_gap(
9172 mut ops: Vec<similar::DiffOp>,
9173 context: usize,
9174 max_common_gap: usize,
9175) -> Vec<Vec<similar::DiffOp>> {
9176 use similar::DiffOp;
9177 if ops.is_empty() {
9178 return vec![];
9179 }
9180
9181 let mut pending_group = Vec::new();
9182 let mut rv = Vec::new();
9183
9184 if let Some(DiffOp::Equal {
9185 old_index,
9186 new_index,
9187 len,
9188 }) = ops.first_mut()
9189 {
9190 let offset = (*len).saturating_sub(context);
9191 *old_index += offset;
9192 *new_index += offset;
9193 *len -= offset;
9194 }
9195
9196 if let Some(DiffOp::Equal { len, .. }) = ops.last_mut() {
9197 *len -= (*len).saturating_sub(context);
9198 }
9199
9200 for op in ops.into_iter() {
9201 if let DiffOp::Equal {
9202 old_index,
9203 new_index,
9204 len,
9205 } = op
9206 {
9207 if len > max_common_gap {
9210 pending_group.push(DiffOp::Equal {
9211 old_index,
9212 new_index,
9213 len: context,
9214 });
9215 rv.push(pending_group);
9216 let offset = len.saturating_sub(context);
9217 pending_group = vec![DiffOp::Equal {
9218 old_index: old_index + offset,
9219 new_index: new_index + offset,
9220 len: len - offset,
9221 }];
9222 continue;
9223 }
9224 }
9225 pending_group.push(op);
9226 }
9227
9228 match &pending_group[..] {
9229 &[] | &[similar::DiffOp::Equal { .. }] => {}
9230 _ => rv.push(pending_group),
9231 }
9232
9233 rv
9234}
9235#[expect(clippy::too_many_arguments)]
9236fn unified_diff_with_function_context(
9238 old_content: &str,
9239 new_content: &str,
9240 old_path: &str,
9241 new_path: &str,
9242 context_lines: usize,
9243 inter_hunk_context: usize,
9244 src_prefix: &str,
9245 dst_prefix: &str,
9246 funcname_matcher: Option<&FuncnameMatcher>,
9247 algorithm: similar::Algorithm,
9248 indent_heuristic: bool,
9249 quote_path_fully: bool,
9250) -> String {
9251 use crate::quote_path::format_diff_path_with_prefix;
9252 use similar::{udiff::UnifiedDiffHunk, TextDiff};
9253
9254 let diff = TextDiff::configure()
9255 .algorithm(algorithm)
9256 .diff_lines(old_content, new_content);
9257
9258 let old_lines: Vec<&str> = old_content.lines().collect();
9259 let new_lines: Vec<&str> = new_content.lines().collect();
9260 let n_old = old_lines.len();
9261 let n_new = new_lines.len();
9262
9263 let max_common_gap = context_lines
9270 .saturating_mul(2)
9271 .saturating_add(inter_hunk_context);
9272 let all_ops = diff.ops().to_vec();
9273 let op_groups = group_diff_ops_gap(all_ops.clone(), context_lines, max_common_gap);
9274
9275 let mut ranges: Vec<(usize, usize, usize, usize)> = Vec::new();
9276
9277 for ops in op_groups {
9278 if ops.is_empty() {
9279 continue;
9280 }
9281 let i1_anchor = func_context_old_anchor(&ops, n_old);
9282 let i1_end = hunk_old_change_end_exclusive(&ops);
9283 let skip_preimage_pull =
9284 append_with_whole_function_added(&ops, n_old, n_new, &new_lines, funcname_matcher);
9285 let hunk = UnifiedDiffHunk::new(ops, &diff, true);
9286 let hunk_str = format!("{hunk}");
9287 let header_line = hunk_str
9288 .lines()
9289 .next()
9290 .unwrap_or("")
9291 .trim_end_matches(['\r', '\n']);
9292 let Some((base_s1, _base_e1, _base_s2, _base_e2)) =
9293 parse_unified_hunk_header_ranges(header_line)
9294 else {
9295 continue;
9296 };
9297
9298 let ctx = context_lines;
9299 let (s1, e1, s2, e2) = if skip_preimage_pull {
9300 let s = n_old.saturating_sub(ctx);
9301 let s2 = map_old_line_to_new(&all_ops, s, n_new).min(n_new);
9302 (s, n_old, s2, n_new)
9303 } else {
9304 let mut s1 = base_s1.saturating_sub(ctx);
9305 let mut s2 = map_old_line_to_new(&all_ops, s1, n_new).min(n_new);
9306
9307 let base_pre_s1 = i1_anchor.saturating_sub(ctx);
9308 if base_pre_s1 < s1 {
9309 s1 = base_pre_s1;
9310 s2 = map_old_line_to_new(&all_ops, s1, n_new).min(n_new);
9311 }
9312
9313 let fs1 = expand_func_pre_start(s1, i1_anchor, n_old, &old_lines, funcname_matcher);
9314 if fs1 < s1 {
9315 s1 = fs1;
9316 s2 = map_old_line_to_new(&all_ops, s1, n_new).min(n_new);
9317 }
9318
9319 let mut e1 = (i1_end + ctx).min(n_old);
9325 let mut e2 = map_old_line_to_new(&all_ops, e1, n_new).min(n_new);
9326 let fe1 = expand_func_post_end(e1, i1_end, n_old, &old_lines, funcname_matcher);
9327 if fe1 > e1 {
9328 e1 = fe1;
9329 e2 = map_old_line_to_new(&all_ops, e1, n_new).min(n_new);
9330 }
9331 (s1, e1, s2, e2)
9332 };
9333
9334 ranges.push((s1, e1, s2, e2));
9335 }
9336
9337 ranges.sort_by_key(|r| (r.0, r.2));
9340 let mut merged: Vec<(usize, usize, usize, usize)> = Vec::with_capacity(ranges.len());
9341 for (s1, e1, s2, e2) in ranges {
9342 if let Some(last) = merged.last_mut() {
9343 if s1 < last.1 {
9344 last.1 = last.1.max(e1);
9345 last.3 = last.3.max(e2);
9346 continue;
9347 }
9348 }
9349 merged.push((s1, e1, s2, e2));
9350 }
9351 let ranges = merged;
9352
9353 let mut output = String::new();
9354 if old_path == "/dev/null" {
9355 output.push_str("--- /dev/null\n");
9356 } else if src_prefix.is_empty() {
9357 output.push_str(&format!("--- {old_path}\n"));
9358 } else {
9359 output.push_str("--- ");
9360 output.push_str(&format_diff_path_with_prefix(
9361 src_prefix,
9362 old_path,
9363 quote_path_fully,
9364 ));
9365 output.push('\n');
9366 }
9367 if new_path == "/dev/null" {
9368 output.push_str("+++ /dev/null\n");
9369 } else if dst_prefix.is_empty() {
9370 output.push_str(&format!("+++ {new_path}\n"));
9371 } else {
9372 output.push_str("+++ ");
9373 output.push_str(&format_diff_path_with_prefix(
9374 dst_prefix,
9375 new_path,
9376 quote_path_fully,
9377 ));
9378 output.push('\n');
9379 }
9380
9381 for (s1, e1, s2, e2) in ranges {
9382 if s1 >= e1 && s2 >= e2 {
9383 continue;
9384 }
9385 let old_seg =
9386 line_slice_for_diff_with_eof_nl(&old_lines, s1, e1, old_content.ends_with('\n'));
9387 let new_seg =
9388 line_slice_for_diff_with_eof_nl(&new_lines, s2, e2, new_content.ends_with('\n'));
9389 let inner_ctx = old_seg.lines().count().max(new_seg.lines().count()).max(1);
9390 let piece = unified_diff_with_prefix_and_funcname_and_algorithm(
9391 &old_seg,
9392 &new_seg,
9393 old_path,
9394 new_path,
9395 inner_ctx,
9396 0,
9397 src_prefix,
9398 dst_prefix,
9399 funcname_matcher,
9400 algorithm,
9401 false,
9402 false,
9403 indent_heuristic,
9404 quote_path_fully,
9405 );
9406 let shifted = shift_unified_hunk_headers_to_full_file(&piece, s1, s2);
9407 let with_func =
9408 enrich_unified_hunk_headers_funcname(&shifted, &old_lines, funcname_matcher);
9409 for line in with_func.lines() {
9410 if line.starts_with("--- ") || line.starts_with("+++ ") {
9411 continue;
9412 }
9413 output.push_str(line);
9414 output.push('\n');
9415 }
9416 }
9417
9418 output
9419}
9420
9421fn shift_unified_hunk_headers_to_full_file(
9426 patch: &str,
9427 delta_old: usize,
9428 delta_new: usize,
9429) -> String {
9430 if delta_old == 0 && delta_new == 0 {
9431 return patch.to_owned();
9432 }
9433 let mut out = String::with_capacity(patch.len());
9434 for line in patch.lines() {
9435 if let Some(shifted) = shift_one_unified_hunk_header(line, delta_old, delta_new) {
9436 out.push_str(&shifted);
9437 } else {
9438 out.push_str(line);
9439 }
9440 out.push('\n');
9441 }
9442 out
9443}
9444
9445fn shift_one_unified_hunk_header(line: &str, delta_old: usize, delta_new: usize) -> Option<String> {
9446 let rest = line.strip_prefix("@@ ")?;
9447 let (old_chunk, after_plus) = rest.split_once(" +")?;
9448 let old_spec = old_chunk.strip_prefix('-')?;
9449 let (new_spec, suffix) = after_plus.split_once(" @@")?;
9450 let shifted_old = shift_unified_range_spec(old_spec, delta_old)?;
9451 let shifted_new = shift_unified_range_spec(new_spec, delta_new)?;
9452 Some(format!("@@ -{shifted_old} +{shifted_new} @@{suffix}"))
9453}
9454
9455fn shift_unified_range_spec(spec: &str, delta: usize) -> Option<String> {
9456 let spec = spec.trim();
9457 if let Some((start_s, count_s)) = spec.split_once(',') {
9458 let start: usize = start_s.parse().ok()?;
9459 let count: usize = count_s.parse().ok()?;
9460 Some(format!("{},{}", start.saturating_add(delta), count))
9461 } else {
9462 let start: usize = spec.parse().ok()?;
9463 Some(format!("{}", start.saturating_add(delta)))
9464 }
9465}
9466
9467fn enrich_unified_hunk_headers_funcname(
9469 patch: &str,
9470 full_old_lines: &[&str],
9471 funcname_matcher: Option<&FuncnameMatcher>,
9472) -> String {
9473 let mut out = String::with_capacity(patch.len());
9474 for line in patch.lines() {
9475 if let Some(fixed) = enrich_one_hunk_header_funcname(line, full_old_lines, funcname_matcher)
9476 {
9477 out.push_str(&fixed);
9478 } else {
9479 out.push_str(line);
9480 }
9481 out.push('\n');
9482 }
9483 out
9484}
9485
9486fn enrich_one_hunk_header_funcname(
9487 line: &str,
9488 full_old_lines: &[&str],
9489 funcname_matcher: Option<&FuncnameMatcher>,
9490) -> Option<String> {
9491 let after_at = line.strip_prefix("@@ ")?;
9492 let idx = after_at.find(" @@")?;
9493 let mid = after_at[..idx].trim();
9494 let tail = after_at[idx + 3..].trim_start();
9495 let header_for_parse = format!("@@ {mid} @@");
9496 let func = extract_function_context(&header_for_parse, full_old_lines, funcname_matcher, None);
9497 Some(if let Some(f) = func {
9498 format!("@@ {mid} @@ {f}")
9499 } else if !tail.is_empty() {
9500 format!("@@ {mid} @@ {tail}")
9501 } else {
9502 format!("@@ {mid} @@")
9503 })
9504}
9505
9506fn line_slice_for_diff_with_eof_nl(
9507 lines: &[&str],
9508 start: usize,
9509 end: usize,
9510 full_file_ends_with_newline: bool,
9511) -> String {
9512 if start >= end {
9513 return String::new();
9514 }
9515 let mut s = lines[start..end].join("\n");
9516 let slice_is_suffix_of_file = end == lines.len();
9517 let need_trailing_nl = if slice_is_suffix_of_file {
9518 full_file_ends_with_newline
9519 } else {
9520 true
9521 };
9522 if need_trailing_nl && !s.ends_with('\n') {
9523 s.push('\n');
9524 }
9525 s
9526}
9527
9528fn map_old_line_to_new(ops: &[similar::DiffOp], old_line: usize, n_new: usize) -> usize {
9531 use similar::DiffOp;
9532 let mut n = 0usize;
9533 for op in ops {
9534 match *op {
9535 DiffOp::Equal {
9536 old_index,
9537 new_index,
9538 len,
9539 } => {
9540 if old_index + len <= old_line {
9541 n = new_index + len;
9542 continue;
9543 }
9544 if old_index < old_line {
9545 let take = old_line - old_index;
9546 return (new_index + take).min(n_new);
9547 }
9548 return new_index.min(n_new);
9549 }
9550 DiffOp::Delete {
9551 old_index,
9552 old_len,
9553 new_index,
9554 } => {
9555 if old_index + old_len <= old_line {
9556 n = new_index;
9557 continue;
9558 }
9559 if old_index < old_line {
9560 return new_index.min(n_new);
9561 }
9562 }
9563 DiffOp::Insert {
9564 old_index,
9565 new_index,
9566 new_len,
9567 } => {
9568 if old_index < old_line {
9569 n = new_index + new_len;
9570 continue;
9571 }
9572 if old_index == old_line {
9573 return (new_index + new_len).min(n_new);
9576 }
9577 return new_index.min(n_new);
9578 }
9579 DiffOp::Replace {
9580 old_index,
9581 old_len,
9582 new_index,
9583 new_len,
9584 } => {
9585 if old_index + old_len <= old_line {
9586 n = new_index + new_len;
9587 continue;
9588 }
9589 if old_index < old_line {
9590 let into_old = old_line - old_index;
9591 let mapped = new_index + into_old.min(new_len);
9592 return mapped.min(n_new);
9593 }
9594 return new_index.min(n_new);
9595 }
9596 }
9597 }
9598 n.min(n_new)
9599}
9600
9601fn parse_unified_hunk_header_ranges(header: &str) -> Option<(usize, usize, usize, usize)> {
9603 let rest = header.strip_prefix("@@ ")?;
9604 let (old_tok, rest2) = rest.split_once(" +")?;
9605 let old_tok = old_tok.strip_prefix('-')?;
9606 let new_tok = rest2.split_once(" @@").map(|(a, _)| a)?;
9607
9608 fn parse_side(spec: &str) -> Option<(usize, usize)> {
9609 let spec = spec.trim();
9610 let (start_one_based, count) = if let Some((a, b)) = spec.split_once(',') {
9611 (a.parse::<usize>().ok()?, b.parse::<usize>().ok()?)
9612 } else {
9613 let s = spec.parse::<usize>().ok()?;
9614 (s, 1usize)
9615 };
9616 let s0 = start_one_based.saturating_sub(1);
9617 let e0 = s0.saturating_add(count);
9618 Some((s0, e0))
9619 }
9620
9621 let (os, oe) = parse_side(old_tok)?;
9622 let (ns, ne) = parse_side(new_tok)?;
9623 Some((os, oe, ns, ne))
9624}
9625
9626fn append_with_whole_function_added(
9629 ops: &[similar::DiffOp],
9630 n_old: usize,
9631 n_new: usize,
9632 new_lines: &[&str],
9633 matcher: Option<&FuncnameMatcher>,
9634) -> bool {
9635 use similar::DiffOp;
9636 if n_old == 0 {
9637 return false;
9638 }
9639 let mut only_ins_or_eq = true;
9640 let mut min_new_ins = usize::MAX;
9641 for op in ops {
9642 match *op {
9643 DiffOp::Equal { .. } => {}
9644 DiffOp::Insert {
9645 new_index, new_len, ..
9646 } => {
9647 min_new_ins = min_new_ins.min(new_index);
9648 if new_len == 0 {
9649 only_ins_or_eq = false;
9650 }
9651 }
9652 DiffOp::Delete { .. } | DiffOp::Replace { .. } => {
9653 only_ins_or_eq = false;
9654 }
9655 }
9656 }
9657 let mut insert_at_eof = false;
9658 for op in ops {
9659 if let DiffOp::Insert { old_index, .. } = *op {
9660 if old_index == n_old {
9661 insert_at_eof = true;
9662 break;
9663 }
9664 }
9665 }
9666 let append_at_eof = min_new_ins == n_old || insert_at_eof;
9667 if !only_ins_or_eq || !append_at_eof || min_new_ins == usize::MAX {
9668 return false;
9669 }
9670 let mut j = min_new_ins;
9675 while j < n_new {
9676 let line = new_lines[j];
9677 if line.trim().is_empty() {
9678 j += 1;
9679 continue;
9680 }
9681 if let Some(m) = matcher {
9682 if m.match_line(line).is_some() {
9683 return true;
9684 }
9685 } else if inserted_block_starts_with_c_like_function_definition(line) {
9686 return true;
9687 }
9688 j += 1;
9689 }
9690 false
9691}
9692
9693fn inserted_block_starts_with_c_like_function_definition(line: &str) -> bool {
9694 let t = line.trim_start();
9695 let Some(open_paren) = t.find('(') else {
9696 return false;
9697 };
9698 let head = &t[..open_paren];
9699 let tokens: Vec<&str> = head.split_whitespace().collect();
9700 if tokens.len() < 2 {
9701 return false;
9703 }
9704 let nameish = tokens.last().copied().unwrap_or("");
9705 let name = nameish.trim_end_matches(['*', '&']);
9706 if name.is_empty() || !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
9707 return false;
9708 }
9709 let type_or_modifier = |tok: &str| {
9710 matches!(
9711 tok,
9712 "static"
9713 | "extern"
9714 | "inline"
9715 | "void"
9716 | "int"
9717 | "char"
9718 | "short"
9719 | "long"
9720 | "float"
9721 | "double"
9722 | "unsigned"
9723 | "signed"
9724 | "struct"
9725 | "enum"
9726 | "union"
9727 | "const"
9728 | "volatile"
9729 | "typedef"
9730 )
9731 };
9732 tokens[..tokens.len() - 1]
9733 .iter()
9734 .any(|tok| type_or_modifier(tok))
9735}
9736
9737fn hunk_old_change_end_exclusive(ops: &[similar::DiffOp]) -> usize {
9738 use similar::DiffOp;
9739 let mut max_o = 0usize;
9740 for op in ops {
9741 match *op {
9742 DiffOp::Delete {
9743 old_index, old_len, ..
9744 } => {
9745 max_o = max_o.max(old_index + old_len);
9746 }
9747 DiffOp::Replace {
9748 old_index, old_len, ..
9749 } => {
9750 max_o = max_o.max(old_index + old_len);
9751 }
9752 DiffOp::Insert { old_index, .. } => {
9753 max_o = max_o.max(old_index);
9756 }
9757 DiffOp::Equal { .. } => {}
9758 }
9759 }
9760 max_o
9761}
9762
9763fn func_context_old_anchor(ops: &[similar::DiffOp], n_old: usize) -> usize {
9764 use similar::DiffOp;
9765 let mut has_delete_or_replace = false;
9766 let mut min_del = usize::MAX;
9767 let mut min_ins_old = usize::MAX;
9768
9769 for op in ops {
9770 match *op {
9771 DiffOp::Delete {
9772 old_index, old_len, ..
9773 } => {
9774 has_delete_or_replace = true;
9775 min_del = min_del.min(old_index);
9776 min_del = min_del.min(old_index + old_len.saturating_sub(1));
9777 }
9778 DiffOp::Replace {
9779 old_index, old_len, ..
9780 } => {
9781 has_delete_or_replace = true;
9782 min_del = min_del.min(old_index);
9783 min_del = min_del.min(old_index + old_len.saturating_sub(1));
9784 }
9785 DiffOp::Insert { old_index, .. } => {
9786 min_ins_old = min_ins_old.min(old_index);
9787 }
9788 DiffOp::Equal { .. } => {}
9789 }
9790 }
9791
9792 let mut i1 = if has_delete_or_replace {
9793 min_del
9794 } else if min_ins_old != usize::MAX {
9795 min_ins_old
9796 } else {
9797 0
9798 };
9799
9800 let pure_insert = ops
9801 .iter()
9802 .all(|op| matches!(op, DiffOp::Insert { .. } | DiffOp::Equal { .. }))
9803 && ops.iter().any(|op| matches!(op, DiffOp::Insert { .. }));
9804
9805 if pure_insert && i1 >= n_old && n_old > 0 {
9806 i1 = n_old - 1;
9807 }
9808
9809 i1.min(n_old.saturating_sub(1))
9810}
9811
9812fn expand_func_pre_start(
9813 s1: usize,
9814 i1: usize,
9815 n_old: usize,
9816 old_lines: &[&str],
9817 matcher: Option<&FuncnameMatcher>,
9818) -> usize {
9819 if n_old == 0 {
9820 return s1;
9821 }
9822 let i1 = i1.min(n_old.saturating_sub(1));
9823 let mut fs1 = get_func_line_backward(old_lines, i1, matcher).unwrap_or(i1);
9824 while fs1 > 0
9825 && !is_line_empty_for_func_context(old_lines[fs1 - 1])
9826 && !is_func_line(old_lines[fs1 - 1], matcher)
9827 {
9828 fs1 -= 1;
9829 }
9830 s1.min(fs1)
9831}
9832
9833fn expand_func_post_end(
9834 e1: usize,
9835 i1_end: usize,
9836 n_old: usize,
9837 old_lines: &[&str],
9838 matcher: Option<&FuncnameMatcher>,
9839) -> usize {
9840 let from = i1_end.min(n_old);
9841 let fe1 = get_func_line_forward(old_lines, from, matcher).unwrap_or(n_old);
9842 let mut fe1_adj = fe1;
9843 while fe1_adj > 0 && is_line_empty_for_func_context(old_lines[fe1_adj - 1]) {
9844 fe1_adj -= 1;
9845 }
9846 e1.max(fe1_adj).min(n_old)
9847}
9848
9849fn is_line_empty_for_func_context(line: &str) -> bool {
9850 line.chars().all(|c| c.is_whitespace())
9851}
9852
9853pub(crate) fn is_func_line(line: &str, matcher: Option<&FuncnameMatcher>) -> bool {
9854 if let Some(m) = matcher {
9855 return m.match_line(line).is_some();
9856 }
9857 let t = line.trim_end_matches(['\n', '\r']);
9858 if t.is_empty() {
9859 return false;
9860 }
9861 let b = t.as_bytes()[0];
9862 if !(b.is_ascii_alphabetic() || b == b'_' || b == b'$') {
9863 return false;
9864 }
9865 true
9866}
9867
9868fn get_func_line_backward(
9869 old_lines: &[&str],
9870 start: usize,
9871 matcher: Option<&FuncnameMatcher>,
9872) -> Option<usize> {
9873 let mut l = start.min(old_lines.len().saturating_sub(1));
9874 if old_lines.is_empty() {
9875 return None;
9876 }
9877 loop {
9878 if is_func_line(old_lines[l], matcher) {
9879 return Some(l);
9880 }
9881 if l == 0 {
9882 break;
9883 }
9884 l -= 1;
9885 }
9886 None
9887}
9888
9889fn get_func_line_forward(
9890 old_lines: &[&str],
9891 start: usize,
9892 matcher: Option<&FuncnameMatcher>,
9893) -> Option<usize> {
9894 let mut l = start;
9895 while l < old_lines.len() {
9896 if is_func_line(old_lines[l], matcher) {
9897 return Some(l);
9898 }
9899 l += 1;
9900 }
9901 None
9902}
9903#[expect(clippy::too_many_arguments)]
9904pub fn anchored_unified_diff(
9914 old_content: &str,
9915 new_content: &str,
9916 old_path: &str,
9917 new_path: &str,
9918 context_lines: usize,
9919 anchors: &[String],
9920 algorithm: similar::Algorithm,
9921 use_git_histogram: bool,
9922 indent_heuristic: bool,
9923 quote_path_fully: bool,
9924) -> String {
9925 use crate::quote_path::format_diff_path_with_prefix;
9926 use similar::TextDiff;
9927
9928 let old_lines: Vec<&str> = old_content.lines().collect();
9929 let new_lines: Vec<&str> = new_content.lines().collect();
9930
9931 let mut anchor_pairs: Vec<(usize, usize)> = Vec::new(); for anchor in anchors {
9935 let anchor_str = anchor.as_str();
9936
9937 let old_positions: Vec<usize> = old_lines
9939 .iter()
9940 .enumerate()
9941 .filter(|(_, l)| l.trim_end() == anchor_str)
9942 .map(|(i, _)| i)
9943 .collect();
9944
9945 let new_positions: Vec<usize> = new_lines
9947 .iter()
9948 .enumerate()
9949 .filter(|(_, l)| l.trim_end() == anchor_str)
9950 .map(|(i, _)| i)
9951 .collect();
9952
9953 if old_positions.len() == 1 && new_positions.len() == 1 {
9955 anchor_pairs.push((old_positions[0], new_positions[0]));
9956 }
9957 }
9958
9959 if anchor_pairs.is_empty() {
9961 return unified_diff_with_prefix_and_funcname_and_algorithm(
9962 old_content,
9963 new_content,
9964 old_path,
9965 new_path,
9966 context_lines,
9967 0,
9968 "a/",
9969 "b/",
9970 None,
9971 algorithm,
9972 false,
9973 use_git_histogram,
9974 indent_heuristic,
9975 quote_path_fully,
9976 );
9977 }
9978
9979 anchor_pairs.sort_by_key(|&(old_idx, _)| old_idx);
9981
9982 let mut filtered: Vec<(usize, usize)> = Vec::new();
9985 for &pair in &anchor_pairs {
9986 if filtered.is_empty() || filtered.last().is_some_and(|last| pair.1 > last.1) {
9987 filtered.push(pair);
9988 }
9989 }
9990 let anchor_pairs = filtered;
9991
9992 struct LineDiffOp {
10001 tag: char, line: String,
10003 }
10004
10005 let append_segment_diff =
10006 |ops: &mut Vec<LineDiffOp>, old_seg_input: &str, new_seg_input: &str| {
10007 use similar::ChangeTag;
10008 let old_ls: Vec<&str> = old_seg_input.lines().collect();
10009 let new_ls: Vec<&str> = new_seg_input.lines().collect();
10010 if old_ls.is_empty() && new_ls.is_empty() {
10011 return;
10012 }
10013 let seg_diff = TextDiff::configure()
10014 .algorithm(algorithm)
10015 .diff_slices(&old_ls, &new_ls);
10016 let raw = seg_diff.ops().to_vec();
10017 let compacted = diff_indent_heuristic::apply_change_compact_to_ops(
10018 &raw,
10019 &old_ls,
10020 &new_ls,
10021 indent_heuristic,
10022 );
10023 for op in &compacted {
10024 for ch in op.iter_changes(&old_ls, &new_ls) {
10025 let t = match ch.tag() {
10026 ChangeTag::Equal => ' ',
10027 ChangeTag::Delete => '-',
10028 ChangeTag::Insert => '+',
10029 };
10030 ops.push(LineDiffOp {
10031 tag: t,
10032 line: ch.value().to_string(),
10033 });
10034 }
10035 }
10036 };
10037
10038 let mut ops: Vec<LineDiffOp> = Vec::new();
10039 let mut old_pos = 0usize;
10040 let mut new_pos = 0usize;
10041
10042 for &(old_anchor, new_anchor) in &anchor_pairs {
10043 let old_segment: Vec<&str> = old_lines[old_pos..old_anchor].to_vec();
10045 let new_segment: Vec<&str> = new_lines[new_pos..new_anchor].to_vec();
10046
10047 let old_seg_text = old_segment.join("\n");
10048 let new_seg_text = new_segment.join("\n");
10049
10050 if !old_seg_text.is_empty() || !new_seg_text.is_empty() {
10051 let old_seg_input = if old_seg_text.is_empty() {
10052 String::new()
10053 } else {
10054 format!("{}\n", old_seg_text)
10055 };
10056 let new_seg_input = if new_seg_text.is_empty() {
10057 String::new()
10058 } else {
10059 format!("{}\n", new_seg_text)
10060 };
10061 append_segment_diff(&mut ops, &old_seg_input, &new_seg_input);
10062 }
10063
10064 ops.push(LineDiffOp {
10066 tag: ' ',
10067 line: old_lines[old_anchor].to_string(),
10068 });
10069
10070 old_pos = old_anchor + 1;
10071 new_pos = new_anchor + 1;
10072 }
10073
10074 let old_segment: Vec<&str> = old_lines[old_pos..].to_vec();
10076 let new_segment: Vec<&str> = new_lines[new_pos..].to_vec();
10077 let old_seg_text = old_segment.join("\n");
10078 let new_seg_text = new_segment.join("\n");
10079
10080 if !old_seg_text.is_empty() || !new_seg_text.is_empty() {
10081 let old_seg_input = if old_seg_text.is_empty() {
10082 String::new()
10083 } else {
10084 format!("{}\n", old_seg_text)
10085 };
10086 let new_seg_input = if new_seg_text.is_empty() {
10087 String::new()
10088 } else {
10089 format!("{}\n", new_seg_text)
10090 };
10091 append_segment_diff(&mut ops, &old_seg_input, &new_seg_input);
10092 }
10093
10094 let mut output = String::new();
10096 if old_path == "/dev/null" {
10097 output.push_str("--- /dev/null\n");
10098 } else {
10099 output.push_str("--- ");
10100 output.push_str(&format_diff_path_with_prefix(
10101 "a/",
10102 old_path,
10103 quote_path_fully,
10104 ));
10105 output.push('\n');
10106 }
10107 if new_path == "/dev/null" {
10108 output.push_str("+++ /dev/null\n");
10109 } else {
10110 output.push_str("+++ ");
10111 output.push_str(&format_diff_path_with_prefix(
10112 "b/",
10113 new_path,
10114 quote_path_fully,
10115 ));
10116 output.push('\n');
10117 }
10118
10119 let total_ops = ops.len();
10121 if total_ops == 0 {
10122 return output;
10123 }
10124
10125 let mut hunks: Vec<(usize, usize)> = Vec::new(); let mut i = 0;
10128 while i < total_ops {
10129 if ops[i].tag != ' ' {
10130 let start = i.saturating_sub(context_lines);
10131 let mut end = i;
10132 while end < total_ops {
10134 if ops[end].tag != ' ' {
10135 end += 1;
10136 continue;
10137 }
10138 let mut next_change = end;
10140 while next_change < total_ops && ops[next_change].tag == ' ' {
10141 next_change += 1;
10142 }
10143 if next_change < total_ops && next_change - end <= context_lines * 2 {
10144 end = next_change + 1;
10145 } else {
10146 end = (end + context_lines).min(total_ops);
10147 break;
10148 }
10149 }
10150 if let Some(last) = hunks.last_mut() {
10152 if start <= last.1 {
10153 last.1 = end;
10154 } else {
10155 hunks.push((start, end));
10156 }
10157 } else {
10158 hunks.push((start, end));
10159 }
10160 i = end;
10161 } else {
10162 i += 1;
10163 }
10164 }
10165
10166 for (start, end) in hunks {
10168 let mut old_start = 1usize;
10170 let mut new_start = 1usize;
10171 for op in &ops[..start] {
10173 match op.tag {
10174 ' ' => {
10175 old_start += 1;
10176 new_start += 1;
10177 }
10178 '-' => {
10179 old_start += 1;
10180 }
10181 '+' => {
10182 new_start += 1;
10183 }
10184 _ => {}
10185 }
10186 }
10187 let mut old_count = 0usize;
10188 let mut new_count = 0usize;
10189 for op in &ops[start..end] {
10190 match op.tag {
10191 ' ' => {
10192 old_count += 1;
10193 new_count += 1;
10194 }
10195 '-' => {
10196 old_count += 1;
10197 }
10198 '+' => {
10199 new_count += 1;
10200 }
10201 _ => {}
10202 }
10203 }
10204
10205 output.push_str(&format!(
10206 "@@ -{},{} +{},{} @@\n",
10207 old_start, old_count, new_start, new_count
10208 ));
10209 for op in &ops[start..end] {
10210 output.push(op.tag);
10211 output.push_str(&op.line);
10212 output.push('\n');
10213 }
10214 }
10215
10216 output
10217}
10218
10219fn func_context_line_matches(line: &str, funcname_matcher: Option<&FuncnameMatcher>) -> bool {
10225 if let Some(m) = funcname_matcher {
10226 return m.match_line(line).is_some();
10227 }
10228 is_func_line(line, None)
10229}
10230
10231fn extract_function_context(
10232 header: &str,
10233 old_lines: &[&str],
10234 funcname_matcher: Option<&FuncnameMatcher>,
10235 _source_path: Option<&str>,
10236) -> Option<String> {
10237 let at_pos = header.find("-")?;
10239 let rest = &header[at_pos + 1..];
10240 let comma_or_space = rest.find([',', ' '])?;
10241 let start_str = &rest[..comma_or_space];
10242 let start_line: usize = start_str.parse().ok()?;
10243
10244 let old_token_end = rest.find([' ', '\t']).unwrap_or(rest.len());
10248 let old_token = &rest[..old_token_end];
10249 let old_count: usize = if let Some(comma) = old_token.find(',') {
10250 old_token[comma + 1..].parse().unwrap_or(1)
10251 } else {
10252 1
10253 };
10254
10255 if start_line == 0 {
10256 return None;
10257 }
10258
10259 if start_line == 0 {
10266 return None;
10267 }
10268 let search_end = if old_count == 0 {
10269 start_line.min(old_lines.len())
10270 } else {
10271 if start_line <= 1 {
10272 return None;
10273 }
10274 (start_line - 1).min(old_lines.len())
10275 };
10276 let truncate = |line: &str| {
10277 let mut end = line.len().min(80);
10278 while end > 0 && !line.is_char_boundary(end) {
10279 end -= 1;
10280 }
10281 let mut out = line[..end].to_owned();
10282 let tail = line[end..].trim_start();
10283 if tail.is_empty()
10288 || (!tail.starts_with('"')
10289 && !tail.starts_with('\'')
10290 && !line.starts_with("msgstr")
10291 && !line.starts_with("msgid"))
10292 {
10293 out = out.trim_end().to_owned();
10294 }
10295 out
10296 };
10297
10298 for i in (0..search_end).rev() {
10299 let line = old_lines[i];
10300 if line.is_empty() {
10301 continue;
10302 }
10303 if func_context_line_matches(line, funcname_matcher) {
10304 if let Some(matcher) = funcname_matcher {
10305 if let Some(matched) = matcher.match_line(line) {
10306 return Some(truncate(&matched));
10307 }
10308 continue;
10309 }
10310 return Some(truncate(line));
10311 }
10312 }
10313 None
10314}
10315
10316pub fn format_stat_line(
10320 path: &str,
10321 insertions: usize,
10322 deletions: usize,
10323 max_path_len: usize,
10324) -> String {
10325 format_stat_line_width(path, insertions, deletions, max_path_len, 0)
10326}
10327
10328pub fn format_stat_line_width(
10330 path: &str,
10331 insertions: usize,
10332 deletions: usize,
10333 max_path_len: usize,
10334 count_width: usize,
10335) -> String {
10336 let mut line =
10337 crate::diffstat::format_single_stat_line(path, insertions, deletions, max_path_len)
10338 .unwrap_or_else(|_| String::new());
10339 if count_width > 0 && line.contains(" | ") {
10340 let total = insertions + deletions;
10341 let cw = count_width;
10342 if let Some((left, rest)) = line.split_once(" | ") {
10343 if let Some((_, bar)) = rest.split_once(' ') {
10344 line = format!(" {left} | {total:>cw$} {bar}", cw = cw);
10345 } else {
10346 line = format!(" {left} | {total:>cw$}", cw = cw);
10347 }
10348 }
10349 }
10350 line
10351}
10352
10353fn last_line_index_without_trailing_newline(text: &str) -> Option<usize> {
10354 LineSide::from_text(text).incomplete_last_line_index()
10355}
10356
10357fn parse_unified_hunk_starts(header: &str) -> Option<(usize, usize)> {
10358 let rest = header.strip_prefix("@@ ")?;
10359 let (old_part, new_part) = rest.split_once(" +")?;
10360 let old_start = parse_unified_range_start(old_part.strip_prefix('-')?)?;
10361 let new_start = parse_unified_range_start(new_part.split(" @@").next()?)?;
10362 Some((old_start, new_start))
10363}
10364
10365fn parse_unified_range_start(spec: &str) -> Option<usize> {
10366 let spec = spec.trim();
10367 if let Some((start, _)) = spec.split_once(',') {
10368 start.parse().ok()
10369 } else {
10370 spec.parse().ok()
10371 }
10372}
10373
10374struct UnifiedDiffNoNewlineAnnotator {
10376 old_last: Option<usize>,
10377 new_last: Option<usize>,
10378 old_line: usize,
10379 new_line: usize,
10380 passthrough: bool,
10381}
10382
10383impl UnifiedDiffNoNewlineAnnotator {
10384 fn new(old: &str, new: &str) -> Self {
10385 let old_last = last_line_index_without_trailing_newline(old);
10386 let new_last = last_line_index_without_trailing_newline(new);
10387 Self {
10388 old_last,
10389 new_last,
10390 old_line: 1,
10391 new_line: 1,
10392 passthrough: old_last.is_none() && new_last.is_none(),
10393 }
10394 }
10395
10396 fn write_hunk<W: std::io::Write>(&mut self, out: &mut W, patch: &str) -> Result<()> {
10397 if self.passthrough {
10398 out.write_all(patch.as_bytes())?;
10399 return Ok(());
10400 }
10401 let mut lines = patch.lines().peekable();
10402 while let Some(line) = lines.next() {
10403 self.write_annotated_line(out, line, &mut lines)?;
10404 }
10405 Ok(())
10406 }
10407
10408 fn write_annotated_line<W: std::io::Write>(
10409 &mut self,
10410 out: &mut W,
10411 line: &str,
10412 lines: &mut std::iter::Peekable<std::str::Lines<'_>>,
10413 ) -> Result<()> {
10414 if let Some((o, n)) = line
10415 .strip_prefix("@@ ")
10416 .and_then(|_| parse_unified_hunk_starts(line))
10417 {
10418 self.old_line = o;
10419 self.new_line = n;
10420 writeln!(out, "{line}")?;
10421 return Ok(());
10422 }
10423 if line.starts_with("--- ")
10424 || line.starts_with("+++ ")
10425 || line.starts_with("diff --git ")
10426 || line.starts_with("diff --cc ")
10427 || line.starts_with("diff --combined ")
10428 {
10429 writeln!(out, "{line}")?;
10430 return Ok(());
10431 }
10432 if line.starts_with('-') {
10433 writeln!(out, "{line}")?;
10434 let already_marked = lines
10435 .peek()
10436 .is_some_and(|next| *next == "\\ No newline at end of file");
10437 if !already_marked && self.old_last == Some(self.old_line.saturating_sub(1)) {
10438 writeln!(out, "\\ No newline at end of file")?;
10439 }
10440 self.old_line += 1;
10441 return Ok(());
10442 }
10443 if line.starts_with('+') {
10444 writeln!(out, "{line}")?;
10445 let already_marked = lines
10446 .peek()
10447 .is_some_and(|next| *next == "\\ No newline at end of file");
10448 if !already_marked && self.new_last == Some(self.new_line.saturating_sub(1)) {
10449 writeln!(out, "\\ No newline at end of file")?;
10450 }
10451 self.new_line += 1;
10452 return Ok(());
10453 }
10454 if line.starts_with(' ') {
10455 writeln!(out, "{line}")?;
10456 let already_marked = lines
10457 .peek()
10458 .is_some_and(|next| *next == "\\ No newline at end of file");
10459 if !already_marked {
10460 let old_idx = self.old_line.saturating_sub(1);
10461 let new_idx = self.new_line.saturating_sub(1);
10462 if self.old_last == Some(old_idx) || self.new_last == Some(new_idx) {
10463 writeln!(out, "\\ No newline at end of file")?;
10464 }
10465 }
10466 self.old_line += 1;
10467 self.new_line += 1;
10468 return Ok(());
10469 }
10470 writeln!(out, "{line}")?;
10471 Ok(())
10472 }
10473}
10474
10475#[must_use]
10478pub fn annotate_unified_diff_no_newline_at_eof(patch: &str, old: &str, new: &str) -> String {
10479 let mut annotator = UnifiedDiffNoNewlineAnnotator::new(old, new);
10480 if annotator.passthrough {
10481 return patch.to_string();
10482 }
10483 let mut out = Vec::with_capacity(patch.len() + 64);
10484 let mut lines = patch.lines().peekable();
10485 while let Some(line) = lines.next() {
10486 if annotator
10487 .write_annotated_line(&mut out, line, &mut lines)
10488 .is_err()
10489 {
10490 return patch.to_string();
10491 }
10492 }
10493 String::from_utf8(out).unwrap_or_else(|_| patch.to_string())
10494}
10495
10496#[must_use]
10498pub fn normalize_ignore_space_change_line(line: &str) -> String {
10499 let mut result = String::with_capacity(line.len());
10500 let mut in_space = false;
10501 for c in line.chars() {
10502 if c.is_whitespace() {
10503 if !in_space {
10504 result.push(' ');
10505 in_space = true;
10506 }
10507 } else {
10508 result.push(c);
10509 in_space = false;
10510 }
10511 }
10512 while result.ends_with(' ') {
10513 result.pop();
10514 }
10515 result
10516}
10517
10518#[must_use]
10524pub fn normalize_ignore_space_change(content: &str) -> String {
10525 content
10526 .lines()
10527 .map(normalize_ignore_space_change_line)
10528 .collect::<Vec<_>>()
10529 .join("\n")
10530}
10531
10532pub fn count_changes(old_content: &str, new_content: &str) -> (usize, usize) {
10536 count_changes_with_algorithm(old_content, new_content, similar::Algorithm::Myers, false)
10537}
10538
10539#[must_use]
10544pub fn count_changes_with_algorithm(
10545 old_content: &str,
10546 new_content: &str,
10547 algorithm: similar::Algorithm,
10548 use_git_histogram: bool,
10549) -> (usize, usize) {
10550 if use_git_histogram {
10551 use imara_diff::{Algorithm, Diff, InternedInput};
10552 let input = InternedInput::new(old_content, new_content);
10553 let mut d = Diff::compute(Algorithm::Histogram, &input);
10554 d.postprocess_lines(&input);
10555 return (d.count_additions() as usize, d.count_removals() as usize);
10556 }
10557
10558 use similar::{ChangeTag, TextDiff};
10559
10560 let diff = TextDiff::configure()
10561 .algorithm(algorithm)
10562 .diff_lines(old_content, new_content);
10563 let mut ins = 0;
10564 let mut del = 0;
10565
10566 for change in diff.iter_all_changes() {
10567 match change.tag() {
10568 ChangeTag::Insert => ins += 1,
10569 ChangeTag::Delete => del += 1,
10570 ChangeTag::Equal => {}
10571 }
10572 }
10573
10574 (ins, del)
10575}
10576
10577#[must_use]
10579pub fn count_changes_git_histogram(old_content: &str, new_content: &str) -> (usize, usize) {
10580 count_changes_with_algorithm(old_content, new_content, similar::Algorithm::Myers, true)
10581}
10582
10583#[must_use]
10588pub fn count_git_lines(data: &[u8]) -> usize {
10589 if data.is_empty() {
10590 return 0;
10591 }
10592 let mut count = 0usize;
10593 let mut nl_just_seen = false;
10594 for &ch in data {
10595 if ch == b'\n' {
10596 count += 1;
10597 nl_just_seen = true;
10598 } else {
10599 nl_just_seen = false;
10600 }
10601 }
10602 if !nl_just_seen {
10603 count += 1;
10604 }
10605 count
10606}
10607
10608pub const GIT_DIFF_MAX_SCORE: u64 = 60_000;
10610const DIFF_MAX_SCORE: u64 = GIT_DIFF_MAX_SCORE;
10611const DIFF_MINIMUM_BREAK_SIZE: usize = 400;
10612const DIFF_DEFAULT_BREAK_SCORE: u64 = 30_000;
10613pub const GIT_DIFF_DEFAULT_BREAK_SCORE: u64 = DIFF_DEFAULT_BREAK_SCORE;
10615pub const GIT_DIFF_DEFAULT_MERGE_SCORE_AFTER_BREAK: u64 = 36_000;
10618const DIFF_HASHBASE: u32 = 107_927;
10619
10620#[derive(Clone, Copy, Default)]
10621struct SpanSlot {
10622 hashval: u32,
10623 cnt: u32,
10624}
10625
10626struct SpanHashTop {
10627 alloc_log2: u8,
10628 free_slots: i32,
10629 data: Vec<SpanSlot>,
10630}
10631
10632impl SpanHashTop {
10633 fn new(initial_log2: u8) -> Self {
10634 let cap = 1usize << initial_log2;
10635 Self {
10636 alloc_log2: initial_log2,
10637 free_slots: initial_free(initial_log2),
10638 data: vec![SpanSlot::default(); cap],
10639 }
10640 }
10641
10642 fn len(&self) -> usize {
10643 1usize << self.alloc_log2
10644 }
10645
10646 fn add_span(&mut self, hashval: u32, cnt: u32) {
10647 loop {
10648 let lim = self.len();
10649 let mut bucket = (hashval as usize) & (lim - 1);
10650 loop {
10651 let h = &mut self.data[bucket];
10652 if h.cnt == 0 {
10653 h.hashval = hashval;
10654 h.cnt = cnt;
10655 self.free_slots -= 1;
10656 if self.free_slots < 0 {
10657 self.rehash();
10658 break;
10659 }
10660 return;
10661 }
10662 if h.hashval == hashval {
10663 h.cnt = h.cnt.saturating_add(cnt);
10664 return;
10665 }
10666 bucket += 1;
10667 if bucket >= lim {
10668 bucket = 0;
10669 }
10670 }
10671 }
10672 }
10673
10674 fn rehash(&mut self) {
10675 let old = std::mem::take(&mut self.data);
10676 let old_log = self.alloc_log2;
10677 self.alloc_log2 = old_log.saturating_add(1);
10678 let new_len = 1usize << self.alloc_log2;
10679 self.free_slots = initial_free(self.alloc_log2);
10680 self.data = vec![SpanSlot::default(); new_len];
10681 let old_sz = 1usize << old_log;
10682 for o in old.iter().take(old_sz) {
10683 let o = *o;
10684 if o.cnt == 0 {
10685 continue;
10686 }
10687 self.add_span_after_rehash(o.hashval, o.cnt);
10688 }
10689 }
10690
10691 fn add_span_after_rehash(&mut self, hashval: u32, cnt: u32) {
10692 loop {
10693 let lim = self.len();
10694 let mut bucket = (hashval as usize) & (lim - 1);
10695 loop {
10696 let h = &mut self.data[bucket];
10697 if h.cnt == 0 {
10698 h.hashval = hashval;
10699 h.cnt = cnt;
10700 self.free_slots -= 1;
10701 if self.free_slots < 0 {
10702 self.rehash();
10703 break;
10704 }
10705 return;
10706 }
10707 if h.hashval == hashval {
10708 h.cnt = h.cnt.saturating_add(cnt);
10709 return;
10710 }
10711 bucket += 1;
10712 if bucket >= lim {
10713 bucket = 0;
10714 }
10715 }
10716 }
10717 }
10718
10719 fn sort_by_hashval(&mut self) {
10720 let sz = self.len();
10721 self.data[..sz].sort_by(|a, b| {
10722 if a.cnt == 0 {
10723 return std::cmp::Ordering::Greater;
10724 }
10725 if b.cnt == 0 {
10726 return std::cmp::Ordering::Less;
10727 }
10728 a.hashval.cmp(&b.hashval)
10729 });
10730 }
10731}
10732
10733fn initial_free(sz_log2: u8) -> i32 {
10734 let sz = sz_log2 as i32;
10735 ((1i32 << sz_log2) * (sz - 3) / sz).max(0)
10736}
10737
10738fn hash_blob_spans(buf: &[u8], is_text: bool) -> SpanHashTop {
10739 let mut hash = SpanHashTop::new(9);
10740 let mut n = 0u32;
10741 let mut accum1: u32 = 0;
10742 let mut accum2: u32 = 0;
10743 let mut i = 0usize;
10744 while i < buf.len() {
10745 let c = buf[i] as u32;
10746 let old_1 = accum1;
10747 i += 1;
10748
10749 if is_text && c == b'\r' as u32 && i < buf.len() && buf[i] == b'\n' {
10750 continue;
10751 }
10752
10753 accum1 = accum1.wrapping_shl(7) ^ accum2.wrapping_shr(25);
10754 accum2 = accum2.wrapping_shl(7) ^ old_1.wrapping_shr(25);
10755 accum1 = accum1.wrapping_add(c);
10756 n += 1;
10757 if n < 64 && c != b'\n' as u32 {
10758 continue;
10759 }
10760 let hashval = (accum1.wrapping_add(accum2.wrapping_mul(0x61))) % DIFF_HASHBASE;
10761 hash.add_span(hashval, n);
10762 n = 0;
10763 accum1 = 0;
10764 accum2 = 0;
10765 }
10766 if n > 0 {
10767 let hashval = (accum1.wrapping_add(accum2.wrapping_mul(0x61))) % DIFF_HASHBASE;
10768 hash.add_span(hashval, n);
10769 }
10770 hash.sort_by_hashval();
10771 hash
10772}
10773
10774#[must_use]
10779pub fn diffcore_count_changes(old: &[u8], new: &[u8]) -> (u64, u64) {
10780 crate::trace::trace_span!("grit.diff.blob");
10781 let src_is_text = !crate::merge_file::is_binary(old);
10782 let dst_is_text = !crate::merge_file::is_binary(new);
10783 let src_count = hash_blob_spans(old, src_is_text);
10784 let dst_count = hash_blob_spans(new, dst_is_text);
10785 let mut sc: u64 = 0;
10786 let mut la: u64 = 0;
10787 let mut si = 0usize;
10788 let mut di = 0usize;
10789 let src_len = src_count.len();
10790 let dst_len = dst_count.len();
10791 loop {
10792 if si >= src_len || src_count.data[si].cnt == 0 {
10793 break;
10794 }
10795 let s_hash = src_count.data[si].hashval;
10796 let s_cnt = u64::from(src_count.data[si].cnt);
10797 while di < dst_len && dst_count.data[di].cnt != 0 && dst_count.data[di].hashval < s_hash {
10798 la += u64::from(dst_count.data[di].cnt);
10799 di += 1;
10800 }
10801 let mut dst_cnt = 0u64;
10802 if di < dst_len && dst_count.data[di].cnt != 0 && dst_count.data[di].hashval == s_hash {
10803 dst_cnt = u64::from(dst_count.data[di].cnt);
10804 di += 1;
10805 }
10806 if s_cnt < dst_cnt {
10807 la += dst_cnt - s_cnt;
10808 sc += s_cnt;
10809 } else {
10810 sc += dst_cnt;
10811 }
10812 si += 1;
10813 }
10814 while di < dst_len && dst_count.data[di].cnt != 0 {
10815 la += u64::from(dst_count.data[di].cnt);
10816 di += 1;
10817 }
10818 (sc, la)
10819}
10820
10821#[must_use]
10824pub fn should_break_rewrite_for_stat(old: &[u8], new: &[u8]) -> bool {
10825 should_break_rewrite_inner(old, new, DIFF_DEFAULT_BREAK_SCORE)
10826}
10827
10828#[must_use]
10832pub fn should_break_rewrite_pair(old: &[u8], new: &[u8], break_score: u64) -> bool {
10833 should_break_rewrite_inner(old, new, break_score)
10834}
10835
10836#[must_use]
10838pub fn apply_diff_filter(
10839 entries: Vec<DiffEntry>,
10840 filter: Option<super::filter::DiffFilter>,
10841) -> Vec<DiffEntry> {
10842 match filter {
10843 Some(f) => f.filter_entries(entries),
10844 None => entries,
10845 }
10846}
10847
10848#[must_use]
10850pub fn parse_diff_rename_threshold_percent(arg: &str) -> Option<u8> {
10851 let token = if arg.is_empty() { "50" } else { arg };
10852 let internal = parse_diff_rename_score_token(token)?;
10853 Some((internal.saturating_mul(100) / GIT_DIFF_MAX_SCORE).min(100) as u8)
10854}
10855
10856pub fn parse_diff_rename_score_token(arg: &str) -> Option<u64> {
10859 let mut num: u64 = 0;
10860 let mut scale: u64 = 1;
10861 let mut dot = false;
10862 let mut saw_digit = false;
10863 for ch in arg.chars() {
10864 if !dot && ch == '.' {
10865 scale = 1;
10866 dot = true;
10867 continue;
10868 }
10869 if ch == '%' {
10870 scale = if dot { scale.saturating_mul(100) } else { 100 };
10871 break;
10872 }
10873 if ch.is_ascii_digit() {
10874 saw_digit = true;
10875 if scale < 100_000 {
10876 scale = scale.saturating_mul(10);
10877 num = num.saturating_mul(10) + u64::from(ch as u8 - b'0');
10878 }
10879 } else {
10880 break;
10881 }
10882 }
10883 if !saw_digit {
10884 return None;
10885 }
10886 Some(if num >= scale {
10887 GIT_DIFF_MAX_SCORE
10888 } else {
10889 GIT_DIFF_MAX_SCORE * num / scale
10890 })
10891}
10892
10893#[must_use]
10896pub fn rewrite_merge_score(old: &[u8], new: &[u8]) -> Option<u64> {
10897 if old.is_empty() {
10898 return None;
10899 }
10900 let max_size = old.len().max(new.len());
10901 if max_size < DIFF_MINIMUM_BREAK_SIZE {
10902 return None;
10903 }
10904 let (src_copied, _) = diffcore_count_changes(old, new);
10905 let src_copied = src_copied.min(old.len() as u64);
10906 let src_removed = (old.len() as u64).saturating_sub(src_copied);
10907 Some(src_removed * DIFF_MAX_SCORE / old.len() as u64)
10908}
10909
10910#[must_use]
10912pub fn rewrite_dissimilarity_index_percent(old: &[u8], new: &[u8]) -> Option<u32> {
10913 let score = rewrite_merge_score(old, new)?;
10914 Some((score * 100 / DIFF_MAX_SCORE).min(100) as u32)
10915}
10916
10917fn should_break_rewrite_inner(src: &[u8], dst: &[u8], break_score: u64) -> bool {
10918 if src.is_empty() {
10919 return false;
10920 }
10921 let max_size = src.len().max(dst.len());
10922 if max_size < DIFF_MINIMUM_BREAK_SIZE {
10923 return false;
10924 }
10925 let (src_copied, literal_added) = diffcore_count_changes(src, dst);
10926 let src_copied = src_copied.min(src.len() as u64);
10927 let mut literal_added = literal_added;
10928 let dst_len = dst.len() as u64;
10929 if src_copied < dst_len && literal_added + src_copied > dst_len {
10930 literal_added = dst_len.saturating_sub(src_copied);
10931 }
10932 let src_removed = (src.len() as u64).saturating_sub(src_copied);
10933 let merge_score = src_removed * DIFF_MAX_SCORE / src.len() as u64;
10934 if merge_score > break_score {
10935 return true;
10936 }
10937 let delta_size = src_removed.saturating_add(literal_added);
10938 if delta_size * DIFF_MAX_SCORE / (max_size as u64) < break_score {
10939 return false;
10940 }
10941 let s = src.len() as u64;
10942 if (s * break_score < src_removed * DIFF_MAX_SCORE)
10943 && (literal_added * 20 < src_removed)
10944 && (literal_added * 20 < src_copied)
10945 {
10946 return false;
10947 }
10948 true
10949}
10950
10951struct FlatEntry {
10955 path: String,
10956 mode: u32,
10957 oid: ObjectId,
10958}
10959
10960fn flatten_tree(odb: &Odb, tree_oid: &ObjectId, prefix: &str) -> Result<Vec<FlatEntry>> {
10961 let entries = read_tree(odb, tree_oid)?;
10962 let mut result = Vec::new();
10963
10964 for entry in entries {
10965 let name_str = String::from_utf8_lossy(&entry.name);
10966 let path = format_path(prefix, &name_str);
10967 if is_tree_mode(entry.mode) {
10968 let nested = flatten_tree(odb, &entry.oid, &path)?;
10969 result.extend(nested);
10970 } else {
10971 result.push(FlatEntry {
10972 path,
10973 mode: entry.mode,
10974 oid: entry.oid,
10975 });
10976 }
10977 }
10978
10979 Ok(result)
10980}
10981
10982pub fn head_path_states(
10984 odb: &Odb,
10985 head_tree: Option<&ObjectId>,
10986) -> Result<std::collections::BTreeMap<String, (u32, ObjectId)>> {
10987 let mut m = std::collections::BTreeMap::new();
10988 let Some(t) = head_tree else {
10989 return Ok(m);
10990 };
10991 for fe in flatten_tree(odb, t, "")? {
10992 m.insert(fe.path, (fe.mode, fe.oid));
10993 }
10994 Ok(m)
10995}
10996
10997fn is_tree_mode(mode: u32) -> bool {
10999 mode == 0o040000
11000}
11001
11002fn path_order(a: &DiffEntry, b: &DiffEntry) -> std::cmp::Ordering {
11005 use crate::repo_relpath::storage_bytes;
11006 storage_bytes(a.path()).cmp(&storage_bytes(b.path()))
11007}
11008
11009fn entries_sorted_by_path(entries: &[DiffEntry]) -> bool {
11010 entries.is_sorted_by(|a, b| path_order(a, b).is_le())
11011}
11012
11013fn entries_strictly_sorted_by_path(entries: &[DiffEntry]) -> bool {
11014 entries.is_sorted_by(|a, b| path_order(a, b).is_lt())
11015}
11016
11017pub fn sort_entries_by_path(entries: &mut [DiffEntry]) {
11028 if entries_sorted_by_path(entries) {
11029 return;
11030 }
11031 entries.sort_by(path_order);
11032}
11033
11034fn format_path(prefix: &str, name: &str) -> String {
11036 if prefix.is_empty() {
11037 name.to_owned()
11038 } else {
11039 let mut path = String::with_capacity(prefix.len() + 1 + name.len());
11040 path.push_str(prefix);
11041 path.push('/');
11042 path.push_str(name);
11043 path
11044 }
11045}
11046
11047pub fn format_mode(mode: u32) -> String {
11049 format!("{mode:06o}")
11050}
11051
11052#[must_use]
11056pub fn read_submodule_head_for_checkout(sub_dir: &Path) -> Option<ObjectId> {
11057 read_submodule_head(sub_dir)
11058}
11059
11060#[must_use]
11065pub fn submodule_commit_subject_line(c: &CommitData) -> String {
11066 let enc = c.encoding.as_deref().unwrap_or("UTF-8");
11067 let is_latin1 = enc.eq_ignore_ascii_case("ISO8859-1")
11068 || enc.eq_ignore_ascii_case("ISO-8859-1")
11069 || enc.eq_ignore_ascii_case("LATIN1")
11070 || enc.eq_ignore_ascii_case("ISO-8859-15");
11071 if let Some(raw) = c.raw_message.as_deref() {
11072 let line = raw.split(|b| *b == b'\n').next().unwrap_or(raw);
11073 if is_latin1 {
11074 return line
11075 .iter()
11076 .map(|&b| b as char)
11077 .collect::<String>()
11078 .trim()
11079 .to_owned();
11080 }
11081 return String::from_utf8_lossy(line).trim().to_string();
11082 }
11083 c.message.lines().next().unwrap_or("").trim().to_owned()
11084}
11085
11086fn submodule_worktree_is_unpopulated_placeholder(sub_dir: &Path) -> bool {
11089 match fs::read_dir(sub_dir) {
11090 Ok(mut it) => it.next().is_none(),
11091 Err(e) if e.kind() == std::io::ErrorKind::NotFound => true,
11092 Err(_) => false,
11093 }
11094}
11095
11096fn read_submodule_head(sub_dir: &Path) -> Option<ObjectId> {
11097 read_submodule_head_oid(sub_dir)
11098}
11099
11100#[must_use]
11102pub fn submodule_embedded_git_dir(sub_dir: &Path) -> Option<PathBuf> {
11103 let gitfile = sub_dir.join(".git");
11104 if gitfile.is_file() {
11105 let content = fs::read_to_string(&gitfile).ok()?;
11106 let gitdir = content
11107 .lines()
11108 .find_map(|l| l.strip_prefix("gitdir: "))?
11109 .trim();
11110 Some(if Path::new(gitdir).is_absolute() {
11111 PathBuf::from(gitdir)
11112 } else {
11113 sub_dir.join(gitdir)
11114 })
11115 } else if gitfile.is_dir() {
11116 Some(gitfile)
11117 } else {
11118 None
11119 }
11120}
11121
11122#[must_use]
11128pub(crate) fn is_unregistered_embedded_git_repo(index: &Index, rel: &str, abs: &Path) -> bool {
11129 if rel.is_empty() {
11130 return false;
11131 }
11132 if index
11133 .get(rel.as_bytes(), 0)
11134 .is_some_and(|e| e.mode == crate::index::MODE_GITLINK)
11135 {
11136 return false;
11137 }
11138 let Some(git_dir) = submodule_embedded_git_dir(abs) else {
11139 return false;
11140 };
11141 crate::submodule_gitdir::is_valid_git_dir(&git_dir)
11142}
11143
11144fn find_superproject_git(sub_dir: &Path) -> Option<(PathBuf, PathBuf)> {
11146 let mut cur = sub_dir.parent()?;
11147 loop {
11148 let git_path = cur.join(".git");
11149 if git_path.exists() {
11150 let gd = if git_path.is_file() {
11151 let content = fs::read_to_string(&git_path).ok()?;
11152 let line = content
11153 .lines()
11154 .find_map(|l| l.strip_prefix("gitdir: "))?
11155 .trim();
11156 if Path::new(line).is_absolute() {
11157 PathBuf::from(line)
11158 } else {
11159 cur.join(line)
11160 }
11161 } else {
11162 git_path
11163 };
11164 return Some((cur.to_path_buf(), gd));
11165 }
11166 cur = cur.parent()?;
11167 }
11168}
11169
11170pub fn read_submodule_head_oid(sub_dir: &Path) -> Option<ObjectId> {
11176 let mut git_dir = submodule_embedded_git_dir(sub_dir)?;
11179 if let Some((super_wt, super_git_dir)) = find_superproject_git(sub_dir) {
11180 let rel = sub_dir.strip_prefix(&super_wt).ok()?;
11181 let rel_str = rel.to_string_lossy().replace('\\', "/");
11182 let local_mod = super_git_dir
11183 .join("modules")
11184 .join(rel_str.trim_start_matches('/'));
11185 if local_mod.join("HEAD").exists() {
11186 let sg = super_git_dir
11187 .canonicalize_simplified()
11188 .unwrap_or(super_git_dir);
11189 let cur = git_dir
11190 .canonicalize_simplified()
11191 .unwrap_or_else(|_| git_dir.clone());
11192 if !cur.starts_with(&sg) {
11193 git_dir = local_mod;
11194 }
11195 }
11196 }
11197 let head_content = fs::read_to_string(git_dir.join("HEAD")).ok()?;
11198 let head_trimmed = head_content.trim();
11199 if head_trimmed.starts_with("ref: ") {
11200 match crate::refs::resolve_ref(&git_dir, "HEAD") {
11204 Ok(oid) => Some(oid),
11205 Err(_) => {
11206 let mut found = None;
11207 for branch in ["main", "master"] {
11208 let p = git_dir.join("refs/heads").join(branch);
11209 if let Ok(s) = fs::read_to_string(&p) {
11210 if let Ok(o) = ObjectId::from_hex(s.trim()) {
11211 found = Some(o);
11212 break;
11213 }
11214 }
11215 }
11216 found
11217 }
11218 }
11219 } else {
11220 ObjectId::from_hex(head_trimmed).ok()
11221 }
11222}
11223
11224#[must_use]
11236pub fn submodule_head_object_broken(sub_dir: &Path) -> bool {
11237 let Some(sub_git_dir) = submodule_embedded_git_dir(sub_dir) else {
11238 return false;
11239 };
11240 let Some(head_oid) = read_submodule_head_oid(sub_dir) else {
11241 return false;
11242 };
11243 let odb = Odb::new(&sub_git_dir.join("objects"));
11244 match odb.read(&head_oid) {
11245 Ok(obj) => parse_commit(&obj.data).is_err(),
11248 Err(_) => true,
11249 }
11250}
11251
11252#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
11254pub struct SubmodulePorcelainFlags {
11255 pub new_commits: bool,
11257 pub modified: bool,
11259 pub untracked: bool,
11261}
11262
11263pub fn submodule_porcelain_flags(
11269 super_worktree: &Path,
11270 rel_path: &str,
11271 recorded_oid: ObjectId,
11272) -> SubmodulePorcelainFlags {
11273 let sub_dir = super_worktree.join(rel_path);
11274 let Some(sub_git_dir) = submodule_embedded_git_dir(&sub_dir) else {
11275 return SubmodulePorcelainFlags::default();
11276 };
11277 let Some(sub_head) = read_submodule_head_oid(&sub_dir) else {
11278 return SubmodulePorcelainFlags::default();
11279 };
11280
11281 let new_commits = sub_head != recorded_oid;
11282
11283 let index_path = sub_git_dir.join("index");
11284 let mut sub_index = match crate::index::Index::load(&index_path) {
11285 Ok(ix) => ix,
11286 Err(_) => {
11287 return SubmodulePorcelainFlags {
11288 new_commits,
11289 ..Default::default()
11290 }
11291 }
11292 };
11293
11294 let tracked: std::collections::BTreeSet<String> = sub_index
11295 .entries
11296 .iter()
11297 .filter(|e| e.stage() == 0)
11298 .map(|e| String::from_utf8_lossy(&e.path).into_owned())
11299 .collect();
11300 let untracked = submodule_dir_has_untracked_inner(&sub_dir, &sub_dir, &tracked, &sub_index);
11301
11302 let objects_dir = sub_git_dir.join("objects");
11303 let odb = Odb::new(&objects_dir);
11304
11305 let sub_head_tree = (|| -> Option<ObjectId> {
11306 let h = fs::read_to_string(sub_git_dir.join("HEAD")).ok()?;
11307 let h_str = h.trim();
11308 let commit_oid = if let Some(r) = h_str.strip_prefix("ref: ") {
11309 let oid_hex = fs::read_to_string(sub_git_dir.join(r)).ok()?;
11310 ObjectId::from_hex(oid_hex.trim()).ok()?
11311 } else {
11312 ObjectId::from_hex(h_str).ok()?
11313 };
11314 let obj = odb.read(&commit_oid).ok()?;
11315 let commit = parse_commit(&obj.data).ok()?;
11316 Some(commit.tree)
11317 })();
11318
11319 let staged_dirty = sub_head_tree
11320 .as_ref()
11321 .map(|t| diff_index_to_tree(&odb, &sub_index, Some(t), false).map(|v| !v.is_empty()))
11322 .unwrap_or(Ok(false));
11323 let staged_dirty = staged_dirty.unwrap_or(false);
11324
11325 let unstaged_dirty = diff_index_to_worktree(&odb, &mut sub_index, &sub_dir, false, true)
11326 .map(|v| !v.is_empty())
11327 .unwrap_or(false);
11328
11329 let mut modified = staged_dirty || unstaged_dirty;
11330
11331 for e in &sub_index.entries {
11336 if e.stage() != 0 || e.mode != 0o160000 {
11337 continue;
11338 }
11339 let child = String::from_utf8_lossy(&e.path).into_owned();
11340 let full_rel = if rel_path.is_empty() {
11341 child
11342 } else {
11343 format!("{rel_path}/{child}")
11344 };
11345 let nested = submodule_porcelain_flags(super_worktree, &full_rel, e.oid);
11346 modified |= nested.modified;
11347 }
11348
11349 SubmodulePorcelainFlags {
11350 new_commits,
11351 modified,
11352 untracked,
11353 }
11354}
11355
11356fn submodule_dir_has_untracked_inner(
11357 dir: &Path,
11358 root: &Path,
11359 tracked: &std::collections::BTreeSet<String>,
11360 owning_index: &Index,
11361) -> bool {
11362 let entries = match fs::read_dir(dir) {
11363 Ok(e) => e,
11364 Err(_) => return false,
11365 };
11366 let mut sorted: Vec<_> = entries.filter_map(|e| e.ok()).collect();
11367 sorted.sort_by_key(|e| e.file_name());
11368
11369 for entry in sorted {
11370 let name = entry.file_name().to_string_lossy().to_string();
11371 if name == ".git" {
11372 continue;
11373 }
11374 let path = entry.path();
11375 let rel = path
11376 .strip_prefix(root)
11377 .map(|p| p.to_string_lossy().to_string())
11378 .unwrap_or_else(|_| name.clone());
11379
11380 let is_dir = entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false);
11381 if is_dir {
11382 let is_gitlink = owning_index
11383 .get(rel.as_bytes(), 0)
11384 .is_some_and(|e| e.mode == 0o160000);
11385 if is_gitlink {
11386 let Some(nested_git) = submodule_embedded_git_dir(&path) else {
11387 continue;
11388 };
11389 let nested_index_path = nested_git.join("index");
11390 let Ok(nested_ix) = crate::index::Index::load(&nested_index_path) else {
11391 continue;
11392 };
11393 let nested_tracked: std::collections::BTreeSet<String> = nested_ix
11394 .entries
11395 .iter()
11396 .filter(|e| e.stage() == 0)
11397 .map(|e| String::from_utf8_lossy(&e.path).into_owned())
11398 .collect();
11399 if submodule_dir_has_untracked_inner(&path, &path, &nested_tracked, &nested_ix) {
11400 return true;
11401 }
11402 } else if submodule_dir_has_untracked_inner(&path, root, tracked, owning_index) {
11403 return true;
11404 }
11405 } else if !tracked.contains(&rel) {
11406 return true;
11407 }
11408 }
11409 false
11410}
11411
11412pub fn apply_orderfile_entries(
11415 entries: Vec<DiffEntry>,
11416 order_path: &str,
11417 cwd: &Path,
11418) -> Result<Vec<DiffEntry>> {
11419 apply_orderfile(entries, order_path, cwd)
11420}
11421
11422fn apply_orderfile(
11423 mut entries: Vec<DiffEntry>,
11424 order_path: &str,
11425 cwd: &Path,
11426) -> Result<Vec<DiffEntry>> {
11427 let patterns = read_orderfile_patterns(order_path, cwd)?;
11428 let sort_key = |entry: &DiffEntry| -> usize {
11429 let path = entry
11430 .new_path
11431 .as_ref()
11432 .or(entry.old_path.as_ref())
11433 .cloned()
11434 .unwrap_or_default();
11435 for (i, pat) in patterns.iter().enumerate() {
11436 if orderfile_pattern_matches(pat, &path) {
11437 return i;
11438 }
11439 }
11440 patterns.len()
11441 };
11442 entries.sort_by_key(|e| sort_key(e));
11443 Ok(entries)
11444}
11445
11446pub fn read_orderfile_patterns(order_path: &str, cwd: &Path) -> Result<Vec<String>> {
11448 let path = Path::new(order_path);
11449 let resolved = if path.is_absolute() {
11450 path.to_path_buf()
11451 } else {
11452 cwd.join(path)
11453 };
11454 let _meta = std::fs::metadata(&resolved)
11455 .map_err(|e| Error::Message(format!("could not read orderfile {order_path}: {e}")))?;
11456 let mut f = std::fs::File::open(&resolved)
11457 .map_err(|e| Error::Message(format!("could not read orderfile {order_path}: {e}")))?;
11458 let mut content = String::new();
11459 std::io::Read::read_to_string(&mut f, &mut content)
11460 .map_err(|e| Error::Message(format!("could not read orderfile {order_path}: {e}")))?;
11461 Ok(content
11462 .lines()
11463 .map(|l| l.trim().to_string())
11464 .filter(|l| !l.is_empty() && !l.starts_with('#'))
11465 .collect())
11466}
11467
11468pub fn apply_rotate_skip_entries(
11470 mut entries: Vec<DiffEntry>,
11471 rotate_to: Option<&str>,
11472 skip_to: Option<&str>,
11473) -> Result<Vec<DiffEntry>> {
11474 let Some(needle) = rotate_to.or(skip_to) else {
11475 return Ok(entries);
11476 };
11477 let needle = needle.trim();
11478 if needle.is_empty() {
11479 return Ok(entries);
11480 }
11481 let idx = entries
11482 .iter()
11483 .position(|e| e.path() == needle)
11484 .ok_or_else(|| {
11485 Error::DiffPath(crate::error::DiffPathError::NoSuchPathInDiff {
11486 path: needle.to_owned(),
11487 })
11488 })?;
11489 if rotate_to.is_some() {
11490 entries.rotate_left(idx);
11491 }
11492 if let Some(skip) = skip_to.filter(|s| !s.trim().is_empty()) {
11493 let pos = entries
11494 .iter()
11495 .position(|e| e.path() == skip)
11496 .ok_or_else(|| {
11497 Error::DiffPath(crate::error::DiffPathError::NoSuchPathInDiff {
11498 path: skip.to_owned(),
11499 })
11500 })?;
11501 entries.drain(..pos);
11502 }
11503 Ok(entries)
11504}
11505
11506pub fn apply_rotate_skip_log_entries(
11509 odb: &Odb,
11510 commit_tree: &ObjectId,
11511 entries: Vec<DiffEntry>,
11512 rotate_to: Option<&str>,
11513 skip_to: Option<&str>,
11514) -> Result<Vec<DiffEntry>> {
11515 fn trimmed(s: Option<&str>) -> Option<&str> {
11520 s.map(str::trim).filter(|t| !t.is_empty())
11521 }
11522 if trimmed(rotate_to).is_none() && trimmed(skip_to).is_none() {
11523 return Ok(entries);
11524 }
11525 let tree_paths = crate::merge_diff::all_blob_paths_in_tree_order(odb, commit_tree);
11526 apply_rotate_skip_ordered_paths(&tree_paths, entries, rotate_to, skip_to)
11527}
11528
11529fn apply_rotate_skip_ordered_paths(
11530 tree_paths: &[String],
11531 entries: Vec<DiffEntry>,
11532 rotate_to: Option<&str>,
11533 skip_to: Option<&str>,
11534) -> Result<Vec<DiffEntry>> {
11535 let rotate = rotate_to.and_then(|s| {
11536 let t = s.trim();
11537 (!t.is_empty()).then_some(t)
11538 });
11539 let skip = skip_to.and_then(|s| {
11540 let t = s.trim();
11541 (!t.is_empty()).then_some(t)
11542 });
11543 if rotate.is_none() && skip.is_none() {
11544 return Ok(entries);
11545 }
11546
11547 use std::collections::HashMap;
11548 let mut by_path: HashMap<String, DiffEntry> = HashMap::new();
11549 for e in entries {
11550 by_path.insert(e.path().to_string(), e);
11551 }
11552
11553 if rotate.is_none() {
11556 let Some(skip_path) = skip else {
11557 return Ok(by_path.into_values().collect());
11558 };
11559 let idx = tree_paths
11560 .iter()
11561 .position(|p| p == skip_path)
11562 .ok_or_else(|| {
11563 Error::DiffPath(crate::error::DiffPathError::NoSuchPathInDiff {
11564 path: skip_path.to_owned(),
11565 })
11566 })?;
11567 let mut out = Vec::new();
11568 for p in tree_paths.iter().skip(idx) {
11569 if let Some(e) = by_path.remove(p) {
11570 out.push(e);
11571 }
11572 }
11573 return Ok(out);
11574 }
11575
11576 let Some(needle) = rotate else {
11577 return Ok(by_path.into_values().collect());
11578 };
11579 let idx = tree_paths.iter().position(|p| p == needle).ok_or_else(|| {
11580 Error::DiffPath(crate::error::DiffPathError::NoSuchPathInDiff {
11581 path: needle.to_owned(),
11582 })
11583 })?;
11584 let mut order: Vec<String> = tree_paths.to_vec();
11585 order.rotate_left(idx);
11586 if let Some(skip_path) = skip {
11587 let pos = order.iter().position(|p| p == skip_path).ok_or_else(|| {
11588 Error::DiffPath(crate::error::DiffPathError::NoSuchPathInDiff {
11589 path: skip_path.to_owned(),
11590 })
11591 })?;
11592 order.drain(..pos);
11593 }
11594 let mut out = Vec::new();
11595 for p in order {
11596 if let Some(e) = by_path.remove(&p) {
11597 out.push(e);
11598 }
11599 }
11600 Ok(out)
11601}
11602
11603pub fn orderfile_pattern_matches(pattern: &str, path: &str) -> bool {
11606 let name = path.rsplit('/').next().unwrap_or(path);
11607 orderfile_glob_match(pattern, name) || orderfile_glob_match(pattern, path)
11608}
11609
11610fn orderfile_glob_match(pattern: &str, text: &str) -> bool {
11612 let mut pi = 0;
11613 let mut ti = 0;
11614 let pb = pattern.as_bytes();
11615 let tb = text.as_bytes();
11616 let mut star_pi = usize::MAX;
11617 let mut star_ti = 0;
11618
11619 while ti < tb.len() {
11620 if pi < pb.len() && (pb[pi] == b'?' || pb[pi] == tb[ti]) {
11621 pi += 1;
11622 ti += 1;
11623 } else if pi < pb.len() && pb[pi] == b'*' {
11624 star_pi = pi;
11625 star_ti = ti;
11626 pi += 1;
11627 } else if star_pi != usize::MAX {
11628 pi = star_pi + 1;
11629 star_ti += 1;
11630 ti = star_ti;
11631 } else {
11632 return false;
11633 }
11634 }
11635 while pi < pb.len() && pb[pi] == b'*' {
11636 pi += 1;
11637 }
11638 pi == pb.len()
11639}
11640
11641#[cfg(test)]
11642mod line_compare_key_tests {
11643 use super::*;
11644 use crate::diff::unified_emit::line_diff_token;
11645 use crate::diff::WhitespaceMode;
11646
11647 #[test]
11648 fn borrowed_keys_match_owned_line_tokens() {
11649 let texts = [
11650 "",
11651 "\n",
11652 "a",
11653 "a\n",
11654 "a\nb",
11655 "a\r\nb\r\n",
11656 "a\r\nb\r",
11657 "\n\n\nlast",
11658 "x\n\r\n\ry\n",
11659 ];
11660 for text in texts {
11661 let side = LineSide::from_text(text);
11662 let mut normalized = Vec::new();
11663 let keys = line_compare_keys(text, &side, WhitespaceMode::default(), &mut normalized);
11664 let expected: Vec<String> = side.lines.iter().map(line_diff_token).collect();
11665 assert_eq!(keys, expected, "text {text:?}");
11666 let source = text.as_bytes().as_ptr_range();
11667 assert!(
11668 keys.iter()
11669 .all(|key| source.contains(&key.as_ptr()) || key.is_empty()),
11670 "text {text:?}"
11671 );
11672 }
11673 }
11674
11675 #[test]
11676 fn whitespace_modes_compare_normalized_bodies() {
11677 let text = "a b\nab \n";
11678 let side = LineSide::from_text(text);
11679 let ws = WhitespaceMode {
11680 ignore_all_space: true,
11681 ..WhitespaceMode::default()
11682 };
11683 let mut normalized = Vec::new();
11684 let keys = line_compare_keys(text, &side, ws, &mut normalized);
11685 assert_eq!(keys, vec!["ab", "ab"]);
11686 }
11687
11688 #[test]
11689 fn xdiff_marks_only_edited_lines_in_large_file() {
11690 let old: String = (0..5_000).map(|i| format!("line{i}\n")).collect();
11691 let new: String = (0..5_000)
11692 .map(|i| {
11693 if i % 100 == 0 {
11694 format!("LINE{i}\n")
11695 } else {
11696 format!("line{i}\n")
11697 }
11698 })
11699 .collect();
11700 let body =
11701 git_xdiff_unified_body_raw(&old, &new, 3, 0, true, false, WhitespaceMode::default());
11702 assert_eq!(body.matches("\n@@ ").count() + 1, 50);
11703 assert_eq!(body.lines().filter(|l| l.starts_with('-')).count(), 50);
11704 assert_eq!(body.lines().filter(|l| l.starts_with('+')).count(), 50);
11705 assert!(body.starts_with("@@ -1,4 +1,4 @@\n-line0\n+LINE0\n line1\n"));
11706 }
11707}
11708
11709#[cfg(test)]
11710mod entry_order_tests {
11711 use super::*;
11712
11713 fn entry(status: DiffStatus, path: &str, score: Option<u32>) -> DiffEntry {
11714 let mode = |present: bool| {
11715 if present {
11716 FileMode::REGULAR
11717 } else {
11718 FileMode::ABSENT
11719 }
11720 };
11721 DiffEntry {
11722 status,
11723 old_path: (status != DiffStatus::Added).then(|| path.to_owned()),
11724 new_path: (status != DiffStatus::Deleted).then(|| path.to_owned()),
11725 old_mode: mode(status != DiffStatus::Added),
11726 new_mode: mode(status != DiffStatus::Deleted),
11727 old_oid: ObjectId::zero(),
11728 new_oid: ObjectId::zero(),
11729 score,
11730 }
11731 }
11732
11733 #[test]
11734 fn sort_entries_by_path_orders_by_path_and_keeps_equal_paths_in_place() {
11735 let mut entries = vec![
11736 entry(DiffStatus::Modified, "b", None),
11737 entry(DiffStatus::Deleted, "a/x", Some(1)),
11738 entry(DiffStatus::Added, "a/x", Some(2)),
11739 entry(DiffStatus::Added, "a", None),
11740 ];
11741 sort_entries_by_path(&mut entries);
11742 let order: Vec<_> = entries.iter().map(|e| (e.path(), e.score)).collect();
11743 assert_eq!(
11744 order,
11745 [("a", None), ("a/x", Some(1)), ("a/x", Some(2)), ("b", None)]
11746 );
11747 let sorted = entries.clone();
11748 sort_entries_by_path(&mut entries);
11749 assert_eq!(entries, sorted);
11750 sort_entries_by_path(&mut []);
11751 }
11752
11753 #[test]
11754 fn sort_entries_by_path_compares_non_utf8_paths_by_raw_bytes() {
11755 let raw = crate::repo_relpath::format_path_stored("", b"raw-\xff");
11756 assert!(raw.starts_with("grit+pathhex:"));
11757 let mut entries = vec![
11758 entry(DiffStatus::Modified, &raw, None),
11759 entry(DiffStatus::Modified, "node/leaf", None),
11760 entry(DiffStatus::Modified, "zeta", None),
11761 ];
11762 sort_entries_by_path(&mut entries);
11763 let paths: Vec<_> = entries.iter().map(DiffEntry::path).collect();
11764 assert_eq!(paths, ["node/leaf", raw.as_str(), "zeta"]);
11765 }
11766
11767 #[test]
11768 fn rename_pass_returns_unpairable_lists_in_path_order() {
11769 let config = ConfigSet::new();
11770 let policy = ThreadPolicy::default();
11771 let odb = Odb::new(Path::new("/nonexistent-objects"));
11772 let run = |entries: Vec<DiffEntry>| {
11773 detect_renames_with_policy(&odb, None, entries, 50, &config, &policy, None)
11774 .expect("no blob is read without both adds and deletes")
11775 };
11776 let sorted = vec![
11777 entry(DiffStatus::Deleted, "a", None),
11778 entry(DiffStatus::Modified, "b", None),
11779 entry(DiffStatus::Deleted, "c", None),
11780 ];
11781 assert_eq!(run(sorted.clone()), sorted);
11782 let unsorted = vec![
11783 entry(DiffStatus::Added, "z", None),
11784 entry(DiffStatus::Modified, "m", None),
11785 entry(DiffStatus::Added, "a", None),
11786 ];
11787 let paths: Vec<_> = run(unsorted).iter().map(|e| e.path().to_owned()).collect();
11788 assert_eq!(paths, ["a", "m", "z"]);
11789 let duplicate = vec![
11791 entry(DiffStatus::Added, "p", None),
11792 entry(DiffStatus::Modified, "p", None),
11793 ];
11794 let statuses: Vec<_> = run(duplicate).iter().map(|e| e.status).collect();
11795 assert_eq!(statuses, [DiffStatus::Modified, DiffStatus::Added]);
11796 }
11797}
11798
11799#[cfg(test)]
11800mod diff_index_to_tree_cache_tree_tests {
11801 use std::rc::Rc;
11802 use std::sync::atomic::{AtomicUsize, Ordering};
11803
11804 use super::*;
11805 use crate::index::{Index, IndexEntry, MODE_EXECUTABLE, MODE_GITLINK, MODE_REGULAR};
11806 use crate::objects::ObjectKind;
11807 use crate::write_tree::{build_cache_tree_from_index, write_tree_from_index};
11808 use tempfile::TempDir;
11809
11810 struct TreeReadCounter {
11811 counter: Rc<AtomicUsize>,
11812 }
11813
11814 impl TreeReadCounter {
11815 fn start() -> Self {
11816 let counter = Rc::new(AtomicUsize::new(0));
11817 set_diff_tree_read_hook_for_tests(Some(counter.clone()));
11818 Self { counter }
11819 }
11820
11821 fn get(&self) -> usize {
11822 self.counter.load(Ordering::Relaxed)
11823 }
11824 }
11825
11826 impl Drop for TreeReadCounter {
11827 fn drop(&mut self) {
11828 set_diff_tree_read_hook_for_tests(None);
11829 }
11830 }
11831
11832 fn diff_index_to_tree_flatten_reference(
11833 odb: &Odb,
11834 index: &Index,
11835 head_tree: &ObjectId,
11836 ignore_submodules: bool,
11837 ) -> Result<Vec<DiffEntry>> {
11838 let mut flat_index = index.clone();
11839 flat_index.clear_cache_tree();
11840 diff_index_to_tree(odb, &flat_index, Some(head_tree), ignore_submodules)
11841 }
11842
11843 fn index_entry(path: &str, mode: u32, oid: ObjectId) -> IndexEntry {
11844 IndexEntry {
11845 ctime_sec: 0,
11846 ctime_nsec: 0,
11847 mtime_sec: 0,
11848 mtime_nsec: 0,
11849 dev: 0,
11850 ino: 0,
11851 mode,
11852 uid: 0,
11853 gid: 0,
11854 size: 0,
11855 oid,
11856 flags: path.len().min(0xfff) as u16,
11857 flags_extended: None,
11858 path: path.as_bytes().to_vec(),
11859 base_index_pos: 0,
11860 }
11861 }
11862
11863 fn repo_with_tree(depth: usize, width: usize) -> (TempDir, Odb, ObjectId, Index) {
11864 let temp = TempDir::new().expect("tempdir");
11865 let odb = Odb::new(temp.path());
11866 let mut index = Index::new();
11867 for d in 0..depth {
11868 for w in 0..width {
11869 let path = format!("unchanged/d{d}/f{w}.txt");
11870 let oid = odb
11871 .write(ObjectKind::Blob, format!("body-{d}-{w}").as_bytes())
11872 .expect("blob");
11873 index.add_or_replace(index_entry(&path, MODE_REGULAR, oid));
11874 }
11875 }
11876 let head_tree = write_tree_from_index(&odb, &index, "").expect("write tree");
11877 let cache = build_cache_tree_from_index(&odb, &index).expect("cache-tree");
11878 index.set_cache_tree(cache);
11879 index.cache_tree_root = Some(head_tree);
11880 (temp, odb, head_tree, index)
11881 }
11882
11883 #[test]
11884 fn diff_index_to_tree_cache_tree_fast_path_matches_slow() {
11885 let (_temp, odb, head_tree, mut index) = repo_with_tree(4, 5);
11886 assert!(index.cache_tree.as_ref().is_some_and(|c| c.is_valid()));
11887
11888 let mut state: u64 = 0xDEAD_BEEF;
11889 for _ in 0..24 {
11890 state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
11891 let d = (state % 4) as usize;
11892 state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
11893 let w = (state % 5) as usize;
11894 let path = format!("unchanged/d{d}/f{w}.txt");
11895 index.invalidate_cache_tree_for_path(path.as_bytes());
11896 assert!(
11897 index.cache_tree_root.is_none(),
11898 "partial invalidation clears cache_tree_root like Index::add"
11899 );
11900 assert!(
11901 index.cache_tree.as_ref().is_some_and(|r| !r.is_valid()),
11902 "root cache node must be invalid after partial invalidation"
11903 );
11904 if let Some(e) = index.get_mut(path.as_bytes(), 0) {
11905 let new_oid = odb
11906 .write(ObjectKind::Blob, format!("new-{d}-{w}").as_bytes())
11907 .expect("blob");
11908 e.oid = new_oid;
11909 }
11910 let fast = diff_index_to_tree(&odb, &index, Some(&head_tree), false).expect("fast");
11911 let flat = diff_index_to_tree_flatten_reference(&odb, &index, &head_tree, false)
11912 .expect("flat");
11913 assert_eq!(fast, flat, "mismatch after mutating {path}");
11914 }
11915 }
11916
11917 #[test]
11918 fn diff_index_to_tree_skips_unchanged_subtrees() {
11919 let (_temp, odb, head_tree, index) = repo_with_tree(5, 6);
11920 let reads = TreeReadCounter::start();
11921 diff_index_to_tree(&odb, &index, Some(&head_tree), false).expect("cached diff");
11922 let cached_reads = reads.get();
11923
11924 let mut no_cache = index.clone();
11925 no_cache.clear_cache_tree();
11926 let reads = TreeReadCounter::start();
11927 diff_index_to_tree(&odb, &no_cache, Some(&head_tree), false).expect("flat diff");
11928 let flat_reads = reads.get();
11929
11930 assert!(
11931 cached_reads < flat_reads,
11932 "expected fewer tree reads with cache-tree (cached={cached_reads}, flat={flat_reads})"
11933 );
11934 assert_eq!(
11935 cached_reads, 1,
11936 "unchanged repo should only read the root tree"
11937 );
11938 }
11939
11940 #[test]
11941 fn diff_index_to_tree_partial_invalidation_skips_sibling_subtree() {
11942 let (_temp, odb, head_tree, mut index) = repo_with_tree(5, 6);
11943 let path = "unchanged/d1/f2.txt";
11944 index.invalidate_cache_tree_for_path(path.as_bytes());
11945 let new_oid = odb.write(ObjectKind::Blob, b"changed").expect("blob");
11946 index.get_mut(path.as_bytes(), 0).expect("entry").oid = new_oid;
11947
11948 let flat =
11949 diff_index_to_tree_flatten_reference(&odb, &index, &head_tree, false).expect("flat");
11950 let reads = TreeReadCounter::start();
11951 let fast = diff_index_to_tree(&odb, &index, Some(&head_tree), false).expect("fast");
11952 let cached_reads = reads.get();
11953 assert_eq!(fast, flat);
11954
11955 let reads = TreeReadCounter::start();
11956 diff_index_to_tree_flatten_reference(&odb, &index, &head_tree, false).expect("flat");
11957 let flat_reads = reads.get();
11958
11959 assert!(
11960 cached_reads < flat_reads,
11961 "partial cache-tree should skip sibling dirs (cached={cached_reads}, flat={flat_reads})"
11962 );
11963 }
11964
11965 fn cache_leaf(name: &str) -> CacheTreeNode {
11966 CacheTreeNode::valid(name.as_bytes().to_vec(), 1, ObjectId::zero(), Vec::new())
11967 }
11968
11969 #[test]
11970 fn cache_children_lookup_matches_linear_scan_on_wide_unsorted_nodes() {
11971 let mut children: Vec<CacheTreeNode> = (0..CACHE_CHILD_LINEAR_SCAN_MAX * 3)
11972 .rev()
11973 .map(|i| cache_leaf(&format!("d{i:03}")))
11974 .collect();
11975 let mut first_dup = cache_leaf("d007");
11976 first_dup.entry_count = 7;
11977 children.insert(3, first_dup);
11978 let root = CacheTreeNode::valid(Vec::new(), 0, ObjectId::zero(), children);
11979
11980 let lookup = CacheChildren::new(Some(&root));
11981 assert!(
11982 !lookup.sorted.is_empty(),
11983 "wide nodes must use the sorted lookup"
11984 );
11985 for name in ["d000", "d007", "d020", "d047", "zzz", "d", ""] {
11986 let linear = root.children.iter().find(|c| c.name == name.as_bytes());
11987 let fast = lookup.get(name.as_bytes());
11988 assert_eq!(
11989 fast.map(|c| (&c.name, c.entry_count)),
11990 linear.map(|c| (&c.name, c.entry_count)),
11991 "lookup of {name:?}"
11992 );
11993 }
11994 assert_eq!(lookup.get(b"d007").map(|c| c.entry_count), Some(7));
11995
11996 let narrow = CacheTreeNode::valid(
11997 Vec::new(),
11998 0,
11999 ObjectId::zero(),
12000 vec![cache_leaf("b"), cache_leaf("a")],
12001 );
12002 let lookup = CacheChildren::new(Some(&narrow));
12003 assert!(lookup.sorted.is_empty());
12004 assert_eq!(lookup.get(b"a").map(|c| c.name.as_slice()), Some(&b"a"[..]));
12005 assert!(lookup.get(b"c").is_none());
12006 assert!(CacheChildren::new(None).get(b"a").is_none());
12007 }
12008
12009 #[test]
12010 fn index_relative_in_dir_requires_a_component_boundary() {
12011 assert_eq!(index_relative_in_dir(b"a/b", b""), Some(&b"a/b"[..]));
12012 assert_eq!(index_relative_in_dir(b"a/b", b"a"), Some(&b"b"[..]));
12013 assert_eq!(index_relative_in_dir(b"a", b"a"), Some(&b""[..]));
12014 assert_eq!(index_relative_in_dir(b"ab/c", b"a"), None);
12015 assert_eq!(index_relative_in_dir(b"a.c", b"a"), None);
12016 assert_eq!(index_relative_in_dir(b"b", b"a"), None);
12017 assert_eq!(join_index_path(b"", b"x"), b"x".to_vec());
12018 assert_eq!(join_index_path(b"a/b", b"x"), b"a/b/x".to_vec());
12019 }
12020
12021 #[test]
12024 fn diff_index_to_tree_wide_unsorted_cache_tree_matches_flatten() {
12025 let temp = TempDir::new().expect("tempdir");
12026 let odb = Odb::new(temp.path());
12027 let mut index = Index::new();
12028 let width = CACHE_CHILD_LINEAR_SCAN_MAX * 4;
12029 for d in 0..width {
12030 for f in 0..2 {
12031 let path = format!("dir{d:03}/sub/f{f}.txt");
12032 let oid = odb.write(ObjectKind::Blob, path.as_bytes()).expect("blob");
12033 index.add_or_replace(index_entry(&path, MODE_REGULAR, oid));
12034 }
12035 }
12036 let head_tree = write_tree_from_index(&odb, &index, "").expect("write tree");
12037 let mut cache = build_cache_tree_from_index(&odb, &index).expect("cache-tree");
12038 cache.children.reverse();
12039 index.set_cache_tree(cache);
12040 index.cache_tree_root = Some(head_tree);
12041
12042 let changed = [
12043 "dir003/sub/f1.txt",
12044 "dir031/sub/f0.txt",
12045 "dir063/sub/f1.txt",
12046 ];
12047 for path in changed {
12048 index.invalidate_cache_tree_for_path(path.as_bytes());
12049 let oid = odb.write(ObjectKind::Blob, b"changed").expect("blob");
12050 index.get_mut(path.as_bytes(), 0).expect("entry").oid = oid;
12051 }
12052 for path in ["dir010/sub/new.txt", "dir999/new.txt", "top.txt"] {
12053 index.invalidate_cache_tree_for_path(path.as_bytes());
12054 let oid = odb.write(ObjectKind::Blob, path.as_bytes()).expect("blob");
12055 index.add_or_replace(index_entry(path, MODE_REGULAR, oid));
12056 }
12057 for path in [
12058 "dir020/sub/f0.txt",
12059 "dir020/sub/f1.txt",
12060 "dir021/sub/f0.txt",
12061 ] {
12062 index.invalidate_cache_tree_for_path(path.as_bytes());
12063 index.remove(path.as_bytes());
12064 }
12065
12066 let flat =
12067 diff_index_to_tree_flatten_reference(&odb, &index, &head_tree, false).expect("flat");
12068 let reads = TreeReadCounter::start();
12069 let fast = diff_index_to_tree(&odb, &index, Some(&head_tree), false).expect("fast");
12070 let cached_reads = reads.get();
12071 assert_eq!(fast, flat);
12072 assert_eq!(fast.len(), changed.len() + 3 + 3);
12073 assert!(
12074 cached_reads <= 1 + 2 * 6,
12075 "unchanged siblings must be skipped through the cache-tree (reads={cached_reads})"
12076 );
12077 }
12078
12079 #[test]
12080 fn diff_index_to_tree_cache_tree_handles_non_utf8_directory_names() {
12081 let temp = TempDir::new().expect("tempdir");
12082 let odb = Odb::new(temp.path());
12083 let mut index = Index::new();
12084 let raw_dirs: [&[u8]; 3] = [b"caf\xe9", b"caf\xef\xbf\xbd", b"plain"];
12085 for dir in raw_dirs {
12086 for f in [&b"a.txt"[..], b"b.txt"] {
12087 let path = [dir, b"/", f].concat();
12088 let oid = odb.write(ObjectKind::Blob, &path).expect("blob");
12089 let mut entry = index_entry("x", MODE_REGULAR, oid);
12090 entry.flags = path.len().min(0xfff) as u16;
12091 entry.path = path;
12092 index.add_or_replace(entry);
12093 }
12094 }
12095 let head_tree = write_tree_from_index(&odb, &index, "").expect("write tree");
12096 let cache = build_cache_tree_from_index(&odb, &index).expect("cache-tree");
12097 index.set_cache_tree(cache);
12098 index.cache_tree_root = Some(head_tree);
12099
12100 let changed: &[u8] = b"caf\xe9/b.txt";
12101 index.invalidate_cache_tree_for_path(changed);
12102 let oid = odb.write(ObjectKind::Blob, b"changed").expect("blob");
12103 index.get_mut(changed, 0).expect("entry").oid = oid;
12104
12105 let fast = diff_index_to_tree(&odb, &index, Some(&head_tree), false).expect("fast");
12108 assert_eq!(fast.len(), 1, "{fast:?}");
12109 assert_eq!(fast[0].status, DiffStatus::Modified);
12110 assert_eq!(
12111 fast[0].old_oid,
12112 odb.write(ObjectKind::Blob, changed).expect("blob")
12113 );
12114 assert_eq!(fast[0].new_oid, oid);
12115 }
12116
12117 #[test]
12118 fn diff_index_to_tree_reports_mode_gitlink_and_intent_to_add() {
12119 let temp = TempDir::new().expect("tempdir");
12120 let odb = Odb::new(temp.path());
12121 let blob = odb.write(ObjectKind::Blob, b"x").expect("blob");
12122 let mut index = Index::new();
12123 index.add_or_replace(index_entry("tracked.txt", MODE_REGULAR, blob));
12124 let head_tree = write_tree_from_index(&odb, &index, "").expect("tree");
12125 let cache = build_cache_tree_from_index(&odb, &index).expect("cache");
12126 index.set_cache_tree(cache);
12127
12128 let mode_entry = index_entry("tracked.txt", MODE_EXECUTABLE, blob);
12129 index.add_or_replace(mode_entry);
12130 index.invalidate_cache_tree_for_path(b"tracked.txt");
12131
12132 let diffs = diff_index_to_tree(&odb, &index, Some(&head_tree), false).expect("diff");
12133 assert_eq!(diffs.len(), 1);
12134 assert_eq!(diffs[0].status, DiffStatus::Modified);
12135 assert_eq!(diffs[0].old_mode, FileMode::new(MODE_REGULAR));
12136 assert_eq!(diffs[0].new_mode, FileMode::new(MODE_EXECUTABLE));
12137
12138 let mut ita = index_entry("new.txt", MODE_REGULAR, blob);
12140 ita.set_intent_to_add(true);
12141 index.add_or_replace(ita);
12142 let diffs = diff_index_to_tree(&odb, &index, Some(&head_tree), false).expect("diff");
12143 assert!(
12144 diffs.iter().all(|d| d.path() != "new.txt"),
12145 "intent-to-add must not appear in staged diff"
12146 );
12147
12148 let gitlink: ObjectId = "855827c583bc30645ba427885caa40c5b81764d2"
12150 .parse()
12151 .expect("oid");
12152 let mut sub_index = Index::new();
12153 sub_index.add_or_replace(index_entry("sub", MODE_GITLINK, gitlink));
12154 let sub_tree = write_tree_from_index(&odb, &sub_index, "").expect("tree");
12155 let sub_cache = build_cache_tree_from_index(&odb, &sub_index).expect("cache");
12156 sub_index.set_cache_tree(sub_cache);
12157 let diffs =
12158 diff_index_to_tree(&odb, &sub_index, Some(&sub_tree), true).expect("gitlink diff");
12159 assert!(
12160 diffs.is_empty(),
12161 "ignored gitlink should not diff: {diffs:?}"
12162 );
12163 }
12164
12165 #[test]
12166 fn ignore_submodules_skips_gitlink_in_added_directory_cache_walk() {
12167 let temp = TempDir::new().expect("tempdir");
12168 let odb = Odb::new(temp.path());
12169 let gitlink: ObjectId = "855827c583bc30645ba427885caa40c5b81764d2"
12170 .parse()
12171 .expect("oid");
12172 let head_tree = write_tree_from_index(&odb, &Index::new(), "").expect("empty tree");
12173 let mut index = Index::new();
12174 index.add_or_replace(index_entry("newdir/sub", MODE_GITLINK, gitlink));
12175 let cache = build_cache_tree_from_index(&odb, &index).expect("cache");
12176 index.set_cache_tree(cache);
12177 index.invalidate_cache_tree_for_path(b"newdir/sub");
12178
12179 let fast = diff_index_to_tree(&odb, &index, Some(&head_tree), true).expect("fast");
12180 let flat =
12181 diff_index_to_tree_flatten_reference(&odb, &index, &head_tree, true).expect("flat");
12182 assert_eq!(fast, flat);
12183 assert!(
12184 fast.is_empty(),
12185 "nested gitlinks in added dirs must be ignored: {fast:?}"
12186 );
12187 }
12188
12189 #[test]
12190 fn ignore_submodules_skips_gitlink_in_deleted_directory_cache_walk() {
12191 let temp = TempDir::new().expect("tempdir");
12192 let odb = Odb::new(temp.path());
12193 let blob = odb.write(ObjectKind::Blob, b"keep").expect("blob");
12194 let gitlink: ObjectId = "855827c583bc30645ba427885caa40c5b81764d2"
12195 .parse()
12196 .expect("oid");
12197 let mut index = Index::new();
12198 index.add_or_replace(index_entry("keep.txt", MODE_REGULAR, blob));
12199 index.add_or_replace(index_entry("olddir/sub", MODE_GITLINK, gitlink));
12200 let head_tree = write_tree_from_index(&odb, &index, "").expect("head");
12201 let cache = build_cache_tree_from_index(&odb, &index).expect("cache");
12202 index.set_cache_tree(cache);
12203 index
12204 .entries
12205 .retain(|e| !String::from_utf8_lossy(&e.path).starts_with("olddir/"));
12206 index.invalidate_cache_tree_for_path(b"olddir/sub");
12207
12208 let fast = diff_index_to_tree(&odb, &index, Some(&head_tree), true).expect("fast");
12209 let flat =
12210 diff_index_to_tree_flatten_reference(&odb, &index, &head_tree, true).expect("flat");
12211 assert_eq!(fast, flat);
12212 assert!(
12213 fast.is_empty(),
12214 "nested gitlinks in deleted dirs must be ignored: {fast:?}"
12215 );
12216 }
12217
12218 #[test]
12219 fn diff_trees_skips_identical_subtree_oids() {
12220 let temp = TempDir::new().expect("tempdir");
12221 let odb = Odb::new(temp.path());
12222 let leaf = odb.write(ObjectKind::Blob, b"leaf").expect("blob");
12223 let mut deep = Index::new();
12224 for i in 0..20 {
12225 deep.add_or_replace(index_entry(&format!("deep/n{i}.txt"), MODE_REGULAR, leaf));
12226 }
12227 let tree = write_tree_from_index(&odb, &deep, "").expect("tree");
12228 let reads = TreeReadCounter::start();
12229 let entries = diff_trees(&odb, Some(&tree), Some(&tree), "").expect("diff");
12230 assert!(entries.is_empty());
12231 assert_eq!(
12232 reads.get(),
12233 0,
12234 "identical tree oids should short-circuit without reading tree objects"
12235 );
12236 }
12237
12238 #[test]
12239 fn diff_tree_path_change_count_matches_diff_trees() {
12240 let temp = TempDir::new().expect("tempdir");
12241 let odb = Odb::new(temp.path());
12242 let a = odb.write(ObjectKind::Blob, b"a").expect("blob");
12243 let b = odb.write(ObjectKind::Blob, b"b").expect("blob");
12244 let mut left = Index::new();
12245 left.add_or_replace(index_entry("f.txt", MODE_REGULAR, a));
12246 let mut right = Index::new();
12247 right.add_or_replace(index_entry("f.txt", MODE_REGULAR, b));
12248 right.add_or_replace(index_entry("g.txt", MODE_REGULAR, a));
12249 let old_tree = write_tree_from_index(&odb, &left, "").expect("old");
12250 let new_tree = write_tree_from_index(&odb, &right, "").expect("new");
12251 let entries = diff_trees(&odb, Some(&old_tree), Some(&new_tree), "").expect("diff");
12252 let count =
12253 diff_tree_path_change_count(&odb, Some(&old_tree), Some(&new_tree), "").expect("count");
12254 assert_eq!(count, entries.len());
12255 assert_eq!(count, 2);
12256 }
12257}
12258
12259#[cfg(test)]
12260mod changed_flags_shared_tests {
12261 use super::git_xdiff;
12262 use crate::line_interner::LineInterner;
12263
12264 #[test]
12267 fn shared_counts_stand_in_for_records_left_out() {
12268 let mut old: Vec<String> = Vec::new();
12271 let mut new: Vec<String> = Vec::new();
12272 for i in 0..60 {
12273 for side in [&mut old, &mut new] {
12274 side.push(format!("fn f{i}() {{\n"));
12275 side.push("}\n".to_owned());
12276 }
12277 }
12278 old.extend(
12279 [
12280 "u1\n", "u2\n", "u3\n", "u4\n", "}\n", "u5\n", "u6\n", "u7\n",
12281 ]
12282 .map(str::to_owned),
12283 );
12284 new.extend(
12285 [
12286 "v1\n", "v2\n", "v3\n", "v4\n", "}\n", "v5\n", "v6\n", "v7\n",
12287 ]
12288 .map(str::to_owned),
12289 );
12290 for i in 0..40 {
12291 for side in [&mut old, &mut new] {
12292 side.push(format!("fn g{i}() {{\n"));
12293 side.push("}\n".to_owned());
12294 }
12295 }
12296 let old: Vec<&str> = old.iter().map(String::as_str).collect();
12297 let new: Vec<&str> = new.iter().map(String::as_str).collect();
12298 let (head, tail) = (120, 80);
12299
12300 let mut interner = LineInterner::with_capacity(old.len() + new.len());
12301 let ids1 = interner.intern_all(&old);
12302 let ids2 = interner.intern_all(&new);
12303 let (full1, full2) = git_xdiff::changed_flags(&ids1, &ids2, false);
12304
12305 let mut cut = LineInterner::default();
12306 let cut1 = cut.intern_all(&old[head..old.len() - tail]);
12307 let cut2 = cut.intern_all(&new[head..new.len() - tail]);
12308 let mut shared = vec![0u32; cut.len()];
12309 for line in old[..head].iter().chain(&old[old.len() - tail..]) {
12310 if let Some(id) = cut.get(line) {
12311 shared[id as usize] += 1;
12312 }
12313 }
12314 let (part1, part2) =
12315 git_xdiff::changed_flags_with_shared(&cut1, &cut2, &shared, head + tail, false);
12316 assert_eq!(part1, full1[head..old.len() - tail]);
12317 assert_eq!(part2, full2[head..new.len() - tail]);
12318 assert!(full1[..head]
12319 .iter()
12320 .chain(&full1[old.len() - tail..])
12321 .all(|c| !c));
12322 assert!(full1[head..old.len() - tail].iter().all(|&c| c));
12325 let (alone1, _) = git_xdiff::changed_flags(&cut1, &cut2, false);
12326 assert!(
12327 !alone1[4],
12328 "counted alone, the cut keeps the `}}` as a match"
12329 );
12330 }
12331}
12332
12333#[cfg(test)]
12334mod gitlink_tree_worktree_tests {
12335 use std::fs;
12336
12337 use crate::index::{Index, IndexEntry, MODE_GITLINK};
12338 use crate::objects::{ObjectId, ObjectKind};
12339 use crate::repo::init_repository;
12340
12341 use super::diff_tree_to_worktree;
12342
12343 fn encode_tree_gitlink(name: &str, oid: &ObjectId) -> Vec<u8> {
12344 let mut data = format!("160000 {name}\0").into_bytes();
12345 data.extend_from_slice(oid.as_bytes());
12346 data
12347 }
12348
12349 #[test]
12350 fn unpopulated_gitlink_matching_index_produces_no_diff_entry() {
12351 let dir = tempfile::tempdir().expect("tempdir");
12352 let work_tree = dir.path();
12353 fs::create_dir(work_tree.join("sub")).expect("sub placeholder");
12354
12355 let repo = init_repository(
12356 work_tree,
12357 false,
12358 "main",
12359 None,
12360 crate::RefStorageFormat::Files,
12361 )
12362 .expect("init");
12363 let gitlink: ObjectId = "855827c583bc30645ba427885caa40c5b81764d2"
12364 .parse()
12365 .expect("gitlink oid");
12366 let tree_oid = repo
12367 .odb()
12368 .write(ObjectKind::Tree, &encode_tree_gitlink("sub", &gitlink))
12369 .expect("write tree");
12370
12371 let path = b"sub".to_vec();
12372 let mut index = Index::new();
12373 index.add_or_replace(IndexEntry {
12374 ctime_sec: 0,
12375 ctime_nsec: 0,
12376 mtime_sec: 0,
12377 mtime_nsec: 0,
12378 dev: 0,
12379 ino: 0,
12380 mode: MODE_GITLINK,
12381 uid: 0,
12382 gid: 0,
12383 size: 0,
12384 oid: gitlink,
12385 flags: (path.len().min(0xfff)) as u16,
12386 flags_extended: None,
12387 path,
12388 base_index_pos: 0,
12389 });
12390
12391 let entries = diff_tree_to_worktree(&repo.odb(), Some(&tree_oid), work_tree, &index)
12392 .expect("diff_tree_to_worktree");
12393 assert!(
12394 entries.is_empty(),
12395 "clean uninitialized gitlink should not appear in tree→worktree diff: {entries:?}"
12396 );
12397 }
12398
12399 #[cfg_attr(
12400 not(feature = "full-tests"),
12401 ignore = "starts system git or is heavy; run with --features full-tests"
12402 )]
12403 #[test]
12404 fn untracked_only_in_checked_out_submodule_produces_no_diff_entry() {
12405 use std::process::Command;
12406
12407 let dir = tempfile::tempdir().expect("tempdir");
12408 let work_tree = dir.path();
12409 fs::create_dir(work_tree.join("sub")).expect("sub dir");
12410
12411 let repo = init_repository(
12412 work_tree,
12413 false,
12414 "main",
12415 None,
12416 crate::RefStorageFormat::Files,
12417 )
12418 .expect("init");
12419 let sub = work_tree.join("sub");
12420 for args in [
12421 &["init", "-q"][..],
12422 &["config", "user.name", "T"][..],
12423 &["config", "user.email", "t@e.com"][..],
12424 ] {
12425 let status = Command::new("git")
12426 .current_dir(&sub)
12427 .args(args)
12428 .status()
12429 .expect("git");
12430 assert!(status.success(), "git {args:?} failed");
12431 }
12432 fs::write(sub.join("tracked.txt"), b"v1\n").expect("write");
12433 for args in [&["add", "tracked.txt"][..], &["commit", "-qm", "sub"][..]] {
12434 let status = Command::new("git")
12435 .current_dir(&sub)
12436 .args(args)
12437 .status()
12438 .expect("git");
12439 assert!(status.success());
12440 }
12441 let head_out = Command::new("git")
12442 .current_dir(&sub)
12443 .args(["rev-parse", "HEAD"])
12444 .output()
12445 .expect("rev-parse");
12446 let gitlink = std::str::from_utf8(&head_out.stdout)
12447 .expect("utf8")
12448 .trim()
12449 .parse::<ObjectId>()
12450 .expect("oid");
12451
12452 let tree_oid = repo
12453 .odb()
12454 .write(ObjectKind::Tree, &encode_tree_gitlink("sub", &gitlink))
12455 .expect("tree");
12456 let path = b"sub".to_vec();
12457 let mut index = Index::new();
12458 index.add_or_replace(IndexEntry {
12459 ctime_sec: 0,
12460 ctime_nsec: 0,
12461 mtime_sec: 0,
12462 mtime_nsec: 0,
12463 dev: 0,
12464 ino: 0,
12465 mode: MODE_GITLINK,
12466 uid: 0,
12467 gid: 0,
12468 size: 0,
12469 oid: gitlink,
12470 flags: (path.len().min(0xfff)) as u16,
12471 flags_extended: None,
12472 path,
12473 base_index_pos: 0,
12474 });
12475
12476 fs::write(sub.join("untracked.txt"), b"extra\n").expect("untracked");
12477
12478 let entries = diff_tree_to_worktree(&repo.odb(), Some(&tree_oid), work_tree, &index)
12479 .expect("diff_tree_to_worktree");
12480 assert!(
12481 entries.is_empty(),
12482 "untracked-only submodule dirt must not appear in default tree→worktree diff: {entries:?}"
12483 );
12484 }
12485}
12486
12487#[cfg(test)]
12488mod racy_index_mtime_diff_tests {
12489 use super::*;
12490 use crate::index::{entry_from_metadata, Index, MODE_REGULAR};
12491 use crate::objects::{HashAlgo, ObjectKind};
12492 use filetime::FileTime;
12493 use std::fs;
12494 use std::path::PathBuf;
12495
12496 const INDEX_MTIME: (u32, u32) = (1_700_000_000, 123_456_789);
12497
12498 fn pin_mtime(path: &Path, sec: u32, nsec: u32) {
12499 filetime::set_file_mtime(path, FileTime::from_unix_time(i64::from(sec), nsec))
12500 .expect("set mtime");
12501 }
12502
12503 fn minimal_sha256_repo(work_tree: &Path) -> (PathBuf, Odb) {
12504 let git_dir = work_tree.join(".git");
12505 fs::create_dir_all(git_dir.join("objects")).expect("objects dir");
12506 fs::write(
12507 git_dir.join("config"),
12508 "[extensions]\n\tobjectformat = sha256\n",
12509 )
12510 .expect("config");
12511 let odb = Odb::new(&git_dir.join("objects")).with_config_git_dir(git_dir.clone());
12512 (git_dir, odb)
12513 }
12514
12515 #[test]
12516 fn detects_same_size_change_when_entry_mtime_matches_index_mtime() {
12517 let td = tempfile::tempdir().expect("tempdir");
12518 let wt = td.path();
12519 let content_a = b"same length!!";
12520 let content_b = b"same length??";
12521 assert_eq!(content_a.len(), content_b.len());
12522
12523 fs::write(wt.join("f.txt"), content_a).expect("write");
12524 pin_mtime(&wt.join("f.txt"), INDEX_MTIME.0, INDEX_MTIME.1);
12525 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
12526 let git_dir = wt.join(".git");
12527 fs::create_dir_all(git_dir.join("objects")).expect("objects dir");
12528 let odb = Odb::new(&git_dir.join("objects"));
12529 let oid_a = odb.hash(ObjectKind::Blob, content_a).expect("hash");
12530 let entry = entry_from_metadata(&meta, b"f.txt", oid_a, MODE_REGULAR);
12531
12532 let mut index = Index::new();
12533 index.source_mtime = Some(INDEX_MTIME);
12534 index.entries.push(entry);
12535
12536 fs::write(wt.join("f.txt"), content_b).expect("rewrite");
12537 pin_mtime(&wt.join("f.txt"), INDEX_MTIME.0, INDEX_MTIME.1);
12538
12539 let diff = diff_index_to_worktree(&odb, &mut index, wt, false, false).expect("diff");
12540 assert_eq!(
12541 diff.len(),
12542 1,
12543 "racy entry must be re-hashed, not stat-trusted"
12544 );
12545 assert_eq!(diff[0].path(), "f.txt");
12546 }
12547
12548 #[test]
12549 #[cfg(unix)]
12550 fn non_utf8_symlink_clean_racy_equal_mtime_reports_no_diff() {
12551 use crate::index::MODE_SYMLINK;
12552 use std::os::unix::ffi::OsStrExt as _;
12553 use std::os::unix::fs::symlink;
12554
12555 let td = tempfile::tempdir().expect("tempdir");
12556 let wt = td.path();
12557 let target = std::ffi::OsStr::from_bytes(b"bad-\xff-target");
12558 let link_path = wt.join("link");
12559 symlink(target, &link_path).expect("symlink");
12560
12561 let ft = FileTime::from_unix_time(i64::from(INDEX_MTIME.0), INDEX_MTIME.1);
12562 filetime::set_symlink_file_times(&link_path, ft, ft).expect("set symlink mtime");
12563 let meta = fs::symlink_metadata(&link_path).expect("stat");
12564 let git_dir = wt.join(".git");
12565 fs::create_dir_all(git_dir.join("objects")).expect("objects dir");
12566 let odb = Odb::new(&git_dir.join("objects"));
12567 let oid = odb.hash(ObjectKind::Blob, target.as_bytes()).expect("hash");
12568 let entry = entry_from_metadata(&meta, b"link", oid, MODE_SYMLINK);
12569
12570 let mut index = Index::new();
12571 index.source_mtime = Some(INDEX_MTIME);
12572 index.entries.push(entry);
12573
12574 let diff = diff_index_to_worktree(&odb, &mut index, wt, false, false).expect("diff");
12575 assert!(
12576 diff.is_empty(),
12577 "clean non-UTF-8 symlink must not be reported modified: {diff:?}"
12578 );
12579 }
12580
12581 #[test]
12582 fn sha256_clean_racy_equal_mtime_reports_no_diff() {
12583 let td = tempfile::tempdir().expect("tempdir");
12584 let wt = td.path();
12585 let (_git_dir, odb) = minimal_sha256_repo(wt);
12586 assert_eq!(odb.hash_algo(), HashAlgo::Sha256);
12587
12588 let bytes = b"sha256 width!!";
12589 fs::write(wt.join("f.txt"), bytes).expect("write");
12590 pin_mtime(&wt.join("f.txt"), INDEX_MTIME.0, INDEX_MTIME.1);
12591 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
12592 let oid = odb.hash(ObjectKind::Blob, bytes).expect("hash");
12593 let entry = entry_from_metadata(&meta, b"f.txt", oid, MODE_REGULAR);
12594
12595 let mut index = Index::new();
12596 index.hash_algo = HashAlgo::Sha256;
12597 index.source_mtime = Some(INDEX_MTIME);
12598 index.entries.push(entry);
12599
12600 let diff = diff_index_to_worktree(&odb, &mut index, wt, false, false).expect("diff");
12601 assert!(
12602 diff.is_empty(),
12603 "clean SHA-256 racy entry must not be reported modified: {diff:?}"
12604 );
12605 }
12606}
12607
12608#[cfg(test)]
12609mod smudge_racily_clean_tests {
12610 use super::*;
12611 use crate::config::ConfigSet;
12612 use crate::crlf::{self, FileAttrs};
12613 use crate::index::{entry_from_metadata, Index, MODE_REGULAR};
12614 use crate::objects::{HashAlgo, ObjectKind};
12615 use filetime::FileTime;
12616 use std::fs;
12617 use std::path::PathBuf;
12618
12619 const INDEX_MTIME_PIN: (u32, u32) = (1_700_000_000, 123_456_789);
12620
12621 fn mtime_from_metadata(meta: &fs::Metadata) -> (u32, u32) {
12622 #[cfg(unix)]
12623 {
12624 use std::os::unix::fs::MetadataExt;
12625 (meta.mtime() as u32, meta.mtime_nsec() as u32)
12626 }
12627 #[cfg(not(unix))]
12628 {
12629 use std::time::UNIX_EPOCH;
12630 let mtime = meta
12631 .modified()
12632 .ok()
12633 .and_then(|t| t.duration_since(UNIX_EPOCH).ok())
12634 .unwrap_or_default();
12635 (mtime.as_secs() as u32, mtime.subsec_nanos())
12636 }
12637 }
12638
12639 fn pin_mtime_and_sample(path: &Path, sec: u32, nsec: u32) -> (u32, u32) {
12641 filetime::set_file_mtime(path, FileTime::from_unix_time(i64::from(sec), nsec))
12642 .expect("set mtime");
12643 let meta = fs::symlink_metadata(path).expect("stat file");
12644 mtime_from_metadata(&meta)
12645 }
12646
12647 fn setup_index_mtime(work_tree: &Path) -> (u32, u32) {
12649 let probe = work_tree.join(".grit-index-mtime-probe");
12650 fs::write(&probe, b"").expect("probe file");
12651 pin_mtime_and_sample(&probe, INDEX_MTIME_PIN.0, INDEX_MTIME_PIN.1)
12652 }
12653
12654 fn minimal_repo(work_tree: &Path, config: &str) -> (PathBuf, Odb) {
12655 let git_dir = work_tree.join(".git");
12656 fs::create_dir_all(git_dir.join("objects")).expect("objects dir");
12657 fs::write(git_dir.join("config"), config).expect("config");
12658 let odb = Odb::new(&git_dir.join("objects")).with_config_git_dir(git_dir.clone());
12659 (git_dir, odb)
12660 }
12661
12662 fn staged_entry_for_file(
12663 odb: &Odb,
12664 work_tree: &Path,
12665 rel: &str,
12666 bytes: &[u8],
12667 mtime: (u32, u32),
12668 ) -> IndexEntry {
12669 let path = work_tree.join(rel);
12670 fs::write(&path, bytes).expect("write file");
12671 pin_mtime_and_sample(&path, mtime.0, mtime.1);
12672 let meta = fs::symlink_metadata(&path).expect("stat file");
12673 let oid = odb.hash(ObjectKind::Blob, bytes).expect("hash");
12674 entry_from_metadata(&meta, rel.as_bytes(), oid, MODE_REGULAR)
12675 }
12676
12677 fn staged_entry_with_oid(
12678 work_tree: &Path,
12679 rel: &str,
12680 worktree_bytes: &[u8],
12681 oid: crate::objects::ObjectId,
12682 mtime: (u32, u32),
12683 ) -> IndexEntry {
12684 let path = work_tree.join(rel);
12685 fs::write(&path, worktree_bytes).expect("write file");
12686 pin_mtime_and_sample(&path, mtime.0, mtime.1);
12687 let meta = fs::symlink_metadata(&path).expect("stat file");
12688 entry_from_metadata(&meta, rel.as_bytes(), oid, MODE_REGULAR)
12689 }
12690
12691 #[test]
12692 fn index_stat_cache_trustworthy_rejects_unknown_mtime() {
12693 assert!(!super::index_stat_cache_trustworthy(None));
12694 assert!(!super::index_stat_cache_trustworthy(Some((0, 0))));
12695 assert!(super::index_stat_cache_trustworthy(Some((1, 0))));
12696 }
12697
12698 #[test]
12699 fn index_entry_skips_content_hash_after_stat_match_when_trusted_and_not_racy() {
12700 use crate::index::StatMatchPolicy;
12701 let td = tempfile::tempdir().expect("tempdir");
12702 let wt = td.path();
12703 let (git_dir, odb) = minimal_repo(wt, "[core]\n\tfilemode = true\n");
12704 let rel = "trusted.txt";
12705 let file_mtime = (1_700_000_000, 0);
12706 let mut entry = staged_entry_for_file(&odb, wt, rel, b"payload\n", file_mtime);
12707 let index_mtime = (file_mtime.0 + 1, 0);
12708 let policy = StatMatchPolicy::from_config(None);
12709 assert!(
12710 super::index_entry_skips_worktree_content_hash_after_stat_match(
12711 &entry,
12712 Some(index_mtime),
12713 policy,
12714 )
12715 );
12716 entry.size = 0;
12717 entry.mtime_sec = 0;
12718 entry.mtime_nsec = 0;
12719 assert!(
12720 !super::index_entry_skips_worktree_content_hash_after_stat_match(
12721 &entry,
12722 Some(index_mtime),
12723 policy,
12724 )
12725 );
12726 let _ = git_dir;
12727 }
12728
12729 #[cfg(unix)]
12730 #[test]
12731 fn symlink_dir_cache_detects_symlinked_ancestors_across_threads() {
12732 let td = tempfile::tempdir().expect("tempdir");
12733 let wt = td.path();
12734 fs::create_dir_all(wt.join("real/deep")).expect("mkdir");
12735 fs::create_dir_all(wt.join("target/deep")).expect("mkdir");
12736 std::os::unix::fs::symlink(wt.join("target"), wt.join("linked")).expect("symlink");
12737 let cache = super::SymlinkDirCache::default();
12738 std::thread::scope(|scope| {
12739 for _ in 0..4 {
12740 scope.spawn(|| {
12741 assert!(!cache.has_symlink_in_path_bytes(wt, b"top.txt", false, false));
12742 assert!(!cache.has_symlink_in_path_bytes(wt, b"linked", false, false));
12743 assert!(!cache.has_symlink_in_path_bytes(wt, b"real/deep/f", false, false));
12744 assert!(cache.has_symlink_in_path_bytes(wt, b"linked/f", false, false));
12745 assert!(cache.has_symlink_in_path_bytes(wt, b"linked/deep/f", false, false));
12746 assert!(!cache.has_symlink_in_path_bytes(wt, b"\xff\xfe", false, false));
12747 });
12748 }
12749 });
12750 }
12751
12752 #[test]
12753 fn content_hash_skip_trusts_empty_blob_but_not_smudged_size() {
12754 use crate::index::StatMatchPolicy;
12755 let td = tempfile::tempdir().expect("tempdir");
12756 let wt = td.path();
12757 let (_git_dir, odb) = minimal_repo(wt, "[core]\n\tfilemode = false\n");
12758 let file_mtime = (1_700_000_000, 0);
12759 let index_mtime = Some((file_mtime.0 + 1, 0));
12760 let policy = StatMatchPolicy::from_config(None);
12761
12762 let empty = staged_entry_for_file(&odb, wt, "empty.txt", b"", file_mtime);
12763 assert_eq!(empty.size, 0);
12764 assert!(
12765 super::index_entry_skips_worktree_content_hash_after_stat_match(
12766 &empty,
12767 index_mtime,
12768 policy,
12769 )
12770 );
12771
12772 let smudged_oid = odb.hash(ObjectKind::Blob, b"abc\n").expect("hash");
12773 let smudged = staged_entry_with_oid(wt, "smudged.txt", b"", smudged_oid, file_mtime);
12774 assert_eq!(smudged.size, 0);
12775 assert!(
12776 !super::index_entry_skips_worktree_content_hash_after_stat_match(
12777 &smudged,
12778 index_mtime,
12779 policy,
12780 )
12781 );
12782 }
12783
12784 #[test]
12785 fn status_diff_reports_smudged_entry_whose_file_became_empty() {
12786 let td = tempfile::tempdir().expect("tempdir");
12787 let wt = td.path();
12788 let (git_dir, odb) = minimal_repo(wt, "[core]\n\tfilemode = false\n");
12789 let oid = odb.write(ObjectKind::Blob, b"abc\n").expect("write blob");
12790 let file_mtime = (1_700_000_000, 0);
12791 let mut index = Index::new();
12792 index.entries.push(staged_entry_with_oid(
12793 wt,
12794 "smudged.txt",
12795 b"",
12796 oid,
12797 file_mtime,
12798 ));
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 index_mtime: Some((file_mtime.0 + 1, 0)),
12806 for_status: true,
12807 stat_parallel_threads: Some(1),
12808 ..Default::default()
12809 },
12810 )
12811 .expect("diff");
12812 assert_eq!(entries.len(), 1, "{entries:?}");
12813 assert_eq!(entries[0].status, DiffStatus::Modified);
12814 }
12815
12816 #[test]
12817 fn status_diff_verifies_content_when_index_stat_cache_unset() {
12818 let td = tempfile::tempdir().expect("tempdir");
12819 let wt = td.path();
12820 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
12821 let rel = "unset-stat.txt";
12822 let bytes = b"same bytes\n";
12823 let oid = odb.hash(ObjectKind::Blob, bytes).expect("hash");
12824 fs::write(wt.join(rel), bytes).expect("write");
12825 let mut index = Index::new();
12826 let mut entry = staged_entry_for_file(&odb, wt, rel, bytes, (100, 0));
12827 entry.size = 0;
12828 entry.mtime_sec = 0;
12829 entry.mtime_nsec = 0;
12830 entry.oid = oid;
12831 index.entries.push(entry);
12832 let (entries, _) = super::diff_index_to_worktree_with_options(
12833 &odb,
12834 &mut index,
12835 wt,
12836 super::DiffIndexToWorktreeOptions {
12837 repository_git_dir: Some(git_dir),
12838 for_status: true,
12839 ..Default::default()
12840 },
12841 )
12842 .expect("diff");
12843 assert!(
12844 entries.is_empty(),
12845 "unset stat must not size-mismatch a matching worktree: {entries:?}"
12846 );
12847 }
12848
12849 #[cfg_attr(
12850 not(feature = "full-tests"),
12851 ignore = "starts system git or is heavy; run with --features full-tests"
12852 )]
12853 #[test]
12854 fn status_reports_modified_when_ctime_differs_but_mtime_sec_and_size_match_non_racy() {
12855 let td = tempfile::tempdir().expect("tempdir");
12856 let wt = td.path();
12857 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
12858 let rel = "tracked.txt";
12859 let original = b"original\n";
12860 let modified = b"modified\n";
12861 assert_eq!(original.len(), modified.len());
12862
12863 let entry_mtime = (1_700_000_000u32, 0u32);
12864 let index_file_mtime = (1_700_000_100u32, 0u32);
12865
12866 std::process::Command::new("git")
12867 .args(["-C", &wt.to_string_lossy(), "init", "-q"])
12868 .status()
12869 .expect("git init");
12870 std::process::Command::new("git")
12871 .args([
12872 "-C",
12873 &wt.to_string_lossy(),
12874 "config",
12875 "user.email",
12876 "t@example.com",
12877 ])
12878 .status()
12879 .expect("git config email");
12880 std::process::Command::new("git")
12881 .args(["-C", &wt.to_string_lossy(), "config", "user.name", "T"])
12882 .status()
12883 .expect("git config name");
12884
12885 fs::write(wt.join(rel), original).expect("write original");
12886 pin_mtime_and_sample(&wt.join(rel), entry_mtime.0, entry_mtime.1);
12887 std::process::Command::new("git")
12888 .args(["-C", &wt.to_string_lossy(), "add", rel])
12889 .status()
12890 .expect("git add");
12891 std::process::Command::new("git")
12892 .args([
12893 "-C",
12894 &wt.to_string_lossy(),
12895 "commit",
12896 "-qm",
12897 "seed",
12898 "--no-gpg-sign",
12899 ])
12900 .status()
12901 .expect("git commit");
12902
12903 let mut index = Index::new();
12904 index.source_mtime = Some(index_file_mtime);
12905 let staged = staged_entry_for_file(&odb, wt, rel, original, entry_mtime);
12906 let oid_original = staged.oid;
12907 index.entries.push(staged);
12908
12909 fs::write(wt.join(rel), modified).expect("modify worktree");
12910 let git_dirty = std::process::Command::new("git")
12911 .args([
12912 "-C",
12913 &wt.to_string_lossy(),
12914 "--no-optional-locks",
12915 "status",
12916 "--short",
12917 ])
12918 .output()
12919 .expect("git status");
12920 assert!(
12921 git_dirty.status.success(),
12922 "git status failed: {}",
12923 String::from_utf8_lossy(&git_dirty.stderr)
12924 );
12925 assert!(
12926 String::from_utf8_lossy(&git_dirty.stdout).contains("tracked.txt"),
12927 "system git must see modification before mtime pin: {}",
12928 String::from_utf8_lossy(&git_dirty.stdout)
12929 );
12930
12931 pin_mtime_and_sample(&wt.join(rel), entry_mtime.0, entry_mtime.1);
12932
12933 let meta = fs::symlink_metadata(wt.join(rel)).expect("stat");
12934 let ie = &mut index.entries[0];
12935 assert_eq!(ie.oid, oid_original);
12936 if stat_matches(ie, &meta) {
12937 ie.ctime_sec = ie.ctime_sec.wrapping_sub(3600);
12938 }
12939 assert!(
12940 !stat_matches(ie, &meta),
12941 "ctime/inode change must fail full stat match"
12942 );
12943 let stat_policy = crate::index::StatMatchPolicy::default();
12944 assert!(
12945 !entry_is_racy_with_policy(ie, Some(index_file_mtime), stat_policy),
12946 "fixture must be non-racy"
12947 );
12948
12949 let (entries, index_changed) = super::diff_index_to_worktree_with_options(
12950 &odb,
12951 &mut index,
12952 wt,
12953 super::DiffIndexToWorktreeOptions {
12954 index_mtime: Some(index_file_mtime),
12955 repository_git_dir: Some(git_dir),
12956 refresh_index_stat_in_pass: true,
12957 for_status: true,
12958 ..Default::default()
12959 },
12960 )
12961 .expect("diff");
12962 assert_eq!(
12963 entries.len(),
12964 1,
12965 "must report modified, not refresh stat clean: {entries:?}, index_changed={index_changed}"
12966 );
12967 assert_eq!(entries[0].path(), rel);
12968 }
12969
12970 #[test]
12971 fn status_diff_skips_hash_when_worktree_size_differs() {
12972 let td = tempfile::tempdir().expect("tempdir");
12973 let wt = td.path();
12974 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
12975 let rel = "size-change.txt";
12976 let original = b"short\n";
12977 let mut index = Index::new();
12978 index
12979 .entries
12980 .push(staged_entry_for_file(&odb, wt, rel, original, (100, 0)));
12981 fs::write(wt.join(rel), b"longer content\n").expect("rewrite");
12982 let (entries, _) = super::diff_index_to_worktree_with_options(
12983 &odb,
12984 &mut index,
12985 wt,
12986 super::DiffIndexToWorktreeOptions {
12987 repository_git_dir: Some(git_dir),
12988 for_status: true,
12989 ..Default::default()
12990 },
12991 )
12992 .expect("diff");
12993 assert_eq!(entries.len(), 1);
12994 assert_eq!(entries[0].status, super::DiffStatus::Modified);
12995 assert_eq!(entries[0].path(), rel);
12996 }
12997
12998 #[test]
12999 fn stat_matches_ignores_subsecond_nsec_by_default() {
13000 let td = tempfile::tempdir().expect("tempdir");
13001 let wt = td.path();
13002 fs::write(wt.join("f.txt"), b"x\n").expect("write");
13003 pin_mtime_and_sample(&wt.join("f.txt"), 100, 0);
13004 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
13005 let mut entry = entry_from_metadata(&meta, b"f.txt", zero_oid(), MODE_REGULAR);
13006 entry.mtime_nsec = entry.mtime_nsec.wrapping_add(1_000_000);
13007 assert!(stat_matches(&entry, &meta));
13008 }
13009
13010 #[test]
13011 fn classify_add_up_to_date_when_stat_matches() {
13012 let td = tempfile::tempdir().expect("tempdir");
13013 let wt = td.path();
13014 let (_git_dir, odb) = minimal_repo(wt, "[core]\n");
13015 let bytes = b"hello world\n";
13016 let entry = staged_entry_for_file(&odb, wt, "f.txt", bytes, (100, 0));
13017 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
13018 let conv = crlf::ConversionConfig::from_config(&ConfigSet::new());
13019 let attrs = FileAttrs::default();
13020 let params = WorktreeAddRefreshParams {
13021 odb: &odb,
13022 ie: &entry,
13023 meta: &meta,
13024 abs_path: &wt.join("f.txt"),
13025 rel_path: "f.txt",
13026 conv: &conv,
13027 file_attrs: &attrs,
13028 index_mtime: Some((200, 0)),
13029 staged_mode: MODE_REGULAR,
13030 filter_process: None,
13031 };
13032 let refresh = classify_worktree_entry_for_add(¶ms).expect("classify");
13033 assert_eq!(refresh, WorktreeAddRefresh::UpToDate);
13034 }
13035
13036 #[test]
13037 fn classify_add_rehashes_when_index_mtime_unknown_and_stat_matches() {
13038 let td = tempfile::tempdir().expect("tempdir");
13039 let wt = td.path();
13040 let (_git_dir, odb) = minimal_repo(wt, "[core]\n");
13041 let entry = staged_entry_for_file(&odb, wt, "f.txt", b"old content\n", (100, 0));
13042 fs::write(wt.join("f.txt"), b"new content\n").expect("rewrite");
13043 pin_mtime_and_sample(&wt.join("f.txt"), 100, 0);
13044 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
13045 let conv = crlf::ConversionConfig::from_config(&ConfigSet::new());
13046 let attrs = FileAttrs::default();
13047 let params = WorktreeAddRefreshParams {
13048 odb: &odb,
13049 ie: &entry,
13050 meta: &meta,
13051 abs_path: &wt.join("f.txt"),
13052 rel_path: "f.txt",
13053 conv: &conv,
13054 file_attrs: &attrs,
13055 index_mtime: None,
13056 staged_mode: MODE_REGULAR,
13057 filter_process: None,
13058 };
13059 let refresh = classify_worktree_entry_for_add(¶ms).expect("classify");
13060 assert_eq!(refresh, WorktreeAddRefresh::NeedsRestage);
13061 }
13062
13063 #[test]
13064 fn classify_add_needs_restaging_when_content_differs() {
13065 let td = tempfile::tempdir().expect("tempdir");
13066 let wt = td.path();
13067 let (_git_dir, odb) = minimal_repo(wt, "[core]\n");
13068 let entry = staged_entry_for_file(&odb, wt, "f.txt", b"old content\n", (100, 0));
13069 fs::write(wt.join("f.txt"), b"new content\n").expect("rewrite");
13070 pin_mtime_and_sample(&wt.join("f.txt"), 101, 0);
13071 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
13072 let conv = crlf::ConversionConfig::from_config(&ConfigSet::new());
13073 let attrs = FileAttrs::default();
13074 let params = WorktreeAddRefreshParams {
13075 odb: &odb,
13076 ie: &entry,
13077 meta: &meta,
13078 abs_path: &wt.join("f.txt"),
13079 rel_path: "f.txt",
13080 conv: &conv,
13081 file_attrs: &attrs,
13082 index_mtime: Some((200, 0)),
13083 staged_mode: MODE_REGULAR,
13084 filter_process: None,
13085 };
13086 let refresh = classify_worktree_entry_for_add(¶ms).expect("classify");
13087 assert_eq!(refresh, WorktreeAddRefresh::NeedsRestage);
13088 }
13089
13090 #[test]
13091 fn smudges_racily_clean_modified_entry() {
13092 let td = tempfile::tempdir().expect("tempdir");
13093 let wt = td.path();
13094 let index_mtime = setup_index_mtime(wt);
13095 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
13096 let content_a = b"same length!!";
13097 let content_b = b"same length??";
13098 assert_eq!(content_a.len(), content_b.len());
13099
13100 let mut index = Index::new();
13101 let entry = staged_entry_for_file(&odb, wt, "f.txt", content_a, index_mtime);
13102 let stale_oid = entry.oid;
13103
13104 fs::write(wt.join("f.txt"), content_b).expect("rewrite");
13105 pin_mtime_and_sample(&wt.join("f.txt"), index_mtime.0, index_mtime.1);
13106 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
13107 let entry = entry_from_metadata(&meta, b"f.txt", stale_oid, MODE_REGULAR);
13109 index.entries.push(entry.clone());
13110
13111 assert!(stat_matches(&entry, &meta));
13112 assert!(entry_is_racy(&entry, Some(index_mtime)));
13113
13114 let smudged = smudge_racily_clean_entries(
13115 &odb,
13116 &git_dir,
13117 &mut index,
13118 wt,
13119 Some(index_mtime),
13120 None,
13121 None,
13122 );
13123 assert!(smudged);
13124 assert_eq!(index.entries[0].size, 0);
13125
13126 let diff = diff_index_to_worktree(&odb, &mut index, wt, false, false).expect("diff");
13127 assert_eq!(diff.len(), 1);
13128 assert_eq!(diff[0].path(), "f.txt");
13129 }
13130
13131 fn racy_stale_entry(wt: &Path) -> (PathBuf, Odb, Index, (u32, u32), ObjectId) {
13133 let index_mtime = setup_index_mtime(wt);
13134 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
13135 let entry = staged_entry_for_file(&odb, wt, "f.txt", b"same length!!", index_mtime);
13136 let stale_oid = entry.oid;
13137 fs::write(wt.join("f.txt"), b"same length??").expect("rewrite");
13138 pin_mtime_and_sample(&wt.join("f.txt"), index_mtime.0, index_mtime.1);
13139 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
13140 let mut index = Index::new();
13141 index.entries.push(entry_from_metadata(
13142 &meta,
13143 b"f.txt",
13144 stale_oid,
13145 MODE_REGULAR,
13146 ));
13147 (git_dir, odb, index, index_mtime, stale_oid)
13148 }
13149
13150 #[test]
13151 fn content_verified_entry_is_not_reread_and_the_mark_is_consumed() {
13152 let td = tempfile::tempdir().expect("tempdir");
13153 let wt = td.path();
13154 let (git_dir, odb, mut index, index_mtime, stale_oid) = racy_stale_entry(wt);
13155 index.mark_content_verified(b"f.txt", stale_oid);
13156
13157 let smudge = |index: &mut Index| {
13158 smudge_racily_clean_entries(&odb, &git_dir, index, wt, Some(index_mtime), None, None)
13159 };
13160 assert!(!smudge(&mut index), "a verified entry is trusted");
13161 assert_ne!(index.entries[0].size, 0);
13162 assert!(index.content_verified.is_empty());
13163 assert!(smudge(&mut index), "without the mark the entry is checked");
13164 assert_eq!(index.entries[0].size, 0);
13165 }
13166
13167 #[test]
13168 fn content_verified_mark_for_another_oid_does_not_skip_the_check() {
13169 let td = tempfile::tempdir().expect("tempdir");
13170 let wt = td.path();
13171 let (git_dir, odb, mut index, index_mtime, _) = racy_stale_entry(wt);
13172 let other = odb.hash(ObjectKind::Blob, b"other").expect("hash");
13173 index.mark_content_verified(b"f.txt", other);
13174 assert!(smudge_racily_clean_entries(
13175 &odb,
13176 &git_dir,
13177 &mut index,
13178 wt,
13179 Some(index_mtime),
13180 None,
13181 None,
13182 ));
13183 assert_eq!(index.entries[0].size, 0);
13184 }
13185
13186 #[test]
13187 fn leaves_non_racy_entry_alone() {
13188 let td = tempfile::tempdir().expect("tempdir");
13189 let wt = td.path();
13190 let index_mtime = setup_index_mtime(wt);
13191 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
13192 let content_a = b"same length!!";
13193 let content_b = b"same length??";
13194 let entry_mtime = (index_mtime.0 - 10, 0);
13195
13196 let mut index = Index::new();
13197 let entry = staged_entry_for_file(&odb, wt, "f.txt", content_a, entry_mtime);
13198 index.entries.push(entry.clone());
13199
13200 fs::write(wt.join("f.txt"), content_b).expect("rewrite");
13201 pin_mtime_and_sample(&wt.join("f.txt"), entry_mtime.0, entry_mtime.1);
13202
13203 assert!(!entry_is_racy(&entry, Some(index_mtime)));
13204 let smudged = smudge_racily_clean_entries(
13205 &odb,
13206 &git_dir,
13207 &mut index,
13208 wt,
13209 Some(index_mtime),
13210 None,
13211 None,
13212 );
13213 assert!(!smudged);
13214 assert_eq!(index.entries[0].size, entry.size);
13215 }
13216
13217 #[test]
13218 fn leaves_matching_content_alone() {
13219 let td = tempfile::tempdir().expect("tempdir");
13220 let wt = td.path();
13221 let index_mtime = setup_index_mtime(wt);
13222 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
13223 let bytes = b"unchanged!!!!";
13224
13225 let mut index = Index::new();
13226 let entry = staged_entry_for_file(&odb, wt, "f.txt", bytes, index_mtime);
13227 index.entries.push(entry.clone());
13228
13229 assert!(entry_is_racy(&entry, Some(index_mtime)));
13230 let smudged = smudge_racily_clean_entries(
13231 &odb,
13232 &git_dir,
13233 &mut index,
13234 wt,
13235 Some(index_mtime),
13236 None,
13237 None,
13238 );
13239 assert!(!smudged);
13240 assert_eq!(index.entries[0].size, entry.size);
13241 }
13242
13243 #[test]
13244 fn status_diff_skips_refresh_rewrite_for_racy_clean_matching_stat() {
13245 let td = tempfile::tempdir().expect("tempdir");
13246 let wt = td.path();
13247 let index_mtime = setup_index_mtime(wt);
13248 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
13249 let bytes = b"racy clean payload\n";
13250
13251 let mut index = Index::new();
13252 index.source_mtime = Some(index_mtime);
13253 let entry = staged_entry_for_file(&odb, wt, "f.txt", bytes, index_mtime);
13254 index.entries.push(entry.clone());
13255 assert!(entry_is_racy(&entry, Some(index_mtime)));
13256
13257 let (entries, index_changed) = super::diff_index_to_worktree_with_options(
13258 &odb,
13259 &mut index,
13260 wt,
13261 super::DiffIndexToWorktreeOptions {
13262 repository_git_dir: Some(git_dir),
13263 index_mtime: Some(index_mtime),
13264 refresh_index_stat_in_pass: true,
13265 for_status: true,
13266 ..Default::default()
13267 },
13268 )
13269 .expect("status diff");
13270 assert!(entries.is_empty(), "unexpected unstaged: {entries:?}");
13271 assert!(
13272 !index_changed,
13273 "matching stat on a racy entry must skip refresh without rewriting the index"
13274 );
13275 }
13276
13277 #[test]
13278 fn leaves_matching_sha256_racy_content_alone() {
13279 let td = tempfile::tempdir().expect("tempdir");
13280 let wt = td.path();
13281 let index_mtime = setup_index_mtime(wt);
13282 let (git_dir, odb) = minimal_repo(wt, "[extensions]\n\tobjectformat = sha256\n");
13283 assert_eq!(odb.hash_algo(), HashAlgo::Sha256);
13284
13285 let bytes = b"sha256 width!!";
13286 let oid = odb.hash(ObjectKind::Blob, bytes).expect("hash");
13287 let mut index = Index::new();
13288 let entry = staged_entry_with_oid(wt, "f.txt", bytes, oid, index_mtime);
13289 index.entries.push(entry.clone());
13290
13291 assert!(entry_is_racy(&entry, Some(index_mtime)));
13292 let smudged = smudge_racily_clean_entries(
13293 &odb,
13294 &git_dir,
13295 &mut index,
13296 wt,
13297 Some(index_mtime),
13298 None,
13299 None,
13300 );
13301 assert!(!smudged, "matching SHA-256 blob must not be smudged");
13302 assert_eq!(index.entries[0].size, entry.size);
13303 }
13304
13305 #[test]
13306 fn materialize_big_binary_under_autocrlf_streams_into_pack() {
13307 use std::io::Write;
13308
13309 let td = tempfile::tempdir().expect("tempdir");
13310 let wt = td.path();
13311 let (git_dir, odb) =
13312 minimal_repo(wt, "[core]\n\tautocrlf = true\n\tbigFileThreshold = 64k\n");
13313 let path = wt.join("big.bin");
13314 {
13315 let mut f = fs::File::create(&path).expect("create");
13316 f.write_all(&[0u8; 9000]).expect("nul prefix");
13317 let chunk = vec![1u8; 256 * 1024];
13318 for _ in 0..20 {
13319 f.write_all(&chunk).expect("chunk");
13320 }
13321 }
13322 let meta = fs::metadata(&path).expect("stat");
13323 let mut conv = crlf::ConversionConfig::from_config(&ConfigSet::new());
13324 conv.autocrlf = crlf::AutoCrlf::True;
13325 let attrs = FileAttrs::default();
13326 let oid =
13327 materialize_worktree_blob(&odb, &path, &meta, &conv, &attrs, "big.bin", None, None)
13328 .expect("materialize");
13329 assert!(
13330 !odb.test_loose_object_path(&oid).exists(),
13331 "big binary under autocrlf should pack, not write loose"
13332 );
13333 let pack_dir = git_dir.join("objects/pack");
13334 let names: Vec<_> = fs::read_dir(&pack_dir)
13335 .expect("pack dir")
13336 .map(|e| e.expect("entry").file_name())
13337 .collect();
13338 assert!(
13339 names.iter().any(|n| n.to_string_lossy().ends_with(".pack")),
13340 "{names:?}"
13341 );
13342 assert!(odb.read(&oid).expect("read blob").data.len() > 9000);
13343 }
13344
13345 #[test]
13346 fn indexed_binary_under_autocrlf_rehash_streams() {
13347 use std::io::Write;
13348
13349 let td = tempfile::tempdir().expect("tempdir");
13350 let wt = td.path();
13351 let (_git_dir, odb) =
13352 minimal_repo(wt, "[core]\n\tautocrlf = true\n\tbigFileThreshold = 1m\n");
13353 let path = wt.join("big.bin");
13354 {
13355 let mut f = fs::File::create(&path).expect("create");
13356 f.write_all(&[0u8; 9000]).expect("nul prefix");
13357 let chunk = vec![1u8; 512 * 1024];
13358 for _ in 0..3 {
13359 f.write_all(&chunk).expect("chunk");
13360 }
13361 }
13362 let meta = fs::symlink_metadata(&path).expect("stat");
13363 let oid = hash_worktree_file(
13364 &odb,
13365 &path,
13366 &meta,
13367 &crlf::ConversionConfig::from_config(&ConfigSet::new()),
13368 &FileAttrs::default(),
13369 "big.bin",
13370 None,
13371 None,
13372 )
13373 .expect("hash");
13374 let entry = entry_from_metadata(&meta, b"big.bin", oid, MODE_REGULAR);
13375 let mut conv = crlf::ConversionConfig::from_config(&ConfigSet::new());
13376 conv.autocrlf = crlf::AutoCrlf::True;
13377 let attrs = FileAttrs::default();
13378 let content = read_regular_worktree_blob(
13379 &odb,
13380 &path,
13381 &conv,
13382 &attrs,
13383 "big.bin",
13384 Some(&entry),
13385 None,
13386 1024 * 1024,
13387 )
13388 .expect("read blob");
13389 assert!(
13390 matches!(content, WorktreeBlobContent::Stream { .. }),
13391 "indexed binary re-hash must stream, not buffer"
13392 );
13393 assert_eq!(content.hash(&odb).expect("hash"), entry.oid);
13394 }
13395
13396 #[test]
13397 fn leaves_matching_crlf_worktree_content_alone() {
13398 let td = tempfile::tempdir().expect("tempdir");
13399 let wt = td.path();
13400 let index_mtime = setup_index_mtime(wt);
13401 let (git_dir, odb) = minimal_repo(wt, "[core]\n\tautocrlf = true\n");
13402 let git_form = b"line one\nline two\n";
13403 let worktree_form = b"line one\r\nline two\r\n";
13404 let oid = odb.hash(ObjectKind::Blob, git_form).expect("hash");
13405
13406 let mut index = Index::new();
13407 let entry = staged_entry_with_oid(wt, "f.txt", worktree_form, oid, index_mtime);
13408 index.entries.push(entry.clone());
13409
13410 assert!(entry_is_racy(&entry, Some(index_mtime)));
13411 let smudged = smudge_racily_clean_entries(
13412 &odb,
13413 &git_dir,
13414 &mut index,
13415 wt,
13416 Some(index_mtime),
13417 None,
13418 None,
13419 );
13420 assert!(
13421 !smudged,
13422 "CRLF worktree matching clean index OID must not be smudged"
13423 );
13424 assert_eq!(index.entries[0].size, entry.size);
13425 }
13426
13427 #[test]
13428 fn skips_skip_worktree_and_intent_to_add() {
13429 let td = tempfile::tempdir().expect("tempdir");
13430 let wt = td.path();
13431 let index_mtime = setup_index_mtime(wt);
13432 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
13433 let content_a = b"same length!!";
13434 let content_b = b"same length??";
13435
13436 let mut index = Index::new();
13437 let mut entry = staged_entry_for_file(&odb, wt, "f.txt", content_a, index_mtime);
13438 entry.set_skip_worktree(true);
13439 index.entries.push(entry);
13440 fs::write(wt.join("f.txt"), content_b).expect("rewrite");
13441 pin_mtime_and_sample(&wt.join("f.txt"), index_mtime.0, index_mtime.1);
13442 assert!(!smudge_racily_clean_entries(
13443 &odb,
13444 &git_dir,
13445 &mut index,
13446 wt,
13447 Some(index_mtime),
13448 None,
13449 None,
13450 ));
13451 assert_ne!(index.entries[0].size, 0);
13452
13453 let mut index = Index::new();
13454 let mut entry = staged_entry_for_file(&odb, wt, "f.txt", content_a, index_mtime);
13455 entry.set_intent_to_add(true);
13456 index.entries.push(entry);
13457 fs::write(wt.join("f.txt"), content_b).expect("rewrite");
13458 pin_mtime_and_sample(&wt.join("f.txt"), index_mtime.0, index_mtime.1);
13459 assert!(!smudge_racily_clean_entries(
13460 &odb,
13461 &git_dir,
13462 &mut index,
13463 wt,
13464 Some(index_mtime),
13465 None,
13466 None,
13467 ));
13468 assert_ne!(index.entries[0].size, 0);
13469 }
13470
13471 #[test]
13472 fn smudges_nothing_without_prior_index_mtime() {
13473 let td = tempfile::tempdir().expect("tempdir");
13474 let wt = td.path();
13475 let index_mtime = setup_index_mtime(wt);
13476 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
13477 let mut index = Index::new();
13478 let entry = staged_entry_for_file(&odb, wt, "f.txt", b"same length!!", index_mtime);
13479 index.entries.push(entry);
13480
13481 fs::write(wt.join("f.txt"), b"same length??").expect("rewrite");
13482 pin_mtime_and_sample(&wt.join("f.txt"), index_mtime.0, index_mtime.1);
13483
13484 let smudged = smudge_racily_clean_entries(&odb, &git_dir, &mut index, wt, None, None, None);
13485 assert!(!smudged);
13486 assert_ne!(index.entries[0].size, 0);
13487 }
13488}
13489
13490#[cfg(all(test, unix))]
13491mod read_worktree_bytes_for_mode_tests {
13492 use super::*;
13493 use crate::index::MODE_SYMLINK;
13494 use std::fs;
13495 use std::os::unix::fs::symlink;
13496
13497 #[test]
13498 fn symlink_mode_reads_link_target_not_followed_file() {
13499 let td = tempfile::tempdir().expect("tempdir");
13500 let wt = td.path();
13501 fs::write(wt.join("payload.txt"), b"secret\n").expect("write");
13502 symlink("payload.txt", wt.join("link")).expect("symlink");
13503 let bytes =
13504 read_worktree_bytes_for_mode(&wt.join("link"), MODE_SYMLINK).expect("read link");
13505 assert_eq!(bytes, b"payload.txt");
13506 }
13507
13508 #[test]
13509 fn symlink_mode_reads_plain_file_holding_the_target() {
13510 let td = tempfile::tempdir().expect("tempdir");
13511 let path = td.path().join("link");
13512 fs::write(&path, b"payload.txt").expect("write");
13513 let bytes = read_worktree_bytes_for_mode(&path, MODE_SYMLINK).expect("read plain link");
13514 assert_eq!(bytes, b"payload.txt");
13515 }
13516
13517 #[test]
13518 fn regular_mode_reads_file_contents() {
13519 let td = tempfile::tempdir().expect("tempdir");
13520 let path = td.path().join("f.txt");
13521 fs::write(&path, b"hello\n").expect("write");
13522 let bytes = read_worktree_bytes_for_mode(&path, crate::index::MODE_REGULAR).expect("read");
13523 assert_eq!(bytes, b"hello\n");
13524 }
13525
13526 #[test]
13527 fn annotate_marks_context_line_when_eof_line_is_unchanged() {
13528 let old = "a\nb\nc";
13529 let new = "a\nx\nc";
13530 let raw = super::unified_diff_histogram_hunks_only(old, new, 3, 0, false);
13531 let out = annotate_unified_diff_no_newline_at_eof(&raw, old, new);
13532 assert!(
13533 out.contains(" c\n\\ No newline at end of file"),
13534 "histogram hunk should mark trailing context line:\n{out}"
13535 );
13536 }
13537
13538 #[test]
13539 fn annotate_does_not_treat_removed_dash_lines_as_file_headers() {
13540 use super::unified_diff_with_prefix_and_funcname_and_algorithm;
13541 use similar::Algorithm;
13542
13543 let old = "--old";
13544 let new = "--new";
13545 let raw = unified_diff_with_prefix_and_funcname_and_algorithm(
13546 old,
13547 new,
13548 "a/x",
13549 "b/x",
13550 3,
13551 0,
13552 "",
13553 "",
13554 None,
13555 Algorithm::Myers,
13556 false,
13557 false,
13558 false,
13559 false,
13560 );
13561 assert_eq!(
13562 raw.matches("\\ No newline at end of file").count(),
13563 2,
13564 "expected two markers like git, got:\n{raw}"
13565 );
13566 }
13567}
13568
13569#[cfg(test)]
13570mod annotate_no_newline_tests {
13571 use super::annotate_unified_diff_no_newline_at_eof;
13572
13573 #[test]
13574 fn annotate_marks_both_sides_without_trailing_newline() {
13575 let raw = "@@ -1 +1 @@\n-foo\n+f o o\n";
13576 let out = annotate_unified_diff_no_newline_at_eof(raw, "foo", "f o o");
13577 assert!(out.contains("\\ No newline at end of file"));
13578 assert_eq!(out.matches("\\ No newline at end of file").count(), 2);
13579 }
13580}
13581
13582#[cfg(test)]
13583mod blob_diff_algorithm_smoke {
13584 use super::{
13585 anchored_unified_diff, count_changes_with_algorithm, unified_diff_histogram_hunks_only,
13586 unified_diff_with_prefix_and_funcname_and_algorithm, unified_hunk_body_for_diff_options,
13587 };
13588 use similar::Algorithm;
13589
13590 const OLD: &str = "alpha\nbeta\ngamma\n";
13591 const NEW: &str = "alpha\nBETA\ngamma\ndelta\n";
13592
13593 #[test]
13594 fn seed15_histogram_u0_body_matches_git() {
13595 use crate::diff::options::{DiffAlgorithm, DiffOptions};
13596 use std::collections::hash_map::DefaultHasher;
13597 use std::hash::{Hash, Hasher};
13598 let seed = 15u64;
13599 let mut hasher = DefaultHasher::new();
13600 seed.hash(&mut hasher);
13601 let h = hasher.finish();
13602 let line_count = 4 + (h as usize % 12);
13603 let mut old = String::new();
13604 let mut new = String::new();
13605 for i in 0..line_count {
13606 let token = (h.wrapping_add(i as u64 * 131) % 7) as u8 + b'a';
13607 let ch = token as char;
13608 old.push(ch);
13609 if (h >> (i % 8)) & 1 == 1 {
13610 new.push(((token as u8).wrapping_add(1)) as char);
13611 } else {
13612 new.push(ch);
13613 }
13614 old.push('\n');
13615 new.push('\n');
13616 }
13617 let opts = DiffOptions {
13618 context_lines: 0,
13619 algorithm: DiffAlgorithm::Histogram,
13620 indent_heuristic: false,
13621 ..DiffOptions::default()
13622 };
13623 let body = unified_hunk_body_for_diff_options(&old, &new, &opts);
13624 assert_eq!(
13625 body.matches("@@ ").count(),
13626 2,
13627 "git 2.55 splits u0 histogram hunks at seed 15: {body}"
13628 );
13629 }
13630
13631 #[test]
13632 fn t4071_histogram_changed_flags_git_myers_completes() {
13633 let old: Vec<&str> = "x\nx\nx\nx\n".lines().collect();
13634 let new: Vec<&str> = "x\nx\nx\nA\nB\nC\nD\nx\nE\nF\nG\n".lines().collect();
13635 let (c1, c2) = super::git_xdiff::histogram_changed_flags(&old, &new);
13636 assert_eq!(c1.len(), 4);
13637 assert_eq!(c2.len(), 11);
13638 let ops = super::changed_flags_to_ops(&c1, &c2, old.len(), new.len());
13639 assert!(!ops.is_empty());
13640 }
13641
13642 #[test]
13643 fn t4071_histogram_compacted_ops_completes() {
13644 use crate::diff::WhitespaceMode;
13645 let old = "x\nx\nx\nx\n";
13646 let new = "x\nx\nx\nA\nB\nC\nD\nx\nE\nF\nG\n";
13647 let ops = super::histogram_compacted_line_ops(old, new, false, WhitespaceMode::default());
13648 assert!(!ops.is_empty(), "expected diff ops");
13649 }
13650
13651 #[test]
13652 fn t4071_histogram_u0_body_completes() {
13653 use crate::diff::options::{DiffAlgorithm, DiffOptions};
13654 let old = "x\nx\nx\nx\n";
13655 let new = "x\nx\nx\nA\nB\nC\nD\nx\nE\nF\nG\n";
13656 let opts = DiffOptions {
13657 context_lines: 0,
13658 algorithm: DiffAlgorithm::Histogram,
13659 indent_heuristic: false,
13660 ..DiffOptions::default()
13661 };
13662 let body = unified_hunk_body_for_diff_options(old, new, &opts);
13663 assert!(body.contains("@@"), "expected hunk header: {body}");
13664 }
13665
13666 #[test]
13667 fn t4071_minimal_u0_matches_git_hunk_headers() {
13668 use crate::diff::options::{DiffAlgorithm, DiffOptions};
13669 let old = "x\nx\nx\nx\n";
13670 let new = "x\nx\nx\nA\nB\nC\nD\nx\nE\nF\nG\n";
13671 let opts = DiffOptions {
13672 context_lines: 0,
13673 algorithm: DiffAlgorithm::Minimal,
13674 indent_heuristic: false,
13675 ..DiffOptions::default()
13676 };
13677 let body = unified_hunk_body_for_diff_options(old, new, &opts);
13678 assert_eq!(
13679 body.matches("@@ ").count(),
13680 2,
13681 "git 2.55+ splits u0 minimal hunks at repeated anchors: {body}"
13682 );
13683 }
13684
13685 #[test]
13686 fn myers_patience_histogram_minimal_emit_unified_hunks() {
13687 for algo in [Algorithm::Myers, Algorithm::Patience, Algorithm::Histogram] {
13688 let patch = unified_diff_with_prefix_and_funcname_and_algorithm(
13689 OLD,
13690 NEW,
13691 "a/old.txt",
13692 "b/new.txt",
13693 3,
13694 0,
13695 "a/",
13696 "b/",
13697 None,
13698 algo,
13699 false,
13700 false,
13701 true,
13702 false,
13703 );
13704 assert!(patch.contains("@@"), "algo {algo:?} should emit hunks");
13705 assert!(patch.contains("-beta") || patch.contains("-BETA"));
13706 }
13707 }
13708
13709 #[test]
13710 fn git_histogram_printer_and_anchored_diff_smoke() {
13711 let hist = unified_diff_histogram_hunks_only(OLD, NEW, 3, 0, false);
13712 assert!(!hist.is_empty());
13713 let anchors = vec!["beta".to_string()];
13714 let anchored = anchored_unified_diff(
13715 OLD,
13716 NEW,
13717 "old",
13718 "new",
13719 3,
13720 &anchors,
13721 Algorithm::Myers,
13722 false,
13723 true,
13724 false,
13725 );
13726 assert!(anchored.contains("@@"));
13727 }
13728
13729 #[test]
13730 fn count_changes_git_histogram_vs_similar() {
13731 let hist = count_changes_with_algorithm(OLD, NEW, Algorithm::Myers, true);
13732 let myers = count_changes_with_algorithm(OLD, NEW, Algorithm::Myers, false);
13733 assert!(hist.0 + hist.1 > 0);
13734 assert!(myers.0 + myers.1 > 0);
13735 }
13736}
13737
13738#[cfg(test)]
13739mod line_coverage_tests {
13740 use super::*;
13741 use crate::config::ConfigSet;
13742 use crate::index::StatMatchPolicy;
13743 use crate::index::{Index, IndexEntry, MODE_REGULAR};
13744 use crate::objects::ObjectKind;
13745 use crate::parallel::ThreadPolicy;
13746 use crate::write_tree::write_tree_from_index;
13747 use similar::Algorithm;
13748 use tempfile::TempDir;
13749
13750 fn index_entry(path: &str, mode: u32, oid: ObjectId) -> IndexEntry {
13751 IndexEntry {
13752 ctime_sec: 0,
13753 ctime_nsec: 0,
13754 mtime_sec: 0,
13755 mtime_nsec: 0,
13756 dev: 0,
13757 ino: 0,
13758 mode,
13759 uid: 0,
13760 gid: 0,
13761 size: 0,
13762 oid,
13763 flags: path.len().min(0xfff) as u16,
13764 flags_extended: None,
13765 path: path.as_bytes().to_vec(),
13766 base_index_pos: 0,
13767 }
13768 }
13769
13770 fn write_tree(odb: &Odb, entries: &[(&str, ObjectId)]) -> ObjectId {
13771 let mut body = Vec::new();
13772 for (name, oid) in entries {
13773 body.extend_from_slice(format!("100644 {name}\0").as_bytes());
13774 body.extend_from_slice(oid.as_bytes());
13775 }
13776 odb.write(ObjectKind::Tree, &body).expect("tree")
13777 }
13778
13779 fn pair_of_trees_with_rename() -> (TempDir, Odb, ObjectId, ObjectId) {
13780 let temp = TempDir::new().expect("tempdir");
13781 let odb = Odb::new(temp.path());
13782 let old_blob = odb
13783 .write(ObjectKind::Blob, b"line one\nline two\n")
13784 .expect("old");
13785 let new_blob = odb
13786 .write(ObjectKind::Blob, b"line one\nline TWO\nline three\n")
13787 .expect("new");
13788 let old_tree = write_tree(&odb, &[("old.txt", old_blob)]);
13789 let new_tree = write_tree(&odb, &[("new.txt", new_blob), ("kept.txt", old_blob)]);
13790 (temp, odb, old_tree, new_tree)
13791 }
13792
13793 #[test]
13794 fn detect_copies_reports_source_modes_without_tree_entries() {
13795 let temp = TempDir::new().expect("tempdir");
13796 let odb = Odb::new(temp.path());
13797 let content = odb
13798 .write(ObjectKind::Blob, b"shared content\n")
13799 .expect("blob");
13800 let changed = odb.write(ObjectKind::Blob, b"changed\n").expect("blob");
13801 let old_tree = write_tree(&odb, &[("gone.txt", content), ("mod.txt", content)]);
13802 let new_tree = write_tree(
13803 &odb,
13804 &[
13805 ("copy.txt", content),
13806 ("mod.txt", changed),
13807 ("moved.txt", content),
13808 ],
13809 );
13810 let entries = diff_trees(&odb, Some(&old_tree), Some(&new_tree), "").expect("diff");
13811 let result = detect_copies(&odb, None, entries, 50, false, &[]).expect("copies");
13812 let pairs: Vec<_> = result
13813 .iter()
13814 .filter(|e| matches!(e.status, DiffStatus::Renamed | DiffStatus::Copied))
13815 .map(|e| (e.old_mode, e.new_mode))
13816 .collect();
13817 assert_eq!(pairs.len(), 2, "{result:?}");
13818 assert!(
13819 pairs
13820 .iter()
13821 .all(|p| *p == (FileMode::REGULAR, FileMode::REGULAR)),
13822 "{pairs:?}"
13823 );
13824 }
13825
13826 #[test]
13827 fn diff_helper_and_format_sweep() {
13828 let cfg = ConfigSet::new();
13829 assert!(resolve_indent_heuristic(&cfg, false, false));
13830 assert!(!resolve_indent_heuristic(&cfg, false, true));
13831
13832 let old = "alpha beta\ngamma\n";
13833 let new = "alpha BETA\ngamma delta\n";
13834 let _ = normalize_ignore_space_change(old);
13835 let _ = normalize_ignore_space_change_line("- alpha beta ");
13836 let _ = count_changes(old, new);
13837 let _ = count_git_lines(old.as_bytes());
13838 let _ = diffcore_count_changes(old.as_bytes(), new.as_bytes());
13839 let _ = should_break_rewrite_for_stat(b"x", b"totally different content");
13840 assert!(parse_diff_rename_score_token("50%").is_some());
13841 let _ = rewrite_merge_score(b"a", b"b");
13842 let _ = rewrite_dissimilarity_index_percent(b"abc", b"abd");
13843 assert_eq!(format_mode(0o100644), "100644");
13844 assert!(!format_rename_path("a/old", "b/new").is_empty());
13845
13846 let old_lines = ["a", "b", "c"];
13847 let new_lines = ["a", "b", "d"];
13848 let compact = diff_slice_ops_compacted(&old_lines, &new_lines, Algorithm::Myers, false);
13849 assert!(!compact.is_empty());
13850 let _ =
13851 map_new_to_old_lines_compacted("a\nb\nc\n", "a\nb\nd\n", Algorithm::Myers, false, 3);
13852 }
13853
13854 #[test]
13855 fn tree_diff_rename_copy_postprocess_matrix() -> Result<()> {
13856 let (_temp, odb, old_tree, new_tree) = pair_of_trees_with_rename();
13857 let raw = diff_trees(&odb, Some(&old_tree), Some(&new_tree), "")?;
13858 assert!(!raw.is_empty());
13859 let with_renames = diff_trees_with_renames(&odb, Some(&old_tree), Some(&new_tree), "", 50)?;
13860 assert!(!with_renames.is_empty());
13861 let config = ConfigSet::new();
13862 let policy = ThreadPolicy::default();
13863 let detected =
13864 detect_renames_with_policy(&odb, None, raw.clone(), 50, &config, &policy, None)?;
13865 let source_flat = flatten_tree_entries(&odb, &old_tree)?;
13866 let _ = detect_copies_with_policy(
13867 &odb,
13868 None,
13869 detected.clone(),
13870 50,
13871 true,
13872 &source_flat,
13873 &config,
13874 &policy,
13875 )?;
13876 let opts = DiffPostprocessOptions {
13877 break_score: Some(50),
13878 rename_threshold: 50,
13879 copy_threshold: Some(50),
13880 find_copies_harder: true,
13881 materialize_break_worktree_new_oid: false,
13882 };
13883 let processed = postprocess_diff_entries(
13884 &odb,
13885 raw,
13886 Some(&old_tree),
13887 None,
13888 None,
13889 &config,
13890 &policy,
13891 &opts,
13892 )?;
13893 assert!(!processed.is_empty());
13894 let _ = status_apply_rename_copy_detection(
13895 &odb,
13896 with_renames.clone(),
13897 50,
13898 true,
13899 Some(&old_tree),
13900 &config,
13901 &policy,
13902 None,
13903 )?;
13904 let flat = flatten_tree_entries(&odb, &new_tree)?;
13905 assert!(!flat.is_empty());
13906 let _ = diff_trees_show_tree_entries(&odb, Some(&old_tree), Some(&new_tree), "")?;
13907 Ok(())
13908 }
13909
13910 #[test]
13911 fn unified_diff_flag_matrix() {
13912 let old = "fn main() {\n println!(\"hi\");\n}\n";
13913 let new = "fn main() {\n println!(\"bye\");\n}\n";
13914 for algo in [Algorithm::Myers, Algorithm::Patience, Algorithm::Histogram] {
13915 for (ws, ws_all, fc, ig) in [(true, false, false, false), (false, true, true, true)] {
13916 let patch = unified_diff_with_prefix_and_funcname_and_algorithm(
13917 old,
13918 new,
13919 "a/main.rs",
13920 "b/main.rs",
13921 3,
13922 0,
13923 "a/",
13924 "b/",
13925 None,
13926 algo,
13927 ws,
13928 ws_all,
13929 fc,
13930 ig,
13931 );
13932 assert!(patch.contains("@@"), "algo {algo:?}");
13933 }
13934 }
13935 let hist = unified_diff_histogram_ignore_with_prefix_and_funcname(
13936 old,
13937 new,
13938 "a/x",
13939 "b/x",
13940 3,
13941 0,
13942 "",
13943 "",
13944 None,
13945 false,
13946 false,
13947 false,
13948 |_old, _new| false,
13949 );
13950 assert!(!hist.is_empty());
13951 let raw = format_raw_abbrev(
13952 &DiffEntry {
13953 status: DiffStatus::Modified,
13954 old_path: Some("x".into()),
13955 new_path: Some("x".into()),
13956 old_mode: FileMode::REGULAR,
13957 new_mode: FileMode::REGULAR,
13958 old_oid: empty_blob_oid(),
13959 new_oid: empty_blob_oid(),
13960 score: Some(80),
13961 },
13962 7,
13963 );
13964 assert!(raw.contains('M') || raw.contains('x'));
13965 let stat = format_stat_line_width("path", 10, 20, 40, 8);
13966 assert!(stat.contains("path"));
13967 }
13968
13969 #[test]
13970 fn index_and_commit_diff_paths() -> Result<()> {
13971 let temp = TempDir::new().expect("tempdir");
13972 let odb = Odb::new(temp.path());
13973 let b1 = odb.write(ObjectKind::Blob, b"v1\n").expect("b1");
13974 let b2 = odb.write(ObjectKind::Blob, b"v2\n").expect("b2");
13975 let b3 = odb.write(ObjectKind::Blob, b"v3\n").expect("b3");
13976 let mut index = Index::new();
13977 index.add_or_replace(index_entry("file.txt", MODE_REGULAR, b2));
13978 let head = write_tree_from_index(&odb, &index, "").expect("tree");
13979 index.entries_mut()[0].oid = b1;
13980 let idx_diff = diff_index_to_tree(&odb, &index, Some(&head), false)?;
13981 assert!(!idx_diff.is_empty());
13982 let commit_body = format!(
13983 "tree {head}\nauthor T <t@e.com> 1 +0000\ncommitter T <t@e.com> 1 +0000\n\nm\n"
13984 );
13985 let c1 = odb
13986 .write(ObjectKind::Commit, commit_body.as_bytes())
13987 .expect("c1");
13988 let mut index2 = Index::new();
13989 index2.add_or_replace(index_entry("file.txt", MODE_REGULAR, b3));
13990 let head2 = write_tree_from_index(&odb, &index2, "").expect("head2");
13991 let c2_body = format!(
13992 "tree {head2}\nparent {c1}\nauthor T <t@e.com> 2 +0000\ncommitter T <t@e.com> 2 +0000\n\nm2\n"
13993 );
13994 let c2 = odb
13995 .write(ObjectKind::Commit, c2_body.as_bytes())
13996 .expect("c2");
13997 let commit_diff = diff_commits_with_renames(&odb, &c1, &c2, 50)?;
13998 assert!(!commit_diff.is_empty());
13999 let _ = tree_oid_of_commit(&odb, &c2)?;
14000 let mtime = index_mtime_for_diff(&index, Some((1, 0)));
14001 let _ = entry_is_racy(&index.entries()[0], mtime);
14002 let _ = entry_is_racy_with_policy(&index.entries()[0], mtime, StatMatchPolicy::default());
14003 Ok(())
14004 }
14005
14006 #[test]
14007 fn worktree_index_diff_and_rename_score() -> Result<()> {
14008 let temp = TempDir::new().expect("tempdir");
14009 let wt = temp.path();
14010 let git_dir = wt.join(".git");
14011 std::fs::create_dir_all(git_dir.join("objects")).expect("objects");
14012 let objects_dir = git_dir.join("objects");
14013 let odb = Odb::new(&objects_dir);
14014 let content = b"hello world\n";
14015 std::fs::write(wt.join("tracked.txt"), content).expect("write");
14016 let oid = odb.write(ObjectKind::Blob, content).expect("blob");
14017 let mut index = Index::new();
14018 index.add_or_replace(index_entry("tracked.txt", MODE_REGULAR, oid));
14019 let head = write_tree_from_index(&odb, &index, "").expect("tree");
14020 std::fs::write(wt.join("tracked.txt"), b"hello WORLD\n").expect("mutate");
14021 let mut index_mut = index.clone();
14022 let diff = diff_index_to_worktree(&odb, &mut index_mut, wt, false, false)?;
14023 assert!(!diff.is_empty());
14024 let _ = diff_tree_to_worktree(&odb, Some(&head), wt, &index)?;
14025 assert_eq!(rename_similarity_score(b"a", b"a\n"), 0);
14026 assert_eq!(rename_similarity_score(b"a\n", b"a"), 0);
14027 let score = rename_similarity_score(b"line one\nline two\n", b"line one\nline TWO\n");
14028 assert!(score >= 50);
14029 let _ = detect_renames(&odb, Some(wt), diff, 50);
14030 Ok(())
14031 }
14032
14033 #[test]
14034 fn extended_diff_helpers_and_rotate_order() -> Result<()> {
14035 use crate::crlf::{ConversionConfig, FileAttrs};
14036 let temp = TempDir::new().expect("tempdir");
14037 let wt = temp.path();
14038 let git_dir = wt.join(".git");
14039 std::fs::create_dir_all(git_dir.join("objects")).expect("objects");
14040 let objects_dir = git_dir.join("objects");
14041 let odb = Odb::new(&objects_dir);
14042 let b_old = odb.write(ObjectKind::Blob, b"alpha\nbeta\n").expect("old");
14043 let b_new = odb
14044 .write(ObjectKind::Blob, b"alpha\nBETA\ngamma\n")
14045 .expect("new");
14046 let old_tree = write_tree(&odb, &[("a.txt", b_old), ("b.txt", b_old)]);
14047 let new_tree = write_tree(
14048 &odb,
14049 &[("a.txt", b_new), ("b.txt", b_new), ("c.txt", b_new)],
14050 );
14051 let entries = diff_trees(&odb, Some(&old_tree), Some(&new_tree), "")?;
14052 assert!(entries.len() >= 2);
14053 let rotated = apply_rotate_skip_entries(entries.clone(), Some("b.txt"), None)?;
14054 assert!(!rotated.is_empty());
14055 let _ =
14056 apply_rotate_skip_log_entries(&odb, &new_tree, entries.clone(), Some("a.txt"), None);
14057 let _ =
14058 apply_rotate_skip_log_entries(&odb, &new_tree, entries.clone(), None, Some("a.txt"));
14059 let order_path = wt.join("order");
14060 std::fs::write(&order_path, b"c.txt\na.txt\n").expect("order");
14061 let _ = apply_orderfile_entries(entries.clone(), order_path.to_str().expect("utf8"), wt);
14062 let _ = read_orderfile_patterns(order_path.to_str().expect("utf8"), wt)?;
14063
14064 let cfg = ConfigSet::new();
14065 assert!(indent_heuristic_from_config(&cfg));
14066 let _ = unified_diff_histogram_hunks_only("a\nb\n", "a\nB\n", 3, 0, false);
14067 let _ = unified_diff_histogram_with_prefix_and_funcname(
14068 "x\n", "y\n", "a/f", "b/f", 3, 0, "", "", None, false, false,
14069 );
14070 let _ = count_changes_with_algorithm("a\n", "b\n", Algorithm::Myers, false);
14071 let _ = should_break_rewrite_pair(b"short", b"totally different body", 50);
14072 let _ = head_path_states(&odb, Some(&new_tree))?;
14073 std::fs::write(wt.join("tracked.txt"), b"hello\n").expect("wt");
14074 let abs = wt.join("tracked.txt");
14075 let meta = std::fs::metadata(&abs).expect("meta");
14076 let mut index = Index::new();
14077 index.add_or_replace(index_entry("tracked.txt", MODE_REGULAR, b_old));
14078 let _ = stat_matches_with_policy(&index.entries()[0], &meta, StatMatchPolicy::default());
14079 let _ = worktree_differs_from_index_entry(
14080 &odb,
14081 &git_dir,
14082 wt,
14083 &index.entries()[0],
14084 false,
14085 None,
14086 None,
14087 )?;
14088 let conv = ConversionConfig::from_config(&cfg);
14089 let attrs = FileAttrs::default();
14090 let params = WorktreeAddRefreshParams {
14091 odb: &odb,
14092 ie: &index.entries()[0],
14093 meta: &meta,
14094 abs_path: &abs,
14095 rel_path: "tracked.txt",
14096 conv: &conv,
14097 file_attrs: &attrs,
14098 index_mtime: None,
14099 staged_mode: MODE_REGULAR,
14100 filter_process: None,
14101 };
14102 let _ = classify_worktree_entry_for_add(¶ms)?;
14103 let _ = hash_worktree_file(
14104 &odb,
14105 &abs,
14106 &meta,
14107 &conv,
14108 &attrs,
14109 "tracked.txt",
14110 Some(&index.entries()[0]),
14111 None,
14112 )?;
14113 let _ = materialize_worktree_blob(
14114 &odb,
14115 &abs,
14116 &meta,
14117 &conv,
14118 &attrs,
14119 "tracked.txt",
14120 Some(&index.entries()[0]),
14121 None,
14122 )?;
14123 let _ = read_worktree_bytes_for_mode(&abs, MODE_REGULAR);
14124 let _ = mode_from_metadata(&meta);
14125 let _ = smudge_racily_clean_entries(&odb, &git_dir, &mut index, wt, None, Some(&cfg), None);
14126 let _ = refresh_index_stat_content_verified(
14127 &odb,
14128 &git_dir,
14129 &mut index,
14130 wt,
14131 None,
14132 Some(&cfg),
14133 None,
14134 )?;
14135 let _ = refresh_index_stat_content_verified_with_rules(
14136 &odb,
14137 &git_dir,
14138 &mut index,
14139 wt,
14140 None,
14141 Some(&cfg),
14142 None,
14143 None,
14144 )?;
14145 let _ = path_checkout_skip_blob_write_when_up_to_date(
14146 &odb,
14147 &git_dir,
14148 wt,
14149 &index,
14150 &index.entries()[0],
14151 &b_old,
14152 MODE_REGULAR,
14153 "tracked.txt",
14154 None,
14155 )?;
14156 let patch = anchored_unified_diff(
14157 "a\nb\n",
14158 "a\nB\n",
14159 "f",
14160 "f",
14161 3,
14162 &[String::from("b")],
14163 Algorithm::Myers,
14164 false,
14165 true,
14166 false,
14167 );
14168 assert!(patch.contains("@@"));
14169 let _ = annotate_unified_diff_no_newline_at_eof("@@\n+a\n\\ No newline\n", "a", "a\n");
14170 Ok(())
14171 }
14172
14173 #[test]
14174 fn unmerged_only_tree_worktree_diff_and_func_heuristics() -> Result<()> {
14175 use similar::DiffOp;
14176 assert!(inserted_block_starts_with_c_like_function_definition(
14177 "static int foo(int bar) {"
14178 ));
14179 assert!(!inserted_block_starts_with_c_like_function_definition(
14180 "printf(hi);"
14181 ));
14182 let ops = vec![
14183 DiffOp::Equal {
14184 old_index: 0,
14185 new_index: 0,
14186 len: 4,
14187 },
14188 DiffOp::Insert {
14189 old_index: 4,
14190 new_index: 4,
14191 new_len: 2,
14192 },
14193 ];
14194 let new_lines = ["a\n", "b\n", "c\n", "d\n", "void added() {\n", "}\n"];
14195 assert!(append_with_whole_function_added(
14196 &ops, 4, 6, &new_lines, None
14197 ));
14198 let _ = hunk_old_change_end_exclusive(&ops);
14199
14200 let temp = TempDir::new().expect("tempdir");
14201 let wt = temp.path();
14202 let git_dir = wt.join(".git");
14203 let objects_dir = git_dir.join("objects");
14204 std::fs::create_dir_all(&objects_dir).expect("objects");
14205 let odb = Odb::new(&objects_dir);
14206 let tree_blob = odb.write(ObjectKind::Blob, b"base\n").expect("blob");
14207 std::fs::write(wt.join("conflict.txt"), b"worktree\n").expect("wt");
14208 let tree = write_tree(&odb, &[("conflict.txt", tree_blob)]);
14209 let mut index = Index::new();
14210 let mut stage1 = index_entry("conflict.txt", MODE_REGULAR, tree_blob);
14211 stage1.flags = (stage1.flags & 0x0FFF) | (1 << 12);
14212 index.entries_mut().push(stage1);
14213 let cfg = ConfigSet::new();
14214 let diff = diff_tree_to_worktree_with_git_dir(
14215 &odb,
14216 Some(&tree),
14217 wt,
14218 &git_dir,
14219 &index,
14220 Some(&cfg),
14221 )?;
14222 assert!(!diff.is_empty());
14223 Ok(())
14224 }
14225
14226 #[test]
14227 fn high_volume_unified_diff_and_postprocess() -> Result<()> {
14228 let (_temp, odb, old_tree, new_tree) = pair_of_trees_with_rename();
14229 let config = ConfigSet::new();
14230 let policy = ThreadPolicy::default();
14231 for i in 0..600u32 {
14232 let old = format!("seed {i}\nalpha\n");
14233 let new = format!("seed {i}\nalpha\nvoid fn_{i}(int arg) {{\n return arg;\n}}\n");
14234 let _ = unified_diff_with_prefix_and_funcname_and_algorithm(
14235 &old,
14236 &new,
14237 "a/f.c",
14238 "b/f.c",
14239 3,
14240 0,
14241 "a/",
14242 "b/",
14243 None,
14244 Algorithm::Myers,
14245 i % 2 == 0,
14246 i % 3 == 0,
14247 i % 5 == 0,
14248 i % 7 == 0,
14249 );
14250 let threshold = 30 + (i % 40);
14251 let raw = diff_trees(&odb, Some(&old_tree), Some(&new_tree), "")?;
14252 let opts = DiffPostprocessOptions {
14253 rename_threshold: threshold,
14254 copy_threshold: Some(threshold),
14255 find_copies_harder: i % 2 == 0,
14256 break_score: Some(threshold as u64),
14257 materialize_break_worktree_new_oid: i % 4 == 0,
14258 };
14259 let _ = postprocess_diff_entries(
14260 &odb,
14261 raw,
14262 Some(&old_tree),
14263 None,
14264 None,
14265 &config,
14266 &policy,
14267 &opts,
14268 )?;
14269 }
14270 Ok(())
14271 }
14272}