1use crate::git_path::CanonicalizeExt;
25use std::borrow::Cow;
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, SubmoduleError};
38use crate::index::{CacheTreeNode, Index, IndexEntry, MODE_SYMLINK, MODE_TREE};
39use crate::objects::{
40 parse_commit, CommitData, FileMode, ObjectId, ObjectKind, TreeEntry, TreeEntryRef, TreeRefIter,
41};
42use crate::odb::{Odb, SessionTree};
43use crate::parallel::ThreadPolicy;
44use crate::repo::Repository;
45use crate::sparse_checkout::SparseCheckout;
46use crate::userdiff::FuncnameMatcher;
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>(
60 side: &'a LineSide<'a>,
61 ws: super::WhitespaceMode,
62 normalized: &'a mut Vec<Cow<'a, str>>,
63) -> Cow<'a, [&'a str]> {
64 if !ws.normalizes_line_compare() {
65 return Cow::Borrowed(side.records());
66 }
67 normalized.extend(
69 side.lines()
70 .map(|line| ws.normalize_line_for_compare(line.body)),
71 );
72 Cow::Owned(normalized.iter().map(Cow::as_ref).collect())
73}
74
75#[path = "../structured_diff.rs"]
76pub mod structured;
77
78pub(crate) fn imara_unified_hunk_slices(body: &str) -> Vec<&str> {
79 let mut starts: Vec<usize> = Vec::new();
80 if body.starts_with("@@") {
81 starts.push(0);
82 }
83 for (idx, _) in body.match_indices("\n@@ ") {
84 starts.push(idx + 1);
85 }
86 starts.push(body.len());
87 starts.windows(2).map(|w| &body[w[0]..w[1]]).collect()
88}
89
90pub(crate) fn histogram_compacted_line_ops(
92 old_content: &str,
93 new_content: &str,
94 indent_heuristic: bool,
95 ws: super::WhitespaceMode,
96) -> Vec<similar::DiffOp> {
97 let before = LineSide::from_text(old_content);
98 let after = LineSide::from_text(new_content);
99 let old_lines: Vec<&str> = before.lines().map(|l| l.body).collect();
100 let new_lines: Vec<&str> = after.lines().map(|l| l.body).collect();
101 let old_len = old_lines.len();
102 let new_len = new_lines.len();
103 let (mut old_normalized, mut new_normalized) = (Vec::new(), Vec::new());
104 let old_compare = line_compare_keys(&before, ws, &mut old_normalized);
105 let new_compare = line_compare_keys(&after, ws, &mut new_normalized);
106
107 let (changed1, changed2) = git_xdiff::histogram_changed_flags(&old_compare, &new_compare);
108
109 if !changed1.iter().any(|c| *c) && !changed2.iter().any(|c| *c) {
110 return Vec::new();
111 }
112 let raw = changed_flags_to_ops(&changed1, &changed2, old_len, new_len);
113 diff_indent_heuristic::apply_change_compact_to_ops_with_ws_histogram(
114 &raw,
115 diff_indent_heuristic::CompactSide {
116 lines: &old_lines,
117 keys: &old_compare,
118 compare_ids: None,
119 },
120 diff_indent_heuristic::CompactSide {
121 lines: &new_lines,
122 keys: &new_compare,
123 compare_ids: None,
124 },
125 indent_heuristic,
126 ws,
127 true,
128 )
129}
130
131pub(crate) fn histogram_unified_body_raw(
132 old_content: &str,
133 new_content: &str,
134 context_lines: usize,
135 inter_hunk_context: usize,
136 indent_heuristic: bool,
137 ws: super::WhitespaceMode,
138) -> String {
139 let before_side = LineSide::from_text(old_content);
140 let after_side = LineSide::from_text(new_content);
141 let ops = histogram_compacted_line_ops(old_content, new_content, indent_heuristic, ws);
142 let mut edits = diff_ops_to_edit_spans(&ops);
143 if edits.is_empty() {
144 if old_content != new_content && !ws.normalizes_line_compare() {
145 edits = line_ending_only_edits(&before_side, &after_side);
146 }
147 if edits.is_empty() {
148 return String::new();
149 }
150 }
151 emit_unified_body_from_edits(
152 &before_side,
153 &after_side,
154 &edits,
155 UnifiedEmitOptions {
156 context_lines,
157 inter_hunk_context,
158 function_context: false,
159 },
160 None,
161 )
162}
163
164#[allow(clippy::too_many_arguments)]
180fn diff_ops_to_ignorable_edit_spans<F>(
181 ops: &[similar::DiffOp],
182 old_lines: &[&str],
183 new_lines: &[&str],
184 is_ignorable_change: F,
185) -> Vec<EditSpan>
186where
187 F: Fn(&[&str], &[&str]) -> bool,
188{
189 use similar::DiffOp;
190 let mut out = Vec::new();
191 for op in ops {
192 match *op {
193 DiffOp::Equal { .. } => {}
194 DiffOp::Delete {
195 old_index,
196 old_len,
197 new_index,
198 } => {
199 let removed = &old_lines[old_index..old_index + old_len];
200 out.push(EditSpan {
201 before_start: old_index as i64,
202 before_len: old_len as i64,
203 after_start: new_index as i64,
204 after_len: 0,
205 skip_in_output: is_ignorable_change(removed, &[]),
206 });
207 }
208 DiffOp::Insert {
209 old_index,
210 new_index,
211 new_len,
212 } => {
213 let added = &new_lines[new_index..new_index + new_len];
214 out.push(EditSpan {
215 before_start: old_index as i64,
216 before_len: 0,
217 after_start: new_index as i64,
218 after_len: new_len as i64,
219 skip_in_output: is_ignorable_change(&[], added),
220 });
221 }
222 DiffOp::Replace {
223 old_index,
224 old_len,
225 new_index,
226 new_len,
227 } => {
228 let removed = &old_lines[old_index..old_index + old_len];
229 let added = &new_lines[new_index..new_index + new_len];
230 out.push(EditSpan {
231 before_start: old_index as i64,
232 before_len: old_len as i64,
233 after_start: new_index as i64,
234 after_len: new_len as i64,
235 skip_in_output: is_ignorable_change(removed, added),
236 });
237 }
238 }
239 }
240 out
241}
242
243#[allow(clippy::too_many_arguments)]
244pub(crate) fn histogram_unified_body_ignore_fc<F>(
245 old_content: &str,
246 new_content: &str,
247 context_lines: usize,
248 inter_hunk_context: usize,
249 function_context: bool,
250 funcname_matcher: Option<&FuncnameMatcher>,
251 indent_heuristic: bool,
252 ws: super::WhitespaceMode,
253 is_ignorable_change: F,
254) -> String
255where
256 F: Fn(&[&str], &[&str]) -> bool,
257{
258 let before = LineSide::from_text(old_content);
259 let after = LineSide::from_text(new_content);
260 let old_lines: Vec<&str> = before.lines().map(|l| l.body).collect();
261 let new_lines: Vec<&str> = after.lines().map(|l| l.body).collect();
262 let ops = histogram_compacted_line_ops(old_content, new_content, indent_heuristic, ws);
263 if ops.is_empty() {
264 return String::new();
265 }
266 let edits = diff_ops_to_ignorable_edit_spans(&ops, &old_lines, &new_lines, is_ignorable_change);
267 emit_unified_body_from_edits(
268 &before,
269 &after,
270 &edits,
271 UnifiedEmitOptions {
272 context_lines,
273 inter_hunk_context,
274 function_context,
275 },
276 funcname_matcher,
277 )
278}
279
280fn line_bodies<'a>(side: &LineSide<'a>) -> Vec<&'a str> {
282 side.lines().map(|l| l.body).collect()
283}
284
285#[must_use]
287fn count_diff_ops(ops: &[similar::DiffOp]) -> (usize, usize) {
288 use similar::DiffOp;
289 let mut insertions = 0usize;
290 let mut deletions = 0usize;
291 for op in ops {
292 match *op {
293 DiffOp::Equal { .. } => {}
294 DiffOp::Delete { old_len, .. } => deletions += old_len,
295 DiffOp::Insert { new_len, .. } => insertions += new_len,
296 DiffOp::Replace {
297 old_len, new_len, ..
298 } => {
299 deletions += old_len;
300 insertions += new_len;
301 }
302 }
303 }
304 (insertions, deletions)
305}
306
307pub(crate) fn compacted_line_ops_for_options(
309 old_content: &str,
310 new_content: &str,
311 options: &crate::diff::options::DiffOptions,
312) -> Vec<similar::DiffOp> {
313 use crate::diff::options::DiffAlgorithm;
314
315 let ws = options.whitespace;
316 match options.algorithm {
317 DiffAlgorithm::Histogram => {
318 histogram_compacted_line_ops(old_content, new_content, options.indent_heuristic, ws)
319 }
320 DiffAlgorithm::Patience => {
321 let before = LineSide::from_text(old_content);
322 let after = LineSide::from_text(new_content);
323 if before.is_empty() && after.is_empty() {
324 return Vec::new();
325 }
326 let old_refs: Vec<&str> = before.lines().map(|l| l.body).collect();
327 let new_refs: Vec<&str> = after.lines().map(|l| l.body).collect();
328 let (mut old_normalized, mut new_normalized) = (Vec::new(), Vec::new());
329 let old_compare = line_compare_keys(&before, ws, &mut old_normalized);
330 let new_compare = line_compare_keys(&after, ws, &mut new_normalized);
331 let (changed1, changed2) =
332 git_xdiff::patience_changed_flags(&old_compare, &new_compare);
333 let raw = changed_flags_to_ops(&changed1, &changed2, old_refs.len(), new_refs.len());
334 diff_indent_heuristic::apply_change_compact_to_ops_with_ws_histogram(
335 &raw,
336 diff_indent_heuristic::CompactSide {
337 lines: &old_refs,
338 keys: &old_compare,
339 compare_ids: None,
340 },
341 diff_indent_heuristic::CompactSide {
342 lines: &new_refs,
343 keys: &new_compare,
344 compare_ids: None,
345 },
346 options.indent_heuristic,
347 ws,
348 false,
349 )
350 }
351 DiffAlgorithm::Myers | DiffAlgorithm::Minimal => {
352 let before = LineSide::from_text(old_content);
353 let after = LineSide::from_text(new_content);
354 if before.is_empty() && after.is_empty() {
355 return Vec::new();
356 }
357 let (mut old_normalized, mut new_normalized) = (Vec::new(), Vec::new());
358 let old_compare = line_compare_keys(&before, ws, &mut old_normalized);
359 let new_compare = line_compare_keys(&after, ws, &mut new_normalized);
360 git_xdiff_compacted_line_ops(
361 &old_compare,
362 &new_compare,
363 &before,
364 &after,
365 ws,
366 options.indent_heuristic,
367 options.algorithm == DiffAlgorithm::Minimal,
368 )
369 }
370 }
371}
372
373#[must_use]
375pub(crate) fn count_line_changes_with_options(
376 old_content: &str,
377 new_content: &str,
378 options: &crate::diff::options::DiffOptions,
379) -> (usize, usize) {
380 let ws = options.whitespace;
381 let ops = compacted_line_ops_for_options(old_content, new_content, options);
382 let (mut insertions, mut deletions) = count_diff_ops(&ops);
383 if insertions == 0
384 && deletions == 0
385 && old_content != new_content
386 && !ws.normalizes_line_compare()
387 {
388 let before = LineSide::from_text(old_content);
389 let after = LineSide::from_text(new_content);
390 for edit in line_ending_only_edits(&before, &after) {
391 insertions += edit.after_len as usize;
392 deletions += edit.before_len as usize;
393 }
394 }
395 (insertions, deletions)
396}
397
398type XdiffTrimmedIds = ((Vec<bool>, Vec<bool>), Vec<u32>, Vec<u32>);
400
401fn xdiff_changed_flags_trimmed_ids(
402 old_compare: &[&str],
403 new_compare: &[&str],
404 need_min: bool,
405) -> XdiffTrimmedIds {
406 use crate::line_interner::LineInterner;
407
408 let nrec1 = old_compare.len();
409 let nrec2 = new_compare.len();
410 if nrec1 == 0 && nrec2 == 0 {
411 return ((Vec::new(), Vec::new()), Vec::new(), Vec::new());
412 }
413 let mut interner = LineInterner::with_capacity(nrec1 + nrec2);
414 let ids1_full = interner.intern_all(old_compare);
415 let ids2_full = interner.intern_all(new_compare);
416 let nclasses = interner.len();
417 drop(interner);
420
421 let lim = nrec1.min(nrec2);
422 let mut head = 0usize;
423 while head < lim && ids1_full[head] == ids2_full[head] {
424 head += 1;
425 }
426 let mut tail = 0usize;
427 let rem = lim - head;
428 while tail < rem && ids1_full[nrec1 - 1 - tail] == ids2_full[nrec2 - 1 - tail] {
429 tail += 1;
430 }
431 let shared_records = head + tail;
432 let (full1, full2) = if shared_records == 0 {
433 git_xdiff::changed_flags(&ids1_full, &ids2_full, need_min)
434 } else {
435 let cut_ids1 = &ids1_full[head..nrec1 - tail];
436 let cut_ids2 = &ids2_full[head..nrec2 - tail];
437 let mut shared_counts = vec![0u32; nclasses];
438 for &id in &ids1_full[..head] {
439 shared_counts[id as usize] += 1;
440 }
441 for &id in &ids1_full[nrec1 - tail..] {
442 shared_counts[id as usize] += 1;
443 }
444 let (mid1, mid2) = git_xdiff::changed_flags_with_shared(
445 cut_ids1,
446 cut_ids2,
447 &shared_counts,
448 shared_records,
449 need_min,
450 );
451 let mut full1 = vec![false; nrec1];
452 let mut full2 = vec![false; nrec2];
453 if !mid1.is_empty() {
454 full1[head..nrec1 - tail].copy_from_slice(&mid1);
455 }
456 if !mid2.is_empty() {
457 full2[head..nrec2 - tail].copy_from_slice(&mid2);
458 }
459 (full1, full2)
460 };
461 ((full1, full2), ids1_full, ids2_full)
462}
463
464fn xdiff_changed_flags_trimmed_preinterned(
465 ids1_full: &[u32],
466 ids2_full: &[u32],
467 need_min: bool,
468) -> (Vec<bool>, Vec<bool>) {
469 let nrec1 = ids1_full.len();
470 let nrec2 = ids2_full.len();
471 if nrec1 == 0 && nrec2 == 0 {
472 return (Vec::new(), Vec::new());
473 }
474
475 let lim = nrec1.min(nrec2);
476 let mut head = 0usize;
477 while head < lim && ids1_full[head] == ids2_full[head] {
478 head += 1;
479 }
480 let mut tail = 0usize;
481 let rem = lim - head;
482 while tail < rem && ids1_full[nrec1 - 1 - tail] == ids2_full[nrec2 - 1 - tail] {
483 tail += 1;
484 }
485 let shared_records = head + tail;
486 if shared_records == 0 {
487 return git_xdiff::changed_flags(ids1_full, ids2_full, need_min);
488 }
489 let cut_ids1 = &ids1_full[head..nrec1 - tail];
490 let cut_ids2 = &ids2_full[head..nrec2 - tail];
491 let nclasses = ids1_full
492 .iter()
493 .chain(ids2_full)
494 .max()
495 .map_or(0, |&max| max as usize + 1);
496 let mut shared_counts = vec![0u32; nclasses];
497 for &id in &ids1_full[..head] {
498 shared_counts[id as usize] += 1;
499 }
500 for &id in &ids1_full[nrec1 - tail..] {
501 shared_counts[id as usize] += 1;
502 }
503 let (mid1, mid2) = git_xdiff::changed_flags_with_shared(
504 cut_ids1,
505 cut_ids2,
506 &shared_counts,
507 shared_records,
508 need_min,
509 );
510 let mut full1 = vec![false; nrec1];
511 let mut full2 = vec![false; nrec2];
512 if !mid1.is_empty() {
513 full1[head..nrec1 - tail].copy_from_slice(&mid1);
514 }
515 if !mid2.is_empty() {
516 full2[head..nrec2 - tail].copy_from_slice(&mid2);
517 }
518 (full1, full2)
519}
520
521fn xdiff_changed_flags_trimmed_byte_ids(
522 old_compare: &[Vec<u8>],
523 new_compare: &[Vec<u8>],
524 need_min: bool,
525) -> XdiffTrimmedIds {
526 use crate::line_interner::ByteLineInterner;
527
528 let nrec1 = old_compare.len();
529 let nrec2 = new_compare.len();
530 if nrec1 == 0 && nrec2 == 0 {
531 return ((Vec::new(), Vec::new()), Vec::new(), Vec::new());
532 }
533 let mut interner = ByteLineInterner::with_capacity(nrec1 + nrec2);
534 let ids1_full = interner.intern_all_vecs(old_compare);
535 let ids2_full = interner.intern_all_vecs(new_compare);
536 let (full1, full2) = xdiff_changed_flags_trimmed_preinterned(&ids1_full, &ids2_full, need_min);
537 ((full1, full2), ids1_full, ids2_full)
538}
539
540pub(crate) fn intern_merge_compare_three_way(
542 base: &[Vec<u8>],
543 ours: &[Vec<u8>],
544 theirs: &[Vec<u8>],
545) -> (Vec<u32>, Vec<u32>, Vec<u32>) {
546 use crate::line_interner::ByteLineInterner;
547
548 let mut interner = ByteLineInterner::with_capacity(base.len() + ours.len() + theirs.len());
549 (
550 interner.intern_all_vecs(base),
551 interner.intern_all_vecs(ours),
552 interner.intern_all_vecs(theirs),
553 )
554}
555
556pub(crate) fn merge_myops_from_preinterned_ids(
557 old_ids: &[u32],
558 new_ids: &[u32],
559 need_min: bool,
560) -> Vec<similar::DiffOp> {
561 const INDENT_HEURISTIC: bool = false;
562 let (changed1, changed2) = xdiff_changed_flags_trimmed_preinterned(old_ids, new_ids, need_min);
563 let old_lines = merge_dummy_line_refs(old_ids.len());
564 let new_lines = merge_dummy_line_refs(new_ids.len());
565 diff_indent_heuristic::apply_change_compact_to_masks(
566 changed1,
567 changed2,
568 diff_indent_heuristic::CompactSide {
569 lines: &old_lines,
570 keys: &old_lines,
571 compare_ids: Some(old_ids),
572 },
573 diff_indent_heuristic::CompactSide {
574 lines: &new_lines,
575 keys: &new_lines,
576 compare_ids: Some(new_ids),
577 },
578 INDENT_HEURISTIC,
579 )
580}
581
582fn merge_dummy_line_refs(len: usize) -> Vec<&'static str> {
583 vec![""; len]
584}
585
586enum MergeFileCompactAlgorithm {
587 Myers { need_min: bool },
588 Histogram,
589 Patience,
590}
591
592fn parse_merge_file_compact_algorithm(name: Option<&str>) -> MergeFileCompactAlgorithm {
593 match name.map(str::to_lowercase).as_deref() {
594 None | Some("myers") => MergeFileCompactAlgorithm::Myers { need_min: false },
595 Some("minimal") => MergeFileCompactAlgorithm::Myers { need_min: true },
596 Some("histogram") => MergeFileCompactAlgorithm::Histogram,
597 Some("patience") => MergeFileCompactAlgorithm::Patience,
598 Some(_) => MergeFileCompactAlgorithm::Myers { need_min: false },
599 }
600}
601
602pub(crate) fn merge_file_compacted_line_ops(
607 old_compare: &[Vec<u8>],
608 new_compare: &[Vec<u8>],
609 diff_algorithm: Option<&str>,
610) -> Vec<similar::DiffOp> {
611 use crate::diff::git_histogram::histogram_changed_flags_bytes;
612 use crate::line_interner::ByteLineInterner;
613
614 const INDENT_HEURISTIC: bool = false;
615 let algo = parse_merge_file_compact_algorithm(diff_algorithm);
616 match algo {
617 MergeFileCompactAlgorithm::Myers { need_min } => {
618 let ((_, _), old_key_ids, new_key_ids) =
619 xdiff_changed_flags_trimmed_byte_ids(old_compare, new_compare, need_min);
620 merge_myops_from_preinterned_ids(&old_key_ids, &new_key_ids, need_min)
621 }
622 MergeFileCompactAlgorithm::Histogram | MergeFileCompactAlgorithm::Patience => {
623 let old_refs: Vec<&[u8]> = old_compare.iter().map(|line| line.as_slice()).collect();
624 let new_refs: Vec<&[u8]> = new_compare.iter().map(|line| line.as_slice()).collect();
625 let mut interner =
626 ByteLineInterner::with_capacity(old_compare.len() + new_compare.len());
627 let old_key_ids = interner.intern_all_vecs(old_compare);
628 let new_key_ids = interner.intern_all_vecs(new_compare);
629 let histogram = matches!(algo, MergeFileCompactAlgorithm::Histogram);
630 let (changed1, changed2) = if histogram {
631 histogram_changed_flags_bytes(&old_refs, &new_refs, |c1, c2, o0, o1, n0, n1| {
632 git_xdiff::merge_myers_region_bytes(
633 &old_refs, &new_refs, c1, c2, o0, o1, n0, n1,
634 );
635 })
636 } else {
637 git_xdiff::patience_changed_flags_bytes(&old_refs, &new_refs)
638 };
639 let raw =
640 changed_flags_to_ops(&changed1, &changed2, old_compare.len(), new_compare.len());
641 let old_lines = merge_dummy_line_refs(old_compare.len());
642 let new_lines = merge_dummy_line_refs(new_compare.len());
643 diff_indent_heuristic::apply_change_compact_to_ops_with_ws_histogram(
644 &raw,
645 diff_indent_heuristic::CompactSide {
646 lines: &old_lines,
647 keys: &old_lines,
648 compare_ids: Some(&old_key_ids),
649 },
650 diff_indent_heuristic::CompactSide {
651 lines: &new_lines,
652 keys: &new_lines,
653 compare_ids: Some(&new_key_ids),
654 },
655 INDENT_HEURISTIC,
656 super::WhitespaceMode::default(),
657 histogram,
658 )
659 }
660 }
661}
662
663pub(crate) fn git_xdiff_compacted_line_ops(
667 old_compare: &[&str],
668 new_compare: &[&str],
669 before: &LineSide<'_>,
670 after: &LineSide<'_>,
671 ws: super::WhitespaceMode,
672 indent_heuristic: bool,
673 need_min: bool,
674) -> Vec<similar::DiffOp> {
675 let ((changed1, changed2), old_key_ids, new_key_ids) =
676 xdiff_changed_flags_trimmed_ids(old_compare, new_compare, need_min);
677 let bodies;
681 let (old_lines, new_lines): (&[&str], &[&str]) = if ws.normalizes_line_compare() {
682 bodies = (line_bodies(before), line_bodies(after));
683 (&bodies.0, &bodies.1)
684 } else {
685 (old_compare, new_compare)
686 };
687 diff_indent_heuristic::apply_change_compact_to_masks(
688 changed1,
689 changed2,
690 diff_indent_heuristic::CompactSide {
691 lines: old_lines,
692 keys: old_compare,
693 compare_ids: Some(&old_key_ids),
694 },
695 diff_indent_heuristic::CompactSide {
696 lines: new_lines,
697 keys: new_compare,
698 compare_ids: Some(&new_key_ids),
699 },
700 indent_heuristic,
701 )
702}
703
704fn diff_ops_to_edit_spans(ops: &[similar::DiffOp]) -> Vec<EditSpan> {
705 use similar::DiffOp;
706 let mut out = Vec::new();
707 for op in ops {
708 match *op {
709 DiffOp::Equal { .. } => {}
710 DiffOp::Delete {
711 old_index,
712 old_len,
713 new_index,
714 } => out.push(EditSpan {
715 before_start: old_index as i64,
716 before_len: old_len as i64,
717 after_start: new_index as i64,
718 after_len: 0,
719 skip_in_output: false,
720 }),
721 DiffOp::Insert {
722 old_index,
723 new_index,
724 new_len,
725 } => out.push(EditSpan {
726 before_start: old_index as i64,
727 before_len: 0,
728 after_start: new_index as i64,
729 after_len: new_len as i64,
730 skip_in_output: false,
731 }),
732 DiffOp::Replace {
733 old_index,
734 old_len,
735 new_index,
736 new_len,
737 } => out.push(EditSpan {
738 before_start: old_index as i64,
739 before_len: old_len as i64,
740 after_start: new_index as i64,
741 after_len: new_len as i64,
742 skip_in_output: false,
743 }),
744 }
745 }
746 out
747}
748
749fn git_xdiff_unified_body_raw(
751 old_content: &str,
752 new_content: &str,
753 context_lines: usize,
754 inter_hunk_context: usize,
755 indent_heuristic: bool,
756 need_min: bool,
757 ws: super::WhitespaceMode,
758) -> String {
759 git_xdiff_unified_body_raw_with_ignorable(
760 old_content,
761 new_content,
762 context_lines,
763 inter_hunk_context,
764 indent_heuristic,
765 need_min,
766 ws,
767 None::<fn(&[&str], &[&str]) -> bool>,
768 )
769}
770
771#[allow(clippy::too_many_arguments)]
772fn git_xdiff_unified_body_raw_with_ignorable<F>(
773 old_content: &str,
774 new_content: &str,
775 context_lines: usize,
776 inter_hunk_context: usize,
777 indent_heuristic: bool,
778 need_min: bool,
779 ws: super::WhitespaceMode,
780 is_ignorable_change: Option<F>,
781) -> String
782where
783 F: Fn(&[&str], &[&str]) -> bool,
784{
785 let before = LineSide::from_text(old_content);
786 let after = LineSide::from_text(new_content);
787 if before.is_empty() && after.is_empty() {
788 return String::new();
789 }
790 let (mut old_normalized, mut new_normalized) = (Vec::new(), Vec::new());
791 let old_compare = line_compare_keys(&before, ws, &mut old_normalized);
792 let new_compare = line_compare_keys(&after, ws, &mut new_normalized);
793 let ops = git_xdiff_compacted_line_ops(
794 &old_compare,
795 &new_compare,
796 &before,
797 &after,
798 ws,
799 indent_heuristic,
800 need_min,
801 );
802 let mut edits = if let Some(is_ignorable) = is_ignorable_change {
803 diff_ops_to_ignorable_edit_spans(
804 &ops,
805 &line_bodies(&before),
806 &line_bodies(&after),
807 is_ignorable,
808 )
809 } else {
810 diff_ops_to_edit_spans(&ops)
811 };
812 if edits.is_empty() {
813 if old_content != new_content && !ws.normalizes_line_compare() {
814 edits = line_ending_only_edits(&before, &after);
815 }
816 if edits.is_empty() {
817 return String::new();
818 }
819 }
820 emit_unified_body_from_edits(
821 &before,
822 &after,
823 &edits,
824 UnifiedEmitOptions {
825 context_lines,
826 inter_hunk_context,
827 function_context: false,
828 },
829 None,
830 )
831}
832
833#[allow(clippy::too_many_arguments)]
834fn git_patience_unified_body_raw_with_ignorable<F>(
835 old_content: &str,
836 new_content: &str,
837 context_lines: usize,
838 inter_hunk_context: usize,
839 indent_heuristic: bool,
840 ws: super::WhitespaceMode,
841 is_ignorable_change: Option<F>,
842) -> String
843where
844 F: Fn(&[&str], &[&str]) -> bool,
845{
846 let before = LineSide::from_text(old_content);
847 let after = LineSide::from_text(new_content);
848 if before.is_empty() && after.is_empty() {
849 return String::new();
850 }
851 let old_refs: Vec<&str> = before.lines().map(|l| l.body).collect();
852 let new_refs: Vec<&str> = after.lines().map(|l| l.body).collect();
853 let (mut old_normalized, mut new_normalized) = (Vec::new(), Vec::new());
854 let old_compare = line_compare_keys(&before, ws, &mut old_normalized);
855 let new_compare = line_compare_keys(&after, ws, &mut new_normalized);
856 let (changed1, changed2) = git_xdiff::patience_changed_flags(&old_compare, &new_compare);
857 let raw = changed_flags_to_ops(&changed1, &changed2, old_refs.len(), new_refs.len());
858 let ops = diff_indent_heuristic::apply_change_compact_to_ops_with_ws_histogram(
859 &raw,
860 diff_indent_heuristic::CompactSide {
861 lines: &old_refs,
862 keys: &old_compare,
863 compare_ids: None,
864 },
865 diff_indent_heuristic::CompactSide {
866 lines: &new_refs,
867 keys: &new_compare,
868 compare_ids: None,
869 },
870 indent_heuristic,
871 ws,
872 false,
873 );
874 let mut edits = if let Some(is_ignorable) = is_ignorable_change {
875 diff_ops_to_ignorable_edit_spans(&ops, &old_refs, &new_refs, is_ignorable)
876 } else {
877 diff_ops_to_edit_spans(&ops)
878 };
879 if edits.is_empty() {
880 if old_content != new_content && !ws.normalizes_line_compare() {
881 edits = line_ending_only_edits(&before, &after);
882 }
883 if edits.is_empty() {
884 return String::new();
885 }
886 }
887 emit_unified_body_from_edits(
888 &before,
889 &after,
890 &edits,
891 UnifiedEmitOptions {
892 context_lines,
893 inter_hunk_context,
894 function_context: false,
895 },
896 None,
897 )
898}
899
900fn blank_line_ignorable(ws: super::WhitespaceMode) -> impl Fn(&[&str], &[&str]) -> bool + Copy {
901 move |removed: &[&str], added: &[&str]| {
902 ws.ignore_blank_lines
903 && removed.iter().all(|l| crate::ws::ws_blank_line(l))
904 && added.iter().all(|l| crate::ws::ws_blank_line(l))
905 }
906}
907
908pub(crate) fn unified_hunk_body_for_diff_options(
910 old_content: &str,
911 new_content: &str,
912 options: &crate::diff::options::DiffOptions,
913) -> String {
914 use crate::diff::options::DiffAlgorithm;
915
916 let ws = options.whitespace;
917 if ws.is_active() {
918 let blank_only = blank_line_ignorable(ws);
919 if ws.normalizes_line_compare() {
920 let ignorable = ws.ignore_blank_lines.then_some(blank_only);
921 return match options.algorithm {
922 DiffAlgorithm::Myers => git_xdiff_unified_body_raw_with_ignorable(
923 old_content,
924 new_content,
925 options.context_lines,
926 options.inter_hunk_context,
927 options.indent_heuristic,
928 false,
929 ws,
930 ignorable,
931 ),
932 DiffAlgorithm::Minimal => git_xdiff_unified_body_raw_with_ignorable(
933 old_content,
934 new_content,
935 options.context_lines,
936 options.inter_hunk_context,
937 options.indent_heuristic,
938 true,
939 ws,
940 ignorable,
941 ),
942 DiffAlgorithm::Patience => git_patience_unified_body_raw_with_ignorable(
943 old_content,
944 new_content,
945 options.context_lines,
946 options.inter_hunk_context,
947 options.indent_heuristic,
948 ws,
949 ignorable,
950 ),
951 DiffAlgorithm::Histogram => {
952 if ws.ignore_blank_lines {
953 histogram_unified_body_ignore_fc(
954 old_content,
955 new_content,
956 options.context_lines,
957 options.inter_hunk_context,
958 false,
959 None,
960 options.indent_heuristic,
961 ws,
962 blank_only,
963 )
964 } else {
965 histogram_unified_body_raw(
966 old_content,
967 new_content,
968 options.context_lines,
969 options.inter_hunk_context,
970 options.indent_heuristic,
971 ws,
972 )
973 }
974 }
975 };
976 }
977 if ws.ignore_blank_lines {
978 return match options.algorithm {
979 DiffAlgorithm::Myers => git_xdiff_unified_body_raw_with_ignorable(
980 old_content,
981 new_content,
982 options.context_lines,
983 options.inter_hunk_context,
984 options.indent_heuristic,
985 false,
986 ws,
987 Some(blank_only),
988 ),
989 DiffAlgorithm::Minimal => git_xdiff_unified_body_raw_with_ignorable(
990 old_content,
991 new_content,
992 options.context_lines,
993 options.inter_hunk_context,
994 options.indent_heuristic,
995 true,
996 ws,
997 Some(blank_only),
998 ),
999 DiffAlgorithm::Patience => git_patience_unified_body_raw_with_ignorable(
1000 old_content,
1001 new_content,
1002 options.context_lines,
1003 options.inter_hunk_context,
1004 options.indent_heuristic,
1005 ws,
1006 Some(blank_only),
1007 ),
1008 DiffAlgorithm::Histogram => histogram_unified_body_ignore_fc(
1009 old_content,
1010 new_content,
1011 options.context_lines,
1012 options.inter_hunk_context,
1013 false,
1014 None,
1015 options.indent_heuristic,
1016 ws,
1017 blank_only,
1018 ),
1019 };
1020 }
1021 }
1022 match options.algorithm {
1023 DiffAlgorithm::Myers => git_xdiff_unified_body_raw(
1024 old_content,
1025 new_content,
1026 options.context_lines,
1027 options.inter_hunk_context,
1028 options.indent_heuristic,
1029 false,
1030 ws,
1031 ),
1032 DiffAlgorithm::Minimal => git_xdiff_unified_body_raw(
1033 old_content,
1034 new_content,
1035 options.context_lines,
1036 options.inter_hunk_context,
1037 options.indent_heuristic,
1038 true,
1039 ws,
1040 ),
1041 DiffAlgorithm::Patience => git_patience_unified_body_raw(
1042 old_content,
1043 new_content,
1044 options.context_lines,
1045 options.inter_hunk_context,
1046 options.indent_heuristic,
1047 ),
1048 DiffAlgorithm::Histogram => histogram_unified_body_raw(
1049 old_content,
1050 new_content,
1051 options.context_lines,
1052 options.inter_hunk_context,
1053 options.indent_heuristic,
1054 ws,
1055 ),
1056 }
1057}
1058
1059fn git_patience_unified_body_raw(
1061 old_content: &str,
1062 new_content: &str,
1063 context_lines: usize,
1064 inter_hunk_context: usize,
1065 indent_heuristic: bool,
1066) -> String {
1067 let before = LineSide::from_text(old_content);
1068 let after = LineSide::from_text(new_content);
1069 if before.is_empty() && after.is_empty() {
1070 return String::new();
1071 }
1072 let old_refs: Vec<&str> = before.lines().map(|l| l.body).collect();
1073 let new_refs: Vec<&str> = after.lines().map(|l| l.body).collect();
1074 let ws = super::WhitespaceMode::default();
1075 let (mut old_normalized, mut new_normalized) = (Vec::new(), Vec::new());
1076 let old_compare = line_compare_keys(&before, ws, &mut old_normalized);
1077 let new_compare = line_compare_keys(&after, ws, &mut new_normalized);
1078 let (changed1, changed2) = git_xdiff::patience_changed_flags(&old_compare, &new_compare);
1079 let raw = changed_flags_to_ops(&changed1, &changed2, old_refs.len(), new_refs.len());
1080 let ops = diff_indent_heuristic::apply_change_compact_to_ops_with_ws_histogram(
1081 &raw,
1082 diff_indent_heuristic::CompactSide {
1083 lines: &old_refs,
1084 keys: &old_compare,
1085 compare_ids: None,
1086 },
1087 diff_indent_heuristic::CompactSide {
1088 lines: &new_refs,
1089 keys: &new_compare,
1090 compare_ids: None,
1091 },
1092 indent_heuristic,
1093 ws,
1094 false,
1095 );
1096 let mut edits = diff_ops_to_edit_spans(&ops);
1097 if edits.is_empty() {
1098 if old_content != new_content {
1099 edits = line_ending_only_edits(&before, &after);
1100 }
1101 if edits.is_empty() {
1102 return String::new();
1103 }
1104 }
1105 emit_unified_body_from_edits(
1106 &before,
1107 &after,
1108 &edits,
1109 UnifiedEmitOptions {
1110 context_lines,
1111 inter_hunk_context,
1112 function_context: false,
1113 },
1114 None,
1115 )
1116}
1117
1118pub(crate) fn unified_hunk_header_counts(
1120 header_line: &str,
1121) -> Option<(usize, usize, usize, usize)> {
1122 let rest = header_line.strip_prefix("@@ ")?;
1123 let (old_part, rest2) = rest.split_once(" +")?;
1124 let old_tok = old_part.strip_prefix('-')?.split(' ').next()?;
1125 let new_tok = rest2.split(" @@").next()?.split(' ').next()?;
1126
1127 fn parse_side(spec: &str) -> (usize, usize) {
1128 if let Some((start, count)) = spec.split_once(',') {
1129 (start.parse().unwrap_or(0), count.parse().unwrap_or(0))
1130 } else {
1131 (spec.parse().unwrap_or(0), 1)
1132 }
1133 }
1134
1135 let (os, ol) = parse_side(old_tok);
1136 let (ns, nl) = parse_side(new_tok);
1137 Some((os, ol, ns, nl))
1138}
1139
1140#[allow(clippy::too_many_arguments)]
1142pub(crate) fn write_unified_diff_from_diff_options<W: std::io::Write>(
1143 out: &mut W,
1144 old_content: &str,
1145 new_content: &str,
1146 old_path: &str,
1147 new_path: &str,
1148 src_prefix: &str,
1149 dst_prefix: &str,
1150 funcname_matcher: Option<&FuncnameMatcher>,
1151 options: &crate::diff::options::DiffOptions,
1152 quote_path_fully: bool,
1153) -> Result<()> {
1154 use crate::quote_path::format_diff_path_with_prefix;
1155
1156 if old_path == "/dev/null" {
1157 out.write_all(b"--- /dev/null\n")?;
1158 } else if src_prefix.is_empty() {
1159 writeln!(out, "--- {old_path}")?;
1160 } else {
1161 out.write_all(b"--- ")?;
1162 out.write_all(
1163 format_diff_path_with_prefix(src_prefix, old_path, quote_path_fully).as_bytes(),
1164 )?;
1165 out.write_all(b"\n")?;
1166 }
1167 if new_path == "/dev/null" {
1168 out.write_all(b"+++ /dev/null\n")?;
1169 } else if dst_prefix.is_empty() {
1170 writeln!(out, "+++ {new_path}")?;
1171 } else {
1172 out.write_all(b"+++ ")?;
1173 out.write_all(
1174 format_diff_path_with_prefix(dst_prefix, new_path, quote_path_fully).as_bytes(),
1175 )?;
1176 out.write_all(b"\n")?;
1177 }
1178
1179 let body = unified_hunk_body_for_diff_options(old_content, new_content, options);
1180 if body.is_empty() {
1181 return Ok(());
1182 }
1183 let old_lines: Vec<&str> = old_content.lines().collect();
1184 for hunk_str in imara_unified_hunk_slices(&body) {
1185 if hunk_str.is_empty() {
1186 continue;
1187 }
1188 if let Some(first_newline) = hunk_str.find('\n') {
1189 let header_line = &hunk_str[..first_newline];
1190 let rest = &hunk_str[first_newline..];
1191 if let Some(func_ctx) =
1192 extract_function_context(header_line, &old_lines, funcname_matcher, Some(old_path))
1193 {
1194 out.write_all(header_line.as_bytes())?;
1195 out.write_all(b" ")?;
1196 out.write_all(func_ctx.as_bytes())?;
1197 out.write_all(rest.as_bytes())?;
1198 } else {
1199 out.write_all(hunk_str.as_bytes())?;
1200 }
1201 } else {
1202 out.write_all(hunk_str.as_bytes())?;
1203 }
1204 }
1205 Ok(())
1206}
1207
1208#[expect(clippy::too_many_arguments)]
1209fn unified_diff_git_myers_with_prefix_and_funcname(
1210 old_content: &str,
1211 new_content: &str,
1212 old_path: &str,
1213 new_path: &str,
1214 context_lines: usize,
1215 inter_hunk_context: usize,
1216 src_prefix: &str,
1217 dst_prefix: &str,
1218 funcname_matcher: Option<&FuncnameMatcher>,
1219 indent_heuristic: bool,
1220 quote_path_fully: bool,
1221) -> String {
1222 use crate::quote_path::format_diff_path_with_prefix;
1223
1224 let body = git_xdiff_unified_body_raw(
1225 old_content,
1226 new_content,
1227 context_lines,
1228 inter_hunk_context,
1229 indent_heuristic,
1230 false,
1231 super::WhitespaceMode::default(),
1232 );
1233
1234 let mut output = String::new();
1235 if old_path == "/dev/null" {
1236 output.push_str("--- /dev/null\n");
1237 } else if src_prefix.is_empty() {
1238 output.push_str(&format!("--- {old_path}\n"));
1239 } else {
1240 output.push_str("--- ");
1241 output.push_str(&format_diff_path_with_prefix(
1242 src_prefix,
1243 old_path,
1244 quote_path_fully,
1245 ));
1246 output.push('\n');
1247 }
1248 if new_path == "/dev/null" {
1249 output.push_str("+++ /dev/null\n");
1250 } else if dst_prefix.is_empty() {
1251 output.push_str(&format!("+++ {new_path}\n"));
1252 } else {
1253 output.push_str("+++ ");
1254 output.push_str(&format_diff_path_with_prefix(
1255 dst_prefix,
1256 new_path,
1257 quote_path_fully,
1258 ));
1259 output.push('\n');
1260 }
1261
1262 let old_lines: Vec<&str> = old_content.lines().collect();
1263 for hunk_str in imara_unified_hunk_slices(&body) {
1264 if hunk_str.is_empty() {
1265 continue;
1266 }
1267 if let Some(first_newline) = hunk_str.find('\n') {
1268 let header_line = &hunk_str[..first_newline];
1269 let rest = &hunk_str[first_newline..];
1270 if let Some(func_ctx) =
1271 extract_function_context(header_line, &old_lines, funcname_matcher, Some(old_path))
1272 {
1273 output.push_str(header_line);
1274 output.push(' ');
1275 output.push_str(&func_ctx);
1276 output.push_str(rest);
1277 } else {
1278 output.push_str(hunk_str);
1279 }
1280 } else {
1281 output.push_str(hunk_str);
1282 }
1283 }
1284
1285 annotate_unified_diff_no_newline_at_eof(&output, old_content, new_content)
1286}
1287
1288#[expect(clippy::too_many_arguments)]
1289fn unified_diff_git_minimal_with_prefix_and_funcname(
1290 old_content: &str,
1291 new_content: &str,
1292 old_path: &str,
1293 new_path: &str,
1294 context_lines: usize,
1295 inter_hunk_context: usize,
1296 src_prefix: &str,
1297 dst_prefix: &str,
1298 funcname_matcher: Option<&FuncnameMatcher>,
1299 indent_heuristic: bool,
1300 quote_path_fully: bool,
1301) -> String {
1302 use crate::quote_path::format_diff_path_with_prefix;
1303
1304 let body = git_xdiff_unified_body_raw(
1305 old_content,
1306 new_content,
1307 context_lines,
1308 inter_hunk_context,
1309 indent_heuristic,
1310 true,
1311 super::WhitespaceMode::default(),
1312 );
1313
1314 let mut output = String::new();
1315 if old_path == "/dev/null" {
1316 output.push_str("--- /dev/null\n");
1317 } else if src_prefix.is_empty() {
1318 output.push_str(&format!("--- {old_path}\n"));
1319 } else {
1320 output.push_str("--- ");
1321 output.push_str(&format_diff_path_with_prefix(
1322 src_prefix,
1323 old_path,
1324 quote_path_fully,
1325 ));
1326 output.push('\n');
1327 }
1328 if new_path == "/dev/null" {
1329 output.push_str("+++ /dev/null\n");
1330 } else if dst_prefix.is_empty() {
1331 output.push_str(&format!("+++ {new_path}\n"));
1332 } else {
1333 output.push_str("+++ ");
1334 output.push_str(&format_diff_path_with_prefix(
1335 dst_prefix,
1336 new_path,
1337 quote_path_fully,
1338 ));
1339 output.push('\n');
1340 }
1341
1342 let old_lines: Vec<&str> = old_content.lines().collect();
1343 for hunk_str in imara_unified_hunk_slices(&body) {
1344 if hunk_str.is_empty() {
1345 continue;
1346 }
1347 if let Some(first_newline) = hunk_str.find('\n') {
1348 let header_line = &hunk_str[..first_newline];
1349 let rest = &hunk_str[first_newline..];
1350 if let Some(func_ctx) =
1351 extract_function_context(header_line, &old_lines, funcname_matcher, Some(old_path))
1352 {
1353 output.push_str(header_line);
1354 output.push(' ');
1355 output.push_str(&func_ctx);
1356 output.push_str(rest);
1357 } else {
1358 output.push_str(hunk_str);
1359 }
1360 } else {
1361 output.push_str(hunk_str);
1362 }
1363 }
1364
1365 annotate_unified_diff_no_newline_at_eof(&output, old_content, new_content)
1366}
1367
1368#[expect(clippy::too_many_arguments)]
1369fn unified_diff_git_patience_with_prefix_and_funcname(
1370 old_content: &str,
1371 new_content: &str,
1372 old_path: &str,
1373 new_path: &str,
1374 context_lines: usize,
1375 inter_hunk_context: usize,
1376 src_prefix: &str,
1377 dst_prefix: &str,
1378 funcname_matcher: Option<&FuncnameMatcher>,
1379 indent_heuristic: bool,
1380 quote_path_fully: bool,
1381) -> String {
1382 use crate::quote_path::format_diff_path_with_prefix;
1383
1384 let body = git_patience_unified_body_raw(
1385 old_content,
1386 new_content,
1387 context_lines,
1388 inter_hunk_context,
1389 indent_heuristic,
1390 );
1391
1392 let mut output = String::new();
1393 if old_path == "/dev/null" {
1394 output.push_str("--- /dev/null\n");
1395 } else if src_prefix.is_empty() {
1396 output.push_str(&format!("--- {old_path}\n"));
1397 } else {
1398 output.push_str("--- ");
1399 output.push_str(&format_diff_path_with_prefix(
1400 src_prefix,
1401 old_path,
1402 quote_path_fully,
1403 ));
1404 output.push('\n');
1405 }
1406 if new_path == "/dev/null" {
1407 output.push_str("+++ /dev/null\n");
1408 } else if dst_prefix.is_empty() {
1409 output.push_str(&format!("+++ {new_path}\n"));
1410 } else {
1411 output.push_str("+++ ");
1412 output.push_str(&format_diff_path_with_prefix(
1413 dst_prefix,
1414 new_path,
1415 quote_path_fully,
1416 ));
1417 output.push('\n');
1418 }
1419
1420 let old_lines: Vec<&str> = old_content.lines().collect();
1421 for hunk_str in imara_unified_hunk_slices(&body) {
1422 if hunk_str.is_empty() {
1423 continue;
1424 }
1425 if let Some(first_newline) = hunk_str.find('\n') {
1426 let header_line = &hunk_str[..first_newline];
1427 let rest = &hunk_str[first_newline..];
1428 if let Some(func_ctx) =
1429 extract_function_context(header_line, &old_lines, funcname_matcher, Some(old_path))
1430 {
1431 output.push_str(header_line);
1432 output.push(' ');
1433 output.push_str(&func_ctx);
1434 output.push_str(rest);
1435 } else {
1436 output.push_str(hunk_str);
1437 }
1438 } else {
1439 output.push_str(hunk_str);
1440 }
1441 }
1442
1443 annotate_unified_diff_no_newline_at_eof(&output, old_content, new_content)
1444}
1445
1446#[must_use]
1450pub fn unified_diff_histogram_hunks_only(
1451 old_content: &str,
1452 new_content: &str,
1453 context_lines: usize,
1454 inter_hunk_context: usize,
1455 indent_heuristic: bool,
1456) -> String {
1457 histogram_unified_body_raw(
1458 old_content,
1459 new_content,
1460 context_lines,
1461 inter_hunk_context,
1462 indent_heuristic,
1463 super::WhitespaceMode::default(),
1464 )
1465}
1466
1467#[expect(clippy::too_many_arguments)]
1468fn unified_diff_histogram_with_prefix_and_funcname_indent(
1469 old_content: &str,
1470 new_content: &str,
1471 old_path: &str,
1472 new_path: &str,
1473 context_lines: usize,
1474 inter_hunk_context: usize,
1475 src_prefix: &str,
1476 dst_prefix: &str,
1477 funcname_matcher: Option<&FuncnameMatcher>,
1478 indent_heuristic: bool,
1479 quote_path_fully: bool,
1480) -> String {
1481 use crate::quote_path::format_diff_path_with_prefix;
1482
1483 let body = histogram_unified_body_raw(
1484 old_content,
1485 new_content,
1486 context_lines,
1487 inter_hunk_context,
1488 indent_heuristic,
1489 super::WhitespaceMode::default(),
1490 );
1491
1492 let mut output = String::new();
1493 if old_path == "/dev/null" {
1494 output.push_str("--- /dev/null\n");
1495 } else if src_prefix.is_empty() {
1496 output.push_str(&format!("--- {old_path}\n"));
1497 } else {
1498 output.push_str("--- ");
1499 output.push_str(&format_diff_path_with_prefix(
1500 src_prefix,
1501 old_path,
1502 quote_path_fully,
1503 ));
1504 output.push('\n');
1505 }
1506 if new_path == "/dev/null" {
1507 output.push_str("+++ /dev/null\n");
1508 } else if dst_prefix.is_empty() {
1509 output.push_str(&format!("+++ {new_path}\n"));
1510 } else {
1511 output.push_str("+++ ");
1512 output.push_str(&format_diff_path_with_prefix(
1513 dst_prefix,
1514 new_path,
1515 quote_path_fully,
1516 ));
1517 output.push('\n');
1518 }
1519
1520 let old_lines: Vec<&str> = old_content.lines().collect();
1521 for hunk_str in imara_unified_hunk_slices(&body) {
1522 if hunk_str.is_empty() {
1523 continue;
1524 }
1525 if let Some(first_newline) = hunk_str.find('\n') {
1526 let header_line = &hunk_str[..first_newline];
1527 let rest = &hunk_str[first_newline..];
1528 if let Some(func_ctx) =
1529 extract_function_context(header_line, &old_lines, funcname_matcher, Some(old_path))
1530 {
1531 output.push_str(header_line);
1532 output.push(' ');
1533 output.push_str(&func_ctx);
1534 output.push_str(rest);
1535 } else {
1536 output.push_str(hunk_str);
1537 }
1538 } else {
1539 output.push_str(hunk_str);
1540 }
1541 }
1542
1543 annotate_unified_diff_no_newline_at_eof(&output, old_content, new_content)
1544}
1545
1546#[allow(clippy::too_many_arguments)]
1556pub fn unified_diff_histogram_ignore_with_prefix_and_funcname<F>(
1557 old_content: &str,
1558 new_content: &str,
1559 old_path: &str,
1560 new_path: &str,
1561 context_lines: usize,
1562 inter_hunk_context: usize,
1563 src_prefix: &str,
1564 dst_prefix: &str,
1565 funcname_matcher: Option<&FuncnameMatcher>,
1566 quote_path_fully: bool,
1567 function_context: bool,
1568 indent_heuristic: bool,
1569 is_ignorable_change: F,
1570) -> String
1571where
1572 F: Fn(&[&str], &[&str]) -> bool,
1573{
1574 use crate::quote_path::format_diff_path_with_prefix;
1575
1576 let body = histogram_unified_body_ignore_fc(
1577 old_content,
1578 new_content,
1579 context_lines,
1580 inter_hunk_context,
1581 function_context,
1582 funcname_matcher,
1583 indent_heuristic,
1584 super::WhitespaceMode::default(),
1585 is_ignorable_change,
1586 );
1587 if body.is_empty() {
1588 return String::new();
1589 }
1590
1591 let mut output = String::new();
1592 if old_path == "/dev/null" {
1593 output.push_str("--- /dev/null\n");
1594 } else if src_prefix.is_empty() {
1595 output.push_str(&format!("--- {old_path}\n"));
1596 } else {
1597 output.push_str("--- ");
1598 output.push_str(&format_diff_path_with_prefix(
1599 src_prefix,
1600 old_path,
1601 quote_path_fully,
1602 ));
1603 output.push('\n');
1604 }
1605 if new_path == "/dev/null" {
1606 output.push_str("+++ /dev/null\n");
1607 } else if dst_prefix.is_empty() {
1608 output.push_str(&format!("+++ {new_path}\n"));
1609 } else {
1610 output.push_str("+++ ");
1611 output.push_str(&format_diff_path_with_prefix(
1612 dst_prefix,
1613 new_path,
1614 quote_path_fully,
1615 ));
1616 output.push('\n');
1617 }
1618
1619 let old_lines: Vec<&str> = old_content.lines().collect();
1620 for hunk_str in imara_unified_hunk_slices(&body) {
1621 if hunk_str.is_empty() {
1622 continue;
1623 }
1624 if let Some(first_newline) = hunk_str.find('\n') {
1625 let header_line = &hunk_str[..first_newline];
1626 let rest = &hunk_str[first_newline..];
1627 if let Some(func_ctx) =
1628 extract_function_context(header_line, &old_lines, funcname_matcher, Some(old_path))
1629 {
1630 output.push_str(header_line);
1631 output.push(' ');
1632 output.push_str(&func_ctx);
1633 output.push_str(rest);
1634 } else {
1635 output.push_str(hunk_str);
1636 }
1637 } else {
1638 output.push_str(hunk_str);
1639 }
1640 }
1641
1642 output
1643}
1644
1645#[must_use]
1647pub(crate) fn indent_heuristic_from_config(config: &ConfigSet) -> bool {
1648 config.bool_or("diff.indentHeuristic", true)
1649}
1650
1651#[must_use]
1653pub fn resolve_indent_heuristic(
1654 config: &ConfigSet,
1655 cli_indent_heuristic: bool,
1656 cli_no_indent_heuristic: bool,
1657) -> bool {
1658 if cli_no_indent_heuristic {
1659 false
1660 } else if cli_indent_heuristic {
1661 true
1662 } else {
1663 indent_heuristic_from_config(config)
1664 }
1665}
1666
1667mod git_xdiff {
1677 use std::cell::RefCell;
1678
1679 const XDL_MAX_COST_MIN: i64 = 256;
1680 const XDL_HEUR_MIN_COST: i64 = 256;
1681 const XDL_SNAKE_CNT: i64 = 20;
1682 const XDL_K_HEUR: i64 = 4;
1683 const XDL_LINE_MAX: i64 = i64::MAX;
1684 const XDL_KPDIS_RUN: i64 = 4;
1685 const XDL_MAX_EQLIMIT: i64 = 1024;
1686 const XDL_SIMSCAN_WINDOW: i64 = 100;
1687
1688 const DISCARD: u8 = 0;
1690 const KEEP: u8 = 1;
1691 const INVESTIGATE: u8 = 2;
1692
1693 fn bogosqrt(mut n: i64) -> i64 {
1695 let mut i: i64 = 1;
1696 while n > 0 {
1697 i <<= 1;
1698 n >>= 2;
1699 }
1700 i
1701 }
1702
1703 struct XdAlgoEnv {
1704 mxcost: i64,
1705 snake_cnt: i64,
1706 heur_min: i64,
1707 }
1708
1709 struct XdpSplit {
1710 i1: i64,
1711 i2: i64,
1712 min_lo: bool,
1713 min_hi: bool,
1714 }
1715
1716 struct XdFile {
1719 changed: Vec<u8>,
1720 reference_index: Vec<usize>,
1721 effective_ids: Vec<u32>,
1723 dstart: i64,
1724 dend: i64,
1725 nreff: i64,
1726 }
1727
1728 #[inline]
1729 fn effective_id(xdf: &XdFile, idx: i64) -> u32 {
1730 xdf.effective_ids[idx as usize]
1731 }
1732
1733 struct XdiffScratch {
1734 count1: Vec<u32>,
1735 count2: Vec<u32>,
1736 action1: Vec<u8>,
1737 action2: Vec<u8>,
1738 kvd: Vec<i64>,
1739 }
1740
1741 impl XdiffScratch {
1742 fn prepare_counts(&mut self, nclasses: usize) {
1743 if self.count1.len() < nclasses {
1744 self.count1.resize(nclasses, 0);
1745 self.count2.resize(nclasses, 0);
1746 }
1747 self.count1[..nclasses].fill(0);
1748 self.count2[..nclasses].fill(0);
1749 }
1750
1751 fn prepare_actions(&mut self, nrec1: usize, nrec2: usize) {
1752 let a1 = nrec1 + 1;
1753 let a2 = nrec2 + 1;
1754 if self.action1.len() < a1 {
1755 self.action1.resize(a1, 0);
1756 } else {
1757 self.action1[..a1].fill(0);
1758 }
1759 if self.action2.len() < a2 {
1760 self.action2.resize(a2, 0);
1761 } else {
1762 self.action2[..a2].fill(0);
1763 }
1764 }
1765
1766 fn prepare_kvd(&mut self, kvd_len: usize) {
1767 if self.kvd.len() < kvd_len {
1771 self.kvd = vec![0; kvd_len];
1772 }
1773 }
1774 }
1775
1776 thread_local! { static XDIFF_SCRATCH: RefCell<XdiffScratch> = const {
1778 RefCell::new(XdiffScratch {
1779 count1: Vec::new(),
1780 count2: Vec::new(),
1781 action1: Vec::new(),
1782 action2: Vec::new(),
1783 kvd: Vec::new(),
1784 })
1785 };
1786 }
1787
1788 fn bool_changed(changed: Vec<u8>) -> Vec<bool> {
1789 changed.into_iter().map(|b| b != 0).collect()
1790 }
1791
1792 #[allow(clippy::too_many_arguments)]
1795 fn xdl_split(
1796 xdf1: &XdFile,
1797 off1: i64,
1798 lim1: i64,
1799 xdf2: &XdFile,
1800 off2: i64,
1801 lim2: i64,
1802 kvdf: &mut [i64],
1803 kvdb: &mut [i64],
1804 koff: i64,
1805 need_min: bool,
1806 spl: &mut XdpSplit,
1807 xenv: &XdAlgoEnv,
1808 ) -> i64 {
1809 let dmin = off1 - lim2;
1810 let dmax = lim1 - off2;
1811 let fmid = off1 - off2;
1812 let bmid = lim1 - lim2;
1813 let odd = ((fmid - bmid) & 1) != 0;
1814 let mut fmin = fmid;
1815 let mut fmax = fmid;
1816 let mut bmin = bmid;
1817 let mut bmax = bmid;
1818
1819 let kf = |k: i64| -> usize { (k + koff) as usize };
1820
1821 kvdf[kf(fmid)] = off1;
1822 kvdb[kf(bmid)] = lim1;
1823
1824 let mut ec: i64 = 1;
1825 loop {
1826 let mut got_snake = false;
1827
1828 if fmin > dmin {
1829 fmin -= 1;
1830 kvdf[kf(fmin - 1)] = -1;
1831 } else {
1832 fmin += 1;
1833 }
1834 if fmax < dmax {
1835 fmax += 1;
1836 kvdf[kf(fmax + 1)] = -1;
1837 } else {
1838 fmax -= 1;
1839 }
1840
1841 let mut d = fmax;
1842 while d >= fmin {
1843 let mut i1 = if kvdf[kf(d - 1)] >= kvdf[kf(d + 1)] {
1844 kvdf[kf(d - 1)] + 1
1845 } else {
1846 kvdf[kf(d + 1)]
1847 };
1848 let prev1 = i1;
1849 let mut i2 = i1 - d;
1850 while i1 < lim1 && i2 < lim2 && effective_id(xdf1, i1) == effective_id(xdf2, i2) {
1851 i1 += 1;
1852 i2 += 1;
1853 }
1854 if i1 - prev1 > xenv.snake_cnt {
1855 got_snake = true;
1856 }
1857 kvdf[kf(d)] = i1;
1858 if odd && bmin <= d && d <= bmax && kvdb[kf(d)] <= i1 {
1859 spl.i1 = i1;
1860 spl.i2 = i2;
1861 spl.min_lo = true;
1862 spl.min_hi = true;
1863 return ec;
1864 }
1865 d -= 2;
1866 }
1867
1868 if bmin > dmin {
1869 bmin -= 1;
1870 kvdb[kf(bmin - 1)] = XDL_LINE_MAX;
1871 } else {
1872 bmin += 1;
1873 }
1874 if bmax < dmax {
1875 bmax += 1;
1876 kvdb[kf(bmax + 1)] = XDL_LINE_MAX;
1877 } else {
1878 bmax -= 1;
1879 }
1880
1881 let mut d = bmax;
1882 while d >= bmin {
1883 let mut i1 = if kvdb[kf(d - 1)] < kvdb[kf(d + 1)] {
1884 kvdb[kf(d - 1)]
1885 } else {
1886 kvdb[kf(d + 1)] - 1
1887 };
1888 let prev1 = i1;
1889 let mut i2 = i1 - d;
1890 while i1 > off1
1891 && i2 > off2
1892 && effective_id(xdf1, i1 - 1) == effective_id(xdf2, i2 - 1)
1893 {
1894 i1 -= 1;
1895 i2 -= 1;
1896 }
1897 if prev1 - i1 > xenv.snake_cnt {
1898 got_snake = true;
1899 }
1900 kvdb[kf(d)] = i1;
1901 if !odd && fmin <= d && d <= fmax && i1 <= kvdf[kf(d)] {
1902 spl.i1 = i1;
1903 spl.i2 = i2;
1904 spl.min_lo = true;
1905 spl.min_hi = true;
1906 return ec;
1907 }
1908 d -= 2;
1909 }
1910
1911 if need_min {
1912 ec += 1;
1913 continue;
1914 }
1915
1916 if got_snake && ec > xenv.heur_min {
1917 let mut best = 0i64;
1918 let mut d = fmax;
1919 while d >= fmin {
1920 let dd = if d > fmid { d - fmid } else { fmid - d };
1921 let i1 = kvdf[kf(d)];
1922 let i2 = i1 - d;
1923 let v = (i1 - off1) + (i2 - off2) - dd;
1924 if v > XDL_K_HEUR * ec
1925 && v > best
1926 && off1 + xenv.snake_cnt <= i1
1927 && i1 < lim1
1928 && off2 + xenv.snake_cnt <= i2
1929 && i2 < lim2
1930 {
1931 let mut k = 1i64;
1932 while effective_id(xdf1, i1 - k) == effective_id(xdf2, i2 - k) {
1933 if k == xenv.snake_cnt {
1934 best = v;
1935 spl.i1 = i1;
1936 spl.i2 = i2;
1937 break;
1938 }
1939 k += 1;
1940 }
1941 }
1942 d -= 2;
1943 }
1944 if best > 0 {
1945 spl.min_lo = true;
1946 spl.min_hi = false;
1947 return ec;
1948 }
1949
1950 let mut best = 0i64;
1951 let mut d = bmax;
1952 while d >= bmin {
1953 let dd = if d > bmid { d - bmid } else { bmid - d };
1954 let i1 = kvdb[kf(d)];
1955 let i2 = i1 - d;
1956 let v = (lim1 - i1) + (lim2 - i2) - dd;
1957 if v > XDL_K_HEUR * ec
1958 && v > best
1959 && off1 < i1
1960 && i1 <= lim1 - xenv.snake_cnt
1961 && off2 < i2
1962 && i2 <= lim2 - xenv.snake_cnt
1963 {
1964 let mut k = 0i64;
1965 while effective_id(xdf1, i1 + k) == effective_id(xdf2, i2 + k) {
1966 if k == xenv.snake_cnt - 1 {
1967 best = v;
1968 spl.i1 = i1;
1969 spl.i2 = i2;
1970 break;
1971 }
1972 k += 1;
1973 }
1974 }
1975 d -= 2;
1976 }
1977 if best > 0 {
1978 spl.min_lo = false;
1979 spl.min_hi = true;
1980 return ec;
1981 }
1982 }
1983
1984 if ec >= xenv.mxcost {
1985 let mut fbest = -1i64;
1986 let mut fbest1 = -1i64;
1987 let mut d = fmax;
1988 while d >= fmin {
1989 let mut i1 = kvdf[kf(d)].min(lim1);
1990 let mut i2 = i1 - d;
1991 if lim2 < i2 {
1992 i1 = lim2 + d;
1993 i2 = lim2;
1994 }
1995 if fbest < i1 + i2 {
1996 fbest = i1 + i2;
1997 fbest1 = i1;
1998 }
1999 d -= 2;
2000 }
2001
2002 let mut bbest = XDL_LINE_MAX;
2003 let mut bbest1 = XDL_LINE_MAX;
2004 let mut d = bmax;
2005 while d >= bmin {
2006 let mut i1 = off1.max(kvdb[kf(d)]);
2007 let mut i2 = i1 - d;
2008 if i2 < off2 {
2009 i1 = off2 + d;
2010 i2 = off2;
2011 }
2012 if i1 + i2 < bbest {
2013 bbest = i1 + i2;
2014 bbest1 = i1;
2015 }
2016 d -= 2;
2017 }
2018
2019 if (lim1 + lim2) - bbest < fbest - (off1 + off2) {
2020 spl.i1 = fbest1;
2021 spl.i2 = fbest - fbest1;
2022 spl.min_lo = true;
2023 spl.min_hi = false;
2024 } else {
2025 spl.i1 = bbest1;
2026 spl.i2 = bbest - bbest1;
2027 spl.min_lo = false;
2028 spl.min_hi = true;
2029 }
2030 return ec;
2031 }
2032
2033 ec += 1;
2034 }
2035 }
2036
2037 #[allow(clippy::too_many_arguments)]
2039 fn xdl_recs_cmp(
2040 xdf1: &mut XdFile,
2041 mut off1: i64,
2042 mut lim1: i64,
2043 xdf2: &mut XdFile,
2044 mut off2: i64,
2045 mut lim2: i64,
2046 kvdf: &mut [i64],
2047 kvdb: &mut [i64],
2048 koff: i64,
2049 need_min: bool,
2050 xenv: &XdAlgoEnv,
2051 ) {
2052 while off1 < lim1 && off2 < lim2 && effective_id(xdf1, off1) == effective_id(xdf2, off2) {
2053 off1 += 1;
2054 off2 += 1;
2055 }
2056 while off1 < lim1
2057 && off2 < lim2
2058 && effective_id(xdf1, lim1 - 1) == effective_id(xdf2, lim2 - 1)
2059 {
2060 lim1 -= 1;
2061 lim2 -= 1;
2062 }
2063
2064 if off1 == lim1 {
2065 while off2 < lim2 {
2066 let r = xdf2.reference_index[off2 as usize];
2067 xdf2.changed[r] = 1;
2068 off2 += 1;
2069 }
2070 } else if off2 == lim2 {
2071 while off1 < lim1 {
2072 let r = xdf1.reference_index[off1 as usize];
2073 xdf1.changed[r] = 1;
2074 off1 += 1;
2075 }
2076 } else {
2077 let mut spl = XdpSplit {
2078 i1: 0,
2079 i2: 0,
2080 min_lo: false,
2081 min_hi: false,
2082 };
2083 xdl_split(
2084 xdf1, off1, lim1, xdf2, off2, lim2, kvdf, kvdb, koff, need_min, &mut spl, xenv,
2085 );
2086 xdl_recs_cmp(
2087 xdf1, off1, spl.i1, xdf2, off2, spl.i2, kvdf, kvdb, koff, spl.min_lo, xenv,
2088 );
2089 xdl_recs_cmp(
2090 xdf1, spl.i1, lim1, xdf2, spl.i2, lim2, kvdf, kvdb, koff, spl.min_hi, xenv,
2091 );
2092 }
2093 }
2094
2095 fn clean_mmatch(action: &[u8], i: i64, mut s: i64, mut e: i64) -> bool {
2097 if i - s > XDL_SIMSCAN_WINDOW {
2098 s = i - XDL_SIMSCAN_WINDOW;
2099 }
2100 if e - i > XDL_SIMSCAN_WINDOW {
2101 e = i + XDL_SIMSCAN_WINDOW;
2102 }
2103
2104 let mut rdis0 = 0i64;
2105 let mut rpdis0 = 1i64;
2106 let mut r = 1i64;
2107 while i - r >= s {
2108 match action[(i - r) as usize] {
2109 DISCARD => rdis0 += 1,
2110 INVESTIGATE => rpdis0 += 1,
2111 _ => break, }
2113 r += 1;
2114 }
2115 if rdis0 == 0 {
2116 return false;
2117 }
2118 let mut rdis1 = 0i64;
2119 let mut rpdis1 = 1i64;
2120 let mut r = 1i64;
2121 while i + r <= e {
2122 match action[(i + r) as usize] {
2123 DISCARD => rdis1 += 1,
2124 INVESTIGATE => rpdis1 += 1,
2125 _ => break, }
2127 r += 1;
2128 }
2129 if rdis1 == 0 {
2130 return false;
2131 }
2132 rdis1 += rdis0;
2133 rpdis1 += rpdis0;
2134 rpdis1 * XDL_KPDIS_RUN < (rpdis1 + rdis1)
2135 }
2136
2137 pub(crate) fn changed_flags(
2143 ids1: &[u32],
2144 ids2: &[u32],
2145 need_min: bool,
2146 ) -> (Vec<bool>, Vec<bool>) {
2147 changed_flags_with_shared(ids1, ids2, &[], 0, need_min)
2148 }
2149
2150 pub(crate) fn changed_flags_with_shared(
2157 ids1: &[u32],
2158 ids2: &[u32],
2159 shared_counts: &[u32],
2160 shared_records: usize,
2161 need_min: bool,
2162 ) -> (Vec<bool>, Vec<bool>) {
2163 XDIFF_SCRATCH.with(|cell| {
2164 changed_flags_with_shared_inner(
2165 cell,
2166 ids1,
2167 ids2,
2168 shared_counts,
2169 shared_records,
2170 need_min,
2171 )
2172 })
2173 }
2174
2175 fn changed_flags_with_shared_inner(
2176 scratch: &RefCell<XdiffScratch>,
2177 ids1: &[u32],
2178 ids2: &[u32],
2179 shared_counts: &[u32],
2180 shared_records: usize,
2181 need_min: bool,
2182 ) -> (Vec<bool>, Vec<bool>) {
2183 let nrec1 = ids1.len();
2184 let nrec2 = ids2.len();
2185
2186 let nclasses = ids1
2188 .iter()
2189 .chain(ids2)
2190 .max()
2191 .map_or(0, |&max| max as usize + 1);
2192 let mut work = scratch.borrow_mut();
2193 work.prepare_counts(nclasses);
2194 for &id in ids1 {
2195 work.count1[id as usize] += 1;
2196 }
2197 for &id in ids2 {
2198 work.count2[id as usize] += 1;
2199 }
2200 for (id, &shared) in shared_counts.iter().enumerate().take(nclasses) {
2201 work.count1[id] += shared;
2202 work.count2[id] += shared;
2203 }
2204
2205 let mut xdf1 = XdFile {
2206 changed: vec![0u8; nrec1],
2207 reference_index: Vec::new(),
2208 effective_ids: Vec::new(),
2209 dstart: 0,
2210 dend: nrec1 as i64 - 1,
2211 nreff: 0,
2212 };
2213 let mut xdf2 = XdFile {
2214 changed: vec![0u8; nrec2],
2215 reference_index: Vec::new(),
2216 effective_ids: Vec::new(),
2217 dstart: 0,
2218 dend: nrec2 as i64 - 1,
2219 nreff: 0,
2220 };
2221
2222 let lim = nrec1.min(nrec2) as i64;
2224 let mut i = 0i64;
2225 while i < lim && ids1[i as usize] == ids2[i as usize] {
2226 i += 1;
2227 }
2228 xdf1.dstart = i;
2229 xdf2.dstart = i;
2230 let mut j = 0i64;
2231 let rem = lim - i;
2232 while j < rem && ids1[nrec1 - 1 - j as usize] == ids2[nrec2 - 1 - j as usize] {
2233 j += 1;
2234 }
2235 xdf1.dend = nrec1 as i64 - j - 1;
2236 xdf2.dend = nrec2 as i64 - j - 1;
2237
2238 work.prepare_actions(nrec1, nrec2);
2240 for xdf in [&mut xdf1, &mut xdf2] {
2241 let span = usize::try_from(xdf.dend - xdf.dstart + 1).unwrap_or(0);
2242 xdf.reference_index.reserve_exact(span);
2243 xdf.effective_ids.reserve_exact(span);
2244 }
2245
2246 let mlim1 = if need_min {
2247 i64::MAX
2248 } else {
2249 let mut mlim = bogosqrt((nrec1 + shared_records) as i64);
2250 if mlim > XDL_MAX_EQLIMIT {
2251 mlim = XDL_MAX_EQLIMIT;
2252 }
2253 mlim
2254 };
2255 let mut idx = xdf1.dstart;
2256 while idx <= xdf1.dend {
2257 let id = ids1[idx as usize];
2258 let nm = i64::from(work.count2[id as usize]);
2259 work.action1[idx as usize] = if nm == 0 {
2260 DISCARD
2261 } else if nm >= mlim1 {
2262 INVESTIGATE
2263 } else {
2264 KEEP
2265 };
2266 idx += 1;
2267 }
2268
2269 let mlim2 = if need_min {
2270 i64::MAX
2271 } else {
2272 let mut mlim = bogosqrt((nrec2 + shared_records) as i64);
2273 if mlim > XDL_MAX_EQLIMIT {
2274 mlim = XDL_MAX_EQLIMIT;
2275 }
2276 mlim
2277 };
2278 let mut idx = xdf2.dstart;
2279 while idx <= xdf2.dend {
2280 let id = ids2[idx as usize];
2281 let nm = i64::from(work.count1[id as usize]);
2282 work.action2[idx as usize] = if nm == 0 {
2283 DISCARD
2284 } else if nm >= mlim2 {
2285 INVESTIGATE
2286 } else {
2287 KEEP
2288 };
2289 idx += 1;
2290 }
2291
2292 let mut idx = xdf1.dstart;
2293 while idx <= xdf1.dend {
2294 let id = ids1[idx as usize];
2295 let a = work.action1[idx as usize];
2296 if a == KEEP
2297 || (a == INVESTIGATE
2298 && !clean_mmatch(&work.action1[..nrec1 + 1], idx, xdf1.dstart, xdf1.dend))
2299 {
2300 xdf1.reference_index.push(idx as usize);
2301 xdf1.effective_ids.push(id);
2302 } else {
2303 xdf1.changed[idx as usize] = 1;
2304 }
2305 idx += 1;
2306 }
2307 xdf1.nreff = xdf1.reference_index.len() as i64;
2308
2309 let mut idx = xdf2.dstart;
2310 while idx <= xdf2.dend {
2311 let id = ids2[idx as usize];
2312 let a = work.action2[idx as usize];
2313 if a == KEEP
2314 || (a == INVESTIGATE
2315 && !clean_mmatch(&work.action2[..nrec2 + 1], idx, xdf2.dstart, xdf2.dend))
2316 {
2317 xdf2.reference_index.push(idx as usize);
2318 xdf2.effective_ids.push(id);
2319 } else {
2320 xdf2.changed[idx as usize] = 1;
2321 }
2322 idx += 1;
2323 }
2324 xdf2.nreff = xdf2.reference_index.len() as i64;
2325
2326 let ndiags = xdf1.nreff + xdf2.nreff + 3;
2328 let kvd_len = (2 * ndiags + 2) as usize;
2329 work.prepare_kvd(kvd_len);
2330 let kvd = &mut work.kvd[..kvd_len];
2331 let koff = xdf2.nreff + 1;
2333 let (kvdf_slice, kvdb_slice) = kvd.split_at_mut(ndiags as usize);
2334 let xenv = XdAlgoEnv {
2337 mxcost: bogosqrt(ndiags).max(XDL_MAX_COST_MIN),
2338 snake_cnt: XDL_SNAKE_CNT,
2339 heur_min: XDL_HEUR_MIN_COST,
2340 };
2341
2342 let nreff1 = xdf1.nreff;
2343 let nreff2 = xdf2.nreff;
2344 xdl_recs_cmp(
2345 &mut xdf1, 0, nreff1, &mut xdf2, 0, nreff2, kvdf_slice, kvdb_slice, koff, need_min,
2346 &xenv,
2347 );
2348
2349 (bool_changed(xdf1.changed), bool_changed(xdf2.changed))
2350 }
2351
2352 pub(crate) fn patience_changed_flags(
2354 lines1: &[&str],
2355 lines2: &[&str],
2356 ) -> (Vec<bool>, Vec<bool>) {
2357 let b1: Vec<&[u8]> = lines1.iter().map(|l| l.as_bytes()).collect();
2358 let b2: Vec<&[u8]> = lines2.iter().map(|l| l.as_bytes()).collect();
2359 patience_changed_flags_bytes(&b1, &b2)
2360 }
2361
2362 pub(crate) fn patience_changed_flags_bytes(
2363 lines1: &[&[u8]],
2364 lines2: &[&[u8]],
2365 ) -> (Vec<bool>, Vec<bool>) {
2366 let mut changed1 = vec![false; lines1.len()];
2367 let mut changed2 = vec![false; lines2.len()];
2368 patience_recurse_bytes(
2369 lines1,
2370 lines2,
2371 &mut changed1,
2372 &mut changed2,
2373 0,
2374 lines1.len(),
2375 0,
2376 lines2.len(),
2377 );
2378 (changed1, changed2)
2379 }
2380
2381 #[allow(clippy::too_many_arguments)]
2382 fn patience_recurse_bytes(
2383 lines1: &[&[u8]],
2384 lines2: &[&[u8]],
2385 changed1: &mut [bool],
2386 changed2: &mut [bool],
2387 o_start: usize,
2388 o_end: usize,
2389 n_start: usize,
2390 n_end: usize,
2391 ) {
2392 if o_start >= o_end && n_start >= n_end {
2393 return;
2394 }
2395 if o_start >= o_end {
2396 changed2[n_start..n_end].fill(true);
2397 return;
2398 }
2399 if n_start >= n_end {
2400 changed1[o_start..o_end].fill(true);
2401 return;
2402 }
2403
2404 let anchors = patience_anchors_bytes(lines1, lines2, o_start, o_end, n_start, n_end);
2405 if anchors.is_empty() {
2406 merge_myers_region_bytes(
2407 lines1, lines2, changed1, changed2, o_start, o_end, n_start, n_end,
2408 );
2409 return;
2410 }
2411
2412 let mut prev_o = o_start;
2413 let mut prev_n = n_start;
2414 for (ao, an) in anchors {
2415 patience_recurse_bytes(lines1, lines2, changed1, changed2, prev_o, ao, prev_n, an);
2416 prev_o = ao + 1;
2417 prev_n = an + 1;
2418 }
2419 patience_recurse_bytes(
2420 lines1, lines2, changed1, changed2, prev_o, o_end, prev_n, n_end,
2421 );
2422 }
2423
2424 #[allow(clippy::too_many_arguments)]
2425 fn patience_anchors_bytes(
2426 lines1: &[&[u8]],
2427 lines2: &[&[u8]],
2428 o_start: usize,
2429 o_end: usize,
2430 n_start: usize,
2431 n_end: usize,
2432 ) -> Vec<(usize, usize)> {
2433 use hashbrown::HashMap;
2434
2435 let old_slice = &lines1[o_start..o_end];
2436 let new_slice = &lines2[n_start..n_end];
2437 let mut old_pos: HashMap<&[u8], Vec<usize>> = HashMap::new();
2438 let mut new_pos: HashMap<&[u8], Vec<usize>> = HashMap::new();
2439 for (i, line) in old_slice.iter().enumerate() {
2440 old_pos.entry(*line).or_default().push(o_start + i);
2441 }
2442 for (i, line) in new_slice.iter().enumerate() {
2443 new_pos.entry(*line).or_default().push(n_start + i);
2444 }
2445
2446 let mut pairs: Vec<(usize, usize)> = Vec::new();
2447 for (line, o_indices) in &old_pos {
2448 if o_indices.len() != 1 {
2449 continue;
2450 }
2451 if let Some(n_indices) = new_pos.get(line) {
2452 if n_indices.len() == 1 {
2453 pairs.push((o_indices[0], n_indices[0]));
2454 }
2455 }
2456 }
2457 pairs.sort_by_key(|p| p.0);
2458 longest_increasing_by_second(&pairs)
2459 }
2460
2461 fn longest_increasing_by_second(pairs: &[(usize, usize)]) -> Vec<(usize, usize)> {
2462 if pairs.is_empty() {
2463 return Vec::new();
2464 }
2465 let n = pairs.len();
2466 let mut tails = vec![0usize; n];
2467 let mut prev = vec![usize::MAX; n];
2468 let mut len = 0usize;
2469 for i in 0..n {
2470 let v = pairs[i].1;
2471 let mut lo = 0usize;
2472 let mut hi = len;
2473 while lo < hi {
2474 let mid = (lo + hi) / 2;
2475 if pairs[tails[mid]].1 < v {
2476 lo = mid + 1;
2477 } else {
2478 hi = mid;
2479 }
2480 }
2481 if lo > 0 {
2482 prev[i] = tails[lo - 1];
2483 }
2484 tails[lo] = i;
2485 if lo == len {
2486 len += 1;
2487 }
2488 }
2489 let mut out = Vec::with_capacity(len);
2490 let mut k = tails[len.saturating_sub(1)];
2491 while k != usize::MAX {
2492 out.push(pairs[k]);
2493 k = prev[k];
2494 }
2495 out.reverse();
2496 out
2497 }
2498
2499 pub(crate) fn histogram_changed_flags(
2500 lines1: &[&str],
2501 lines2: &[&str],
2502 ) -> (Vec<bool>, Vec<bool>) {
2503 crate::diff::git_histogram::histogram_changed_flags(
2504 lines1,
2505 lines2,
2506 |changed1, changed2, o_start, o_end, n_start, n_end| {
2507 merge_myers_region(
2508 lines1, lines2, changed1, changed2, o_start, o_end, n_start, n_end,
2509 );
2510 },
2511 )
2512 }
2513
2514 #[allow(clippy::too_many_arguments)]
2515 fn merge_myers_region(
2516 lines1: &[&str],
2517 lines2: &[&str],
2518 changed1: &mut [bool],
2519 changed2: &mut [bool],
2520 o_start: usize,
2521 o_end: usize,
2522 n_start: usize,
2523 n_end: usize,
2524 ) {
2525 let b1: Vec<&[u8]> = lines1.iter().map(|l| l.as_bytes()).collect();
2526 let b2: Vec<&[u8]> = lines2.iter().map(|l| l.as_bytes()).collect();
2527 merge_myers_region_bytes(&b1, &b2, changed1, changed2, o_start, o_end, n_start, n_end);
2528 }
2529
2530 #[allow(clippy::too_many_arguments)]
2531 pub(super) fn merge_myers_region_bytes(
2532 lines1: &[&[u8]],
2533 lines2: &[&[u8]],
2534 changed1: &mut [bool],
2535 changed2: &mut [bool],
2536 o_start: usize,
2537 o_end: usize,
2538 n_start: usize,
2539 n_end: usize,
2540 ) {
2541 use crate::line_interner::ByteLineInterner;
2542
2543 let old_slice = &lines1[o_start..o_end];
2544 let new_slice = &lines2[n_start..n_end];
2545 if old_slice.is_empty() && new_slice.is_empty() {
2546 return;
2547 }
2548 let mut interner = ByteLineInterner::with_capacity(old_slice.len() + new_slice.len());
2549 let ids1 = interner.intern_all_bytes(old_slice);
2550 let ids2 = interner.intern_all_bytes(new_slice);
2551 let (sub1, sub2) = changed_flags(&ids1, &ids2, false);
2552 for (i, ch) in sub1.iter().enumerate() {
2553 if *ch {
2554 changed1[o_start + i] = true;
2555 }
2556 }
2557 for (j, ch) in sub2.iter().enumerate() {
2558 if *ch {
2559 changed2[n_start + j] = true;
2560 }
2561 }
2562 }
2563}
2564
2565pub(crate) fn myers_rediff_changed_masks_for_region<'a>(
2567 old_lines: &[&'a str],
2568 new_lines: &[&'a str],
2569 ws: super::WhitespaceMode,
2570) -> (Vec<bool>, Vec<bool>) {
2571 use crate::line_interner::LineInterner;
2572
2573 if old_lines.is_empty() && new_lines.is_empty() {
2574 return (Vec::new(), Vec::new());
2575 }
2576 let normalize = |lines: &[&'a str]| -> Vec<Cow<'a, str>> {
2577 lines
2578 .iter()
2579 .map(|line| ws.normalize_line_for_compare(line))
2580 .collect()
2581 };
2582 let (old_normalized, new_normalized) = (normalize(old_lines), normalize(new_lines));
2583 let old_keys: Vec<&str> = old_normalized.iter().map(Cow::as_ref).collect();
2584 let new_keys: Vec<&str> = new_normalized.iter().map(Cow::as_ref).collect();
2585 let mut interner = LineInterner::with_capacity(old_keys.len() + new_keys.len());
2586 let ids1 = interner.intern_all(&old_keys);
2587 let ids2 = interner.intern_all(&new_keys);
2588 git_xdiff::changed_flags(&ids1, &ids2, false)
2589}
2590
2591pub(crate) fn myers_rediff_changed_masks_for_region_ids(
2593 old_ids: &[u32],
2594 new_ids: &[u32],
2595) -> (Vec<bool>, Vec<bool>) {
2596 if old_ids.is_empty() && new_ids.is_empty() {
2597 return (Vec::new(), Vec::new());
2598 }
2599 git_xdiff::changed_flags(old_ids, new_ids, false)
2600}
2601
2602fn changed_flags_to_ops(
2604 changed1: &[bool],
2605 changed2: &[bool],
2606 old_len: usize,
2607 new_len: usize,
2608) -> Vec<similar::DiffOp> {
2609 use similar::DiffOp;
2610 let mut ops: Vec<DiffOp> = Vec::new();
2611 let mut i = 0usize;
2612 let mut j = 0usize;
2613 while i < old_len || j < new_len {
2614 let del = i < old_len && changed1[i];
2615 let ins = j < new_len && changed2[j];
2616 if !del && !ins {
2617 if i < old_len && j < new_len {
2618 let start_i = i;
2620 let start_j = j;
2621 while i < old_len && j < new_len && !changed1[i] && !changed2[j] {
2622 i += 1;
2623 j += 1;
2624 }
2625 ops.push(DiffOp::Equal {
2626 old_index: start_i,
2627 new_index: start_j,
2628 len: i - start_i,
2629 });
2630 } else if i < old_len {
2631 let start_i = i;
2632 while i < old_len {
2633 i += 1;
2634 }
2635 ops.push(DiffOp::Delete {
2636 old_index: start_i,
2637 old_len: i - start_i,
2638 new_index: j,
2639 });
2640 } else if j < new_len {
2641 let start_j = j;
2642 while j < new_len {
2643 j += 1;
2644 }
2645 ops.push(DiffOp::Insert {
2646 old_index: i,
2647 new_index: start_j,
2648 new_len: j - start_j,
2649 });
2650 }
2651 } else {
2652 let start_i = i;
2654 let start_j = j;
2655 while i < old_len && changed1[i] {
2656 i += 1;
2657 }
2658 while j < new_len && changed2[j] {
2659 j += 1;
2660 }
2661 let dlen = i - start_i;
2662 let ilen = j - start_j;
2663 if dlen > 0 && ilen > 0 {
2664 ops.push(DiffOp::Replace {
2665 old_index: start_i,
2666 old_len: dlen,
2667 new_index: start_j,
2668 new_len: ilen,
2669 });
2670 } else if dlen > 0 {
2671 ops.push(DiffOp::Delete {
2672 old_index: start_i,
2673 old_len: dlen,
2674 new_index: start_j,
2675 });
2676 } else if ilen > 0 {
2677 ops.push(DiffOp::Insert {
2678 old_index: start_i,
2679 new_index: start_j,
2680 new_len: ilen,
2681 });
2682 }
2683 }
2684 }
2685 ops
2686}
2687
2688#[must_use]
2690#[allow(dead_code)]
2691fn diff_slice_ops_compacted(
2692 old_lines: &[&str],
2693 new_lines: &[&str],
2694 algorithm: similar::Algorithm,
2695 indent_heuristic: bool,
2696) -> Vec<similar::DiffOp> {
2697 diff_indent_heuristic::diff_slice_ops_compacted(
2698 old_lines,
2699 new_lines,
2700 algorithm,
2701 indent_heuristic,
2702 )
2703}
2704
2705#[must_use]
2708#[allow(clippy::too_many_arguments)]
2712pub(crate) fn line_diff_ops_git_xdiff_compacted_ids(
2713 old_ids: &[u32],
2714 new_ids: &[u32],
2715 shared_counts: &[u32],
2716 shared_records: usize,
2717 old_lines: &[&str],
2718 new_lines: &[&str],
2719 indent_heuristic: bool,
2720 ws: super::WhitespaceMode,
2721) -> Vec<similar::DiffOp> {
2722 let (changed1, changed2) = git_xdiff::changed_flags_with_shared(
2723 old_ids,
2724 new_ids,
2725 shared_counts,
2726 shared_records,
2727 false,
2728 );
2729 let ops = changed_flags_to_ops(&changed1, &changed2, old_lines.len(), new_lines.len());
2730 diff_indent_heuristic::apply_change_compact_to_ops_with_ws(
2731 &ops,
2732 old_lines,
2733 new_lines,
2734 indent_heuristic,
2735 ws,
2736 )
2737}
2738
2739#[cfg(test)]
2741#[must_use]
2742pub(crate) fn xdiff_new_line_changed_flags(
2743 old_lines: &[&str],
2744 new_lines: &[&str],
2745 indent_heuristic: bool,
2746) -> Vec<bool> {
2747 let mut interner =
2748 crate::line_interner::LineInterner::with_capacity(old_lines.len() + new_lines.len());
2749 let ids1 = interner.intern_all(old_lines);
2750 let ids2 = interner.intern_all(new_lines);
2751 let (changed1, changed2) = git_xdiff::changed_flags(&ids1, &ids2, false);
2752 let ops = changed_flags_to_ops(&changed1, &changed2, old_lines.len(), new_lines.len());
2753 let compacted = diff_indent_heuristic::apply_change_compact_to_ops(
2754 &ops,
2755 old_lines,
2756 new_lines,
2757 indent_heuristic,
2758 );
2759 let mut flags = vec![false; new_lines.len()];
2760 for op in compacted {
2761 match op.tag() {
2762 similar::DiffTag::Insert | similar::DiffTag::Replace => {
2763 for idx in op.new_range() {
2764 if idx < flags.len() {
2765 flags[idx] = true;
2766 }
2767 }
2768 }
2769 similar::DiffTag::Equal | similar::DiffTag::Delete => {}
2770 }
2771 }
2772 flags
2773}
2774
2775#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2777#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
2778#[cfg_attr(feature = "serde-models", serde(rename_all = "snake_case"))]
2779pub enum DiffStatus {
2780 Added,
2782 Deleted,
2784 Modified,
2786 Renamed,
2788 Copied,
2790 TypeChanged,
2792 Unmerged,
2794}
2795
2796#[must_use]
2798pub(crate) fn gitlink_unstaged_status_letter(entry: &DiffEntry) -> char {
2799 if entry.old_mode.is_gitlink() && entry.status == DiffStatus::Modified {
2800 if entry.new_oid.is_zero() {
2801 return 'm';
2802 }
2803 return 'M';
2804 }
2805 entry.status.letter()
2806}
2807
2808impl DiffStatus {
2809 #[must_use]
2811 pub fn letter(&self) -> char {
2812 match self {
2813 Self::Added => 'A',
2814 Self::Deleted => 'D',
2815 Self::Modified => 'M',
2816 Self::Renamed => 'R',
2817 Self::Copied => 'C',
2818 Self::TypeChanged => 'T',
2819 Self::Unmerged => 'U',
2820 }
2821 }
2822
2823 #[must_use]
2830 pub(crate) fn for_content_change(old_mode: u32, new_mode: u32) -> Self {
2831 if old_mode & 0o170000 != new_mode & 0o170000 {
2832 Self::TypeChanged
2833 } else {
2834 Self::Modified
2835 }
2836 }
2837}
2838
2839#[derive(Debug, Clone, PartialEq, Eq)]
2841#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
2842pub struct DiffEntry {
2843 pub status: DiffStatus,
2845 pub old_path: Option<String>,
2847 pub new_path: Option<String>,
2849 pub old_mode: FileMode,
2851 pub new_mode: FileMode,
2853 pub old_oid: ObjectId,
2855 pub new_oid: ObjectId,
2858 pub score: Option<u32>,
2860}
2861
2862impl DiffEntry {
2863 #[must_use]
2865 pub fn path(&self) -> &str {
2866 self.new_path
2867 .as_deref()
2868 .or(self.old_path.as_deref())
2869 .unwrap_or("")
2870 }
2871
2872 #[must_use]
2877 pub fn display_path(&self) -> String {
2878 match self.status {
2879 DiffStatus::Renamed | DiffStatus::Copied => {
2880 let old = self.old_path.as_deref().unwrap_or("");
2881 let new = self.new_path.as_deref().unwrap_or("");
2882 if old.is_empty() || new.is_empty() {
2883 self.path().to_owned()
2884 } else {
2885 format!("{old} -> {new}")
2886 }
2887 }
2888 _ => self.path().to_owned(),
2889 }
2890 }
2891}
2892
2893#[must_use]
2895pub fn zero_oid() -> ObjectId {
2896 ObjectId::from_bytes(&[0u8; 20]).unwrap_or_else(|_| {
2897 panic!("internal error: failed to create zero OID");
2899 })
2900}
2901
2902#[must_use]
2904pub fn empty_blob_oid() -> ObjectId {
2905 ObjectId::from_hex("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391").unwrap_or_else(|_| {
2906 panic!("internal error: failed to create empty blob OID");
2908 })
2909}
2910
2911pub fn diff_trees(
2926 odb: &Odb,
2927 old_tree_oid: Option<&ObjectId>,
2928 new_tree_oid: Option<&ObjectId>,
2929 prefix: &str,
2930) -> Result<Vec<DiffEntry>> {
2931 crate::trace::trace_span!("grit.diff.tree");
2932 diff_trees_with_cancel(odb, old_tree_oid, new_tree_oid, prefix, None)
2933}
2934
2935pub(crate) fn diff_trees_with_cancel(
2936 odb: &Odb,
2937 old_tree_oid: Option<&ObjectId>,
2938 new_tree_oid: Option<&ObjectId>,
2939 prefix: &str,
2940 cancel_watch: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
2941) -> Result<Vec<DiffEntry>> {
2942 crate::trace::trace_span!("grit.diff.tree");
2943 let hint = cancel_watch.map(|watch| DiffTreesWalkHint {
2944 cancel_watch: Some(watch),
2945 ..DiffTreesWalkHint::default()
2946 });
2947 diff_trees_opts(
2948 odb,
2949 old_tree_oid,
2950 new_tree_oid,
2951 prefix,
2952 false,
2953 hint.as_ref(),
2954 )
2955}
2956
2957pub(crate) fn diff_trees_with_index(
2960 odb: &Odb,
2961 index: &Index,
2962 old_tree_oid: Option<&ObjectId>,
2963 new_tree_oid: Option<&ObjectId>,
2964 prefix: &str,
2965 trust_index_for_old_tree: bool,
2966 cancel_watch: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
2967) -> Result<Vec<DiffEntry>> {
2968 let hint = (trust_index_for_old_tree
2969 && old_tree_oid.is_some()
2970 && index.entries_sorted
2971 && !index.entries.iter().any(|e| e.stage() != 0))
2972 .then_some(DiffTreesWalkHint {
2973 index: Some(index),
2974 old_tree_matches: true,
2975 cancel_watch,
2976 ..DiffTreesWalkHint::default()
2977 });
2978 diff_trees_opts(
2979 odb,
2980 old_tree_oid,
2981 new_tree_oid,
2982 prefix,
2983 false,
2984 hint.as_ref(),
2985 )
2986}
2987
2988pub(crate) fn diff_trees_outside_sparse_directories(
2992 odb: &Odb,
2993 index: &Index,
2994 old_tree_oid: Option<&ObjectId>,
2995 new_tree_oid: Option<&ObjectId>,
2996 sparse: Option<&SparseCheckout>,
2997 index_sparse: bool,
2998 cancel_watch: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
2999) -> Result<Vec<DiffEntry>> {
3000 crate::trace::trace_span!("grit.diff.tree");
3001 let hint = index
3002 .has_sparse_directory_placeholders()
3003 .then_some(DiffTreesWalkHint {
3004 index: Some(index),
3005 skip_sparse_directories: true,
3006 sparse,
3007 index_sparse,
3008 cancel_watch,
3009 ..DiffTreesWalkHint::default()
3010 });
3011 diff_trees_opts(odb, old_tree_oid, new_tree_oid, "", false, hint.as_ref())
3012}
3013
3014pub(crate) fn diff_trees_coalesced_for_checkout(
3017 odb: &Odb,
3018 index: &Index,
3019 old_tree_oid: Option<&ObjectId>,
3020 new_tree_oid: Option<&ObjectId>,
3021 sparse: Option<&SparseCheckout>,
3022 index_sparse: bool,
3023) -> Result<Vec<DiffEntry>> {
3024 crate::trace::trace_span!("grit.diff.tree");
3025 let trust_index_for_old_tree = old_tree_oid.is_some()
3026 && index.entries_sorted
3027 && !index.entries.iter().any(|e| e.stage() != 0);
3028 let hint = DiffTreesWalkHint {
3029 index: trust_index_for_old_tree.then_some(index),
3030 old_tree_matches: trust_index_for_old_tree,
3031 coalesce_subtrees_for_checkout: true,
3032 skip_sparse_directories: index.has_sparse_directory_placeholders(),
3033 sparse,
3034 index_sparse,
3035 ..DiffTreesWalkHint::default()
3036 };
3037 diff_trees_opts(odb, old_tree_oid, new_tree_oid, "", false, Some(&hint))
3038}
3039
3040fn tree_walk_directory_relevant(
3042 dir: &str,
3043 pathspecs: &[String],
3044 pathspec_matcher: Option<&crate::pathspec::PathspecMatcher<'_>>,
3045) -> bool {
3046 if pathspecs.is_empty() {
3047 return true;
3048 }
3049 if let Some(matcher) = pathspec_matcher {
3050 return matcher.matches_entry(dir, 0o040000)
3051 || crate::pathspec::pathspec_wants_descent_for_rev_tree(dir, pathspecs);
3052 }
3053 crate::pathspec::pathspec_entry_is_relevant(dir, true, pathspecs)
3054}
3055
3056fn diff_entry_matches_pathspecs(
3057 entry: &DiffEntry,
3058 pathspecs: &[String],
3059 pathspec_matcher: Option<&crate::pathspec::PathspecMatcher<'_>>,
3060) -> bool {
3061 let path = entry.path();
3062 if path.is_empty() {
3063 return false;
3064 }
3065 if let Some(matcher) = pathspec_matcher {
3066 return matcher.matches_change(path, entry.old_path.as_deref());
3067 }
3068 crate::pathspec::matches_pathspec_list(path, pathspecs)
3069}
3070
3071pub(crate) fn diff_trees_for_pathspecs(
3077 odb: &Odb,
3078 old_tree_oid: Option<&ObjectId>,
3079 new_tree_oid: Option<&ObjectId>,
3080 pathspecs: &[String],
3081 pathspec_matcher: Option<&crate::pathspec::PathspecMatcher<'_>>,
3082) -> Result<Vec<DiffEntry>> {
3083 if pathspecs.is_empty() {
3084 return diff_trees(odb, old_tree_oid, new_tree_oid, "");
3085 }
3086 crate::trace::trace_span!("grit.diff.tree");
3087 let hint = DiffTreesWalkHint {
3088 pathspecs,
3089 pathspec_matcher,
3090 ..DiffTreesWalkHint::default()
3091 };
3092 let mut entries = diff_trees_opts(odb, old_tree_oid, new_tree_oid, "", false, Some(&hint))?;
3093 entries.retain(|entry| diff_entry_matches_pathspecs(entry, pathspecs, pathspec_matcher));
3094 Ok(entries)
3095}
3096
3097#[derive(Clone, Default)]
3099struct DiffTreesWalkHint<'a> {
3100 index: Option<&'a Index>,
3101 old_tree_matches: bool,
3103 coalesce_subtrees_for_checkout: bool,
3105 skip_sparse_directories: bool,
3107 sparse: Option<&'a SparseCheckout>,
3108 index_sparse: bool,
3109 pathspecs: &'a [String],
3111 pathspec_matcher: Option<&'a crate::pathspec::PathspecMatcher<'a>>,
3112 cancel_watch: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
3114}
3115
3116impl DiffTreesWalkHint<'_> {
3117 fn sparse_excludes_tree(&self, dir: &str) -> bool {
3119 if !self.index_sparse {
3120 return false;
3121 }
3122 let Some(sparse) = self.sparse else {
3123 return false;
3124 };
3125 let mut path = crate::repo_relpath::storage_bytes(dir).into_owned();
3126 if !path.ends_with(b"/") {
3127 path.push(b'/');
3128 }
3129 sparse.excludes_directory(&path)
3130 }
3131
3132 fn skips(&self, dir: &str) -> bool {
3134 if dir.is_empty() {
3135 return false;
3136 }
3137 if !self.pathspecs.is_empty()
3138 && !tree_walk_directory_relevant(dir, self.pathspecs, self.pathspec_matcher)
3139 {
3140 return true;
3141 }
3142 let Some(index) = self.index.filter(|_| self.skip_sparse_directories) else {
3143 return false;
3144 };
3145 let mut row = crate::repo_relpath::storage_bytes(dir).into_owned();
3146 row.push(b'/');
3147 index
3148 .find_entry_pos(&row, 0)
3149 .is_some_and(|pos| index.entries[pos].is_sparse_directory_placeholder())
3150 }
3151}
3152
3153pub(crate) fn diff_tree_path_change_count(
3161 odb: &Odb,
3162 old_tree_oid: Option<&ObjectId>,
3163 new_tree_oid: Option<&ObjectId>,
3164 prefix: &str,
3165) -> Result<usize> {
3166 if old_tree_oid == new_tree_oid {
3167 return Ok(0);
3168 }
3169 let old_entries = match old_tree_oid {
3170 Some(oid) => read_tree(odb, oid)?,
3171 None => Vec::new(),
3172 };
3173 let new_entries = match new_tree_oid {
3174 Some(oid) => read_tree(odb, oid)?,
3175 None => Vec::new(),
3176 };
3177 let mut count = 0usize;
3178 diff_tree_entries_count(odb, &old_entries, &new_entries, prefix, false, &mut count)?;
3179 Ok(count)
3180}
3181
3182pub fn diff_trees_with_renames(
3191 odb: &Odb,
3192 old_tree_oid: Option<&ObjectId>,
3193 new_tree_oid: Option<&ObjectId>,
3194 prefix: &str,
3195 rename_threshold: u32,
3196) -> Result<Vec<DiffEntry>> {
3197 let config = ConfigSet::new();
3198 let policy = ThreadPolicy::default();
3199 diff_trees_with_renames_and_policy(
3200 odb,
3201 old_tree_oid,
3202 new_tree_oid,
3203 prefix,
3204 rename_threshold,
3205 &config,
3206 &policy,
3207 )
3208}
3209
3210pub(crate) fn diff_trees_with_renames_and_policy(
3216 odb: &Odb,
3217 old_tree_oid: Option<&ObjectId>,
3218 new_tree_oid: Option<&ObjectId>,
3219 prefix: &str,
3220 rename_threshold: u32,
3221 config: &ConfigSet,
3222 thread_policy: &ThreadPolicy,
3223) -> Result<Vec<DiffEntry>> {
3224 Ok(diff_trees_with_renames_and_policy_result(
3225 odb,
3226 old_tree_oid,
3227 new_tree_oid,
3228 prefix,
3229 rename_threshold,
3230 config,
3231 thread_policy,
3232 )?
3233 .entries)
3234}
3235
3236pub(crate) fn diff_trees_with_renames_and_policy_result(
3242 odb: &Odb,
3243 old_tree_oid: Option<&ObjectId>,
3244 new_tree_oid: Option<&ObjectId>,
3245 prefix: &str,
3246 rename_threshold: u32,
3247 config: &ConfigSet,
3248 thread_policy: &ThreadPolicy,
3249) -> Result<DiffEntriesResult> {
3250 diff_trees_with_renames_for_pathspecs_and_policy_result(
3251 odb,
3252 old_tree_oid,
3253 new_tree_oid,
3254 prefix,
3255 &[],
3256 None,
3257 rename_threshold,
3258 config,
3259 thread_policy,
3260 )
3261}
3262
3263#[allow(clippy::too_many_arguments)]
3272pub(crate) fn diff_trees_with_renames_for_pathspecs_and_policy(
3273 odb: &Odb,
3274 old_tree_oid: Option<&ObjectId>,
3275 new_tree_oid: Option<&ObjectId>,
3276 prefix: &str,
3277 pathspecs: &[String],
3278 pathspec_matcher: Option<&crate::pathspec::PathspecMatcher<'_>>,
3279 rename_threshold: u32,
3280 config: &ConfigSet,
3281 thread_policy: &ThreadPolicy,
3282) -> Result<Vec<DiffEntry>> {
3283 Ok(diff_trees_with_renames_for_pathspecs_and_policy_result(
3284 odb,
3285 old_tree_oid,
3286 new_tree_oid,
3287 prefix,
3288 pathspecs,
3289 pathspec_matcher,
3290 rename_threshold,
3291 config,
3292 thread_policy,
3293 )?
3294 .entries)
3295}
3296
3297#[allow(clippy::too_many_arguments)]
3299pub(crate) fn diff_trees_with_renames_for_pathspecs_and_policy_result(
3300 odb: &Odb,
3301 old_tree_oid: Option<&ObjectId>,
3302 new_tree_oid: Option<&ObjectId>,
3303 prefix: &str,
3304 pathspecs: &[String],
3305 pathspec_matcher: Option<&crate::pathspec::PathspecMatcher<'_>>,
3306 rename_threshold: u32,
3307 config: &ConfigSet,
3308 thread_policy: &ThreadPolicy,
3309) -> Result<DiffEntriesResult> {
3310 let entries = if pathspecs.is_empty() {
3311 diff_trees(odb, old_tree_oid, new_tree_oid, prefix)?
3312 } else {
3313 diff_trees_for_pathspecs(odb, old_tree_oid, new_tree_oid, pathspecs, pathspec_matcher)?
3314 };
3315 if rename_threshold <= 100 {
3316 let out = detect_renames_with_policy(
3317 odb,
3318 None,
3319 entries,
3320 rename_threshold,
3321 config,
3322 thread_policy,
3323 None,
3324 DetectRenamesOptions::default(),
3325 )?;
3326 return Ok(DiffEntriesResult {
3327 entries: out.entries,
3328 inexact_rename_skipped: out.inexact_skipped,
3329 });
3330 }
3331 Ok(DiffEntriesResult {
3332 entries,
3333 inexact_rename_skipped: None,
3334 })
3335}
3336
3337pub fn tree_oid_of_commit(odb: &Odb, commit_oid: &ObjectId) -> Result<ObjectId> {
3344 odb.commit_tree(commit_oid).map_err(|err| match err {
3345 Error::UnexpectedObjectKind { .. } => {
3346 Error::InvalidRef(format!("invalid ref: {commit_oid} is not a commit"))
3347 }
3348 other => other,
3349 })
3350}
3351
3352pub(crate) fn tree_oid_for_diff_side(odb: &Odb, oid: &ObjectId) -> Result<ObjectId> {
3359 use crate::objects::{CommitRef, ObjectKind};
3360 let object = odb.read(oid)?;
3361 match object.kind {
3362 ObjectKind::Commit => Ok(CommitRef::parse(&object.data)?.tree()),
3363 ObjectKind::Tree => Ok(*oid),
3364 actual => Err(Error::InvalidRef(format!(
3365 "invalid ref: {oid} is not a commit or tree ({actual:?})"
3366 ))),
3367 }
3368}
3369
3370pub(crate) fn tree_oid_of_commit_in_repo(
3376 repo: &crate::repo::Repository,
3377 commit_oid: &ObjectId,
3378) -> Result<ObjectId> {
3379 if repo.object_id_after_replace(commit_oid) == *commit_oid {
3380 if let Some(chain) = repo.odb().commit_graph_chain() {
3381 if let Some(tree) = chain.graph_commit_tree(commit_oid) {
3382 return Ok(tree);
3383 }
3384 }
3385 }
3386 let effective = repo.object_id_after_replace(commit_oid);
3387 tree_oid_of_commit(repo.odb(), &effective)
3388}
3389
3390pub fn diff_commits(odb: &Odb, base: &ObjectId, tip: &ObjectId) -> Result<Vec<DiffEntry>> {
3396 let base_tree = tree_oid_of_commit(odb, base)?;
3397 let tip_tree = tree_oid_of_commit(odb, tip)?;
3398 diff_trees(odb, Some(&base_tree), Some(&tip_tree), "")
3399}
3400
3401pub fn diff_commits_with_renames(
3410 odb: &Odb,
3411 base: &ObjectId,
3412 tip: &ObjectId,
3413 rename_threshold: u32,
3414) -> Result<Vec<DiffEntry>> {
3415 let config = ConfigSet::new();
3416 let policy = ThreadPolicy::default();
3417 diff_commits_with_renames_and_policy(odb, base, tip, rename_threshold, &config, &policy)
3418}
3419
3420pub(crate) fn diff_commits_with_renames_and_policy(
3426 odb: &Odb,
3427 base: &ObjectId,
3428 tip: &ObjectId,
3429 rename_threshold: u32,
3430 config: &ConfigSet,
3431 thread_policy: &ThreadPolicy,
3432) -> Result<Vec<DiffEntry>> {
3433 let base_tree = tree_oid_of_commit(odb, base)?;
3434 let tip_tree = tree_oid_of_commit(odb, tip)?;
3435 diff_trees_with_renames_and_policy(
3436 odb,
3437 Some(&base_tree),
3438 Some(&tip_tree),
3439 "",
3440 rename_threshold,
3441 config,
3442 thread_policy,
3443 )
3444}
3445
3446pub fn diff_trees_show_tree_entries(
3453 odb: &Odb,
3454 old_tree_oid: Option<&ObjectId>,
3455 new_tree_oid: Option<&ObjectId>,
3456 prefix: &str,
3457) -> Result<Vec<DiffEntry>> {
3458 diff_trees_opts(odb, old_tree_oid, new_tree_oid, prefix, true, None)
3459}
3460
3461fn diff_subtrees_in_place(
3462 odb: &Odb,
3463 old_tree_oid: Option<&ObjectId>,
3464 new_tree_oid: Option<&ObjectId>,
3465 prefix: &str,
3466 show_trees: bool,
3467 index_hint: Option<&DiffTreesWalkHint<'_>>,
3468 result: &mut Vec<DiffEntry>,
3469) -> Result<()> {
3470 if index_hint.is_some_and(|hint| hint.skips(prefix)) {
3471 return Ok(());
3472 }
3473 if !show_trees {
3474 if let (Some(a), Some(b)) = (old_tree_oid, new_tree_oid) {
3475 if a == b {
3476 return Ok(());
3477 }
3478 }
3479 }
3480 let old_tree = old_tree_oid
3483 .map(|oid| odb.cached_tree_for_walk(oid))
3484 .transpose()?;
3485 let new_tree = new_tree_oid
3486 .map(|oid| odb.cached_tree_for_walk(oid))
3487 .transpose()?;
3488 let oid_len = odb.hash_algo().len();
3489 diff_tree_entries_opts(
3490 odb,
3491 WalkEntries::new(old_tree.as_ref(), oid_len),
3492 WalkEntries::new(new_tree.as_ref(), oid_len),
3493 prefix,
3494 show_trees,
3495 index_hint,
3496 result,
3497 )
3498}
3499
3500enum WalkEntries<'a> {
3503 Parsed(std::slice::Iter<'a, TreeEntry>),
3504 Raw(TreeRefIter<'a>),
3505}
3506
3507impl<'a> WalkEntries<'a> {
3508 fn new(tree: Option<&'a SessionTree>, oid_len: usize) -> Self {
3510 match tree {
3511 Some(SessionTree::Parsed(entries)) => Self::Parsed(entries.iter()),
3512 Some(SessionTree::Raw(data)) => Self::Raw(TreeRefIter::with_oid_len(data, oid_len)),
3513 None => Self::Parsed([].iter()),
3514 }
3515 }
3516
3517 fn next_entry(&mut self) -> Result<Option<TreeEntryRef<'a>>> {
3518 match self {
3519 Self::Parsed(entries) => Ok(entries.next().map(|entry| TreeEntryRef {
3520 mode: entry.mode,
3521 name: &entry.name,
3522 oid: entry.oid,
3523 })),
3524 Self::Raw(entries) => entries.next().transpose(),
3525 }
3526 }
3527}
3528
3529fn diff_trees_opts(
3530 odb: &Odb,
3531 old_tree_oid: Option<&ObjectId>,
3532 new_tree_oid: Option<&ObjectId>,
3533 prefix: &str,
3534 show_trees: bool,
3535 index_hint: Option<&DiffTreesWalkHint<'_>>,
3536) -> Result<Vec<DiffEntry>> {
3537 let mut result = Vec::new();
3538 diff_subtrees_in_place(
3539 odb,
3540 old_tree_oid,
3541 new_tree_oid,
3542 prefix,
3543 show_trees,
3544 index_hint,
3545 &mut result,
3546 )?;
3547 Ok(result)
3548}
3549
3550#[cfg(test)]
3551use std::cell::RefCell;
3552#[cfg(test)]
3553use std::rc::Rc;
3554#[cfg(test)]
3555use std::sync::atomic::{AtomicUsize, Ordering};
3556
3557#[cfg(test)]
3558thread_local! {
3559 static DIFF_TREE_READ_HOOK: RefCell<Option<Rc<AtomicUsize>>> = const { RefCell::new(None) };
3560}
3561
3562#[cfg(test)]
3564pub(crate) fn set_diff_tree_read_hook_for_tests(hook: Option<Rc<AtomicUsize>>) {
3565 DIFF_TREE_READ_HOOK.with(|cell| *cell.borrow_mut() = hook);
3566}
3567
3568fn read_tree_from_odb(odb: &Odb, oid: &ObjectId) -> Result<Vec<TreeEntry>> {
3570 #[cfg(test)]
3571 if let Some(counter) = DIFF_TREE_READ_HOOK.with(|cell| cell.borrow().clone()) {
3572 counter.fetch_add(1, Ordering::Relaxed);
3573 }
3574 Ok((*odb.cached_tree_entries(oid)?).clone())
3575}
3576
3577fn read_tree(odb: &Odb, oid: &ObjectId) -> Result<Vec<TreeEntry>> {
3579 read_tree_from_odb(odb, oid)
3580}
3581
3582fn diff_tree_entries_count(
3583 odb: &Odb,
3584 old: &[TreeEntry],
3585 new: &[TreeEntry],
3586 prefix: &str,
3587 show_trees: bool,
3588 count: &mut usize,
3589) -> Result<()> {
3590 let mut oi = 0;
3591 let mut ni = 0;
3592
3593 while oi < old.len() || ni < new.len() {
3594 match (old.get(oi), new.get(ni)) {
3595 (Some(o), Some(n)) => {
3596 let cmp = crate::objects::tree_entry_cmp(
3597 &o.name,
3598 is_tree_mode(o.mode),
3599 &n.name,
3600 is_tree_mode(n.mode),
3601 );
3602 match cmp {
3603 std::cmp::Ordering::Less => {
3604 emit_deleted_count(odb, o, prefix, show_trees, count)?;
3605 oi += 1;
3606 }
3607 std::cmp::Ordering::Greater => {
3608 emit_added_count(odb, n, prefix, show_trees, count)?;
3609 ni += 1;
3610 }
3611 std::cmp::Ordering::Equal => {
3612 if o.oid != n.oid
3613 || crate::index::canon_mode(o.mode) != crate::index::canon_mode(n.mode)
3614 {
3615 let path = crate::repo_relpath::format_path_stored(prefix, &o.name);
3616 if is_tree_mode(o.mode) && is_tree_mode(n.mode) {
3617 if show_trees {
3618 *count += 1;
3619 }
3620 *count += diff_tree_path_change_count(
3621 odb,
3622 Some(&o.oid),
3623 Some(&n.oid),
3624 &path,
3625 )?;
3626 } else if is_tree_mode(o.mode) != is_tree_mode(n.mode) {
3627 emit_deleted_count(odb, o, prefix, show_trees, count)?;
3628 emit_added_count(odb, n, prefix, show_trees, count)?;
3629 } else {
3630 *count += 1;
3631 }
3632 }
3633 oi += 1;
3634 ni += 1;
3635 }
3636 }
3637 }
3638 (Some(o), None) => {
3639 emit_deleted_count(odb, o, prefix, show_trees, count)?;
3640 oi += 1;
3641 }
3642 (None, Some(n)) => {
3643 emit_added_count(odb, n, prefix, show_trees, count)?;
3644 ni += 1;
3645 }
3646 (None, None) => break,
3647 }
3648 }
3649
3650 Ok(())
3651}
3652
3653fn emit_deleted_count(
3654 odb: &Odb,
3655 entry: &TreeEntry,
3656 prefix: &str,
3657 show_trees: bool,
3658 count: &mut usize,
3659) -> Result<()> {
3660 let path = crate::repo_relpath::format_path_stored(prefix, &entry.name);
3661 if is_tree_mode(entry.mode) {
3662 if show_trees {
3663 *count += 1;
3664 }
3665 *count += diff_tree_path_change_count(odb, Some(&entry.oid), None, &path)?;
3666 } else {
3667 *count += 1;
3668 }
3669 Ok(())
3670}
3671
3672fn emit_added_count(
3673 odb: &Odb,
3674 entry: &TreeEntry,
3675 prefix: &str,
3676 show_trees: bool,
3677 count: &mut usize,
3678) -> Result<()> {
3679 let path = crate::repo_relpath::format_path_stored(prefix, &entry.name);
3680 if is_tree_mode(entry.mode) {
3681 if show_trees {
3682 *count += 1;
3683 }
3684 *count += diff_tree_path_change_count(odb, None, Some(&entry.oid), &path)?;
3685 } else {
3686 *count += 1;
3687 }
3688 Ok(())
3689}
3690
3691fn poll_tree_diff_cancel(
3693 index_hint: Option<&DiffTreesWalkHint<'_>>,
3694 stride: &mut usize,
3695) -> Result<()> {
3696 use crate::progress::{poll_watch, CANCEL_POLL_STRIDE};
3697 *stride = stride.wrapping_add(1);
3698 if stride.is_multiple_of(CANCEL_POLL_STRIDE) {
3699 if let Some(watch) = index_hint.and_then(|hint| hint.cancel_watch.as_ref()) {
3700 poll_watch(watch)?;
3701 }
3702 }
3703 Ok(())
3704}
3705
3706fn diff_tree_entries_opts(
3707 odb: &Odb,
3708 mut old: WalkEntries<'_>,
3709 mut new: WalkEntries<'_>,
3710 prefix: &str,
3711 show_trees: bool,
3712 index_hint: Option<&DiffTreesWalkHint<'_>>,
3713 result: &mut Vec<DiffEntry>,
3714) -> Result<()> {
3715 let mut old_entry = old.next_entry()?;
3716 let mut new_entry = new.next_entry()?;
3717 let mut cancel_stride = 0usize;
3718
3719 loop {
3720 poll_tree_diff_cancel(index_hint, &mut cancel_stride)?;
3721 match (&old_entry, &new_entry) {
3722 (Some(o), Some(n)) => {
3723 let cmp = crate::objects::tree_entry_cmp(
3724 o.name,
3725 is_tree_mode(o.mode),
3726 n.name,
3727 is_tree_mode(n.mode),
3728 );
3729 match cmp {
3730 std::cmp::Ordering::Less => {
3731 emit_deleted_opts(odb, o, prefix, show_trees, index_hint, result)?;
3733 old_entry = old.next_entry()?;
3734 }
3735 std::cmp::Ordering::Greater => {
3736 emit_added_opts(odb, n, prefix, show_trees, index_hint, result)?;
3738 new_entry = new.next_entry()?;
3739 }
3740 std::cmp::Ordering::Equal => {
3741 if o.oid != n.oid
3743 || crate::index::canon_mode(o.mode) != crate::index::canon_mode(n.mode)
3744 {
3745 let path = crate::repo_relpath::format_path_stored(prefix, o.name);
3746 if is_tree_mode(o.mode) && is_tree_mode(n.mode) {
3747 if show_trees {
3749 result.push(DiffEntry {
3750 status: DiffStatus::Modified,
3751 old_path: Some(path.clone()),
3752 new_path: Some(path.clone()),
3753 old_mode: FileMode::new(crate::index::canon_mode(o.mode)),
3754 new_mode: FileMode::new(crate::index::canon_mode(n.mode)),
3755 old_oid: o.oid,
3756 new_oid: n.oid,
3757 score: None,
3758 });
3759 }
3760 diff_subtrees_in_place(
3761 odb,
3762 Some(&o.oid),
3763 Some(&n.oid),
3764 &path,
3765 show_trees,
3766 index_hint,
3767 result,
3768 )?;
3769 } else if is_tree_mode(o.mode) && !is_tree_mode(n.mode) {
3770 emit_deleted_opts(odb, o, prefix, show_trees, index_hint, result)?;
3772 emit_added_opts(odb, n, prefix, show_trees, index_hint, result)?;
3773 } else if !is_tree_mode(o.mode) && is_tree_mode(n.mode) {
3774 emit_deleted_opts(odb, o, prefix, show_trees, index_hint, result)?;
3776 emit_added_opts(odb, n, prefix, show_trees, index_hint, result)?;
3777 } else {
3778 result.push(DiffEntry {
3779 status: DiffStatus::for_content_change(o.mode, n.mode),
3780 old_path: Some(path.clone()),
3781 new_path: Some(path),
3782 old_mode: FileMode::new(crate::index::canon_mode(o.mode)),
3783 new_mode: FileMode::new(crate::index::canon_mode(n.mode)),
3784 old_oid: o.oid,
3785 new_oid: n.oid,
3786 score: None,
3787 });
3788 }
3789 }
3790 old_entry = old.next_entry()?;
3791 new_entry = new.next_entry()?;
3792 }
3793 }
3794 }
3795 (Some(o), None) => {
3796 emit_deleted_opts(odb, o, prefix, show_trees, index_hint, result)?;
3797 old_entry = old.next_entry()?;
3798 }
3799 (None, Some(n)) => {
3800 emit_added_opts(odb, n, prefix, show_trees, index_hint, result)?;
3801 new_entry = new.next_entry()?;
3802 }
3803 (None, None) => break,
3804 }
3805 }
3806
3807 Ok(())
3808}
3809
3810fn emit_deleted_tree_from_index(
3811 index: &Index,
3812 dir_prefix: &str,
3813 index_hint: Option<&DiffTreesWalkHint<'_>>,
3814 result: &mut Vec<DiffEntry>,
3815) -> Result<()> {
3816 let scan_prefix: Vec<u8> = if dir_prefix.is_empty() {
3817 Vec::new()
3818 } else {
3819 format!("{dir_prefix}/").into_bytes()
3820 };
3821 let mut cancel_stride = 0usize;
3822 if index.entries_sorted {
3823 let start = if scan_prefix.is_empty() {
3824 0
3825 } else {
3826 index
3827 .entries
3828 .partition_point(|e| e.path.as_slice() < scan_prefix.as_slice())
3829 };
3830 for ie in index.entries[start..].iter() {
3831 poll_tree_diff_cancel(index_hint, &mut cancel_stride)?;
3832 if ie.stage() != 0 {
3833 continue;
3834 }
3835 if !scan_prefix.is_empty() && !ie.path.starts_with(&scan_prefix) {
3836 break;
3837 }
3838 if scan_prefix.is_empty() && ie.path.contains(&b'/') {
3839 continue;
3840 }
3841 let path = crate::repo_relpath::storage_string(&ie.path);
3842 result.push(DiffEntry {
3843 status: DiffStatus::Deleted,
3844 old_path: Some(path),
3845 new_path: None,
3846 old_mode: FileMode::new(ie.mode),
3847 new_mode: FileMode::ABSENT,
3848 old_oid: ie.oid,
3849 new_oid: zero_oid(),
3850 score: None,
3851 });
3852 }
3853 return Ok(());
3854 }
3855 for ie in &index.entries {
3856 poll_tree_diff_cancel(index_hint, &mut cancel_stride)?;
3857 if ie.stage() != 0 {
3858 continue;
3859 }
3860 let Ok(path) = std::str::from_utf8(&ie.path) else {
3861 continue;
3862 };
3863 let under = if dir_prefix.is_empty() {
3864 !path.contains('/')
3865 } else if path == dir_prefix {
3866 false
3867 } else {
3868 path.strip_prefix(dir_prefix)
3869 .is_some_and(|rest| rest.starts_with('/'))
3870 };
3871 if !under {
3872 continue;
3873 }
3874 result.push(DiffEntry {
3875 status: DiffStatus::Deleted,
3876 old_path: Some(path.to_owned()),
3877 new_path: None,
3878 old_mode: FileMode::new(ie.mode),
3879 new_mode: FileMode::ABSENT,
3880 old_oid: ie.oid,
3881 new_oid: zero_oid(),
3882 score: None,
3883 });
3884 }
3885 Ok(())
3886}
3887
3888fn emit_deleted_opts(
3889 odb: &Odb,
3890 entry: &TreeEntryRef<'_>,
3891 prefix: &str,
3892 show_trees: bool,
3893 index_hint: Option<&DiffTreesWalkHint<'_>>,
3894 result: &mut Vec<DiffEntry>,
3895) -> Result<()> {
3896 let path = crate::repo_relpath::format_path_stored(prefix, entry.name);
3897 if is_tree_mode(entry.mode) {
3898 if index_hint.is_some_and(|hint| hint.sparse_excludes_tree(&path)) {
3899 return Ok(());
3900 }
3901 if show_trees || index_hint.is_some_and(|hint| hint.coalesce_subtrees_for_checkout) {
3902 result.push(DiffEntry {
3903 status: DiffStatus::Deleted,
3904 old_path: Some(path.clone()),
3905 new_path: None,
3906 old_mode: FileMode::new(entry.mode),
3907 new_mode: FileMode::ABSENT,
3908 old_oid: entry.oid,
3909 new_oid: zero_oid(),
3910 score: None,
3911 });
3912 }
3913 if index_hint.is_some_and(|hint| hint.coalesce_subtrees_for_checkout) {
3914 return Ok(());
3915 }
3916 if let Some(index) = index_hint
3917 .filter(|hint| hint.old_tree_matches)
3918 .and_then(|hint| hint.index)
3919 .filter(|index| !index.has_sparse_directory_placeholders())
3920 {
3921 emit_deleted_tree_from_index(index, &path, index_hint, result)?;
3922 return Ok(());
3923 }
3924 diff_subtrees_in_place(
3925 odb,
3926 Some(&entry.oid),
3927 None,
3928 &path,
3929 show_trees,
3930 index_hint,
3931 result,
3932 )?;
3933 } else {
3934 result.push(DiffEntry {
3935 status: DiffStatus::Deleted,
3936 old_path: Some(path.clone()),
3937 new_path: None,
3938 old_mode: FileMode::new(crate::index::canon_mode(entry.mode)),
3939 new_mode: FileMode::ABSENT,
3940 old_oid: entry.oid,
3941 new_oid: zero_oid(),
3942 score: None,
3943 });
3944 }
3945 Ok(())
3946}
3947
3948fn emit_added_opts(
3949 odb: &Odb,
3950 entry: &TreeEntryRef<'_>,
3951 prefix: &str,
3952 show_trees: bool,
3953 index_hint: Option<&DiffTreesWalkHint<'_>>,
3954 result: &mut Vec<DiffEntry>,
3955) -> Result<()> {
3956 let path = crate::repo_relpath::format_path_stored(prefix, entry.name);
3957 if is_tree_mode(entry.mode) {
3958 if index_hint.is_some_and(|hint| hint.sparse_excludes_tree(&path)) {
3959 result.push(DiffEntry {
3960 status: DiffStatus::Added,
3961 old_path: None,
3962 new_path: Some(crate::repo_relpath::sparse_directory_stored_path(&path)),
3963 old_mode: FileMode::ABSENT,
3964 new_mode: FileMode::new(MODE_TREE),
3965 old_oid: zero_oid(),
3966 new_oid: entry.oid,
3967 score: None,
3968 });
3969 return Ok(());
3970 }
3971 if show_trees || index_hint.is_some_and(|hint| hint.coalesce_subtrees_for_checkout) {
3972 result.push(DiffEntry {
3973 status: DiffStatus::Added,
3974 old_path: None,
3975 new_path: Some(path.clone()),
3976 old_mode: FileMode::ABSENT,
3977 new_mode: FileMode::new(entry.mode),
3978 old_oid: zero_oid(),
3979 new_oid: entry.oid,
3980 score: None,
3981 });
3982 }
3983 if index_hint.is_some_and(|hint| hint.coalesce_subtrees_for_checkout) {
3984 return Ok(());
3985 }
3986 diff_subtrees_in_place(
3987 odb,
3988 None,
3989 Some(&entry.oid),
3990 &path,
3991 show_trees,
3992 index_hint,
3993 result,
3994 )?;
3995 } else {
3996 result.push(DiffEntry {
3997 status: DiffStatus::Added,
3998 old_path: None,
3999 new_path: Some(path),
4000 old_mode: FileMode::ABSENT,
4001 new_mode: FileMode::new(crate::index::canon_mode(entry.mode)),
4002 old_oid: zero_oid(),
4003 new_oid: entry.oid,
4004 score: None,
4005 });
4006 }
4007 Ok(())
4008}
4009
4010pub fn diff_index_to_tree(
4030 odb: &Odb,
4031 index: &Index,
4032 tree_oid: Option<&ObjectId>,
4033 ignore_submodules: bool,
4034) -> Result<Vec<DiffEntry>> {
4035 match index_cache_tree_walk(index, tree_oid) {
4036 CacheTreeWalk::Fast {
4037 head_tree,
4038 cache_root,
4039 } => {
4040 diff_index_to_tree_with_cache_tree(odb, index, head_tree, cache_root, ignore_submodules)
4041 }
4042 CacheTreeWalk::Flatten => {
4043 diff_index_to_tree_flatten(odb, index, tree_oid, ignore_submodules)
4044 }
4045 }
4046}
4047
4048pub(crate) fn diff_index_to_tree_with_renames(
4057 odb: &Odb,
4058 index: &Index,
4059 tree_oid: Option<&ObjectId>,
4060 ignore_submodules: bool,
4061 rename_threshold: u32,
4062 config: &ConfigSet,
4063 thread_policy: &ThreadPolicy,
4064) -> Result<Vec<DiffEntry>> {
4065 let entries = diff_index_to_tree(odb, index, tree_oid, ignore_submodules)?;
4066 apply_rename_detection_to_diff_entries(
4067 odb,
4068 entries,
4069 None,
4070 rename_threshold,
4071 config,
4072 thread_policy,
4073 )
4074}
4075
4076pub(crate) fn apply_rename_detection_to_diff_entries(
4085 odb: &Odb,
4086 entries: Vec<DiffEntry>,
4087 work_root: Option<&Path>,
4088 rename_threshold: u32,
4089 config: &ConfigSet,
4090 thread_policy: &ThreadPolicy,
4091) -> Result<Vec<DiffEntry>> {
4092 if entries.is_empty() || rename_threshold >= 100 {
4093 return Ok(entries);
4094 }
4095 if work_root.is_some() {
4096 let opts = DiffPostprocessOptions {
4097 rename_threshold,
4098 ..DiffPostprocessOptions::default()
4099 };
4100 postprocess_diff_entries(
4101 odb,
4102 entries,
4103 None,
4104 work_root,
4105 None,
4106 config,
4107 thread_policy,
4108 &opts,
4109 None,
4110 )
4111 } else {
4112 detect_renames_with_policy(
4113 odb,
4114 None,
4115 entries,
4116 rename_threshold,
4117 config,
4118 thread_policy,
4119 None,
4120 DetectRenamesOptions::default(),
4121 )
4122 .map(|r| r.entries)
4123 }
4124}
4125
4126pub fn diff_index_to_worktree_with_renames(
4136 repo: &Repository,
4137 index: &mut Index,
4138 work_tree: &Path,
4139 options: DiffIndexToWorktreeOptions,
4140 rename_threshold: u32,
4141) -> Result<(Vec<DiffEntry>, bool)> {
4142 let config = repo.config()?.as_ref().clone();
4143 let thread_policy = repo.thread_policy();
4144 let odb = repo.odb();
4145 let (entries, changed) = diff_index_to_worktree_with_options(odb, index, work_tree, options)?;
4146 let entries = apply_rename_detection_to_diff_entries(
4147 odb,
4148 entries,
4149 Some(work_tree),
4150 rename_threshold,
4151 &config,
4152 &thread_policy,
4153 )?;
4154 Ok((entries, changed))
4155}
4156
4157enum CacheTreeWalk<'a> {
4158 Fast {
4159 head_tree: &'a ObjectId,
4160 cache_root: &'a CacheTreeNode,
4161 },
4162 Flatten,
4163}
4164
4165fn index_cache_tree_walk<'a>(
4168 index: &'a Index,
4169 tree_oid: Option<&'a ObjectId>,
4170) -> CacheTreeWalk<'a> {
4171 let Some(head_tree) = tree_oid else {
4172 return CacheTreeWalk::Flatten;
4173 };
4174 let Some(cache_root) = index.cache_tree.as_ref() else {
4175 return CacheTreeWalk::Flatten;
4176 };
4177 if index.entries.iter().any(|e| e.stage() != 0) {
4178 return CacheTreeWalk::Flatten;
4179 }
4180 CacheTreeWalk::Fast {
4181 head_tree,
4182 cache_root,
4183 }
4184}
4185
4186fn diff_index_to_tree_flatten(
4188 odb: &Odb,
4189 index: &Index,
4190 tree_oid: Option<&ObjectId>,
4191 ignore_submodules: bool,
4192) -> Result<Vec<DiffEntry>> {
4193 let sparse_index = index.has_sparse_directory_placeholders();
4194 let tree_entries = match tree_oid {
4196 Some(oid) if sparse_index => flatten_tree_with_index(odb, oid, "", Some(index))?,
4197 Some(oid) => flatten_tree(odb, oid, "")?,
4198 None => Vec::new(),
4199 };
4200
4201 let mut tree_map: std::collections::BTreeMap<&str, &FlatEntry> =
4203 std::collections::BTreeMap::new();
4204 for entry in &tree_entries {
4205 tree_map.insert(&entry.path, entry);
4206 }
4207
4208 let mut result = Vec::new();
4209 let mut stage0_paths = std::collections::BTreeSet::new();
4210 let mut unmerged_modes: std::collections::BTreeMap<String, (u8, u32)> =
4211 std::collections::BTreeMap::new();
4212
4213 for ie in &index.entries {
4215 let path = String::from_utf8_lossy(&ie.path).to_string();
4216 if ie.stage() == 0 && ie.intent_to_add() {
4217 continue;
4220 }
4221 if ie.stage() == 0 && ie.is_sparse_directory_placeholder() {
4222 let dir = path.strip_suffix('/').unwrap_or(path.as_str());
4223 let index_flat = flatten_tree(odb, &ie.oid, dir)?;
4224 let head_flat = match tree_oid {
4225 None => Vec::new(),
4226 Some(head_root) => match subtree_oid_at_path(odb, head_root, dir)? {
4227 None => Vec::new(),
4228 Some(head_sub) if head_sub == ie.oid => {
4229 continue;
4230 }
4231 Some(head_sub) => flatten_tree(odb, &head_sub, dir)?,
4232 },
4233 };
4234 diff_flattened_subtrees(
4235 &head_flat,
4236 &index_flat,
4237 ignore_submodules,
4238 &mut result,
4239 &mut stage0_paths,
4240 &mut tree_map,
4241 );
4242 continue;
4243 }
4244 if ie.stage() != 0 {
4245 let rank = match ie.stage() {
4246 2 => 0u8,
4247 3 => 1u8,
4248 1 => 2u8,
4249 _ => 3u8,
4250 };
4251 match unmerged_modes.get(&path) {
4252 Some((existing_rank, _)) if *existing_rank <= rank => {}
4253 _ => {
4254 unmerged_modes.insert(path, (rank, ie.mode));
4255 }
4256 }
4257 continue;
4258 }
4259 if ignore_submodules && ie.mode == 0o160000 {
4260 let _ = tree_map.remove(path.as_str());
4261 stage0_paths.insert(path.clone());
4262 continue;
4263 }
4264 stage0_paths.insert(path.clone());
4265 match tree_map.remove(path.as_str()) {
4266 Some(te) => {
4267 if te.oid != ie.oid
4269 || crate::index::canon_mode(te.mode) != crate::index::canon_mode(ie.mode)
4270 {
4271 let tree_mode = crate::index::canon_mode(te.mode);
4272 let index_mode = crate::index::canon_mode(ie.mode);
4273 result.push(DiffEntry {
4274 status: DiffStatus::for_content_change(tree_mode, index_mode),
4275 old_path: Some(path.clone()),
4276 new_path: Some(path),
4277 old_mode: FileMode::new(tree_mode),
4278 new_mode: FileMode::new(index_mode),
4279 old_oid: te.oid,
4280 new_oid: ie.oid,
4281 score: None,
4282 });
4283 }
4284 }
4285 None => {
4286 result.push(DiffEntry {
4288 status: DiffStatus::Added,
4289 old_path: None,
4290 new_path: Some(path),
4291 old_mode: FileMode::ABSENT,
4292 new_mode: FileMode::new(ie.mode),
4293 old_oid: zero_oid(),
4294 new_oid: ie.oid,
4295 score: None,
4296 });
4297 }
4298 }
4299 }
4300
4301 diff_index_to_tree_finish_unmerged_and_deleted(
4302 ignore_submodules,
4303 &mut tree_map,
4304 &unmerged_modes,
4305 &stage0_paths,
4306 &mut result,
4307 );
4308
4309 sort_entries_by_path(&mut result);
4310 Ok(result)
4311}
4312
4313fn diff_index_to_tree_finish_unmerged_and_deleted(
4314 ignore_submodules: bool,
4315 tree_map: &mut std::collections::BTreeMap<&str, &FlatEntry>,
4316 unmerged_modes: &std::collections::BTreeMap<String, (u8, u32)>,
4317 stage0_paths: &std::collections::BTreeSet<String>,
4318 result: &mut Vec<DiffEntry>,
4319) {
4320 for (path, (_, mode)) in unmerged_modes {
4321 if stage0_paths.contains(path) {
4322 continue;
4323 }
4324 tree_map.remove(path.as_str());
4325 result.push(DiffEntry {
4326 status: DiffStatus::Unmerged,
4327 old_path: Some(path.clone()),
4328 new_path: Some(path.clone()),
4329 old_mode: FileMode::ABSENT,
4330 new_mode: FileMode::new(*mode),
4331 old_oid: zero_oid(),
4332 new_oid: zero_oid(),
4333 score: None,
4334 });
4335 }
4336
4337 for (path, te) in std::mem::take(tree_map) {
4339 if ignore_submodules && te.mode == 0o160000 {
4340 continue;
4341 }
4342 result.push(DiffEntry {
4343 status: DiffStatus::Deleted,
4344 old_path: Some(path.to_owned()),
4345 new_path: None,
4346 old_mode: FileMode::new(te.mode),
4347 new_mode: FileMode::ABSENT,
4348 old_oid: te.oid,
4349 new_oid: zero_oid(),
4350 score: None,
4351 });
4352 }
4353}
4354
4355fn diff_index_to_tree_with_cache_tree(
4358 odb: &Odb,
4359 index: &Index,
4360 head_tree: &ObjectId,
4361 cache_root: &CacheTreeNode,
4362 ignore_submodules: bool,
4363) -> Result<Vec<DiffEntry>> {
4364 let mut result = Vec::new();
4365 let mut idx_cursor = 0usize;
4366
4367 let mut walk = DiffIndexTreeWalk {
4368 odb,
4369 index,
4370 idx_cursor: &mut idx_cursor,
4371 ignore_submodules,
4372 result: &mut result,
4373 abort_to_flatten: false,
4374 };
4375 diff_index_tree_at(&mut walk, head_tree, Some(cache_root), b"")?;
4376 if walk.abort_to_flatten {
4377 return diff_index_to_tree_flatten(odb, index, Some(head_tree), ignore_submodules);
4378 }
4379
4380 while idx_cursor < index.entries.len() {
4382 let ie = &index.entries[idx_cursor];
4383 idx_cursor += 1;
4384 let path = String::from_utf8_lossy(&ie.path).to_string();
4385 if ie.stage() == 0 && ie.intent_to_add() {
4386 continue;
4387 }
4388 if ie.stage() != 0 {
4389 continue;
4390 }
4391 if ignore_submodules && ie.mode == 0o160000 {
4392 continue;
4393 }
4394 result.push(DiffEntry {
4395 status: DiffStatus::Added,
4396 old_path: None,
4397 new_path: Some(path),
4398 old_mode: FileMode::ABSENT,
4399 new_mode: FileMode::new(ie.mode),
4400 old_oid: zero_oid(),
4401 new_oid: ie.oid,
4402 score: None,
4403 });
4404 }
4405
4406 sort_entries_by_path(&mut result);
4407 Ok(result)
4408}
4409
4410enum IndexLevelKind<'a> {
4411 Blob {
4412 entry: &'a IndexEntry,
4413 name: &'a [u8],
4414 },
4415 Tree {
4416 name: &'a [u8],
4417 },
4418}
4419
4420fn index_relative_in_dir<'a>(path: &'a [u8], dir_prefix: &[u8]) -> Option<&'a [u8]> {
4423 if dir_prefix.is_empty() {
4424 return Some(path);
4425 }
4426 let rest = path.strip_prefix(dir_prefix)?;
4427 match rest.split_first() {
4428 None => Some(rest),
4429 Some((b'/', tail)) => Some(tail),
4430 Some(_) => None,
4431 }
4432}
4433
4434fn join_index_path(dir_prefix: &[u8], name: &[u8]) -> Vec<u8> {
4435 if dir_prefix.is_empty() {
4436 return name.to_vec();
4437 }
4438 let mut path = Vec::with_capacity(dir_prefix.len() + 1 + name.len());
4439 path.extend_from_slice(dir_prefix);
4440 path.push(b'/');
4441 path.extend_from_slice(name);
4442 path
4443}
4444
4445fn peek_index_level_entry<'a>(
4446 index: &'a Index,
4447 cursor: &mut usize,
4448 dir_prefix: &[u8],
4449 ignore_submodules: bool,
4450) -> Option<IndexLevelKind<'a>> {
4451 while let Some(ie) = index.entries.get(*cursor) {
4452 let rel = index_relative_in_dir(&ie.path, dir_prefix)?;
4453 if ie.stage() != 0 || ie.intent_to_add() || rel.is_empty() {
4454 *cursor += 1;
4455 continue;
4456 }
4457 if let Some(slash) = rel.iter().position(|&b| b == b'/') {
4458 return Some(IndexLevelKind::Tree {
4459 name: &rel[..slash],
4460 });
4461 }
4462 if ignore_submodules && ie.mode == 0o160000 {
4463 *cursor += 1;
4464 continue;
4465 }
4466 return Some(IndexLevelKind::Blob {
4467 entry: ie,
4468 name: rel,
4469 });
4470 }
4471 None
4472}
4473
4474fn skip_index_subtree(index: &Index, cursor: &mut usize, dir_prefix: &[u8]) {
4475 while index
4476 .entries
4477 .get(*cursor)
4478 .is_some_and(|ie| index_relative_in_dir(&ie.path, dir_prefix).is_some())
4479 {
4480 *cursor += 1;
4481 }
4482}
4483
4484const CACHE_CHILD_LINEAR_SCAN_MAX: usize = 16;
4486
4487struct CacheChildren<'a> {
4493 children: &'a [CacheTreeNode],
4494 sorted: Vec<&'a CacheTreeNode>,
4495}
4496
4497impl<'a> CacheChildren<'a> {
4498 fn new(node: Option<&'a CacheTreeNode>) -> Self {
4499 let children = node.map_or(&[][..], |n| n.children.as_slice());
4500 let sorted = if children.len() > CACHE_CHILD_LINEAR_SCAN_MAX {
4501 let mut sorted: Vec<&CacheTreeNode> = children.iter().collect();
4502 sorted.sort_by(|a, b| a.name.cmp(&b.name));
4503 sorted
4504 } else {
4505 Vec::new()
4506 };
4507 Self { children, sorted }
4508 }
4509
4510 fn get(&self, name: &[u8]) -> Option<&'a CacheTreeNode> {
4511 if self.sorted.is_empty() {
4512 return self.children.iter().find(|c| c.name == name);
4513 }
4514 let pos = self.sorted.partition_point(|c| c.name.as_slice() < name);
4515 self.sorted.get(pos).copied().filter(|c| c.name == name)
4516 }
4517}
4518
4519struct DiffIndexTreeWalk<'a, 'b> {
4520 odb: &'a Odb,
4521 index: &'a Index,
4522 idx_cursor: &'b mut usize,
4523 ignore_submodules: bool,
4524 result: &'b mut Vec<DiffEntry>,
4525 abort_to_flatten: bool,
4527}
4528
4529fn tree_entries_have_duplicate_names(entries: &[TreeEntry]) -> bool {
4530 let mut seen = std::collections::BTreeSet::<&[u8]>::new();
4531 for entry in entries {
4532 if !seen.insert(entry.name.as_slice()) {
4533 return true;
4534 }
4535 }
4536 false
4537}
4538
4539fn diff_index_tree_at(
4541 walk: &mut DiffIndexTreeWalk<'_, '_>,
4542 tree_oid: &ObjectId,
4543 cache_node: Option<&CacheTreeNode>,
4544 dir_prefix: &[u8],
4545) -> Result<()> {
4546 if !dir_prefix.is_empty() {
4547 let mut path = dir_prefix.to_vec();
4548 path.push(b'/');
4549 if let Some(entry) = walk
4550 .index
4551 .entries
4552 .get(*walk.idx_cursor)
4553 .filter(|entry| entry.path == path && entry.is_sparse_directory_placeholder())
4554 {
4555 if entry.oid == *tree_oid {
4556 *walk.idx_cursor += 1;
4557 return Ok(());
4558 }
4559 let dir = String::from_utf8_lossy(dir_prefix);
4560 let head_flat = flatten_tree(walk.odb, tree_oid, dir.as_ref())?;
4561 let index_flat = flatten_tree(walk.odb, &entry.oid, dir.as_ref())?;
4562 let mut stage0_paths = std::collections::BTreeSet::new();
4563 let mut tree_map = std::collections::BTreeMap::new();
4564 diff_flattened_subtrees(
4565 &head_flat,
4566 &index_flat,
4567 walk.ignore_submodules,
4568 walk.result,
4569 &mut stage0_paths,
4570 &mut tree_map,
4571 );
4572 *walk.idx_cursor += 1;
4573 return Ok(());
4574 }
4575 }
4576 let tree_entries = read_tree(walk.odb, tree_oid)?;
4577 if tree_entries_have_duplicate_names(&tree_entries) {
4578 walk.abort_to_flatten = true;
4579 return Ok(());
4580 }
4581 let cache_children = CacheChildren::new(cache_node);
4582 let mut ti = 0usize;
4583
4584 loop {
4585 let te = tree_entries.get(ti);
4586 let ie_kind = peek_index_level_entry(
4587 walk.index,
4588 walk.idx_cursor,
4589 dir_prefix,
4590 walk.ignore_submodules,
4591 );
4592
4593 match (te, ie_kind) {
4594 (Some(t_entry), Some(ie)) => {
4595 let t_name = &t_entry.name;
4596 let (i_name, i_is_tree) = match &ie {
4597 IndexLevelKind::Blob { name, .. } => (*name, false),
4598 IndexLevelKind::Tree { name } => (*name, true),
4599 };
4600 let t_is_tree = is_tree_mode(t_entry.mode);
4601 let cmp = crate::objects::tree_entry_cmp(t_name, t_is_tree, i_name, i_is_tree);
4602 match cmp {
4603 std::cmp::Ordering::Less => {
4604 emit_index_tree_deleted(
4605 walk.odb,
4606 t_entry,
4607 dir_prefix,
4608 walk.ignore_submodules,
4609 walk.result,
4610 )?;
4611 ti += 1;
4612 }
4613 std::cmp::Ordering::Greater => {
4614 emit_index_tree_added(walk, dir_prefix)?;
4615 }
4616 std::cmp::Ordering::Equal => {
4617 if t_is_tree && i_is_tree {
4618 let full_path = join_index_path(dir_prefix, t_name);
4619 if diff_index_tree_skip_sparse_placeholder(walk, &full_path, t_entry)? {
4620 ti += 1;
4621 continue;
4622 }
4623 let child_cache = cache_children.get(t_name);
4624 if child_cache
4625 .is_some_and(|c| c.is_valid() && c.oid == Some(t_entry.oid))
4626 {
4627 skip_index_subtree(walk.index, walk.idx_cursor, &full_path);
4628 ti += 1;
4629 continue;
4630 }
4631 diff_index_tree_at(walk, &t_entry.oid, child_cache, &full_path)?;
4632 } else if t_is_tree != i_is_tree {
4633 emit_index_tree_deleted(
4634 walk.odb,
4635 t_entry,
4636 dir_prefix,
4637 walk.ignore_submodules,
4638 walk.result,
4639 )?;
4640 emit_index_tree_added(walk, dir_prefix)?;
4641 } else {
4642 let IndexLevelKind::Blob { entry: ie, .. } = ie else {
4643 ti += 1;
4644 continue;
4645 };
4646 *walk.idx_cursor += 1;
4647 if t_entry.oid != ie.oid
4648 || crate::index::canon_mode(t_entry.mode)
4649 != crate::index::canon_mode(ie.mode)
4650 {
4651 let path = String::from_utf8_lossy(&ie.path).into_owned();
4652 let tree_mode = crate::index::canon_mode(t_entry.mode);
4653 let index_mode = crate::index::canon_mode(ie.mode);
4654 walk.result.push(DiffEntry {
4655 status: DiffStatus::for_content_change(tree_mode, index_mode),
4656 old_path: Some(path.clone()),
4657 new_path: Some(path),
4658 old_mode: FileMode::new(tree_mode),
4659 new_mode: FileMode::new(index_mode),
4660 old_oid: t_entry.oid,
4661 new_oid: ie.oid,
4662 score: None,
4663 });
4664 }
4665 }
4666 ti += 1;
4667 }
4668 }
4669 }
4670 (Some(t_entry), None) => {
4671 emit_index_tree_deleted(
4672 walk.odb,
4673 t_entry,
4674 dir_prefix,
4675 walk.ignore_submodules,
4676 walk.result,
4677 )?;
4678 ti += 1;
4679 }
4680 (None, Some(_)) => {
4681 emit_index_tree_added(walk, dir_prefix)?;
4682 }
4683 (None, None) => break,
4684 }
4685 }
4686
4687 Ok(())
4688}
4689
4690fn diff_index_tree_skip_sparse_placeholder(
4691 walk: &mut DiffIndexTreeWalk<'_, '_>,
4692 full_path: &[u8],
4693 t_entry: &TreeEntry,
4694) -> Result<bool> {
4695 let Some(entry) = walk.index.entries.get(*walk.idx_cursor) else {
4696 return Ok(false);
4697 };
4698 if !entry.is_sparse_directory_placeholder() {
4699 return Ok(false);
4700 }
4701 let mut dir = full_path.to_vec();
4702 if !dir.ends_with(b"/") {
4703 dir.push(b'/');
4704 }
4705 if entry.path != dir || entry.oid != t_entry.oid {
4706 return Ok(false);
4707 }
4708 *walk.idx_cursor += 1;
4709 Ok(true)
4710}
4711
4712fn emit_index_tree_deleted(
4713 odb: &Odb,
4714 entry: &TreeEntry,
4715 dir_prefix: &[u8],
4716 ignore_submodules: bool,
4717 result: &mut Vec<DiffEntry>,
4718) -> Result<()> {
4719 if ignore_submodules && entry.mode == 0o160000 {
4720 return Ok(());
4721 }
4722 let path = String::from_utf8_lossy(&join_index_path(dir_prefix, &entry.name)).into_owned();
4723 if is_tree_mode(entry.mode) {
4724 for fe in flatten_tree(odb, &entry.oid, &path)? {
4725 if ignore_submodules && fe.mode == 0o160000 {
4726 continue;
4727 }
4728 result.push(DiffEntry {
4729 status: DiffStatus::Deleted,
4730 old_path: Some(fe.path.clone()),
4731 new_path: None,
4732 old_mode: FileMode::new(fe.mode),
4733 new_mode: FileMode::ABSENT,
4734 old_oid: fe.oid,
4735 new_oid: zero_oid(),
4736 score: None,
4737 });
4738 }
4739 } else {
4740 result.push(DiffEntry {
4741 status: DiffStatus::Deleted,
4742 old_path: Some(path),
4743 new_path: None,
4744 old_mode: FileMode::new(entry.mode),
4745 new_mode: FileMode::ABSENT,
4746 old_oid: entry.oid,
4747 new_oid: zero_oid(),
4748 score: None,
4749 });
4750 }
4751 Ok(())
4752}
4753
4754fn emit_index_tree_added(walk: &mut DiffIndexTreeWalk<'_, '_>, dir_prefix: &[u8]) -> Result<()> {
4755 let ie_kind = peek_index_level_entry(
4756 walk.index,
4757 walk.idx_cursor,
4758 dir_prefix,
4759 walk.ignore_submodules,
4760 )
4761 .ok_or_else(|| Error::IndexError("missing index entry during tree walk".to_owned()))?;
4762 match ie_kind {
4763 IndexLevelKind::Blob { entry: ie, .. } => {
4764 let path = String::from_utf8_lossy(&ie.path).into_owned();
4765 *walk.idx_cursor += 1;
4766 walk.result.push(DiffEntry {
4767 status: DiffStatus::Added,
4768 old_path: None,
4769 new_path: Some(path),
4770 old_mode: FileMode::ABSENT,
4771 new_mode: FileMode::new(ie.mode),
4772 old_oid: zero_oid(),
4773 new_oid: ie.oid,
4774 score: None,
4775 });
4776 }
4777 IndexLevelKind::Tree { name } => {
4778 let full_path = join_index_path(dir_prefix, name);
4779 let mut path_slash = full_path.clone();
4780 path_slash.push(b'/');
4781 if let Some(entry) =
4782 walk.index.entries.get(*walk.idx_cursor).filter(|entry| {
4783 entry.path == path_slash && entry.is_sparse_directory_placeholder()
4784 })
4785 {
4786 let dir = String::from_utf8_lossy(if dir_prefix.is_empty() {
4787 name
4788 } else {
4789 &full_path
4790 });
4791 emit_flattened_subtree_as_added(
4792 walk.odb,
4793 &entry.oid,
4794 dir.as_ref(),
4795 walk.ignore_submodules,
4796 walk.result,
4797 )?;
4798 *walk.idx_cursor += 1;
4799 return Ok(());
4800 }
4801 let start = *walk.idx_cursor;
4802 skip_index_subtree(walk.index, walk.idx_cursor, &full_path);
4803 for ie in &walk.index.entries[start..*walk.idx_cursor] {
4804 if ie.stage() != 0 || ie.intent_to_add() {
4805 continue;
4806 }
4807 if walk.ignore_submodules && ie.mode == 0o160000 {
4808 continue;
4809 }
4810 let path = String::from_utf8_lossy(&ie.path).into_owned();
4811 walk.result.push(DiffEntry {
4812 status: DiffStatus::Added,
4813 old_path: None,
4814 new_path: Some(path),
4815 old_mode: FileMode::ABSENT,
4816 new_mode: FileMode::new(ie.mode),
4817 old_oid: zero_oid(),
4818 new_oid: ie.oid,
4819 score: None,
4820 });
4821 }
4822 }
4823 }
4824 Ok(())
4825}
4826
4827#[derive(Default)]
4829pub(crate) struct HardResetIndexPlan {
4830 pub remove_paths: Vec<Vec<u8>>,
4831 pub updates: Vec<(Vec<u8>, u32, ObjectId)>,
4832 pub insert_entries: Vec<IndexEntry>,
4833}
4834
4835pub(crate) fn plan_hard_reset_index_align(
4840 odb: &Odb,
4841 index: &Index,
4842 target_tree: &ObjectId,
4843 sparse: Option<&SparseCheckout>,
4844 index_sparse: bool,
4845) -> Result<HardResetIndexPlan> {
4846 let mut plan = HardResetIndexPlan::default();
4847 let mut idx_cursor = 0usize;
4848 let cache_root = index.cache_tree.as_ref();
4849 let use_cache = cache_root.is_some()
4850 && index
4851 .entries
4852 .iter()
4853 .all(|entry| entry.stage() == 0 && !entry.intent_to_add());
4854 let cache_node = if use_cache { cache_root } else { None };
4855 let retry = {
4856 let mut walk = HardResetAlignWalk {
4857 odb,
4858 index,
4859 idx_cursor: &mut idx_cursor,
4860 ignore_submodules: false,
4861 sparse,
4862 index_sparse,
4863 plan: &mut plan,
4864 retry_without_cache: false,
4865 };
4866 hard_reset_align_index_tree_at(&mut walk, target_tree, cache_node, b"")?;
4867 walk.retry_without_cache
4868 };
4869 if retry {
4870 idx_cursor = 0;
4871 plan = HardResetIndexPlan::default();
4872 let mut walk = HardResetAlignWalk {
4873 odb,
4874 index,
4875 idx_cursor: &mut idx_cursor,
4876 ignore_submodules: false,
4877 sparse,
4878 index_sparse,
4879 plan: &mut plan,
4880 retry_without_cache: false,
4881 };
4882 hard_reset_align_index_tree_at(&mut walk, target_tree, None, b"")?;
4883 }
4884 while idx_cursor < index.entries.len() {
4885 let ie = &index.entries[idx_cursor];
4886 idx_cursor += 1;
4887 if ie.stage() != 0 || ie.intent_to_add() {
4888 continue;
4889 }
4890 plan.remove_paths.push(ie.path.clone());
4891 }
4892 Ok(plan)
4893}
4894
4895pub(crate) fn apply_hard_reset_index_plan(index: &mut Index, plan: HardResetIndexPlan) {
4897 let change_count = plan
4898 .remove_paths
4899 .len()
4900 .saturating_add(plan.updates.len())
4901 .saturating_add(plan.insert_entries.len());
4902 const BULK_HARD_RESET_INDEX_MIN: usize = 64;
4903 let bulk = change_count >= BULK_HARD_RESET_INDEX_MIN;
4904 if bulk {
4905 index.drop_derived_index_extensions();
4906 }
4907 for (path, mode, oid) in plan.updates {
4908 if let Some(pos) = index.find_entry_pos(&path, 0) {
4909 let entry = &mut index.entries[pos];
4910 let content_unchanged = entry.oid == oid && entry.mode == mode;
4911 entry.mode = mode;
4912 entry.oid = oid;
4913 if !content_unchanged {
4914 entry.size = 0;
4915 }
4916 if !bulk {
4917 index.invalidate_cache_tree_for_path(&path);
4918 }
4919 }
4920 }
4921 index.defer_untracked_cache_invalidation = bulk;
4922 index.remove_prefixes_paths_and_insert(
4923 &std::collections::BTreeSet::new(),
4924 plan.remove_paths.iter().map(|p| p.as_slice()),
4925 plan.insert_entries,
4926 );
4927 index.defer_untracked_cache_invalidation = false;
4928 if !bulk {
4929 for path in &plan.remove_paths {
4930 index.invalidate_cache_tree_for_path(path);
4931 }
4932 }
4933 index.sort();
4934}
4935
4936pub(crate) fn rebuild_hard_reset_cache_tree(
4939 odb: &Odb,
4940 index: &mut Index,
4941 target_tree: &ObjectId,
4942) -> Result<()> {
4943 let stage0_count = index
4944 .entries
4945 .iter()
4946 .filter(|entry| entry.stage() == 0 && !entry.intent_to_add())
4947 .count();
4948 let seed_root = index.cache_tree.as_ref();
4949 let mut idx_cursor = 0usize;
4950 let root = rebuild_hard_reset_cache_tree_at(
4951 odb,
4952 index,
4953 &mut idx_cursor,
4954 target_tree,
4955 seed_root,
4956 b"",
4957 Vec::new(),
4958 )?;
4959 if usize::try_from(root.entry_count).ok() == Some(stage0_count) {
4960 index.set_cache_tree(root);
4961 Ok(())
4962 } else {
4963 Err(Error::IndexError(
4964 "hard reset cache-tree entry count does not match index".into(),
4965 ))
4966 }
4967}
4968
4969fn rebuild_hard_reset_cache_tree_at(
4970 odb: &Odb,
4971 index: &Index,
4972 idx_cursor: &mut usize,
4973 tree_oid: &ObjectId,
4974 seed_node: Option<&CacheTreeNode>,
4975 dir_prefix: &[u8],
4976 name: Vec<u8>,
4977) -> Result<CacheTreeNode> {
4978 if !dir_prefix.is_empty() {
4979 let mut path = dir_prefix.to_vec();
4980 path.push(b'/');
4981 if let Some(entry) = index.entries.get(*idx_cursor).filter(|entry| {
4982 entry.path == path && entry.is_sparse_directory_placeholder() && entry.oid == *tree_oid
4983 }) {
4984 let _ = entry;
4985 *idx_cursor += 1;
4986 return Ok(CacheTreeNode::valid(name, 1, *tree_oid, Vec::new()));
4987 }
4988 }
4989 let tree_entries = read_tree(odb, tree_oid)?;
4990 let cache_children = CacheChildren::new(seed_node);
4991 let mut entry_count: i32 = 0;
4992 let mut children: Vec<CacheTreeNode> = Vec::new();
4993 let mut ti = 0usize;
4994
4995 loop {
4996 let te = tree_entries.get(ti);
4997 let ie_kind = peek_index_level_entry(index, idx_cursor, dir_prefix, false);
4998 match (te, ie_kind) {
4999 (Some(t_entry), Some(ie)) => {
5000 let t_name = &t_entry.name;
5001 let (i_name, i_is_tree) = match &ie {
5002 IndexLevelKind::Blob { name, .. } => (*name, false),
5003 IndexLevelKind::Tree { name } => (*name, true),
5004 };
5005 let t_is_tree = is_tree_mode(t_entry.mode);
5006 let cmp = crate::objects::tree_entry_cmp(t_name, t_is_tree, i_name, i_is_tree);
5007 if cmp != std::cmp::Ordering::Equal {
5008 return Err(Error::IndexError(
5009 "hard reset cache-tree rebuild: index out of sync with target tree".into(),
5010 ));
5011 }
5012 if t_is_tree && i_is_tree {
5013 let full_path = join_index_path(dir_prefix, t_name);
5014 if hard_reset_skip_sparse_cache_rebuild(index, idx_cursor, &full_path, t_entry)?
5015 {
5016 entry_count = entry_count.saturating_add(1);
5017 ti += 1;
5018 continue;
5019 }
5020 let child = if let Some(child_cache) = cache_children
5021 .get(t_name)
5022 .filter(|node| node.is_valid() && node.oid == Some(t_entry.oid))
5023 {
5024 skip_index_subtree(index, idx_cursor, &full_path);
5025 let mut reused = (*child_cache).clone();
5026 reused.name = t_name.clone();
5027 reused
5028 } else {
5029 rebuild_hard_reset_cache_tree_at(
5030 odb,
5031 index,
5032 idx_cursor,
5033 &t_entry.oid,
5034 cache_children.get(t_name),
5035 &full_path,
5036 t_name.clone(),
5037 )?
5038 };
5039 entry_count = entry_count.saturating_add(child.entry_count.max(0));
5040 children.push(child);
5041 } else if t_is_tree != i_is_tree {
5042 return Err(Error::IndexError(
5043 "hard reset cache-tree rebuild: tree/index type mismatch".into(),
5044 ));
5045 } else {
5046 *idx_cursor += 1;
5047 entry_count = entry_count.saturating_add(1);
5048 }
5049 ti += 1;
5050 }
5051 (None, None) => break,
5052 _ => {
5053 return Err(Error::IndexError(
5054 "hard reset cache-tree rebuild: index out of sync with target tree".into(),
5055 ));
5056 }
5057 }
5058 }
5059 children.sort_by(|a, b| a.name.cmp(&b.name));
5060 Ok(CacheTreeNode::valid(name, entry_count, *tree_oid, children))
5061}
5062
5063fn hard_reset_skip_sparse_cache_rebuild(
5064 index: &Index,
5065 idx_cursor: &mut usize,
5066 full_path: &[u8],
5067 t_entry: &TreeEntry,
5068) -> Result<bool> {
5069 let Some(entry) = index.entries.get(*idx_cursor) else {
5070 return Ok(false);
5071 };
5072 if !entry.is_sparse_directory_placeholder() {
5073 return Ok(false);
5074 }
5075 let mut dir = full_path.to_vec();
5076 if !dir.ends_with(b"/") {
5077 dir.push(b'/');
5078 }
5079 if entry.path != dir || entry.oid != t_entry.oid {
5080 return Ok(false);
5081 }
5082 *idx_cursor += 1;
5083 Ok(true)
5084}
5085
5086struct HardResetAlignWalk<'a, 'b> {
5087 odb: &'a Odb,
5088 index: &'a Index,
5089 idx_cursor: &'b mut usize,
5090 ignore_submodules: bool,
5091 sparse: Option<&'a SparseCheckout>,
5092 index_sparse: bool,
5093 plan: &'b mut HardResetIndexPlan,
5094 retry_without_cache: bool,
5095}
5096
5097fn hard_reset_align_index_tree_at(
5098 walk: &mut HardResetAlignWalk<'_, '_>,
5099 tree_oid: &ObjectId,
5100 cache_node: Option<&CacheTreeNode>,
5101 dir_prefix: &[u8],
5102) -> Result<()> {
5103 let tree_entries = read_tree(walk.odb, tree_oid)?;
5104 if tree_entries_have_duplicate_names(&tree_entries) {
5105 walk.retry_without_cache = true;
5106 return Ok(());
5107 }
5108 let cache_children = CacheChildren::new(cache_node);
5109 let mut ti = 0usize;
5110
5111 loop {
5112 let te = tree_entries.get(ti);
5113 let ie_kind = peek_index_level_entry(
5114 walk.index,
5115 walk.idx_cursor,
5116 dir_prefix,
5117 walk.ignore_submodules,
5118 );
5119
5120 match (te, ie_kind) {
5121 (Some(t_entry), Some(ie)) => {
5122 let t_name = &t_entry.name;
5123 let (i_name, i_is_tree) = match &ie {
5124 IndexLevelKind::Blob { name, .. } => (*name, false),
5125 IndexLevelKind::Tree { name } => (*name, true),
5126 };
5127 let t_is_tree = is_tree_mode(t_entry.mode);
5128 let cmp = crate::objects::tree_entry_cmp(t_name, t_is_tree, i_name, i_is_tree);
5129 match cmp {
5130 std::cmp::Ordering::Less => {
5131 hard_reset_plan_tree_only(walk, t_entry, dir_prefix)?;
5132 ti += 1;
5133 }
5134 std::cmp::Ordering::Greater => {
5135 hard_reset_plan_index_only(walk, dir_prefix)?;
5136 }
5137 std::cmp::Ordering::Equal => {
5138 if t_is_tree && i_is_tree {
5139 let full_path = join_index_path(dir_prefix, t_name);
5140 if hard_reset_skip_sparse_placeholder(walk, &full_path, t_entry)? {
5141 ti += 1;
5142 continue;
5143 }
5144 let child_cache = cache_children.get(t_name);
5145 if child_cache
5146 .is_some_and(|c| c.is_valid() && c.oid == Some(t_entry.oid))
5147 {
5148 skip_index_subtree(walk.index, walk.idx_cursor, &full_path);
5149 ti += 1;
5150 continue;
5151 }
5152 hard_reset_align_index_tree_at(
5153 walk,
5154 &t_entry.oid,
5155 child_cache,
5156 &full_path,
5157 )?;
5158 } else if t_is_tree != i_is_tree {
5159 hard_reset_plan_tree_only(walk, t_entry, dir_prefix)?;
5160 hard_reset_plan_index_only(walk, dir_prefix)?;
5161 } else {
5162 let IndexLevelKind::Blob { entry: ie, .. } = ie else {
5163 ti += 1;
5164 continue;
5165 };
5166 *walk.idx_cursor += 1;
5167 if t_entry.oid != ie.oid
5168 || crate::index::canon_mode(t_entry.mode)
5169 != crate::index::canon_mode(ie.mode)
5170 {
5171 walk.plan.updates.push((
5172 ie.path.clone(),
5173 crate::index::canon_mode(t_entry.mode),
5174 t_entry.oid,
5175 ));
5176 }
5177 }
5178 ti += 1;
5179 }
5180 }
5181 }
5182 (Some(t_entry), None) => {
5183 hard_reset_plan_tree_only(walk, t_entry, dir_prefix)?;
5184 ti += 1;
5185 }
5186 (None, Some(_)) => {
5187 hard_reset_plan_index_only(walk, dir_prefix)?;
5188 }
5189 (None, None) => break,
5190 }
5191 }
5192 Ok(())
5193}
5194
5195fn hard_reset_skip_sparse_placeholder(
5196 walk: &mut HardResetAlignWalk<'_, '_>,
5197 full_path: &[u8],
5198 t_entry: &TreeEntry,
5199) -> Result<bool> {
5200 let Some(ie) = walk.index.entries.get(*walk.idx_cursor) else {
5201 return Ok(false);
5202 };
5203 if !ie.is_sparse_directory_placeholder() {
5204 return Ok(false);
5205 }
5206 let mut dir = full_path.to_vec();
5207 if !dir.ends_with(b"/") {
5208 dir.push(b'/');
5209 }
5210 if ie.path != dir || ie.oid != t_entry.oid {
5211 return Ok(false);
5212 }
5213 *walk.idx_cursor += 1;
5214 Ok(true)
5215}
5216
5217fn hard_reset_plan_index_only(
5218 walk: &mut HardResetAlignWalk<'_, '_>,
5219 dir_prefix: &[u8],
5220) -> Result<()> {
5221 let ie_kind = peek_index_level_entry(
5222 walk.index,
5223 walk.idx_cursor,
5224 dir_prefix,
5225 walk.ignore_submodules,
5226 )
5227 .ok_or_else(|| Error::IndexError("missing index entry during hard reset align".into()))?;
5228 match ie_kind {
5229 IndexLevelKind::Blob { entry: ie, .. } => {
5230 walk.plan.remove_paths.push(ie.path.clone());
5231 *walk.idx_cursor += 1;
5232 }
5233 IndexLevelKind::Tree { name } => {
5234 let full_path = join_index_path(dir_prefix, name);
5235 let start = *walk.idx_cursor;
5236 skip_index_subtree(walk.index, walk.idx_cursor, &full_path);
5237 for ie in &walk.index.entries[start..*walk.idx_cursor] {
5238 if ie.stage() != 0 || ie.intent_to_add() {
5239 continue;
5240 }
5241 walk.plan.remove_paths.push(ie.path.clone());
5242 }
5243 }
5244 }
5245 Ok(())
5246}
5247
5248fn hard_reset_plan_tree_only(
5249 walk: &mut HardResetAlignWalk<'_, '_>,
5250 entry: &TreeEntry,
5251 dir_prefix: &[u8],
5252) -> Result<()> {
5253 if walk.ignore_submodules && entry.mode == 0o160000 {
5254 return Ok(());
5255 }
5256 let path = join_index_path(dir_prefix, &entry.name);
5257 if is_tree_mode(entry.mode) {
5258 if hard_reset_sparse_excludes_tree(walk, &path) {
5259 walk.plan
5260 .insert_entries
5261 .push(hard_reset_sparse_directory_row(path, entry.oid));
5262 } else {
5263 hard_reset_append_entries_from_tree(
5264 walk.odb,
5265 &entry.oid,
5266 &path,
5267 &mut walk.plan.insert_entries,
5268 )?;
5269 }
5270 } else {
5271 walk.plan
5272 .insert_entries
5273 .push(hard_reset_index_tree_row(path, entry.mode, entry.oid));
5274 }
5275 Ok(())
5276}
5277
5278fn hard_reset_sparse_excludes_tree(walk: &HardResetAlignWalk<'_, '_>, path: &[u8]) -> bool {
5279 if !walk.index_sparse {
5280 return false;
5281 }
5282 let Some(sparse) = walk.sparse else {
5283 return false;
5284 };
5285 let mut dir = path.to_vec();
5286 if !dir.ends_with(b"/") {
5287 dir.push(b'/');
5288 }
5289 sparse.excludes_directory(&dir)
5290}
5291
5292fn hard_reset_sparse_directory_row(mut path: Vec<u8>, oid: ObjectId) -> IndexEntry {
5293 if !path.ends_with(b"/") {
5294 path.push(b'/');
5295 }
5296 let mut row = hard_reset_index_tree_row(path, MODE_TREE, oid);
5297 row.set_skip_worktree(true);
5298 row
5299}
5300
5301fn hard_reset_index_tree_row(path: Vec<u8>, mode: u32, oid: ObjectId) -> IndexEntry {
5302 IndexEntry {
5303 ctime_sec: 0,
5304 ctime_nsec: 0,
5305 mtime_sec: 0,
5306 mtime_nsec: 0,
5307 dev: 0,
5308 ino: 0,
5309 mode: crate::index::canon_mode(mode),
5310 uid: 0,
5311 gid: 0,
5312 size: 0,
5313 oid,
5314 flags: path.len().min(0xFFF) as u16,
5315 flags_extended: None,
5316 path,
5317 base_index_pos: 0,
5318 }
5319}
5320
5321fn hard_reset_append_entries_from_tree(
5322 odb: &Odb,
5323 tree_oid: &ObjectId,
5324 prefix: &[u8],
5325 out: &mut Vec<IndexEntry>,
5326) -> Result<()> {
5327 for te in read_tree(odb, tree_oid)? {
5328 let mut path = prefix.to_vec();
5329 if !path.is_empty() {
5330 path.push(b'/');
5331 }
5332 path.extend_from_slice(&te.name);
5333 if is_tree_mode(te.mode) {
5334 hard_reset_append_entries_from_tree(odb, &te.oid, &path, out)?;
5335 } else {
5336 out.push(hard_reset_index_tree_row(path, te.mode, te.oid));
5337 }
5338 }
5339 Ok(())
5340}
5341
5342pub fn diff_index_to_worktree(
5367 odb: &Odb,
5368 index: &mut Index,
5369 work_tree: &Path,
5370 ignore_submodule_untracked: bool,
5371 simplify_gitlinks: bool,
5372) -> Result<Vec<DiffEntry>> {
5373 diff_index_to_worktree_with_options(
5374 odb,
5375 index,
5376 work_tree,
5377 DiffIndexToWorktreeOptions {
5378 ignore_submodule_untracked,
5379 simplify_gitlinks,
5380 ..DiffIndexToWorktreeOptions::default()
5381 },
5382 )
5383 .map(|(entries, _)| entries)
5384}
5385
5386pub(crate) type IndexPathSkipPredicate = std::sync::Arc<dyn Fn(&[u8]) -> bool + Send + Sync>;
5388
5389#[derive(Clone)]
5391pub struct DiffIndexToWorktreeOptions {
5392 pub index_mtime: Option<(u32, u32)>,
5398 pub ignore_submodule_untracked: bool,
5400 pub simplify_gitlinks: bool,
5402 pub error_on_broken_gitlinks: bool,
5405 pub repository_git_dir: Option<std::path::PathBuf>,
5409 pub refresh_index_stat_in_pass: bool,
5412 pub config: Option<std::sync::Arc<crate::config::ConfigSet>>,
5414 pub stat_parallel_threads: Option<usize>,
5418 pub thread_policy: Option<crate::parallel::ThreadPolicy>,
5420 pub worktree_rules:
5422 Option<std::sync::Arc<std::sync::Mutex<crate::worktree_rules::WorktreeRules>>>,
5423 pub for_status: bool,
5430 pub size_change_proves_modified: bool,
5438 pub refresh_fsmonitor: bool,
5440 pub command_runner: Option<std::sync::Arc<dyn crate::command_runner::CommandRunner>>,
5442 pub fsmonitor_query_boundary: Option<crate::fsmonitor::FsmonitorQueryBoundary>,
5444 pub restrict_to_paths: Option<std::sync::Arc<std::collections::BTreeSet<String>>>,
5451 pub skip_index_path: Option<IndexPathSkipPredicate>,
5453 pub hash_blobs_without_write: bool,
5455 pub cancel_watch: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
5458 pub diagnostics: Option<crate::diagnostics::DiagnosticsHandle>,
5461 pub skip_inaccessible_paths: bool,
5465}
5466
5467impl Default for DiffIndexToWorktreeOptions {
5468 fn default() -> Self {
5469 Self {
5470 index_mtime: None,
5471 ignore_submodule_untracked: false,
5472 simplify_gitlinks: false,
5473 error_on_broken_gitlinks: false,
5474 repository_git_dir: None,
5475 refresh_index_stat_in_pass: false,
5476 config: None,
5477 stat_parallel_threads: None,
5478 thread_policy: None,
5479 worktree_rules: None,
5480 for_status: false,
5481 size_change_proves_modified: false,
5482 refresh_fsmonitor: true,
5483 command_runner: None,
5484 fsmonitor_query_boundary: None,
5485 restrict_to_paths: None,
5486 skip_index_path: None,
5487 hash_blobs_without_write: false,
5488 cancel_watch: None,
5489 diagnostics: None,
5490 skip_inaccessible_paths: false,
5491 }
5492 }
5493}
5494
5495#[derive(Clone, Default)]
5497pub(crate) struct DiffTreeToWorktreeExtras {
5498 pub skip_inaccessible_paths: bool,
5500 pub diagnostics: Option<crate::diagnostics::DiagnosticsHandle>,
5502 pub restrict_to_paths: Option<std::sync::Arc<std::collections::BTreeSet<String>>>,
5504}
5505
5506#[inline]
5507fn worktree_diff_skips_inaccessible_paths(
5508 for_status: bool,
5509 skip_inaccessible_paths: bool,
5510 materialize_dirty_blobs: bool,
5511) -> bool {
5512 (for_status || skip_inaccessible_paths) && !materialize_dirty_blobs
5513}
5514
5515#[inline]
5516fn poll_diff_cancel_watch(
5517 watch: Option<&std::sync::Arc<std::sync::atomic::AtomicBool>>,
5518) -> crate::error::Result<()> {
5519 if let Some(flag) = watch {
5520 crate::progress::poll_watch(flag)?;
5521 }
5522 Ok(())
5523}
5524
5525#[inline]
5526fn poll_diff_cancel(
5527 options: &DiffIndexToWorktreeOptions,
5528 owner_poll: Option<&std::sync::Arc<crate::progress::StackCancelPoll>>,
5529) -> crate::error::Result<()> {
5530 if let Some(poll) = owner_poll {
5531 poll.poll_owner()?;
5532 } else {
5533 poll_diff_cancel_watch(options.cancel_watch.as_ref())?;
5534 }
5535 Ok(())
5536}
5537
5538struct StagingHashJob {
5543 entry_index: usize,
5544 worktree_mode: u32,
5546 file_attrs: Option<Box<crate::crlf::FileAttrs>>,
5548}
5549
5550impl StagingHashJob {
5551 fn new(entry_index: usize, worktree_mode: u32, file_attrs: crate::crlf::FileAttrs) -> Self {
5552 let file_attrs =
5553 (file_attrs != crate::crlf::FileAttrs::default()).then(|| Box::new(file_attrs));
5554 Self {
5555 entry_index,
5556 worktree_mode,
5557 file_attrs,
5558 }
5559 }
5560}
5561
5562struct StagingHashSource<'a> {
5564 index: &'a Index,
5565 work_tree: &'a Path,
5566 precompose_unicode: bool,
5567 ignorecase: bool,
5568}
5569
5570#[allow(clippy::too_many_arguments)]
5571fn parallel_process_blob_worktree_jobs(
5572 odb: &Odb,
5573 source: &StagingHashSource<'_>,
5574 jobs: &[StagingHashJob],
5575 thread_policy: &crate::parallel::ThreadPolicy,
5576 config: &crate::config::ConfigSet,
5577 materialize_dirty_blobs: bool,
5578 hash_blobs_without_write: bool,
5579 for_status: bool,
5580 skip_inaccessible_paths: bool,
5581 diagnostics: Option<&crate::diagnostics::DiagnosticsHandle>,
5582 conv: &crate::crlf::ConversionConfig,
5583 filter_process: Option<&crate::filter_process::FilterProcessState>,
5584 _icase_collisions: Option<&crate::path_icase::IcaseCheckoutCollisionMap>,
5585 result: &mut Vec<DiffEntry>,
5586) -> Result<()> {
5587 use crate::parallel::{try_par_map, ParallelHashError, ParallelOp};
5588
5589 if jobs.is_empty() {
5590 return Ok(());
5591 }
5592 let mut par_opts = thread_policy.resolve(ParallelOp::IndexRefresh, config);
5593 par_opts.min_items = 0;
5594 par_opts.min_total_bytes = None;
5595 let est_bytes = jobs.len().saturating_mul(8192);
5596 let store_dirty_blobs = (materialize_dirty_blobs && !hash_blobs_without_write)
5597 .then(|| odb.bulk_stage_write_options());
5598 let stream_from = if store_dirty_blobs.is_some() {
5599 odb.big_file_threshold()?
5600 } else {
5601 hash_stream_threshold(odb)?
5602 };
5603 let default_attrs = crate::crlf::FileAttrs::default();
5604
5605 let new_oids = try_par_map::<StagingHashJob, Option<ObjectId>, Error, _>(
5608 jobs,
5609 par_opts,
5610 est_bytes,
5611 |job| {
5612 let entry = &source.index.entries[job.entry_index];
5613 let rel_path = crate::repo_relpath::storage_string(&entry.path);
5614 let modified = |new_oid| Ok(Some(new_oid));
5615 let file_path = index_entry_worktree_abs_bytes(
5616 source.work_tree,
5617 &entry.path,
5618 source.precompose_unicode,
5619 source.ignorecase,
5620 );
5621 let content = match read_regular_worktree_blob(
5622 odb,
5623 &file_path,
5624 conv,
5625 job.file_attrs.as_deref().unwrap_or(&default_attrs),
5626 &rel_path,
5627 Some(entry),
5628 filter_process,
5629 stream_from,
5630 ) {
5631 Ok(content) => content,
5632 Err(err)
5633 if status_skips_inaccessible_worktree_error(
5634 &err,
5635 for_status,
5636 skip_inaccessible_paths,
5637 materialize_dirty_blobs,
5638 ) =>
5639 {
5640 warn_and_skip_status_inaccessible(diagnostics, rel_path.as_str(), &err);
5641 return Ok(None);
5642 }
5643 Err(err) if unreadable_worktree_blob_is_modified(&err, materialize_dirty_blobs) => {
5644 return modified(zero_oid());
5645 }
5646 Err(err) => return Err(err),
5647 };
5648 let eff_oid = content.object_id(odb, store_dirty_blobs)?;
5649 if eff_oid != entry.oid || job.worktree_mode != entry.mode {
5650 let new_oid = if eff_oid != entry.oid && !materialize_dirty_blobs {
5651 zero_oid()
5652 } else {
5653 eff_oid
5654 };
5655 modified(new_oid)
5656 } else {
5657 Ok(None)
5658 }
5659 },
5660 )
5661 .map_err(|e| match e {
5662 ParallelHashError::Task(err) => err,
5663 })?;
5664
5665 let mut changed: Vec<(usize, u32, ObjectId)> = jobs
5666 .iter()
5667 .zip(new_oids)
5668 .filter_map(|(job, new_oid)| new_oid.map(|oid| (job.entry_index, job.worktree_mode, oid)))
5669 .collect();
5670 changed.sort_by_key(|(idx, _, _)| *idx);
5671 result.extend(
5672 changed
5673 .into_iter()
5674 .map(|(entry_index, worktree_mode, new_oid)| {
5675 let entry = &source.index.entries[entry_index];
5676 let rel_path = crate::repo_relpath::storage_string(&entry.path);
5677 worktree_content_change_entry(entry, &rel_path, worktree_mode, new_oid)
5678 }),
5679 );
5680 Ok(())
5681}
5682
5683fn worktree_content_change_entry(
5685 ie: &IndexEntry,
5686 path: &str,
5687 worktree_mode: u32,
5688 new_oid: ObjectId,
5689) -> DiffEntry {
5690 DiffEntry {
5691 status: DiffStatus::for_content_change(ie.mode, worktree_mode),
5692 old_path: Some(path.to_owned()),
5693 new_path: Some(path.to_owned()),
5694 old_mode: FileMode::new(ie.mode),
5695 new_mode: FileMode::new(worktree_mode),
5696 old_oid: ie.oid,
5697 new_oid,
5698 score: None,
5699 }
5700}
5701
5702fn unreadable_worktree_blob_is_modified(err: &Error, materialize_dirty_blobs: bool) -> bool {
5709 !materialize_dirty_blobs
5710 && matches!(
5711 err,
5712 Error::WorktreePathIo {
5713 operation: crate::worktree::WorktreePathOperation::Read,
5714 ..
5715 }
5716 )
5717}
5718
5719fn warn_worktree_access_failed(
5720 diagnostics: Option<&crate::diagnostics::DiagnosticsHandle>,
5721 path: &str,
5722 err: &std::io::Error,
5723) {
5724 if let Some(handle) = diagnostics {
5725 crate::diagnostics::warn(
5726 handle,
5727 crate::diagnostics::Warning::WorktreeAccessFailed {
5728 path: path.to_owned(),
5729 detail: err.to_string(),
5730 },
5731 );
5732 }
5733}
5734
5735fn blob_disk_lookup_io_result(
5736 diagnostics: Option<&crate::diagnostics::DiagnosticsHandle>,
5737 path: &str,
5738 err: std::io::Error,
5739 for_status: bool,
5740 skip_inaccessible_paths: bool,
5741 materialize_dirty_blobs: bool,
5742) -> Result<()> {
5743 if worktree_diff_skips_inaccessible_paths(
5744 for_status,
5745 skip_inaccessible_paths,
5746 materialize_dirty_blobs,
5747 ) && io_err_is_permission_denied(&err)
5748 {
5749 warn_worktree_access_failed(diagnostics, path, &err);
5750 return Ok(());
5751 }
5752 Err(Error::WorktreePathIo {
5753 operation: crate::worktree::WorktreePathOperation::Metadata,
5754 path: path.to_owned(),
5755 source: err,
5756 })
5757}
5758
5759fn io_err_is_permission_denied(err: &std::io::Error) -> bool {
5760 err.kind() == std::io::ErrorKind::PermissionDenied
5761}
5762
5763fn status_skips_inaccessible_worktree_error(
5764 err: &Error,
5765 for_status: bool,
5766 skip_inaccessible_paths: bool,
5767 materialize_dirty_blobs: bool,
5768) -> bool {
5769 if !worktree_diff_skips_inaccessible_paths(
5770 for_status,
5771 skip_inaccessible_paths,
5772 materialize_dirty_blobs,
5773 ) {
5774 return false;
5775 }
5776 match err {
5777 Error::Io(e) => io_err_is_permission_denied(e),
5778 Error::WorktreePathIo {
5779 operation: crate::worktree::WorktreePathOperation::Metadata,
5780 source,
5781 ..
5782 } => io_err_is_permission_denied(source),
5783 _ => false,
5784 }
5785}
5786
5787fn warn_and_skip_status_inaccessible(
5788 diagnostics: Option<&crate::diagnostics::DiagnosticsHandle>,
5789 path: &str,
5790 err: &Error,
5791) -> bool {
5792 let detail = match err {
5793 Error::Io(e) => e.to_string(),
5794 Error::WorktreePathIo { source, .. } => source.to_string(),
5795 _ => return false,
5796 };
5797 if let Some(handle) = diagnostics {
5798 crate::diagnostics::warn(
5799 handle,
5800 crate::diagnostics::Warning::WorktreeAccessFailed {
5801 path: path.to_owned(),
5802 detail,
5803 },
5804 );
5805 }
5806 true
5807}
5808
5809enum WorktreeSymlinkMetadata {
5810 Found(fs::Metadata),
5811 Absent,
5812 Inaccessible,
5813}
5814
5815fn probe_worktree_symlink_metadata(
5816 file_path: &Path,
5817 path_str: &str,
5818 diagnostics: Option<&crate::diagnostics::DiagnosticsHandle>,
5819 for_status: bool,
5820 skip_inaccessible_paths: bool,
5821 materialize_dirty_blobs: bool,
5822) -> Result<WorktreeSymlinkMetadata> {
5823 match fs::symlink_metadata(file_path) {
5824 Ok(meta) => Ok(WorktreeSymlinkMetadata::Found(meta)),
5825 Err(e)
5826 if e.kind() == std::io::ErrorKind::NotFound
5827 || e.raw_os_error() == Some(20) =>
5828 {
5829 Ok(WorktreeSymlinkMetadata::Absent)
5830 }
5831 Err(e) => {
5832 if worktree_diff_skips_inaccessible_paths(
5833 for_status,
5834 skip_inaccessible_paths,
5835 materialize_dirty_blobs,
5836 ) && io_err_is_permission_denied(&e)
5837 {
5838 warn_worktree_access_failed(diagnostics, path_str, &e);
5839 Ok(WorktreeSymlinkMetadata::Inaccessible)
5840 } else {
5841 Err(Error::WorktreePathIo {
5842 operation: crate::worktree::WorktreePathOperation::Metadata,
5843 path: path_str.to_owned(),
5844 source: e,
5845 })
5846 }
5847 }
5848 }
5849}
5850
5851fn symlink_metadata_worktree_diff(
5852 file_path: &Path,
5853 path_str: &str,
5854 diagnostics: Option<&crate::diagnostics::DiagnosticsHandle>,
5855 for_status: bool,
5856 skip_inaccessible_paths: bool,
5857 materialize_dirty_blobs: bool,
5858) -> Result<Option<fs::Metadata>> {
5859 Ok(
5860 match probe_worktree_symlink_metadata(
5861 file_path,
5862 path_str,
5863 diagnostics,
5864 for_status,
5865 skip_inaccessible_paths,
5866 materialize_dirty_blobs,
5867 )? {
5868 WorktreeSymlinkMetadata::Found(meta) => Some(meta),
5869 WorktreeSymlinkMetadata::Absent | WorktreeSymlinkMetadata::Inaccessible => None,
5870 },
5871 )
5872}
5873
5874fn status_size_change_proves_modified(
5883 ie: &IndexEntry,
5884 worktree_size: u32,
5885 for_status: bool,
5886 conversion_is_identity: impl FnOnce() -> bool,
5887) -> bool {
5888 for_status && !ie.stat_cache_unset() && worktree_size != ie.size && conversion_is_identity()
5889}
5890
5891#[allow(clippy::too_many_arguments)]
5894fn try_push_status_modified_on_size_mismatch(
5895 odb: &Odb,
5896 ie: &IndexEntry,
5897 path_str_ref: &str,
5898 worktree_size: u32,
5899 worktree_mode: u32,
5900 raw_worktree_len: u64,
5901 for_status: bool,
5902 conversion_is_identity: impl FnOnce() -> bool,
5903 result: &mut Vec<DiffEntry>,
5904) -> Result<bool> {
5905 let identity = conversion_is_identity();
5906 let size_differs = worktree_size != ie.size;
5907 if !size_differs || !identity {
5908 return Ok(false);
5909 }
5910 let threshold = odb.big_file_threshold()?;
5911 let big_file = raw_worktree_len > threshold;
5912 let proves_modified = if big_file {
5913 true
5914 } else {
5915 status_size_change_proves_modified(ie, worktree_size, for_status, || identity)
5916 };
5917 if !proves_modified {
5918 return Ok(false);
5919 }
5920 result.push(worktree_content_change_entry(
5921 ie,
5922 path_str_ref,
5923 worktree_mode,
5924 zero_oid(),
5925 ));
5926 Ok(true)
5927}
5928
5929#[allow(clippy::too_many_arguments)]
5930fn push_index_blob_worktree_diff(
5931 odb: &Odb,
5932 ie: &IndexEntry,
5933 path_str_ref: &str,
5934 file_path: &Path,
5935 meta: fs::Metadata,
5936 index_mtime: Option<(u32, u32)>,
5937 mode_policy: crate::index::WorktreeModePolicy,
5938 stat_policy: crate::index::StatMatchPolicy,
5939 file_attrs_for: &dyn Fn(&str) -> crate::crlf::FileAttrs,
5940 conv: &crate::crlf::ConversionConfig,
5941 materialize_dirty_blobs: bool,
5942 hash_blobs_without_write: bool,
5943 for_status: bool,
5944 size_change_proves_modified: bool,
5945 skip_inaccessible_paths: bool,
5946 diagnostics: Option<&crate::diagnostics::DiagnosticsHandle>,
5947 filter_process: Option<&crate::filter_process::FilterProcessState>,
5948 icase_collisions: Option<&crate::path_icase::IcaseCheckoutCollisionMap>,
5949 result: &mut Vec<DiffEntry>,
5950) -> Result<()> {
5951 if meta.is_dir() {
5952 if file_path.join(".git").exists() {
5953 let head = read_submodule_head_oid(file_path).unwrap_or_else(zero_oid);
5954 let path_owned = path_str_ref.to_owned();
5955 result.push(DiffEntry {
5956 status: DiffStatus::TypeChanged,
5957 old_path: Some(path_owned.clone()),
5958 new_path: Some(path_owned),
5959 old_mode: FileMode::new(ie.mode),
5960 new_mode: FileMode::new(0o160000),
5961 old_oid: ie.oid,
5962 new_oid: head,
5963 score: None,
5964 });
5965 return Ok(());
5966 }
5967 result.push(DiffEntry {
5968 status: DiffStatus::Deleted,
5969 old_path: Some(path_str_ref.to_owned()),
5970 new_path: None,
5971 old_mode: FileMode::new(ie.mode),
5972 new_mode: FileMode::ABSENT,
5973 old_oid: ie.oid,
5974 new_oid: zero_oid(),
5975 score: None,
5976 });
5977 return Ok(());
5978 }
5979
5980 let worktree_mode =
5981 mode_policy.index_mode_for_worktree(mode_from_metadata(&meta), Some(ie.mode));
5982 let stat_same = stat_matches_with_policy(ie, &meta, stat_policy);
5983 if stat_same && worktree_mode != ie.mode {
5984 result.push(worktree_content_change_entry(
5985 ie,
5986 path_str_ref,
5987 worktree_mode,
5988 ie.oid,
5989 ));
5990 return Ok(());
5991 }
5992
5993 if stat_same
5994 && index_entry_skips_worktree_content_hash_after_stat_match(ie, index_mtime, stat_policy)
5995 {
5996 return Ok(());
5997 }
5998
5999 let file_attrs = file_attrs_for(path_str_ref);
6000 if !icase_collisions.is_some_and(|map| map.diff_requires_worktree_hash(ie.path.as_slice()))
6001 && try_push_status_modified_on_size_mismatch(
6002 odb,
6003 ie,
6004 path_str_ref,
6005 crate::index::munge_index_size(meta.len()),
6006 worktree_mode,
6007 meta.len(),
6008 size_change_proves_modified,
6009 || crate::crlf::git_conversion_is_identity(conv, &file_attrs),
6010 result,
6011 )?
6012 {
6013 return Ok(());
6014 }
6015
6016 let store = (materialize_dirty_blobs && !hash_blobs_without_write)
6017 .then(|| odb.bulk_stage_write_options());
6018 let stream_from = if store.is_some() {
6019 odb.big_file_threshold()?
6020 } else {
6021 hash_stream_threshold(odb)?
6022 };
6023 let content = match worktree_blob_content_from(
6024 odb,
6025 file_path,
6026 &meta,
6027 conv,
6028 &file_attrs,
6029 path_str_ref,
6030 Some(ie),
6031 filter_process,
6032 stream_from,
6033 ) {
6034 Ok(content) => content,
6035 Err(err)
6036 if status_skips_inaccessible_worktree_error(
6037 &err,
6038 for_status,
6039 skip_inaccessible_paths,
6040 materialize_dirty_blobs,
6041 ) =>
6042 {
6043 warn_and_skip_status_inaccessible(diagnostics, path_str_ref, &err);
6044 return Ok(());
6045 }
6046 Err(err) if unreadable_worktree_blob_is_modified(&err, materialize_dirty_blobs) => {
6047 result.push(worktree_content_change_entry(
6048 ie,
6049 path_str_ref,
6050 worktree_mode,
6051 zero_oid(),
6052 ));
6053 return Ok(());
6054 }
6055 Err(err) => return Err(err),
6056 };
6057 let eff_oid = content.object_id(odb, store)?;
6058
6059 if eff_oid != ie.oid || worktree_mode != ie.mode {
6060 let new_oid = if eff_oid != ie.oid && !materialize_dirty_blobs {
6063 zero_oid()
6064 } else {
6065 eff_oid
6066 };
6067 result.push(worktree_content_change_entry(
6068 ie,
6069 path_str_ref,
6070 worktree_mode,
6071 new_oid,
6072 ));
6073 }
6074 Ok(())
6075}
6076
6077pub fn diff_index_to_worktree_with_options(
6087 odb: &Odb,
6088 index: &mut Index,
6089 work_tree: &Path,
6090 options: DiffIndexToWorktreeOptions,
6091) -> Result<(Vec<DiffEntry>, bool)> {
6092 let default_git_dir = work_tree.join(".git");
6093 let git_dir = options
6094 .repository_git_dir
6095 .clone()
6096 .unwrap_or(default_git_dir);
6097 diff_index_to_worktree_inner(odb, index, work_tree, &git_dir, options, false, None)
6098 .map(|(entries, changed, _)| (entries, changed))
6099}
6100
6101pub(crate) fn diff_index_to_worktree_with_owner_poll(
6104 odb: &Odb,
6105 index: &mut Index,
6106 work_tree: &Path,
6107 options: DiffIndexToWorktreeOptions,
6108 owner_poll: Option<std::sync::Arc<crate::progress::StackCancelPoll>>,
6109) -> Result<(Vec<DiffEntry>, bool)> {
6110 let default_git_dir = work_tree.join(".git");
6111 let git_dir = options
6112 .repository_git_dir
6113 .clone()
6114 .unwrap_or(default_git_dir);
6115 diff_index_to_worktree_inner(odb, index, work_tree, &git_dir, options, false, owner_poll)
6116 .map(|(entries, changed, _)| (entries, changed))
6117}
6118
6119pub fn diff_index_to_worktree_for_staging(
6125 odb: &Odb,
6126 repository_git_dir: &Path,
6127 index: &mut Index,
6128 work_tree: &Path,
6129 options: DiffIndexToWorktreeOptions,
6130) -> Result<(
6131 Vec<DiffEntry>,
6132 bool,
6133 std::collections::BTreeMap<String, crate::index::IndexStatFields>,
6134)> {
6135 diff_index_to_worktree_inner(
6136 odb,
6137 index,
6138 work_tree,
6139 repository_git_dir,
6140 options,
6141 true,
6142 None,
6143 )
6144}
6145
6146#[allow(clippy::too_many_arguments)]
6148fn worktree_diff_pass1_index_entry(
6149 odb: &Odb,
6150 work_tree: &Path,
6151 ie: &IndexEntry,
6152 path_str_ref: &str,
6153 config: &ConfigSet,
6154 simplify_gitlinks: bool,
6155 ignore_submodule_untracked: bool,
6156 error_on_broken_gitlinks: bool,
6157 materialize_dirty_blobs: bool,
6158 filter_fp: Option<&crate::filter_process::FilterProcessState>,
6159 conv: &crate::crlf::ConversionConfig,
6160 file_attrs_for: &dyn Fn(&str) -> crate::crlf::FileAttrs,
6161 precompose_unicode: bool,
6162 ignorecase: bool,
6163 result: &mut Vec<DiffEntry>,
6164) -> Result<()> {
6165 if ie.skip_worktree() || ie.assume_unchanged() {
6166 return Ok(());
6167 }
6168 let is_intent_to_add = ie.intent_to_add();
6169
6170 if ie.mode == 0o160000 {
6171 let sub_dir =
6172 index_entry_worktree_abs_bytes(work_tree, &ie.path, precompose_unicode, ignorecase);
6173 match fs::symlink_metadata(&sub_dir) {
6174 Err(e)
6175 if e.kind() == std::io::ErrorKind::NotFound
6176 || e.raw_os_error() == Some(20) =>
6177 {
6178 if !simplify_gitlinks {
6179 let path_owned = path_str_ref.to_owned();
6180 result.push(DiffEntry {
6181 status: DiffStatus::Deleted,
6182 old_path: Some(path_owned.clone()),
6183 new_path: Some(path_owned),
6184 old_mode: FileMode::new(ie.mode),
6185 new_mode: FileMode::ABSENT,
6186 old_oid: ie.oid,
6187 new_oid: zero_oid(),
6188 score: None,
6189 });
6190 }
6191 return Ok(());
6192 }
6193 Err(e) => return Err(Error::Io(e)),
6194 Ok(meta) if !meta.is_dir() => {
6195 let worktree_mode = mode_from_metadata(&meta);
6196 let file_attrs = file_attrs_for(path_str_ref);
6197 let worktree_oid = worktree_file_oid(
6198 odb,
6199 &sub_dir,
6200 &meta,
6201 conv,
6202 &file_attrs,
6203 path_str_ref,
6204 None,
6205 materialize_dirty_blobs,
6206 filter_fp,
6207 )?;
6208 let path_owned = path_str_ref.to_owned();
6209 result.push(DiffEntry {
6210 status: DiffStatus::TypeChanged,
6211 old_path: Some(path_owned.clone()),
6212 new_path: Some(path_owned),
6213 old_mode: FileMode::new(ie.mode),
6214 new_mode: FileMode::new(worktree_mode),
6215 old_oid: ie.oid,
6216 new_oid: worktree_oid,
6217 score: None,
6218 });
6219 return Ok(());
6220 }
6221 Ok(_) => {}
6222 }
6223 let sub_head_oid = read_submodule_head_oid(&sub_dir);
6224 let ref_matches = if let Some(oid) = sub_head_oid {
6225 oid == ie.oid
6226 } else {
6227 if error_on_broken_gitlinks && submodule_embedded_git_dir(&sub_dir).is_some() {
6228 return Err(Error::Submodule(SubmoduleError::HeadUnreadable {
6229 path: path_str_ref.to_owned(),
6230 }));
6231 }
6232 uninitialized_gitlink_worktree_matches_index(&sub_dir)
6233 };
6234 if simplify_gitlinks {
6235 if !ref_matches {
6236 let path_owned = path_str_ref.to_owned();
6237 let new_oid = sub_head_oid.unwrap_or_else(zero_oid);
6238 result.push(DiffEntry {
6239 status: DiffStatus::Modified,
6240 old_path: Some(path_owned.clone()),
6241 new_path: Some(path_owned),
6242 old_mode: FileMode::new(ie.mode),
6243 new_mode: FileMode::new(ie.mode),
6244 old_oid: ie.oid,
6245 new_oid,
6246 score: None,
6247 });
6248 }
6249 return Ok(());
6250 }
6251 if sub_head_oid.is_some() && submodule_head_object_broken(&sub_dir) {
6252 return Err(Error::Submodule(SubmoduleError::HeadCommitMissing {
6253 path: path_str_ref.to_owned(),
6254 }));
6255 }
6256 let mut flags = submodule_porcelain_flags(work_tree, path_str_ref, ie.oid);
6257 if ignore_submodule_untracked {
6258 flags.untracked = false;
6259 }
6260 let ignore =
6261 crate::submodule_ignore::submodule_ignore_for_path(config, work_tree, path_str_ref);
6262 if crate::submodule_ignore::gitlink_worktree_change_suppressed(ignore, ref_matches, flags) {
6263 return Ok(());
6264 }
6265 let inner_dirty = flags.modified || flags.untracked;
6266 if !ref_matches || inner_dirty {
6267 let path_owned = path_str_ref.to_owned();
6268 let new_oid = if !ref_matches {
6269 sub_head_oid.unwrap_or_else(zero_oid)
6270 } else {
6271 zero_oid()
6272 };
6273 result.push(DiffEntry {
6274 status: DiffStatus::Modified,
6275 old_path: Some(path_owned.clone()),
6276 new_path: Some(path_owned),
6277 old_mode: FileMode::new(ie.mode),
6278 new_mode: FileMode::new(ie.mode),
6279 old_oid: ie.oid,
6280 new_oid,
6281 score: None,
6282 });
6283 }
6284 return Ok(());
6285 }
6286
6287 let file_path =
6288 index_entry_worktree_abs_bytes(work_tree, &ie.path, precompose_unicode, ignorecase);
6289 let path_match = crate::repo_relpath::as_utf8_for_matching(path_str_ref);
6290
6291 if is_intent_to_add {
6292 match fs::symlink_metadata(&file_path) {
6293 Ok(meta) => {
6294 let file_attrs = file_attrs_for(path_match.as_ref());
6295 let worktree_oid = worktree_file_oid(
6296 odb,
6297 &file_path,
6298 &meta,
6299 conv,
6300 &file_attrs,
6301 path_match.as_ref(),
6302 None,
6303 materialize_dirty_blobs,
6304 filter_fp,
6305 )?;
6306 let worktree_mode = mode_from_metadata(&meta);
6307 result.push(DiffEntry {
6308 status: DiffStatus::Added,
6309 old_path: None,
6310 new_path: Some(path_str_ref.to_owned()),
6311 old_mode: FileMode::ABSENT,
6312 new_mode: FileMode::new(worktree_mode),
6313 old_oid: zero_oid(),
6314 new_oid: worktree_oid,
6315 score: None,
6316 });
6317 }
6318 Err(e)
6319 if e.kind() == std::io::ErrorKind::NotFound
6320 || e.raw_os_error() == Some(20) =>
6321 {
6322 result.push(DiffEntry {
6323 status: DiffStatus::Deleted,
6324 old_path: Some(path_str_ref.to_owned()),
6325 new_path: None,
6326 old_mode: FileMode::new(ie.mode),
6327 new_mode: FileMode::ABSENT,
6328 old_oid: ie.oid,
6329 new_oid: zero_oid(),
6330 score: None,
6331 });
6332 }
6333 Err(e) => return Err(Error::Io(e)),
6334 }
6335 }
6336 Ok(())
6337}
6338
6339#[inline]
6340fn index_entry_needs_worktree_diff_pass1(ie: &IndexEntry) -> bool {
6341 ie.mode == crate::index::MODE_GITLINK || ie.intent_to_add()
6342}
6343
6344fn diff_index_to_worktree_inner(
6345 odb: &Odb,
6346 index: &mut Index,
6347 work_tree: &Path,
6348 repository_git_dir: &Path,
6349 options: DiffIndexToWorktreeOptions,
6350 materialize_dirty_blobs: bool,
6351 owner_poll: Option<std::sync::Arc<crate::progress::StackCancelPoll>>,
6352) -> Result<(
6353 Vec<DiffEntry>,
6354 bool,
6355 std::collections::BTreeMap<String, crate::index::IndexStatFields>,
6356)> {
6357 use crate::config::ConfigSet;
6358 use crate::crlf;
6359 use std::collections::BTreeMap;
6360
6361 let mut staging_stat_cache = BTreeMap::new();
6362 poll_diff_cancel(&options, owner_poll.as_ref())?;
6363 let index_mtime = index_mtime_for_diff(index, options.index_mtime);
6364
6365 let ignore_submodule_untracked = options.ignore_submodule_untracked;
6366 let simplify_gitlinks = options.simplify_gitlinks;
6367 let size_change_proves_modified =
6369 !materialize_dirty_blobs && (options.for_status || options.size_change_proves_modified);
6370
6371 let rules_lock = options
6372 .worktree_rules
6373 .as_ref()
6374 .map(|shared| {
6375 shared
6376 .lock()
6377 .map_err(|e| Error::Message(format!("worktree rules lock poisoned: {e}")))
6378 })
6379 .transpose()?;
6380 let config = if let Some(rules) = rules_lock.as_ref() {
6381 rules.config().clone()
6382 } else if let Some(c) = options.config.as_ref() {
6383 c.as_ref().clone()
6384 } else {
6385 ConfigSet::load(
6386 &crate::environment::Environment::empty(),
6387 Some(repository_git_dir),
6388 true,
6389 )
6390 .unwrap_or_else(|_| ConfigSet::new())
6391 };
6392 let conv = rules_lock
6393 .as_ref()
6394 .map(|r| r.conversion().clone())
6395 .unwrap_or_else(|| crlf::ConversionConfig::from_config(&config));
6396 let legacy_attrs = if rules_lock.is_none() {
6397 Some(crlf::load_gitattributes_parsed(work_tree))
6398 } else {
6399 None
6400 };
6401 let file_attrs_for = |path: &str| -> crlf::FileAttrs {
6402 if let Some(rules) = rules_lock.as_ref() {
6403 rules.file_attrs(path, false)
6404 } else if let Some(parsed) = legacy_attrs.as_ref() {
6405 crlf::file_attrs_from_gitattributes(&parsed.rules, &parsed.macros, path, false, &config)
6406 } else {
6407 crlf::FileAttrs::default()
6408 }
6409 };
6410 let filter_fp = rules_lock.as_ref().map(|r| r.filter_process());
6411 let precompose_unicode = config.precompose_unicode();
6412 let mode_policy = crate::index::WorktreeModePolicy::from_config(&config);
6413 let stat_policy = crate::index::StatMatchPolicy::from_config(Some(&config));
6414 let ignorecase = config.ignore_case();
6415 let icase_collisions = ignorecase
6416 .then(|| crate::path_icase::IcaseCheckoutCollisionMap::from_index(work_tree, index, true));
6417 let hash_blobs_without_write = options.hash_blobs_without_write;
6418
6419 let mut result = Vec::new();
6420 let refresh_index_stat_in_pass = options.refresh_index_stat_in_pass;
6421 let mut index_stat_refresh_changed = false;
6422 let mut fsmonitor_valid_updated = false;
6423 let mut honor_fsmonitor_valid = false;
6424 if options.refresh_fsmonitor {
6425 let default_runner = crate::command_runner::system_command_runner();
6426 let runner = options
6427 .command_runner
6428 .as_deref()
6429 .unwrap_or(default_runner.as_ref());
6430 let boundary = options.fsmonitor_query_boundary.unwrap_or_else(|| {
6431 crate::fsmonitor::FsmonitorQueryBoundary(
6432 crate::environment::wall_clock_unix_timestamp_nanos(),
6433 )
6434 });
6435 let refresh_opts = crate::fsmonitor::FsmonitorRefreshOptions::with_query_boundary(boundary);
6436 let refresh = crate::fsmonitor::refresh_index(
6437 index,
6438 repository_git_dir,
6439 work_tree,
6440 &config,
6441 Some(runner),
6442 &refresh_opts,
6443 )?;
6444 if refresh.index_changed {
6445 index_stat_refresh_changed = true;
6446 }
6447 honor_fsmonitor_valid = refresh.honor_valid_bits;
6448 }
6449 let use_fsmonitor_stat_skip = honor_fsmonitor_valid;
6450 let mut unmerged_base: std::collections::BTreeMap<String, (u8, IndexEntry)> =
6451 std::collections::BTreeMap::new();
6452
6453 let error_on_broken_gitlinks = options.error_on_broken_gitlinks;
6454 if options.restrict_to_paths.is_none() {
6455 for ie in &index.entries {
6456 poll_diff_cancel(&options, owner_poll.as_ref())?;
6457 if ie.stage() != 0 {
6458 let path = String::from_utf8_lossy(&ie.path).to_string();
6459 let rank = match ie.stage() {
6460 2 => 0u8,
6461 3 => 1u8,
6462 1 => 2u8,
6463 _ => 3u8,
6464 };
6465 match unmerged_base.get(&path) {
6466 Some((existing_rank, _)) if *existing_rank <= rank => {}
6467 _ => {
6468 unmerged_base.insert(path, (rank, ie.clone()));
6469 }
6470 }
6471 }
6472 }
6473 }
6474
6475 if let Some(limit) = options.restrict_to_paths.as_ref() {
6476 let mut cursor = 0;
6477 for path in limit.iter() {
6478 let path_bytes = crate::repo_relpath::storage_bytes(path);
6479 let Some(pos) = index.find_stage0_pos_from(path_bytes.as_ref(), &mut cursor) else {
6480 continue;
6481 };
6482 let ie = &index.entries[pos];
6483 if options
6484 .skip_index_path
6485 .as_ref()
6486 .is_some_and(|skip| skip(&ie.path))
6487 {
6488 continue;
6489 }
6490 if !index_entry_needs_worktree_diff_pass1(ie) {
6491 continue;
6492 }
6493 worktree_diff_pass1_index_entry(
6494 odb,
6495 work_tree,
6496 ie,
6497 path.as_str(),
6498 &config,
6499 simplify_gitlinks,
6500 ignore_submodule_untracked,
6501 error_on_broken_gitlinks,
6502 materialize_dirty_blobs,
6503 filter_fp,
6504 &conv,
6505 &file_attrs_for,
6506 precompose_unicode,
6507 ignorecase,
6508 &mut result,
6509 )?;
6510 }
6511 } else {
6512 for ie in &index.entries {
6513 poll_diff_cancel(&options, owner_poll.as_ref())?;
6514 if ie.stage() != 0 {
6515 continue;
6516 }
6517 if options
6518 .skip_index_path
6519 .as_ref()
6520 .is_some_and(|skip| skip(&ie.path))
6521 {
6522 continue;
6523 }
6524 if !index_entry_needs_worktree_diff_pass1(ie) {
6525 continue;
6526 }
6527 let path_stored = crate::repo_relpath::storage_string(&ie.path);
6528 worktree_diff_pass1_index_entry(
6529 odb,
6530 work_tree,
6531 ie,
6532 &path_stored,
6533 &config,
6534 simplify_gitlinks,
6535 ignore_submodule_untracked,
6536 error_on_broken_gitlinks,
6537 materialize_dirty_blobs,
6538 filter_fp,
6539 &conv,
6540 &file_attrs_for,
6541 precompose_unicode,
6542 ignorecase,
6543 &mut result,
6544 )?;
6545 }
6546 }
6547
6548 use crate::worktree_scan::{
6549 collect_blob_lookup_batches_with_literal_paths, group_blob_entries_by_dir,
6550 stat_scan_thread_count, visit_blob_lookup_batches, BlobDiskLookup, BlobScanSparseMode,
6551 WorktreeBlobScanOptions, PARALLEL_STAT_MIN_ENTRIES,
6552 };
6553 let preload_index = config.preload_index();
6554 let blob_scan = WorktreeBlobScanOptions {
6555 preload_index,
6556 stat_parallel_threads: options.stat_parallel_threads,
6557 ..WorktreeBlobScanOptions::default()
6558 };
6559 let blob_dirs = if let Some(limit) = options.restrict_to_paths.as_ref() {
6560 if let Some(skip) = options.skip_index_path.as_ref() {
6561 let mut positions = Vec::new();
6562 let mut cursor = 0;
6563 for path in limit.iter() {
6564 let path_bytes = crate::repo_relpath::storage_bytes(path);
6565 let Some(pos) = index.find_stage0_pos_from(path_bytes.as_ref(), &mut cursor) else {
6566 continue;
6567 };
6568 let ie = &index.entries[pos];
6569 if skip(&ie.path) {
6570 continue;
6571 }
6572 if !crate::worktree_scan::index_blob_scan_eligible(ie, index, honor_fsmonitor_valid)
6573 {
6574 continue;
6575 }
6576 positions.push(pos);
6577 }
6578 crate::worktree_scan::plan_blob_entries_by_dir_for_positions(
6579 index,
6580 &positions,
6581 honor_fsmonitor_valid,
6582 )
6583 } else {
6584 crate::worktree_scan::group_blob_entries_by_dir_for_paths(index, limit)
6585 }
6586 } else if let Some(skip) = options.skip_index_path.as_ref() {
6587 let positions: Vec<usize> = index
6588 .entries
6589 .iter()
6590 .enumerate()
6591 .filter(|(_, ie)| {
6592 ie.stage() == 0
6593 && !skip(&ie.path)
6594 && crate::worktree_scan::index_blob_scan_eligible(
6595 ie,
6596 index,
6597 honor_fsmonitor_valid,
6598 )
6599 })
6600 .map(|(pos, _)| pos)
6601 .collect();
6602 crate::worktree_scan::plan_blob_entries_by_dir_for_positions(
6603 index,
6604 &positions,
6605 honor_fsmonitor_valid,
6606 )
6607 } else {
6608 group_blob_entries_by_dir(index, honor_fsmonitor_valid)
6609 };
6610 let work_tree_owned = work_tree.to_path_buf();
6611 let dir_abs = move |dir: &[u8]| {
6612 if dir.is_empty() {
6613 work_tree_owned.clone()
6614 } else {
6615 index_entry_worktree_abs_bytes(&work_tree_owned, dir, precompose_unicode, ignorecase)
6616 }
6617 };
6618 let work_tree_for_files = work_tree.to_path_buf();
6619 let file_abs = move |rel: &[u8]| {
6620 index_entry_worktree_abs_bytes(&work_tree_for_files, rel, precompose_unicode, ignorecase)
6621 };
6622 let blob_count = blob_dirs.len();
6623 let thread_policy = options.thread_policy.unwrap_or_default();
6624 let hash_threads = stat_scan_thread_count(&thread_policy, &config, blob_scan, blob_count);
6625 let parallel_blob_work = blob_count >= PARALLEL_STAT_MIN_ENTRIES && hash_threads > 1;
6626 let mut staging_hash_jobs: Vec<StagingHashJob> = Vec::new();
6627 let mut refresh_content_verified = vec![false; index.entries.len()];
6629 let sparse = if parallel_blob_work {
6634 BlobScanSparseMode::StatusDiff {
6635 stat_policy,
6636 index_mtime,
6637 mode_policy,
6638 for_status: options.for_status,
6639 refresh_index_stat_in_pass,
6640 parallel_blob_work,
6641 honor_fsmonitor_stat_skip: use_fsmonitor_stat_skip,
6642 }
6643 } else {
6644 BlobScanSparseMode::Dense
6645 };
6646 let blob_stat_batches = collect_blob_lookup_batches_with_literal_paths(
6647 index,
6648 &blob_dirs,
6649 blob_scan,
6650 &thread_policy,
6651 &config,
6652 dir_abs,
6653 file_abs,
6654 !precompose_unicode && !ignorecase,
6655 || {
6656 let mut dir_symlinks = SymlinkDirCache::default();
6657 move |rel_path: &[u8]| {
6658 dir_symlinks.has_symlink_in_path_bytes(
6659 work_tree,
6660 rel_path,
6661 precompose_unicode,
6662 ignorecase,
6663 )
6664 }
6665 },
6666 sparse,
6667 options.cancel_watch.clone(),
6668 owner_poll.clone(),
6669 )?;
6670 let identity_by_entry = std::cell::RefCell::new(std::collections::HashMap::new());
6673 let conversion_is_identity = |entry_index: usize, path: &str| -> bool {
6674 *identity_by_entry
6675 .borrow_mut()
6676 .entry(entry_index)
6677 .or_insert_with(|| crlf::git_conversion_is_identity(&conv, &file_attrs_for(path)))
6678 };
6679 let refresh_checks = if refresh_index_stat_in_pass {
6680 let par_opts =
6681 refresh_hash_parallel_options(&thread_policy, &config, options.stat_parallel_threads);
6682 refresh_content_checks(
6683 odb,
6684 index,
6685 &blob_stat_batches,
6686 index_mtime,
6687 stat_policy,
6688 &|entry_index, ie, stat| {
6689 !(use_fsmonitor_stat_skip && crate::fsmonitor::entry_skips_worktree_stat(ie, index))
6690 && worktree_diff_refreshes_entry(
6691 ie,
6692 stat,
6693 index_mtime,
6694 stat_policy,
6695 options.for_status,
6696 || {
6697 conversion_is_identity(
6698 entry_index,
6699 &crate::repo_relpath::storage_string(&ie.path),
6700 )
6701 },
6702 )
6703 },
6704 &WorktreeHashContext {
6705 work_tree,
6706 precompose_unicode,
6707 ignorecase,
6708 conv: &conv,
6709 file_attrs_for: &file_attrs_for,
6710 filter_process: filter_fp,
6711 },
6712 par_opts,
6713 options.cancel_watch.as_ref(),
6714 )?
6715 } else {
6716 RefreshChecks::default()
6717 };
6718 poll_diff_cancel(&options, owner_poll.as_ref())?;
6719 visit_blob_lookup_batches(blob_stat_batches, |entry_index, lookup| {
6720 poll_diff_cancel(&options, owner_poll.as_ref())?;
6721 let path_owned = crate::repo_relpath::storage_string(&index.entries[entry_index].path);
6722 let path_str_ref = path_owned.as_str();
6723 if use_fsmonitor_stat_skip {
6724 let ie_skip = &index.entries[entry_index];
6725 if crate::fsmonitor::entry_skips_worktree_stat(ie_skip, index) {
6726 return Ok(());
6727 }
6728 }
6729 match lookup {
6730 BlobDiskLookup::Missing => {
6731 if icase_collisions.as_ref().is_some_and(|map| {
6732 map.preserve_missing_collision_loser(&index.entries[entry_index].path)
6733 }) {
6734 return Ok(());
6735 }
6736 let ie = &index.entries[entry_index];
6737 result.push(DiffEntry {
6738 status: DiffStatus::Deleted,
6739 old_path: Some(path_str_ref.to_owned()),
6740 new_path: None,
6741 old_mode: FileMode::new(ie.mode),
6742 new_mode: FileMode::ABSENT,
6743 old_oid: ie.oid,
6744 new_oid: zero_oid(),
6745 score: None,
6746 });
6747 }
6748 BlobDiskLookup::Io(e) => {
6749 blob_disk_lookup_io_result(
6750 options.diagnostics.as_ref(),
6751 path_str_ref,
6752 e,
6753 options.for_status,
6754 options.skip_inaccessible_paths,
6755 materialize_dirty_blobs,
6756 )?;
6757 }
6758 BlobDiskLookup::Present(stat) => {
6759 if materialize_dirty_blobs {
6760 staging_stat_cache.insert(path_str_ref.to_owned(), stat.fields);
6761 }
6762 if refresh_index_stat_in_pass
6763 && worktree_diff_refreshes_entry(
6764 &index.entries[entry_index],
6765 &stat,
6766 index_mtime,
6767 stat_policy,
6768 options.for_status,
6769 || conversion_is_identity(entry_index, path_str_ref),
6770 )
6771 {
6772 let content_matches = RefreshContent::matches(
6773 refresh_checks.get(entry_index),
6774 &index.entries[entry_index].oid,
6775 );
6776 let refresh = refresh_index_blob_stat_from_scan(
6777 &mut index.entries[entry_index],
6778 &stat,
6779 index_mtime,
6780 stat_policy,
6781 content_matches,
6782 );
6783 if refresh.index_changed {
6784 index_stat_refresh_changed = true;
6785 }
6786 if refresh.content_verified {
6787 refresh_content_verified[entry_index] = true;
6788 let path = index.entries[entry_index].path.clone();
6789 let oid = index.entries[entry_index].oid;
6790 index.mark_content_verified(&path, oid);
6791 }
6792 }
6793 let ie = &index.entries[entry_index];
6794 let worktree_mode =
6795 mode_policy.index_mode_for_worktree(stat.git_mode, Some(ie.mode));
6796 if !materialize_dirty_blobs
6799 && !stat.is_dir
6800 && stat.git_mode != crate::index::MODE_SYMLINK
6801 && RefreshContent::differs(refresh_checks.get(entry_index), &ie.oid)
6802 {
6803 result.push(worktree_content_change_entry(
6804 ie,
6805 path_str_ref,
6806 worktree_mode,
6807 zero_oid(),
6808 ));
6809 return Ok(());
6810 }
6811 if parallel_blob_work && !stat.is_dir && stat.git_mode != crate::index::MODE_SYMLINK
6815 {
6816 let stat_same = stat_matches_blob_stat(ie, &stat, stat_policy);
6817 if stat_same
6818 && worktree_mode == ie.mode
6819 && index_entry_skips_worktree_content_hash_after_stat_match(
6820 ie,
6821 index_mtime,
6822 stat_policy,
6823 )
6824 {
6825 if crate::fsmonitor::mark_fsmonitor_valid_proven_clean(index, entry_index) {
6826 fsmonitor_valid_updated = true;
6827 }
6828 return Ok(());
6829 }
6830 if stat_same && worktree_mode != ie.mode {
6831 result.push(worktree_content_change_entry(
6832 ie,
6833 path_str_ref,
6834 worktree_mode,
6835 ie.oid,
6836 ));
6837 return Ok(());
6838 }
6839 if !icase_collisions.as_ref().is_some_and(|map| {
6840 map.diff_requires_worktree_hash(&index.entries[entry_index].path)
6841 }) && try_push_status_modified_on_size_mismatch(
6842 odb,
6843 ie,
6844 path_str_ref,
6845 stat.worktree_size(),
6846 worktree_mode,
6847 stat.raw_size,
6848 size_change_proves_modified,
6849 || conversion_is_identity(entry_index, path_str_ref),
6850 &mut result,
6851 )? {
6852 return Ok(());
6853 }
6854 if refresh_content_verified[entry_index] {
6855 if worktree_mode == ie.mode {
6856 if crate::fsmonitor::mark_fsmonitor_valid_proven_clean(
6857 index,
6858 entry_index,
6859 ) {
6860 fsmonitor_valid_updated = true;
6861 }
6862 } else {
6863 result.push(worktree_content_change_entry(
6864 ie,
6865 path_str_ref,
6866 worktree_mode,
6867 ie.oid,
6868 ));
6869 }
6870 } else {
6871 staging_hash_jobs.push(StagingHashJob::new(
6872 entry_index,
6873 worktree_mode,
6874 file_attrs_for(path_str_ref),
6875 ));
6876 }
6877 } else {
6878 let same_file_type = (worktree_mode == crate::index::MODE_SYMLINK)
6882 == (ie.mode == crate::index::MODE_SYMLINK);
6883 if !stat.is_dir
6884 && worktree_mode == ie.mode
6885 && ((refresh_content_verified[entry_index] && same_file_type)
6886 || (stat_matches_blob_stat(ie, &stat, stat_policy)
6887 && index_entry_skips_worktree_content_hash_after_stat_match(
6888 ie,
6889 index_mtime,
6890 stat_policy,
6891 )))
6892 {
6893 if crate::fsmonitor::mark_fsmonitor_valid_proven_clean(index, entry_index) {
6894 fsmonitor_valid_updated = true;
6895 }
6896 return Ok(());
6897 }
6898 let file_path = index_entry_worktree_abs_bytes(
6899 work_tree,
6900 &index.entries[entry_index].path,
6901 precompose_unicode,
6902 ignorecase,
6903 );
6904 let Some(meta) = symlink_metadata_worktree_diff(
6905 &file_path,
6906 path_str_ref,
6907 options.diagnostics.as_ref(),
6908 options.for_status,
6909 options.skip_inaccessible_paths,
6910 materialize_dirty_blobs,
6911 )?
6912 else {
6913 return Ok(());
6914 };
6915 push_index_blob_worktree_diff(
6916 odb,
6917 ie,
6918 path_str_ref,
6919 &file_path,
6920 meta,
6921 index_mtime,
6922 mode_policy,
6923 stat_policy,
6924 &file_attrs_for,
6925 &conv,
6926 materialize_dirty_blobs,
6927 hash_blobs_without_write,
6928 options.for_status,
6929 size_change_proves_modified,
6930 options.skip_inaccessible_paths,
6931 options.diagnostics.as_ref(),
6932 filter_fp,
6933 icase_collisions.as_ref(),
6934 &mut result,
6935 )?;
6936 }
6937 }
6938 }
6939 Ok(())
6940 })?;
6941 if parallel_blob_work && !staging_hash_jobs.is_empty() {
6942 parallel_process_blob_worktree_jobs(
6943 odb,
6944 &StagingHashSource {
6945 index,
6946 work_tree,
6947 precompose_unicode,
6948 ignorecase,
6949 },
6950 &staging_hash_jobs,
6951 &thread_policy,
6952 &config,
6953 materialize_dirty_blobs,
6954 hash_blobs_without_write,
6955 options.for_status,
6956 options.skip_inaccessible_paths,
6957 options.diagnostics.as_ref(),
6958 &conv,
6959 filter_fp,
6960 icase_collisions.as_ref(),
6961 &mut result,
6962 )?;
6963 }
6964
6965 for (path, (_, base_entry)) in unmerged_base {
6966 let file_path = index_entry_worktree_abs(work_tree, &path, precompose_unicode, ignorecase);
6967 let wt_meta = symlink_metadata_worktree_diff(
6968 &file_path,
6969 &path,
6970 options.diagnostics.as_ref(),
6971 options.for_status,
6972 options.skip_inaccessible_paths,
6973 materialize_dirty_blobs,
6974 )?;
6975
6976 let new_mode = wt_meta.as_ref().map_or_else(
6977 || FileMode::ABSENT,
6978 |meta| FileMode::new(mode_from_metadata(meta)),
6979 );
6980 result.push(DiffEntry {
6981 status: DiffStatus::Unmerged,
6982 old_path: Some(path.clone()),
6983 new_path: Some(path.clone()),
6984 old_mode: FileMode::ABSENT,
6985 new_mode,
6986 old_oid: zero_oid(),
6987 new_oid: zero_oid(),
6988 score: None,
6989 });
6990
6991 if !options.for_status {
6992 if let Some(meta) = wt_meta {
6993 let file_attrs = file_attrs_for(&path);
6994 let wt_oid = worktree_file_oid(
6995 odb,
6996 &file_path,
6997 &meta,
6998 &conv,
6999 &file_attrs,
7000 &path,
7001 Some(&base_entry),
7002 materialize_dirty_blobs,
7003 filter_fp,
7004 )?;
7005 let wt_mode = mode_from_metadata(&meta);
7006 if wt_oid != base_entry.oid || wt_mode != base_entry.mode {
7007 result.push(DiffEntry {
7008 status: DiffStatus::for_content_change(base_entry.mode, wt_mode),
7009 old_path: Some(path.clone()),
7010 new_path: Some(path),
7011 old_mode: FileMode::new(base_entry.mode),
7012 new_mode: FileMode::new(wt_mode),
7013 old_oid: base_entry.oid,
7014 new_oid: wt_oid,
7015 score: None,
7016 });
7017 }
7018 }
7019 }
7020 }
7021
7022 if fsmonitor_valid_updated {
7023 index.fsmonitor_validity_changed();
7024 index_stat_refresh_changed = true;
7025 }
7026
7027 Ok((result, index_stat_refresh_changed, staging_stat_cache))
7028}
7029
7030#[derive(Default)]
7037pub(crate) struct SymlinkDirCache {
7038 real_dirs: Vec<u8>,
7041 blocked: Option<(Vec<u8>, bool)>,
7044 #[cfg(all(target_os = "linux", target_pointer_width = "64"))]
7046 chain: Option<crate::worktree_scan::RealDirChain>,
7047 #[cfg(test)]
7049 fs_queries: usize,
7050}
7051
7052fn path_within_dir(path: &[u8], dir: &[u8]) -> bool {
7054 path.starts_with(dir) && path.get(dir.len()).is_none_or(|&b| b == b'/')
7055}
7056
7057fn common_dir_prefix_len(a: &[u8], b: &[u8]) -> usize {
7059 let same = a.iter().zip(b).take_while(|(x, y)| x == y).count();
7060 let ends_component = |p: &[u8]| p.get(same).is_none_or(|&c| c == b'/');
7061 if ends_component(a) && ends_component(b) {
7062 return same;
7063 }
7064 a[..same].iter().rposition(|&c| c == b'/').unwrap_or(0)
7065}
7066
7067pub(crate) fn index_entry_worktree_abs(
7068 work_tree: &Path,
7069 index_relpath: &str,
7070 precompose_unicode: bool,
7071 ignorecase: bool,
7072) -> PathBuf {
7073 index_entry_worktree_abs_bytes(
7074 work_tree,
7075 index_relpath.as_bytes(),
7076 precompose_unicode,
7077 ignorecase,
7078 )
7079}
7080
7081pub(crate) fn index_entry_worktree_abs_bytes(
7082 work_tree: &Path,
7083 index_relpath: &[u8],
7084 precompose_unicode: bool,
7085 ignorecase: bool,
7086) -> PathBuf {
7087 if precompose_unicode || ignorecase {
7088 if let Ok(s) = std::str::from_utf8(index_relpath) {
7089 if precompose_unicode {
7090 return crate::unicode_normalization::resolve_worktree_path_for_staging(
7091 work_tree, s, true,
7092 )
7093 .abs;
7094 }
7095 return crate::path_icase::worktree_path_for_index_entry(work_tree, s, ignorecase);
7096 }
7097 }
7098 crate::index::worktree_path_from_index_rel(work_tree, index_relpath)
7099}
7100
7101#[must_use]
7106pub fn index_mtime_for_diff(index: &Index, explicit: Option<(u32, u32)>) -> Option<(u32, u32)> {
7107 explicit.or(index.source_mtime)
7108}
7109
7110impl SymlinkDirCache {
7111 pub(crate) fn has_symlink_in_path_bytes(
7116 &mut self,
7117 work_tree: &Path,
7118 rel_path: &[u8],
7119 precompose_unicode: bool,
7120 ignorecase: bool,
7121 ) -> bool {
7122 use crate::worktree_scan::{ancestor_kind, AncestorKind};
7123
7124 let Some(dir_len) = rel_path.iter().rposition(|&b| b == b'/') else {
7125 return false;
7126 };
7127 let dir = &rel_path[..dir_len];
7128 if let Some((blocked, is_symlink)) = &self.blocked {
7129 if path_within_dir(dir, blocked) {
7130 return *is_symlink;
7131 }
7132 }
7133 let verified = common_dir_prefix_len(dir, &self.real_dirs);
7134 self.real_dirs.truncate(verified);
7135 #[cfg(all(target_os = "linux", target_pointer_width = "64"))]
7136 if !precompose_unicode
7137 && !ignorecase
7138 && dir[verified..]
7139 .split(|&b| b == b'/')
7140 .filter(|component| !component.is_empty())
7141 .nth(1)
7142 .is_some()
7143 {
7144 let chain = match &mut self.chain {
7145 Some(chain) if chain.work_tree() == work_tree => chain,
7146 slot => slot.insert(crate::worktree_scan::RealDirChain::open(work_tree)),
7147 };
7148 #[cfg(test)]
7149 {
7150 self.fs_queries += 1;
7151 }
7152 if chain.confirms(dir) {
7153 self.real_dirs.clear();
7154 self.real_dirs.extend_from_slice(dir);
7155 return false;
7156 }
7157 }
7158 let mut start = verified;
7159 while start < dir.len() {
7160 if dir[start] == b'/' {
7161 start += 1;
7162 continue;
7163 }
7164 let end = dir[start..]
7165 .iter()
7166 .position(|&b| b == b'/')
7167 .map_or(dir.len(), |i| start + i);
7168 let prefix = &dir[..end];
7169 let abs =
7170 index_entry_worktree_abs_bytes(work_tree, prefix, precompose_unicode, ignorecase);
7171 #[cfg(test)]
7172 {
7173 self.fs_queries += 1;
7174 }
7175 match ancestor_kind(&abs) {
7176 AncestorKind::Directory => {
7177 let known = self.real_dirs.len();
7178 self.real_dirs.extend_from_slice(&prefix[known..]);
7179 }
7180 AncestorKind::Symlink => {
7181 self.blocked = Some((prefix.to_vec(), true));
7182 return true;
7183 }
7184 AncestorKind::Other => {
7185 self.blocked = Some((prefix.to_vec(), false));
7186 return false;
7187 }
7188 }
7189 start = end;
7190 }
7191 false
7192 }
7193}
7194
7195#[must_use]
7201pub(crate) fn entry_is_racy(ie: &IndexEntry, index_mtime: Option<(u32, u32)>) -> bool {
7202 entry_is_racy_with_policy(ie, index_mtime, crate::index::StatMatchPolicy::default())
7203}
7204
7205#[must_use]
7211pub(crate) fn entry_is_racy_with_policy(
7212 ie: &IndexEntry,
7213 index_mtime: Option<(u32, u32)>,
7214 policy: crate::index::StatMatchPolicy,
7215) -> bool {
7216 let Some((index_mtime_sec, index_mtime_nsec)) = index_mtime else {
7217 return false;
7218 };
7219 if index_mtime_sec == 0 {
7220 return false;
7221 }
7222 if policy.use_nanosec && policy.check_stat {
7223 index_mtime_sec < ie.mtime_sec
7224 || (index_mtime_sec == ie.mtime_sec && index_mtime_nsec <= ie.mtime_nsec)
7225 } else {
7226 index_mtime_sec <= ie.mtime_sec
7227 }
7228}
7229
7230pub(crate) fn smudge_racily_clean_entries(
7247 odb: &Odb,
7248 git_dir: &Path,
7249 index: &mut Index,
7250 work_tree: &Path,
7251 index_mtime: Option<(u32, u32)>,
7252 config: Option<&ConfigSet>,
7253 filter_process: Option<&crate::filter_process::FilterProcessState>,
7254) -> bool {
7255 use crate::crlf;
7256 use crate::index::{MODE_EXECUTABLE, MODE_REGULAR, MODE_SYMLINK};
7257
7258 let Some(index_mtime) = index_mtime else {
7259 return false;
7260 };
7261
7262 let config = config.cloned().unwrap_or_else(|| {
7263 ConfigSet::load(
7264 &crate::environment::Environment::empty(),
7265 Some(git_dir),
7266 true,
7267 )
7268 .unwrap_or_default()
7269 });
7270 let conv = crlf::ConversionConfig::from_config(&config);
7271 let attrs = crlf::load_gitattributes(work_tree);
7272 let verified = std::mem::take(&mut index.content_verified);
7273
7274 let mut changed = false;
7275 for ie in &mut index.entries {
7276 let marked_verified = ie.take_content_verified();
7277 if ie.stage() != 0 || ie.skip_worktree() || ie.assume_unchanged() || ie.intent_to_add() {
7278 continue;
7279 }
7280 if ie.mode != MODE_REGULAR && ie.mode != MODE_EXECUTABLE && ie.mode != MODE_SYMLINK {
7281 continue;
7282 }
7283 if ie.size == 0 {
7284 continue;
7285 }
7286 if !entry_is_racy(ie, Some(index_mtime)) {
7287 continue;
7288 }
7289 if marked_verified || verified.get(&ie.path) == Some(&ie.oid) {
7290 continue;
7291 }
7292 let Ok(path) = std::str::from_utf8(&ie.path) else {
7293 continue;
7294 };
7295 let abs = work_tree.join(path);
7296 let Ok(meta) = fs::symlink_metadata(&abs) else {
7297 continue;
7298 };
7299 if !stat_matches(ie, &meta) {
7300 continue;
7301 }
7302 let file_attrs = crlf::get_file_attrs(&attrs, path, false, &config);
7303 let content_matches = hash_worktree_file(
7304 odb,
7305 &abs,
7306 &meta,
7307 &conv,
7308 &file_attrs,
7309 path,
7310 Some(ie),
7311 filter_process,
7312 )
7313 .map(|oid| oid == ie.oid)
7314 .unwrap_or(false);
7315 if content_matches {
7316 continue;
7317 }
7318 ie.size = 0;
7319 changed = true;
7320 }
7321 changed
7322}
7323#[allow(dead_code)]
7331fn worktree_differs_from_index_entry(
7332 odb: &Odb,
7333 repository_git_dir: &Path,
7334 work_tree: &Path,
7335 ie: &IndexEntry,
7336 ignore_submodule_untracked: bool,
7337 config: Option<&ConfigSet>,
7338 filter_process: Option<&crate::filter_process::FilterProcessState>,
7339) -> Result<bool> {
7340 WorktreeEntryComparer::new(repository_git_dir, work_tree, config).differs(
7341 odb,
7342 ie,
7343 ignore_submodule_untracked,
7344 filter_process,
7345 )
7346}
7347
7348pub(crate) struct WorktreeEntryComparer<'a> {
7352 work_tree: &'a Path,
7353 config: ConfigSet,
7354 mode_policy: crate::index::WorktreeModePolicy,
7355 precompose_unicode: bool,
7356 ignorecase: bool,
7357 conv: std::cell::OnceCell<crate::crlf::ConversionConfig>,
7358 attrs: std::cell::OnceCell<Vec<crate::crlf::AttrRule>>,
7359}
7360
7361impl<'a> WorktreeEntryComparer<'a> {
7362 pub fn new(repository_git_dir: &Path, work_tree: &'a Path, config: Option<&ConfigSet>) -> Self {
7364 let config = config.cloned().unwrap_or_else(|| {
7365 ConfigSet::load(
7366 &crate::environment::Environment::empty(),
7367 Some(repository_git_dir),
7368 true,
7369 )
7370 .unwrap_or_else(|_| ConfigSet::new())
7371 });
7372 let flag = |key: &str, default: bool| config.bool_or(key, default);
7373 Self {
7374 work_tree,
7375 mode_policy: crate::index::WorktreeModePolicy::from_config(&config),
7376 precompose_unicode: flag("core.precomposeunicode", false),
7377 ignorecase: flag("core.ignorecase", false),
7378 config,
7379 conv: std::cell::OnceCell::new(),
7380 attrs: std::cell::OnceCell::new(),
7381 }
7382 }
7383
7384 pub(crate) fn differs(
7386 &self,
7387 odb: &Odb,
7388 ie: &IndexEntry,
7389 ignore_submodule_untracked: bool,
7390 filter_process: Option<&crate::filter_process::FilterProcessState>,
7391 ) -> Result<bool> {
7392 use crate::crlf;
7393
7394 let work_tree = self.work_tree;
7395 let path_stored = crate::repo_relpath::storage_string(&ie.path);
7396 let path_str_ref = crate::repo_relpath::as_utf8_for_matching(&path_stored);
7397 let file_path = index_entry_worktree_abs_bytes(
7398 work_tree,
7399 &ie.path,
7400 self.precompose_unicode,
7401 self.ignorecase,
7402 );
7403
7404 if ie.mode == 0o160000 {
7405 let sub_head_oid = read_submodule_head(&file_path);
7406 let ref_matches = match sub_head_oid {
7407 Some(oid) => oid == ie.oid,
7408 None => uninitialized_gitlink_worktree_matches_index(&file_path),
7409 };
7410 let mut flags = submodule_porcelain_flags(work_tree, path_str_ref.as_ref(), ie.oid);
7411 if ignore_submodule_untracked {
7412 flags.untracked = false;
7413 }
7414 return Ok(!ref_matches || flags.modified || flags.untracked);
7415 }
7416
7417 let meta = match fs::symlink_metadata(&file_path) {
7418 Ok(m) => m,
7419 Err(e)
7420 if e.kind() == std::io::ErrorKind::NotFound
7421 || e.raw_os_error() == Some(20) =>
7422 {
7423 return Ok(true);
7424 }
7425 Err(e) => return Err(Error::Io(e)),
7426 };
7427
7428 if meta.is_dir() {
7429 return Ok(true);
7430 }
7431
7432 if !self
7433 .mode_policy
7434 .worktree_mode_matches(mode_from_metadata(&meta), ie.mode)
7435 {
7436 return Ok(true);
7437 }
7438
7439 let conv = self
7440 .conv
7441 .get_or_init(|| crlf::ConversionConfig::from_config(&self.config));
7442 let attrs = self
7443 .attrs
7444 .get_or_init(|| crlf::load_gitattributes(work_tree));
7445 let file_attrs = crlf::get_file_attrs(attrs, path_str_ref.as_ref(), false, &self.config);
7446 let worktree_oid = hash_worktree_file(
7447 odb,
7448 &file_path,
7449 &meta,
7450 conv,
7451 &file_attrs,
7452 path_str_ref.as_ref(),
7453 Some(ie),
7454 filter_process,
7455 )?;
7456
7457 let mut eff_oid = worktree_oid;
7458 if eff_oid != ie.oid {
7459 if let Ok(raw) = fs::read(&file_path) {
7460 let raw_oid = odb.hash(ObjectKind::Blob, &raw)?;
7461 if raw_oid == ie.oid {
7462 eff_oid = ie.oid;
7463 }
7464 }
7465 }
7466
7467 Ok(eff_oid != ie.oid)
7468 }
7469}
7470
7471pub struct WorktreeAddRefreshParams<'a> {
7473 pub odb: &'a Odb,
7475 pub ie: &'a IndexEntry,
7477 pub meta: &'a fs::Metadata,
7479 pub abs_path: &'a Path,
7481 pub rel_path: &'a str,
7483 pub conv: &'a crate::crlf::ConversionConfig,
7485 pub file_attrs: &'a crate::crlf::FileAttrs,
7487 pub index_mtime: Option<(u32, u32)>,
7489 pub staged_mode: u32,
7492 pub filter_process: Option<&'a crate::filter_process::FilterProcessState>,
7494}
7495
7496#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7498pub enum WorktreeAddRefresh {
7499 UpToDate,
7501 ModeOnly {
7503 mode: u32,
7505 },
7506 StatOnly,
7508 NeedsRestage,
7510}
7511
7512#[must_use]
7514pub(crate) fn index_stat_cache_trustworthy(index_mtime: Option<(u32, u32)>) -> bool {
7515 match index_mtime {
7516 None => false,
7517 Some((0, _)) => false,
7518 Some(_) => true,
7519 }
7520}
7521
7522#[must_use]
7531pub(crate) fn index_entry_skips_worktree_content_hash_after_stat_match(
7532 ie: &IndexEntry,
7533 index_mtime: Option<(u32, u32)>,
7534 stat_policy: crate::index::StatMatchPolicy,
7535) -> bool {
7536 if ie.stat_cache_unset() || (ie.size == 0 && !ie.oid.is_empty_blob()) {
7537 return false;
7538 }
7539 if !index_stat_cache_trustworthy(index_mtime) {
7540 return false;
7541 }
7542 !entry_is_racy_with_policy(ie, index_mtime, stat_policy)
7543}
7544
7545pub(crate) fn classify_worktree_entry_for_add(
7559 params: &WorktreeAddRefreshParams<'_>,
7560) -> Result<WorktreeAddRefresh> {
7561 use crate::index::{MODE_EXECUTABLE, MODE_REGULAR, MODE_SYMLINK};
7562
7563 let ie = params.ie;
7564 let meta = params.meta;
7565 let staged_mode = params.staged_mode;
7566
7567 if ie.intent_to_add() {
7568 return Ok(WorktreeAddRefresh::NeedsRestage);
7569 }
7570
7571 let racy = entry_is_racy(ie, params.index_mtime);
7572 let stat_same = stat_matches(ie, meta);
7573 let trust_stat = index_stat_cache_trustworthy(params.index_mtime);
7574
7575 if ie.mode == MODE_SYMLINK {
7576 if !meta.file_type().is_symlink() && staged_mode != MODE_SYMLINK {
7579 return Ok(WorktreeAddRefresh::NeedsRestage);
7580 }
7581 if stat_same && !racy && trust_stat && staged_mode == ie.mode {
7582 return Ok(WorktreeAddRefresh::UpToDate);
7583 }
7584 if stat_same && !racy && trust_stat && staged_mode != ie.mode {
7585 return Ok(WorktreeAddRefresh::ModeOnly { mode: staged_mode });
7586 }
7587 return worktree_effective_oid_matches_index(params);
7588 }
7589
7590 if ie.mode != MODE_REGULAR && ie.mode != MODE_EXECUTABLE {
7591 return Ok(WorktreeAddRefresh::NeedsRestage);
7592 }
7593
7594 if meta.file_type().is_symlink() || meta.is_dir() {
7595 return Ok(WorktreeAddRefresh::NeedsRestage);
7596 }
7597
7598 if stat_same && !racy && trust_stat {
7599 if staged_mode == ie.mode {
7600 return Ok(WorktreeAddRefresh::UpToDate);
7601 }
7602 return Ok(WorktreeAddRefresh::ModeOnly { mode: staged_mode });
7603 }
7604
7605 worktree_effective_oid_matches_index(params)
7606}
7607
7608fn worktree_effective_oid_matches_index(
7609 params: &WorktreeAddRefreshParams<'_>,
7610) -> Result<WorktreeAddRefresh> {
7611 let WorktreeAddRefreshParams {
7612 odb,
7613 ie,
7614 meta,
7615 abs_path,
7616 rel_path,
7617 conv,
7618 file_attrs,
7619 staged_mode,
7620 ..
7621 } = params;
7622 let worktree_oid = hash_worktree_file(
7623 odb,
7624 abs_path,
7625 meta,
7626 conv,
7627 file_attrs,
7628 rel_path,
7629 Some(ie),
7630 params.filter_process,
7631 )?;
7632 let mut eff_oid = worktree_oid;
7633 if eff_oid != ie.oid {
7634 if let Ok(raw) = fs::read(abs_path) {
7635 let raw_oid = odb.hash(ObjectKind::Blob, &raw)?;
7636 if raw_oid == ie.oid {
7637 eff_oid = ie.oid;
7638 }
7639 }
7640 }
7641 if eff_oid != ie.oid {
7642 return Ok(WorktreeAddRefresh::NeedsRestage);
7643 }
7644 if *staged_mode != ie.mode {
7645 return Ok(WorktreeAddRefresh::ModeOnly { mode: *staged_mode });
7646 }
7647 if stat_matches(ie, meta) {
7648 Ok(WorktreeAddRefresh::UpToDate)
7649 } else {
7650 Ok(WorktreeAddRefresh::StatOnly)
7651 }
7652}
7653
7654#[must_use]
7656pub fn stat_matches(ie: &IndexEntry, meta: &fs::Metadata) -> bool {
7657 stat_matches_with_policy(ie, meta, crate::index::StatMatchPolicy::default())
7658}
7659
7660pub(crate) fn stat_matches_blob_stat(
7662 ie: &IndexEntry,
7663 stat: &crate::worktree_scan::BlobDiskStat,
7664 policy: crate::index::StatMatchPolicy,
7665) -> bool {
7666 stat.fields.matches_index_entry(ie, policy)
7667}
7668
7669#[must_use]
7671pub(crate) fn stat_matches_with_policy(
7672 ie: &IndexEntry,
7673 meta: &fs::Metadata,
7674 policy: crate::index::StatMatchPolicy,
7675) -> bool {
7676 if crate::index::munge_index_size(meta.len()) != ie.size {
7677 return false;
7678 }
7679 #[cfg(unix)]
7680 {
7681 use std::os::unix::fs::MetadataExt;
7682 if meta.mtime() as u32 != ie.mtime_sec {
7683 return false;
7684 }
7685 if policy.use_nanosec && policy.check_stat && meta.mtime_nsec() as u32 != ie.mtime_nsec {
7686 return false;
7687 }
7688 if policy.trust_ctime && policy.check_stat {
7689 if meta.ctime() as u32 != ie.ctime_sec {
7690 return false;
7691 }
7692 if policy.use_nanosec && meta.ctime_nsec() as u32 != ie.ctime_nsec {
7693 return false;
7694 }
7695 }
7696 if policy.check_stat && meta.ino() as u32 != ie.ino {
7697 return false;
7698 }
7699 }
7700 #[cfg(not(unix))]
7701 {
7702 let ((ctime_sec, ctime_nsec), (mtime_sec, mtime_nsec)) = crate::index::metadata_times(meta);
7703 if mtime_sec != ie.mtime_sec {
7704 return false;
7705 }
7706 if policy.use_nanosec && policy.check_stat && mtime_nsec != ie.mtime_nsec {
7707 return false;
7708 }
7709 if policy.trust_ctime && policy.check_stat {
7710 if ctime_sec != ie.ctime_sec {
7711 return false;
7712 }
7713 if policy.use_nanosec && ctime_nsec != ie.ctime_nsec {
7714 return false;
7715 }
7716 }
7717 }
7718 true
7719}
7720
7721pub fn refresh_index_stat_content_verified(
7744 odb: &Odb,
7745 git_dir: &Path,
7746 index: &mut Index,
7747 work_tree: &Path,
7748 index_mtime: Option<(u32, u32)>,
7749 config: Option<&ConfigSet>,
7750 _stat_parallel_threads: Option<usize>,
7751) -> Result<bool> {
7752 let _ = _stat_parallel_threads;
7753 refresh_index_stat_content_verified_with_rules(
7754 odb,
7755 git_dir,
7756 index,
7757 work_tree,
7758 index_mtime,
7759 config,
7760 _stat_parallel_threads,
7761 None,
7762 )
7763}
7764
7765#[allow(clippy::too_many_arguments)]
7773pub(crate) fn refresh_index_stat_content_verified_with_rules(
7774 odb: &Odb,
7775 git_dir: &Path,
7776 index: &mut Index,
7777 work_tree: &Path,
7778 index_mtime: Option<(u32, u32)>,
7779 config: Option<&ConfigSet>,
7780 stat_parallel_threads: Option<usize>,
7781 worktree_rules: Option<&std::sync::Arc<std::sync::Mutex<crate::worktree_rules::WorktreeRules>>>,
7782) -> Result<bool> {
7783 use crate::config::ConfigSet;
7784 use crate::crlf;
7785 use crate::worktree_scan::{
7786 collect_blob_lookup_batches, group_blob_entries_by_dir, visit_blob_lookup_batches,
7787 BlobDiskLookup, BlobScanSparseMode, WorktreeBlobScanOptions,
7788 };
7789
7790 let index_mtime = index_mtime_for_diff(index, index_mtime);
7791 let rules_lock = worktree_rules
7792 .map(|shared| {
7793 shared
7794 .lock()
7795 .map_err(|e| Error::Message(format!("worktree rules lock poisoned: {e}")))
7796 })
7797 .transpose()?;
7798 let config = rules_lock
7799 .as_ref()
7800 .map(|r| r.config().clone())
7801 .unwrap_or_else(|| {
7802 config.cloned().unwrap_or_else(|| {
7803 ConfigSet::load(
7804 &crate::environment::Environment::empty(),
7805 Some(git_dir),
7806 true,
7807 )
7808 .unwrap_or_default()
7809 })
7810 });
7811 let preload_index = config.preload_index();
7812 let blob_scan = WorktreeBlobScanOptions {
7813 preload_index,
7814 stat_parallel_threads,
7815 ..WorktreeBlobScanOptions::default()
7816 };
7817 let conv = rules_lock
7818 .as_ref()
7819 .map(|r| r.conversion().clone())
7820 .unwrap_or_else(|| crlf::ConversionConfig::from_config(&config));
7821 let legacy_attrs = if rules_lock.is_none() {
7822 Some(crlf::load_gitattributes_parsed(work_tree))
7823 } else {
7824 None
7825 };
7826 let file_attrs_for = |path: &str| -> crlf::FileAttrs {
7827 if let Some(rules) = rules_lock.as_ref() {
7828 rules.file_attrs(path, false)
7829 } else if let Some(parsed) = legacy_attrs.as_ref() {
7830 crlf::file_attrs_from_gitattributes(&parsed.rules, &parsed.macros, path, false, &config)
7831 } else {
7832 crlf::FileAttrs::default()
7833 }
7834 };
7835 let precompose_unicode = config.precompose_unicode();
7836 let ignorecase = config.ignore_case();
7837 let stat_policy = crate::index::StatMatchPolicy::from_config(Some(&config));
7838
7839 let blob_dirs = group_blob_entries_by_dir(index, false);
7840 let work_tree_owned = work_tree.to_path_buf();
7841 let dir_abs = move |dir: &[u8]| {
7842 if dir.is_empty() {
7843 work_tree_owned.clone()
7844 } else {
7845 index_entry_worktree_abs_bytes(&work_tree_owned, dir, precompose_unicode, ignorecase)
7846 }
7847 };
7848 let work_tree_for_files = work_tree.to_path_buf();
7849 let file_abs = move |rel: &[u8]| {
7850 index_entry_worktree_abs_bytes(&work_tree_for_files, rel, precompose_unicode, ignorecase)
7851 };
7852 let mut changed = false;
7853 let filter_fp = rules_lock.as_ref().map(|r| r.filter_process());
7854 let thread_policy = crate::parallel::ThreadPolicy::default();
7855 let refresh_batches = collect_blob_lookup_batches(
7857 index,
7858 &blob_dirs,
7859 blob_scan,
7860 &thread_policy,
7861 &config,
7862 dir_abs,
7863 file_abs,
7864 || {
7865 let mut dir_symlinks = SymlinkDirCache::default();
7866 move |rel_path: &[u8]| {
7867 dir_symlinks.has_symlink_in_path_bytes(
7868 work_tree,
7869 rel_path,
7870 precompose_unicode,
7871 ignorecase,
7872 )
7873 }
7874 },
7875 BlobScanSparseMode::StatRefresh {
7876 stat_policy,
7877 index_mtime,
7878 },
7879 )?;
7880 let par_opts = refresh_hash_parallel_options(&thread_policy, &config, stat_parallel_threads);
7881 let refresh_checks = refresh_content_checks(
7882 odb,
7883 index,
7884 &refresh_batches,
7885 index_mtime,
7886 stat_policy,
7887 &|_, _, _| true,
7888 &WorktreeHashContext {
7889 work_tree,
7890 precompose_unicode,
7891 ignorecase,
7892 conv: &conv,
7893 file_attrs_for: &file_attrs_for,
7894 filter_process: filter_fp,
7895 },
7896 par_opts,
7897 None,
7898 )?;
7899 visit_blob_lookup_batches(refresh_batches, |entry_index, lookup| {
7900 let BlobDiskLookup::Present(stat) = lookup else {
7901 return Ok(());
7902 };
7903 let content_matches = RefreshContent::matches(
7904 refresh_checks.get(entry_index),
7905 &index.entries[entry_index].oid,
7906 );
7907 let refresh = refresh_index_blob_stat_from_scan(
7908 &mut index.entries[entry_index],
7909 &stat,
7910 index_mtime,
7911 stat_policy,
7912 content_matches,
7913 );
7914 if refresh.index_changed {
7915 changed = true;
7916 }
7917 if refresh.content_verified {
7918 let path = index.entries[entry_index].path.clone();
7919 let oid = index.entries[entry_index].oid;
7920 index.mark_content_verified(&path, oid);
7921 }
7922 Ok(())
7923 })?;
7924 Ok(changed)
7925}
7926
7927struct BlobStatRefreshOutcome {
7929 index_changed: bool,
7931 content_verified: bool,
7933}
7934
7935fn refresh_index_entry_eligible(ie: &IndexEntry) -> bool {
7937 use crate::index::{MODE_EXECUTABLE, MODE_REGULAR, MODE_SYMLINK};
7938 ie.stage() == 0
7939 && !ie.skip_worktree()
7940 && !ie.assume_unchanged()
7941 && !ie.intent_to_add()
7942 && matches!(ie.mode, MODE_REGULAR | MODE_EXECUTABLE | MODE_SYMLINK)
7943}
7944
7945fn refresh_needs_content_check(
7948 ie: &IndexEntry,
7949 stat: &crate::worktree_scan::BlobDiskStat,
7950 index_mtime: Option<(u32, u32)>,
7951 stat_policy: crate::index::StatMatchPolicy,
7952) -> bool {
7953 refresh_index_entry_eligible(ie)
7954 && !stat.is_dir
7955 && (!stat_matches_blob_stat(ie, stat, stat_policy)
7956 || entry_is_racy_with_policy(ie, index_mtime, stat_policy))
7957}
7958
7959pub(crate) fn worktree_diff_refreshes_entry(
7966 ie: &IndexEntry,
7967 stat: &crate::worktree_scan::BlobDiskStat,
7968 index_mtime: Option<(u32, u32)>,
7969 stat_policy: crate::index::StatMatchPolicy,
7970 for_status: bool,
7971 conversion_is_identity: impl FnOnce() -> bool,
7972) -> bool {
7973 if !for_status {
7974 return true;
7975 }
7976 if stat_matches_blob_stat(ie, stat, stat_policy) {
7977 return entry_is_racy_with_policy(ie, index_mtime, stat_policy);
7978 }
7979 !status_size_change_proves_modified(
7980 ie,
7981 stat.worktree_size(),
7982 for_status,
7983 conversion_is_identity,
7984 )
7985}
7986
7987fn refresh_hash_parallel_options(
7994 thread_policy: &crate::parallel::ThreadPolicy,
7995 config: &ConfigSet,
7996 stat_parallel_threads: Option<usize>,
7997) -> crate::parallel::ParallelOptions {
7998 let policy = crate::parallel::ThreadPolicy {
7999 min_items_override: thread_policy.min_items_override.or(Some(0)),
8000 min_total_bytes_override: thread_policy.min_total_bytes_override.or(Some(0)),
8001 ..*thread_policy
8002 };
8003 let mut par_opts = policy.resolve(crate::parallel::ParallelOp::IndexRefresh, config);
8004 if let Some(cap) = stat_parallel_threads {
8005 par_opts.threads = par_opts
8006 .threads
8007 .min(std::num::NonZeroUsize::new(cap).unwrap_or(std::num::NonZeroUsize::MIN));
8008 }
8009 par_opts.min_items = 0;
8010 par_opts.min_total_bytes = None;
8011 par_opts
8012}
8013
8014struct WorktreeHashContext<'a> {
8016 work_tree: &'a Path,
8017 precompose_unicode: bool,
8018 ignorecase: bool,
8019 conv: &'a crate::crlf::ConversionConfig,
8020 file_attrs_for: &'a dyn Fn(&str) -> crate::crlf::FileAttrs,
8021 filter_process: Option<&'a crate::filter_process::FilterProcessState>,
8022}
8023
8024#[allow(clippy::too_many_arguments)]
8034fn refresh_content_checks(
8035 odb: &Odb,
8036 index: &Index,
8037 batches: &[Vec<(usize, crate::worktree_scan::BlobDiskLookup)>],
8038 index_mtime: Option<(u32, u32)>,
8039 stat_policy: crate::index::StatMatchPolicy,
8040 include: &dyn Fn(usize, &IndexEntry, &crate::worktree_scan::BlobDiskStat) -> bool,
8041 ctx: &WorktreeHashContext<'_>,
8042 par_opts: crate::parallel::ParallelOptions,
8043 cancel_watch: Option<&std::sync::Arc<std::sync::atomic::AtomicBool>>,
8044) -> Result<RefreshChecks> {
8045 use crate::progress::{poll_watch, CANCEL_POLL_STRIDE};
8046 use crate::worktree_scan::BlobDiskLookup;
8047
8048 struct ContentCheck {
8050 entry_index: usize,
8051 worktree_mode: u32,
8052 attrs_index: usize,
8054 }
8055
8056 let mut attrs = DistinctFileAttrs::default();
8057 let mut jobs = Vec::new();
8058 let mut total_bytes = 0usize;
8059 let mut cancel_stride = 0usize;
8060 for (entry_index, lookup) in batches.iter().flatten() {
8061 cancel_stride = cancel_stride.wrapping_add(1);
8062 if cancel_stride.is_multiple_of(CANCEL_POLL_STRIDE) {
8063 if let Some(w) = cancel_watch {
8064 poll_watch(w)?;
8065 }
8066 }
8067 let BlobDiskLookup::Present(stat) = lookup else {
8068 continue;
8069 };
8070 let ie = &index.entries[*entry_index];
8071 if !refresh_needs_content_check(ie, stat, index_mtime, stat_policy)
8072 || !include(*entry_index, ie, stat)
8073 {
8074 continue;
8075 }
8076 total_bytes = total_bytes.saturating_add(ie.size as usize);
8077 let file_attrs = (ctx.file_attrs_for)(&crate::repo_relpath::storage_string(&ie.path));
8078 jobs.push(ContentCheck {
8079 entry_index: *entry_index,
8080 worktree_mode: stat.git_mode,
8081 attrs_index: attrs.intern(file_attrs),
8082 });
8083 }
8084 if jobs.is_empty() {
8085 return Ok(RefreshChecks::default());
8086 }
8087 let attrs = attrs.0;
8088 let mut checks = vec![None; index.entries.len()];
8089 let (work_tree, precompose_unicode, ignorecase) =
8090 (ctx.work_tree, ctx.precompose_unicode, ctx.ignorecase);
8091 let (conv, filter_process) = (ctx.conv, ctx.filter_process);
8092 let entries = index.entries.as_slice();
8093 let watch_for_hash = cancel_watch.cloned();
8094 let hash_one = |job: &ContentCheck| -> Result<RefreshContent> {
8095 if job.entry_index.is_multiple_of(CANCEL_POLL_STRIDE) {
8096 if let Some(w) = watch_for_hash.as_ref() {
8097 poll_watch(w)?;
8098 }
8099 }
8100 let ie = &entries[job.entry_index];
8101 let file_path =
8102 index_entry_worktree_abs_bytes(work_tree, &ie.path, precompose_unicode, ignorecase);
8103 Ok(RefreshContent::from(hash_worktree_file_with_mode(
8104 odb,
8105 &file_path,
8106 job.worktree_mode,
8107 conv,
8108 &attrs[job.attrs_index],
8109 &crate::repo_relpath::storage_string(&ie.path),
8110 Some(ie),
8111 filter_process,
8112 )))
8113 };
8114 let matches = if par_opts.threads.get() > 1 && jobs.len() > 1 {
8115 crate::parallel::try_par_map_force(&jobs, par_opts, hash_one).map_err(|e| match e {
8116 crate::parallel::ParallelHashError::Task(err) => err,
8117 })?
8118 } else {
8119 let mut out = Vec::with_capacity(jobs.len());
8120 let mut serial_stride = 0usize;
8121 for job in &jobs {
8122 serial_stride = serial_stride.wrapping_add(1);
8123 if serial_stride.is_multiple_of(CANCEL_POLL_STRIDE) {
8124 if let Some(w) = cancel_watch {
8125 poll_watch(w)?;
8126 }
8127 }
8128 out.push(hash_one(job)?);
8129 }
8130 out
8131 };
8132 for (job, content) in jobs.iter().zip(matches) {
8133 checks[job.entry_index] = Some(content);
8134 }
8135 Ok(RefreshChecks(checks))
8136}
8137
8138#[derive(Default)]
8145struct DistinctFileAttrs(Vec<crate::crlf::FileAttrs>);
8146
8147impl DistinctFileAttrs {
8148 const SEARCH_LIMIT: usize = 16;
8149
8150 fn intern(&mut self, attrs: crate::crlf::FileAttrs) -> usize {
8152 let len = self.0.len();
8153 let found = self
8154 .0
8155 .iter()
8156 .rev()
8157 .take(Self::SEARCH_LIMIT)
8158 .position(|seen| *seen == attrs);
8159 if let Some(back) = found {
8160 return len - 1 - back;
8161 }
8162 self.0.push(attrs);
8163 len
8164 }
8165}
8166
8167#[derive(Default)]
8172struct RefreshChecks(Vec<Option<RefreshContent>>);
8173
8174impl RefreshChecks {
8175 fn get(&self, entry_index: usize) -> Option<RefreshContent> {
8177 self.0.get(entry_index).copied().flatten()
8178 }
8179}
8180
8181#[derive(Clone, Copy)]
8183enum RefreshContent {
8184 Failed,
8186 Hashed(ObjectId),
8188}
8189
8190impl From<Result<ObjectId>> for RefreshContent {
8191 fn from(hashed: Result<ObjectId>) -> Self {
8192 hashed.map_or(Self::Failed, Self::Hashed)
8193 }
8194}
8195
8196impl RefreshContent {
8197 fn matches(check: Option<Self>, oid: &ObjectId) -> bool {
8199 matches!(check, Some(Self::Hashed(hashed)) if hashed == *oid)
8200 }
8201
8202 fn differs(check: Option<Self>, oid: &ObjectId) -> bool {
8204 matches!(check, Some(Self::Hashed(hashed)) if hashed != *oid)
8205 }
8206}
8207
8208fn refresh_index_blob_stat_from_scan(
8214 ie: &mut IndexEntry,
8215 stat: &crate::worktree_scan::BlobDiskStat,
8216 index_mtime: Option<(u32, u32)>,
8217 stat_policy: crate::index::StatMatchPolicy,
8218 content_matches: bool,
8219) -> BlobStatRefreshOutcome {
8220 const UNCHANGED: BlobStatRefreshOutcome = BlobStatRefreshOutcome {
8221 index_changed: false,
8222 content_verified: false,
8223 };
8224 if !refresh_index_entry_eligible(ie) || stat.is_dir {
8225 return UNCHANGED;
8226 }
8227 let cached = crate::index::IndexStatFields::of_entry(ie);
8228 let stat_same = cached.equivalent_to(&stat.fields, stat_policy);
8229 let stat_matches = stat_matches_blob_stat(ie, stat, stat_policy);
8230 if !stat_matches || entry_is_racy_with_policy(ie, index_mtime, stat_policy) {
8231 if !content_matches {
8232 if stat_matches {
8233 invalidate_index_stat_cache(ie);
8234 return BlobStatRefreshOutcome {
8235 index_changed: true,
8236 content_verified: false,
8237 };
8238 }
8239 return UNCHANGED;
8240 }
8241 if !stat_same {
8242 stat.fields.store_in(ie);
8243 }
8244 return BlobStatRefreshOutcome {
8245 index_changed: !stat_same,
8246 content_verified: true,
8247 };
8248 }
8249 if stat_same {
8250 return BlobStatRefreshOutcome {
8251 index_changed: false,
8252 content_verified: true,
8253 };
8254 }
8255 stat.fields.store_in(ie);
8256 BlobStatRefreshOutcome {
8257 index_changed: true,
8258 content_verified: false,
8259 }
8260}
8261
8262#[allow(clippy::too_many_arguments)]
8274pub(crate) fn path_checkout_skip_blob_write_when_up_to_date_with_config(
8275 odb: &Odb,
8276 git_dir: &Path,
8277 work_tree: &Path,
8278 index: &Index,
8279 entry: &IndexEntry,
8280 expected_oid: &ObjectId,
8281 expected_mode: u32,
8282 rel_path: &str,
8283 filter_process: Option<&crate::filter_process::FilterProcessState>,
8284 preloaded_config: Option<&crate::config::ConfigSet>,
8285) -> Result<bool> {
8286 use crate::config::ConfigSet;
8287 use crate::crlf;
8288 use crate::index::{MODE_EXECUTABLE, MODE_REGULAR};
8289
8290 if entry.oid != *expected_oid || entry.mode != expected_mode {
8291 return Ok(false);
8292 }
8293 if expected_mode != MODE_REGULAR && expected_mode != MODE_EXECUTABLE {
8294 return Ok(false);
8295 }
8296 let abs_path = crate::repo_relpath::worktree_abs(work_tree, rel_path);
8297 let meta = match fs::symlink_metadata(&abs_path) {
8298 Ok(m) => m,
8299 Err(_) => return Ok(false),
8300 };
8301 if !meta.is_file() || !stat_matches(entry, &meta) {
8302 return Ok(false);
8303 }
8304 let owned_config;
8305 let config = match preloaded_config {
8306 Some(cfg) => cfg,
8307 None => {
8308 owned_config = ConfigSet::load(
8309 &crate::environment::Environment::empty(),
8310 Some(git_dir),
8311 true,
8312 )
8313 .unwrap_or_default();
8314 &owned_config
8315 }
8316 };
8317 if !crate::index::WorktreeModePolicy::from_config(config)
8318 .worktree_mode_matches(mode_from_metadata(&meta), expected_mode)
8319 {
8320 return Ok(false);
8321 }
8322 let index_mtime = index_mtime_for_diff(index, None);
8323 if !entry_is_racy(entry, index_mtime) {
8324 return Ok(true);
8325 }
8326 let conv = crlf::ConversionConfig::from_config(config);
8327 let attrs = crlf::load_gitattributes(work_tree);
8328 let file_attrs = crlf::get_file_attrs(&attrs, rel_path, false, config);
8329 let wt_oid = hash_worktree_file(
8330 odb,
8331 &abs_path,
8332 &meta,
8333 &conv,
8334 &file_attrs,
8335 rel_path,
8336 Some(entry),
8337 filter_process,
8338 )?;
8339 Ok(wt_oid == *expected_oid)
8340}
8341
8342fn invalidate_index_stat_cache(ie: &mut IndexEntry) {
8344 ie.ctime_sec = 0;
8345 ie.ctime_nsec = 0;
8346 ie.mtime_sec = 0;
8347 ie.mtime_nsec = 0;
8348 ie.dev = 0;
8349 ie.ino = 0;
8350 ie.size = 0;
8351}
8352
8353pub(crate) fn read_worktree_bytes_for_mode(path: &Path, mode: u32) -> std::io::Result<Vec<u8>> {
8364 if mode == MODE_SYMLINK && fs::symlink_metadata(path)?.file_type().is_symlink() {
8365 let target = fs::read_link(path)?;
8366 return Ok(symlink_target_bytes(&target));
8367 }
8368 fs::read(path)
8369}
8370
8371pub fn read_worktree_side_bytes_for_diff(
8381 odb: &Odb,
8382 work_tree: &Path,
8383 rel_path: &str,
8384 mode: u32,
8385 config: &crate::config::ConfigSet,
8386 index_entry: Option<&IndexEntry>,
8387 worktree_rules: Option<&crate::worktree_rules::WorktreeRules>,
8388) -> Result<Vec<u8>> {
8389 Ok(load_worktree_side_for_diff(
8390 odb,
8391 work_tree,
8392 rel_path,
8393 mode,
8394 config,
8395 index_entry,
8396 worktree_rules,
8397 false,
8398 )?
8399 .0
8400 .data)
8401}
8402
8403#[allow(clippy::too_many_arguments)]
8413pub(crate) fn load_worktree_side_for_diff(
8414 odb: &Odb,
8415 work_tree: &Path,
8416 rel_path: &str,
8417 mode: u32,
8418 config: &crate::config::ConfigSet,
8419 index_entry: Option<&IndexEntry>,
8420 worktree_rules: Option<&crate::worktree_rules::WorktreeRules>,
8421 force_read_content: bool,
8422) -> Result<(super::structured::SideBytes, bool)> {
8423 use super::tree_content::{side_bytes_after_blob_read, treat_side_as_binary_without_reading};
8424 let file_path = crate::repo_relpath::worktree_abs(work_tree, rel_path);
8425 let rel_match = crate::repo_relpath::as_utf8_for_matching(rel_path);
8426 let meta = fs::symlink_metadata(&file_path).map_err(|source| {
8427 crate::worktree::map_worktree_io(
8428 rel_match.as_ref(),
8429 crate::worktree::WorktreePathOperation::Metadata,
8430 source,
8431 )
8432 })?;
8433 if mode == MODE_SYMLINK && meta.file_type().is_symlink() {
8434 let target = fs::read_link(&file_path).map_err(|source| {
8435 crate::worktree::map_worktree_io(
8436 rel_match.as_ref(),
8437 crate::worktree::WorktreePathOperation::Read,
8438 source,
8439 )
8440 })?;
8441 return Ok((
8442 super::structured::SideBytes::from_bytes(symlink_target_bytes(&target)),
8443 false,
8444 ));
8445 }
8446 if !force_read_content
8447 && treat_side_as_binary_without_reading(odb, meta.len(), Some(rel_match.as_ref()), false)?
8448 {
8449 return Ok((
8450 super::structured::SideBytes::empty_with_size(meta.len()),
8451 true,
8452 ));
8453 }
8454 let data = if let Some(rules) = worktree_rules {
8455 worktree_blob_bytes(
8456 odb,
8457 &file_path,
8458 &meta,
8459 rules.conversion(),
8460 &rules.file_attrs(rel_match.as_ref(), meta.is_dir()),
8461 rel_match.as_ref(),
8462 index_entry,
8463 Some(rules.filter_process()),
8464 )?
8465 } else {
8466 let conv = crate::crlf::ConversionConfig::from_config(config);
8467 let attrs = crate::crlf::load_gitattributes(work_tree);
8468 let file_attrs =
8469 crate::crlf::get_file_attrs(&attrs, rel_match.as_ref(), meta.is_dir(), config);
8470 worktree_blob_bytes(
8471 odb,
8472 &file_path,
8473 &meta,
8474 &conv,
8475 &file_attrs,
8476 rel_match.as_ref(),
8477 index_entry,
8478 None,
8479 )?
8480 };
8481 Ok(side_bytes_after_blob_read(data, force_read_content))
8482}
8483
8484pub(crate) fn symlink_target_bytes(target: &Path) -> Vec<u8> {
8490 #[cfg(unix)]
8491 {
8492 use std::os::unix::ffi::OsStrExt as _;
8493 target.as_os_str().as_bytes().to_vec()
8494 }
8495 #[cfg(not(unix))]
8496 {
8497 target.as_os_str().to_string_lossy().as_bytes().to_vec()
8498 }
8499}
8500
8501pub(crate) enum WorktreeBlobContent {
8508 Bytes(Vec<u8>),
8510 Stream {
8512 file: fs::File,
8514 size: u64,
8516 },
8517}
8518
8519impl WorktreeBlobContent {
8520 pub fn hash(self, odb: &Odb) -> Result<ObjectId> {
8522 match self {
8523 Self::Bytes(data) => odb.hash(ObjectKind::Blob, &data),
8524 Self::Stream { mut file, size } => odb.hash_stream(ObjectKind::Blob, size, &mut file),
8525 }
8526 }
8527
8528 pub fn store(self, odb: &Odb, options: crate::odb::WriteOptions) -> Result<ObjectId> {
8530 match self {
8531 Self::Bytes(data) => odb.write_with_options(ObjectKind::Blob, &data, options),
8532 Self::Stream { mut file, size } => {
8533 odb.write_stream(ObjectKind::Blob, size, &mut file, options)
8534 }
8535 }
8536 }
8537
8538 fn object_id(self, odb: &Odb, store: Option<crate::odb::WriteOptions>) -> Result<ObjectId> {
8540 match store {
8541 Some(options) => self.store(odb, options),
8542 None => self.hash(odb),
8543 }
8544 }
8545}
8546
8547#[allow(clippy::too_many_arguments)]
8549fn read_regular_worktree_bytes(
8550 odb: &Odb,
8551 mut file: fs::File,
8552 size: u64,
8553 conv: &crate::crlf::ConversionConfig,
8554 file_attrs: &crate::crlf::FileAttrs,
8555 rel_path: &str,
8556 index_entry: Option<&IndexEntry>,
8557 filter_process: Option<&crate::filter_process::FilterProcessState>,
8558) -> Result<Vec<u8>> {
8559 let needs_index_blob =
8560 index_entry.is_some_and(|_| crate::crlf::clean_uses_autocrlf_index_guard(file_attrs, conv));
8561 let prior_blob: Option<Vec<u8>> = if needs_index_blob {
8562 index_entry
8563 .filter(|e| e.oid != zero_oid())
8564 .and_then(|e| odb.read(&e.oid).ok().map(|o| o.data))
8565 } else {
8566 None
8567 };
8568 let mut raw = Vec::with_capacity(usize::try_from(size).unwrap_or_default());
8569 std::io::Read::read_to_end(&mut file, &mut raw).map_err(|source| {
8570 crate::worktree::map_worktree_io(
8571 rel_path,
8572 crate::worktree::WorktreePathOperation::Read,
8573 source,
8574 )
8575 })?;
8576 #[cfg(test)]
8577 odb.hot_path_metrics().record_blob_content_read();
8578 let opts = crate::crlf::ConvertToGitOpts {
8579 index_blob: prior_blob.as_deref(),
8580 renormalize: false,
8581 check_safecrlf: false,
8582 filter_process,
8583 command_runner: None,
8584 };
8585 Ok(crate::crlf::convert_to_git_owned(
8586 raw, rel_path, conv, file_attrs, opts,
8587 )?)
8588}
8589
8590fn open_with_size(path: &Path, rel_path: &str) -> Result<(fs::File, u64)> {
8591 let file = fs::File::open(path).map_err(|source| {
8592 crate::worktree::map_worktree_io(
8593 rel_path,
8594 crate::worktree::WorktreePathOperation::Read,
8595 source,
8596 )
8597 })?;
8598 let size = file
8599 .metadata()
8600 .map_err(|source| {
8601 crate::worktree::map_worktree_io(
8602 rel_path,
8603 crate::worktree::WorktreePathOperation::Metadata,
8604 source,
8605 )
8606 })?
8607 .len();
8608 Ok((file, size))
8609}
8610
8611#[allow(clippy::too_many_arguments)]
8614fn read_regular_worktree_blob(
8615 odb: &Odb,
8616 path: &Path,
8617 conv: &crate::crlf::ConversionConfig,
8618 file_attrs: &crate::crlf::FileAttrs,
8619 rel_path: &str,
8620 index_entry: Option<&IndexEntry>,
8621 filter_process: Option<&crate::filter_process::FilterProcessState>,
8622 stream_from: u64,
8623) -> Result<WorktreeBlobContent> {
8624 let (mut file, size) = open_with_size(path, rel_path)?;
8625 if size >= stream_from
8626 && crate::crlf::worktree_allows_raw_byte_stream(
8627 conv,
8628 file_attrs,
8629 index_entry,
8630 &mut file,
8631 size,
8632 )?
8633 {
8634 #[cfg(test)]
8635 odb.hot_path_metrics().record_blob_content_read();
8636 return Ok(WorktreeBlobContent::Stream { file, size });
8637 }
8638 read_regular_worktree_bytes(
8639 odb,
8640 file,
8641 size,
8642 conv,
8643 file_attrs,
8644 rel_path,
8645 index_entry,
8646 filter_process,
8647 )
8648 .map(WorktreeBlobContent::Bytes)
8649}
8650
8651#[allow(clippy::too_many_arguments)]
8654pub(crate) fn worktree_blob_content(
8655 odb: &Odb,
8656 path: &Path,
8657 meta: &fs::Metadata,
8658 conv: &crate::crlf::ConversionConfig,
8659 file_attrs: &crate::crlf::FileAttrs,
8660 rel_path: &str,
8661 index_entry: Option<&IndexEntry>,
8662 filter_process: Option<&crate::filter_process::FilterProcessState>,
8663) -> Result<WorktreeBlobContent> {
8664 worktree_blob_content_from(
8665 odb,
8666 path,
8667 meta,
8668 conv,
8669 file_attrs,
8670 rel_path,
8671 index_entry,
8672 filter_process,
8673 hash_stream_threshold(odb)?,
8674 )
8675}
8676
8677#[allow(clippy::too_many_arguments)]
8679fn worktree_blob_content_from(
8680 odb: &Odb,
8681 path: &Path,
8682 meta: &fs::Metadata,
8683 conv: &crate::crlf::ConversionConfig,
8684 file_attrs: &crate::crlf::FileAttrs,
8685 rel_path: &str,
8686 index_entry: Option<&IndexEntry>,
8687 filter_process: Option<&crate::filter_process::FilterProcessState>,
8688 stream_from: u64,
8689) -> Result<WorktreeBlobContent> {
8690 if meta.file_type().is_symlink() {
8691 let target = fs::read_link(path).map_err(|source| {
8692 crate::worktree::map_worktree_io(
8693 rel_path,
8694 crate::worktree::WorktreePathOperation::Read,
8695 source,
8696 )
8697 })?;
8698 return Ok(WorktreeBlobContent::Bytes(symlink_target_bytes(&target)));
8699 }
8700 if meta.is_dir() {
8701 return Ok(WorktreeBlobContent::Bytes(Vec::new()));
8702 }
8703 read_regular_worktree_blob(
8704 odb,
8705 path,
8706 conv,
8707 file_attrs,
8708 rel_path,
8709 index_entry,
8710 filter_process,
8711 stream_from,
8712 )
8713}
8714
8715#[allow(clippy::too_many_arguments)]
8718pub(crate) fn worktree_blob_bytes(
8719 odb: &Odb,
8720 path: &Path,
8721 meta: &fs::Metadata,
8722 conv: &crate::crlf::ConversionConfig,
8723 file_attrs: &crate::crlf::FileAttrs,
8724 rel_path: &str,
8725 index_entry: Option<&IndexEntry>,
8726 filter_process: Option<&crate::filter_process::FilterProcessState>,
8727) -> Result<Vec<u8>> {
8728 if meta.file_type().is_symlink() {
8729 let target = fs::read_link(path).map_err(|source| {
8730 crate::worktree::map_worktree_io(
8731 rel_path,
8732 crate::worktree::WorktreePathOperation::Read,
8733 source,
8734 )
8735 })?;
8736 return Ok(symlink_target_bytes(&target));
8737 }
8738 if meta.is_dir() {
8739 return Ok(Vec::new());
8740 }
8741 let (file, size) = open_with_size(path, rel_path)?;
8742 read_regular_worktree_bytes(
8743 odb,
8744 file,
8745 size,
8746 conv,
8747 file_attrs,
8748 rel_path,
8749 index_entry,
8750 filter_process,
8751 )
8752}
8753
8754#[allow(clippy::too_many_arguments)]
8755fn worktree_file_oid(
8756 odb: &Odb,
8757 path: &Path,
8758 meta: &fs::Metadata,
8759 conv: &crate::crlf::ConversionConfig,
8760 file_attrs: &crate::crlf::FileAttrs,
8761 rel_path: &str,
8762 index_entry: Option<&IndexEntry>,
8763 materialize: bool,
8764 filter_process: Option<&crate::filter_process::FilterProcessState>,
8765) -> Result<ObjectId> {
8766 let stream_from = hash_stream_threshold(odb)?;
8767 worktree_blob_content_from(
8768 odb,
8769 path,
8770 meta,
8771 conv,
8772 file_attrs,
8773 rel_path,
8774 index_entry,
8775 filter_process,
8776 stream_from,
8777 )?
8778 .object_id(odb, materialize.then(|| odb.bulk_stage_write_options()))
8779}
8780
8781#[allow(clippy::too_many_arguments)]
8792pub fn hash_worktree_file(
8793 odb: &Odb,
8794 path: &Path,
8795 meta: &fs::Metadata,
8796 conv: &crate::crlf::ConversionConfig,
8797 file_attrs: &crate::crlf::FileAttrs,
8798 rel_path: &str,
8799 index_entry: Option<&IndexEntry>,
8800 filter_process: Option<&crate::filter_process::FilterProcessState>,
8801) -> Result<ObjectId> {
8802 worktree_file_oid(
8803 odb,
8804 path,
8805 meta,
8806 conv,
8807 file_attrs,
8808 rel_path,
8809 index_entry,
8810 false,
8811 filter_process,
8812 )
8813}
8814
8815const STREAMED_HASH_MIN_SIZE: u64 = 1024 * 1024;
8818
8819fn hash_stream_threshold(odb: &Odb) -> Result<u64> {
8821 Ok(odb.big_file_threshold()?.min(STREAMED_HASH_MIN_SIZE))
8822}
8823
8824#[allow(clippy::too_many_arguments)]
8828pub(crate) fn hash_worktree_file_with_mode(
8829 odb: &Odb,
8830 path: &Path,
8831 worktree_mode: u32,
8832 conv: &crate::crlf::ConversionConfig,
8833 file_attrs: &crate::crlf::FileAttrs,
8834 rel_path: &str,
8835 index_entry: Option<&IndexEntry>,
8836 filter_process: Option<&crate::filter_process::FilterProcessState>,
8837) -> Result<ObjectId> {
8838 if worktree_mode == MODE_SYMLINK {
8839 let data = read_worktree_bytes_for_mode(path, worktree_mode)?;
8840 return odb.hash(ObjectKind::Blob, &data);
8841 }
8842 read_regular_worktree_blob(
8843 odb,
8844 path,
8845 conv,
8846 file_attrs,
8847 rel_path,
8848 index_entry,
8849 filter_process,
8850 hash_stream_threshold(odb)?,
8851 )?
8852 .hash(odb)
8853}
8854
8855#[allow(clippy::too_many_arguments)]
8863pub(crate) fn materialize_worktree_blob(
8864 odb: &Odb,
8865 path: &Path,
8866 meta: &fs::Metadata,
8867 conv: &crate::crlf::ConversionConfig,
8868 file_attrs: &crate::crlf::FileAttrs,
8869 rel_path: &str,
8870 index_entry: Option<&IndexEntry>,
8871 filter_process: Option<&crate::filter_process::FilterProcessState>,
8872) -> Result<ObjectId> {
8873 worktree_file_oid(
8874 odb,
8875 path,
8876 meta,
8877 conv,
8878 file_attrs,
8879 rel_path,
8880 index_entry,
8881 true,
8882 filter_process,
8883 )
8884}
8885
8886pub fn mode_from_metadata(meta: &fs::Metadata) -> u32 {
8888 if meta.file_type().is_symlink() {
8889 0o120000
8890 } else {
8891 #[cfg(unix)]
8892 {
8893 if meta.mode() & 0o111 != 0 {
8894 return 0o100755;
8895 }
8896 }
8897 0o100644
8898 }
8899}
8900
8901pub fn diff_tree_to_worktree(
8918 odb: &Odb,
8919 tree_oid: Option<&ObjectId>,
8920 work_tree: &Path,
8921 index: &Index,
8922) -> Result<Vec<DiffEntry>> {
8923 diff_tree_to_worktree_with_git_dir(
8924 odb,
8925 tree_oid,
8926 work_tree,
8927 &work_tree.join(".git"),
8928 index,
8929 None,
8930 )
8931}
8932
8933pub fn diff_tree_to_worktree_with_git_dir(
8937 odb: &Odb,
8938 tree_oid: Option<&ObjectId>,
8939 work_tree: &Path,
8940 repository_git_dir: &Path,
8941 index: &Index,
8942 config: Option<&ConfigSet>,
8943) -> Result<Vec<DiffEntry>> {
8944 diff_tree_to_worktree_with_git_dir_and_rules(
8945 odb,
8946 tree_oid,
8947 work_tree,
8948 repository_git_dir,
8949 index,
8950 config,
8951 None,
8952 )
8953}
8954
8955pub fn diff_tree_to_worktree_with_git_dir_and_rules(
8957 odb: &Odb,
8958 tree_oid: Option<&ObjectId>,
8959 work_tree: &Path,
8960 repository_git_dir: &Path,
8961 index: &Index,
8962 config: Option<&ConfigSet>,
8963 worktree_rules: Option<&crate::worktree_rules::WorktreeRules>,
8964) -> Result<Vec<DiffEntry>> {
8965 diff_tree_to_worktree_with_git_dir_and_rules_extras(
8966 odb,
8967 tree_oid,
8968 work_tree,
8969 repository_git_dir,
8970 index,
8971 config,
8972 worktree_rules,
8973 DiffTreeToWorktreeExtras::default(),
8974 )
8975}
8976
8977#[allow(clippy::too_many_arguments)]
8979pub fn diff_tree_to_worktree_with_git_dir_and_rules_extras(
8980 odb: &Odb,
8981 tree_oid: Option<&ObjectId>,
8982 work_tree: &Path,
8983 repository_git_dir: &Path,
8984 index: &Index,
8985 config: Option<&ConfigSet>,
8986 worktree_rules: Option<&crate::worktree_rules::WorktreeRules>,
8987 extras: DiffTreeToWorktreeExtras,
8988) -> Result<Vec<DiffEntry>> {
8989 use crate::config::ConfigSet;
8990 use crate::crlf;
8991
8992 if worktree_rules.is_none() {
8993 if let Some(tree_oid) = tree_oid {
8994 if index.cache_tree_root.as_ref() == Some(tree_oid)
8995 && crate::write_tree::cache_tree_fully_valid(odb, index.cache_tree.as_ref())
8996 {
8997 let mut idx = index.clone();
8998 let options = DiffIndexToWorktreeOptions {
8999 repository_git_dir: Some(repository_git_dir.to_path_buf()),
9000 refresh_index_stat_in_pass: false,
9001 for_status: false,
9005 config: config.cloned().map(std::sync::Arc::new),
9006 ..DiffIndexToWorktreeOptions::default()
9007 };
9008 let (entries, _, _) = diff_index_to_worktree_inner(
9009 odb,
9010 &mut idx,
9011 work_tree,
9012 repository_git_dir,
9013 options,
9014 false,
9015 None,
9016 )?;
9017 return Ok(entries);
9018 }
9019 }
9020 }
9021
9022 let config = worktree_rules
9023 .map(|r| r.config().clone())
9024 .unwrap_or_else(|| {
9025 config.cloned().unwrap_or_else(|| {
9026 ConfigSet::load(
9027 &crate::environment::Environment::empty(),
9028 Some(repository_git_dir),
9029 true,
9030 )
9031 .unwrap_or_else(|_| ConfigSet::new())
9032 })
9033 });
9034 let conv = worktree_rules
9035 .map(|r| r.conversion().clone())
9036 .unwrap_or_else(|| crlf::ConversionConfig::from_config(&config));
9037 let legacy_attrs = if worktree_rules.is_none() {
9038 Some(crlf::load_gitattributes_parsed(work_tree))
9039 } else {
9040 None
9041 };
9042 let file_attrs_for = |path: &str| -> crlf::FileAttrs {
9043 if let Some(rules) = worktree_rules {
9044 rules.file_attrs(path, false)
9045 } else if let Some(parsed) = legacy_attrs.as_ref() {
9046 crlf::file_attrs_from_gitattributes(&parsed.rules, &parsed.macros, path, false, &config)
9047 } else {
9048 crlf::FileAttrs::default()
9049 }
9050 };
9051 let filter_fp = worktree_rules.map(|r| r.filter_process());
9052
9053 let sparse_index = index.has_sparse_directory_placeholders();
9054 let tree_flat = match tree_oid {
9056 Some(oid) if sparse_index => flatten_tree_with_index(odb, oid, "", Some(index))?,
9057 Some(oid) => flatten_tree(odb, oid, "")?,
9058 None => Vec::new(),
9059 };
9060 let tree_map: std::collections::BTreeMap<String, &FlatEntry> =
9061 tree_flat.iter().map(|e| (e.path.clone(), e)).collect();
9062
9063 let mut index_entries: std::collections::BTreeMap<&[u8], &IndexEntry> =
9065 std::collections::BTreeMap::new();
9066 let mut index_paths: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
9067 let mut stage0_paths: std::collections::BTreeSet<Vec<u8>> = std::collections::BTreeSet::new();
9068 for ie in &index.entries {
9069 if ie.stage() != 0 {
9070 continue;
9071 }
9072 let path = String::from_utf8_lossy(&ie.path).to_string();
9073 index_entries.insert(&ie.path, ie);
9074 index_paths.insert(path);
9075 stage0_paths.insert(ie.path.clone());
9076 }
9077
9078 let mut unmerged_only_paths: std::collections::BTreeSet<String> =
9081 std::collections::BTreeSet::new();
9082 for ie in &index.entries {
9083 if !(1..=3).contains(&ie.stage()) {
9084 continue;
9085 }
9086 if stage0_paths.contains(&ie.path) {
9087 continue;
9088 }
9089 unmerged_only_paths.insert(String::from_utf8_lossy(&ie.path).into_owned());
9090 }
9091
9092 let mut all_paths: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
9094 all_paths.extend(tree_map.keys().cloned());
9095 all_paths.extend(index_paths.iter().cloned());
9096 all_paths.extend(unmerged_only_paths.iter().cloned());
9097
9098 let index_mtime = index_mtime_for_diff(index, None);
9099 let stat_policy = crate::index::StatMatchPolicy::from_config(Some(&config));
9100 let mode_policy = crate::index::WorktreeModePolicy::from_config(&config);
9101 let mut result = Vec::new();
9102
9103 for path in &all_paths {
9104 if extras
9105 .restrict_to_paths
9106 .as_ref()
9107 .is_some_and(|limit| !limit.contains(path))
9108 {
9109 continue;
9110 }
9111 if index_entries
9112 .get(path.as_bytes())
9113 .is_some_and(|ie| ie.skip_worktree())
9114 {
9115 continue;
9118 }
9119 if index_entries
9120 .get(path.as_bytes())
9121 .is_some_and(|ie| ie.assume_unchanged())
9122 {
9123 continue;
9124 }
9125
9126 let tree_entry = tree_map.get(path.as_str());
9127
9128 let is_gitlink = tree_entry.is_some_and(|te| te.mode == 0o160000)
9130 || index_entries
9131 .get(path.as_bytes())
9132 .is_some_and(|ie| ie.mode == 0o160000);
9133 if is_gitlink {
9134 let sub_dir = work_tree.join(path);
9135 let index_gitlink_oid = index_entries
9136 .get(path.as_bytes())
9137 .filter(|ie| ie.mode == 0o160000)
9138 .map(|ie| ie.oid);
9139 match (tree_entry, index_gitlink_oid) {
9140 (Some(te), _) => {
9141 let sub_head = read_submodule_head_oid(&sub_dir);
9142 if sub_head.is_none() && !sub_dir.exists() {
9146 result.push(DiffEntry {
9147 status: DiffStatus::Deleted,
9148 old_path: Some(path.clone()),
9149 new_path: Some(path.clone()),
9150 old_mode: FileMode::new(te.mode),
9151 new_mode: FileMode::ABSENT,
9152 old_oid: te.oid,
9153 new_oid: zero_oid(),
9154 score: None,
9155 });
9156 continue;
9157 }
9158 let ref_matches = if let Some(head) = sub_head {
9159 head == te.oid
9160 } else {
9161 uninitialized_gitlink_worktree_matches_index(&sub_dir)
9162 };
9163 let index_matches_tree = index_gitlink_oid.is_some_and(|oid| oid == te.oid);
9164 let sub_flags = submodule_porcelain_flags(work_tree, path, te.oid);
9167 let ignore = crate::submodule_ignore::submodule_ignore_for_path(
9168 &config, work_tree, path,
9169 );
9170 if index_matches_tree
9171 && crate::submodule_ignore::gitlink_worktree_change_suppressed(
9172 ignore,
9173 ref_matches,
9174 sub_flags,
9175 )
9176 {
9177 continue;
9178 }
9179 let dirty_while_aligned =
9180 index_matches_tree && ref_matches && sub_flags.modified;
9181 if !ref_matches || dirty_while_aligned {
9182 let new_oid = if !ref_matches {
9186 sub_head.unwrap_or_else(zero_oid)
9187 } else {
9188 zero_oid()
9189 };
9190 result.push(DiffEntry {
9191 status: DiffStatus::Modified,
9192 old_path: Some(path.clone()),
9193 new_path: Some(path.clone()),
9194 old_mode: FileMode::new(te.mode),
9195 new_mode: FileMode::new(te.mode),
9196 old_oid: te.oid,
9197 new_oid,
9198 score: None,
9199 });
9200 }
9201 }
9202 (None, Some(idx_oid)) => {
9203 let new_oid = read_submodule_head_oid(&sub_dir).unwrap_or(idx_oid);
9208 result.push(DiffEntry {
9209 status: DiffStatus::Added,
9210 old_path: Some(path.clone()),
9211 new_path: Some(path.clone()),
9212 old_mode: FileMode::ABSENT,
9213 new_mode: FileMode::new(0o160000),
9214 old_oid: zero_oid(),
9215 new_oid,
9216 score: None,
9217 });
9218 }
9219 (None, None) => {}
9220 }
9221 continue;
9222 }
9223
9224 let file_path = work_tree.join(path);
9225
9226 let wt_meta = match probe_worktree_symlink_metadata(
9227 &file_path,
9228 path,
9229 extras.diagnostics.as_ref(),
9230 false,
9231 extras.skip_inaccessible_paths,
9232 false,
9233 )? {
9234 WorktreeSymlinkMetadata::Found(m) => Some(m),
9235 WorktreeSymlinkMetadata::Inaccessible => continue,
9236 WorktreeSymlinkMetadata::Absent => None,
9237 };
9238
9239 if unmerged_only_paths.contains(path) {
9240 if let (Some(te), Some(meta)) = (tree_entry, wt_meta.as_ref()) {
9241 let tree_mode = crate::index::canon_mode(te.mode);
9242 let file_attrs = file_attrs_for(path);
9243 let wt_oid = hash_worktree_file(
9244 odb,
9245 &file_path,
9246 meta,
9247 &conv,
9248 &file_attrs,
9249 path,
9250 None,
9251 filter_fp,
9252 )?;
9253 let wt_mode =
9254 mode_policy.index_mode_for_worktree(mode_from_metadata(meta), Some(tree_mode));
9255 if wt_oid != te.oid || wt_mode != tree_mode {
9256 result.push(DiffEntry {
9257 status: DiffStatus::for_content_change(tree_mode, wt_mode),
9258 old_path: Some(path.clone()),
9259 new_path: Some(path.clone()),
9260 old_mode: FileMode::new(tree_mode),
9261 new_mode: FileMode::new(wt_mode),
9262 old_oid: te.oid,
9263 new_oid: wt_oid,
9264 score: None,
9265 });
9266 }
9267 }
9268 continue;
9269 }
9270
9271 match (tree_entry, wt_meta) {
9272 (Some(te), Some(ref meta)) => {
9273 let tree_mode = crate::index::canon_mode(te.mode);
9274 let Some(ie) = index_entries.get(path.as_bytes()) else {
9275 continue;
9276 };
9277 let wt_mode =
9278 mode_policy.index_mode_for_worktree(mode_from_metadata(meta), Some(ie.mode));
9279
9280 let index_matches_tree = ie.oid == te.oid && ie.mode == tree_mode;
9281
9282 let stat_trusted = stat_matches_with_policy(ie, meta, stat_policy)
9284 && !entry_is_racy_with_policy(ie, index_mtime, stat_policy);
9285 if index_matches_tree && stat_trusted && wt_mode == tree_mode {
9286 continue;
9287 }
9288
9289 let file_attrs = file_attrs_for(path);
9290 let idx_ent = index_entries.get(path.as_bytes()).copied();
9291
9292 if ie.oid == te.oid && ie.mode != tree_mode && stat_trusted && wt_mode == ie.mode {
9297 result.push(DiffEntry {
9298 status: DiffStatus::for_content_change(tree_mode, ie.mode),
9299 old_path: Some(path.clone()),
9300 new_path: Some(path.clone()),
9301 old_mode: FileMode::new(tree_mode),
9302 new_mode: FileMode::new(ie.mode),
9303 old_oid: te.oid,
9304 new_oid: te.oid,
9305 score: None,
9306 });
9307 continue;
9308 }
9309
9310 if index_matches_tree {
9313 let wt_oid = hash_worktree_file(
9314 odb,
9315 &file_path,
9316 meta,
9317 &conv,
9318 &file_attrs,
9319 path,
9320 None,
9321 filter_fp,
9322 )?;
9323 if wt_oid != te.oid {
9324 result.push(DiffEntry {
9325 status: DiffStatus::for_content_change(tree_mode, wt_mode),
9326 old_path: Some(path.clone()),
9327 new_path: Some(path.clone()),
9328 old_mode: FileMode::new(tree_mode),
9329 new_mode: FileMode::new(wt_mode),
9330 old_oid: te.oid,
9331 new_oid: zero_oid(),
9334 score: None,
9335 });
9336 } else if wt_mode != tree_mode {
9337 result.push(DiffEntry {
9338 status: DiffStatus::for_content_change(tree_mode, wt_mode),
9339 old_path: Some(path.clone()),
9340 new_path: Some(path.clone()),
9341 old_mode: FileMode::new(tree_mode),
9342 new_mode: FileMode::new(wt_mode),
9343 old_oid: te.oid,
9344 new_oid: te.oid,
9345 score: None,
9346 });
9347 }
9348 continue;
9349 }
9350
9351 let wt_oid = hash_worktree_file(
9353 odb,
9354 &file_path,
9355 meta,
9356 &conv,
9357 &file_attrs,
9358 path,
9359 idx_ent,
9360 filter_fp,
9361 )?;
9362 if wt_oid != te.oid || wt_mode != tree_mode {
9363 let new_oid = if wt_oid != te.oid { zero_oid() } else { te.oid };
9364 result.push(DiffEntry {
9365 status: DiffStatus::for_content_change(tree_mode, wt_mode),
9366 old_path: Some(path.clone()),
9367 new_path: Some(path.clone()),
9368 old_mode: FileMode::new(tree_mode),
9369 new_mode: FileMode::new(wt_mode),
9370 old_oid: te.oid,
9371 new_oid,
9372 score: None,
9373 });
9374 }
9375 }
9376 (Some(te), None) => {
9377 let tree_mode = crate::index::canon_mode(te.mode);
9378 result.push(DiffEntry {
9380 status: DiffStatus::Deleted,
9381 old_path: Some(path.clone()),
9382 new_path: None,
9383 old_mode: FileMode::new(tree_mode),
9384 new_mode: FileMode::ABSENT,
9385 old_oid: te.oid,
9386 new_oid: zero_oid(),
9387 score: None,
9388 });
9389 }
9390 (None, Some(ref meta)) => {
9391 let file_attrs = file_attrs_for(path);
9393 let idx_ent = index_entries.get(path.as_bytes()).copied();
9394 let wt_oid = materialize_worktree_blob(
9395 odb,
9396 &file_path,
9397 meta,
9398 &conv,
9399 &file_attrs,
9400 path,
9401 idx_ent,
9402 filter_fp,
9403 )?;
9404 let wt_mode = mode_from_metadata(meta);
9405 result.push(DiffEntry {
9406 status: DiffStatus::Added,
9407 old_path: None,
9408 new_path: Some(path.clone()),
9409 old_mode: FileMode::ABSENT,
9410 new_mode: FileMode::new(wt_mode),
9411 old_oid: zero_oid(),
9412 new_oid: wt_oid,
9413 score: None,
9414 });
9415 }
9416 (None, None) => {
9417 }
9419 }
9420 }
9421
9422 sort_entries_by_path(&mut result);
9423 Ok(result)
9424}
9425
9426#[derive(Debug, Clone, PartialEq, Eq)]
9430pub struct InexactRenameDetectionSkipped {
9431 pub unmatched_sources: usize,
9433 pub unmatched_destinations: usize,
9435 pub configured_limit: Option<usize>,
9437 pub suggested_limit: usize,
9439}
9440
9441#[derive(Debug, Clone, Default, PartialEq, Eq)]
9443pub struct DiffEntriesResult {
9444 pub entries: Vec<DiffEntry>,
9446 pub inexact_rename_skipped: Option<InexactRenameDetectionSkipped>,
9448}
9449
9450#[derive(Debug, Clone, PartialEq, Eq)]
9452pub struct RenameDetectionResult {
9453 pub entries: Vec<DiffEntry>,
9455 pub inexact_skipped: Option<InexactRenameDetectionSkipped>,
9457}
9458
9459#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
9461pub struct DetectRenamesOptions {
9462 pub skip_basename_heuristic: bool,
9464}
9465
9466fn read_added_entry_bytes(
9467 odb: &Odb,
9468 entry: &DiffEntry,
9469 work_root: Option<&Path>,
9470) -> Option<Vec<u8>> {
9471 if entry.new_oid != zero_oid() {
9472 if let Ok(obj) = odb.read(&entry.new_oid) {
9473 return Some(obj.data);
9474 }
9475 }
9476 let path = entry.new_path.as_deref()?;
9477 let root = work_root?;
9478 fs::read(root.join(path)).ok()
9479}
9480
9481#[allow(clippy::expect_used)]
9493pub(crate) fn detect_renames(
9494 odb: &Odb,
9495 work_root: Option<&Path>,
9496 entries: Vec<DiffEntry>,
9497 threshold: u32,
9498) -> Vec<DiffEntry> {
9499 let config = ConfigSet::new();
9500 let policy = ThreadPolicy::default();
9501 detect_renames_with_policy(
9502 odb,
9503 work_root,
9504 entries,
9505 threshold,
9506 &config,
9507 &policy,
9508 None,
9509 DetectRenamesOptions::default(),
9510 )
9511 .expect("rename detection")
9512 .entries
9513}
9514
9515#[inline]
9516fn poll_rename_cancel(
9517 cancel_watch: Option<&std::sync::atomic::AtomicBool>,
9518) -> crate::error::Result<()> {
9519 if let Some(watch) = cancel_watch {
9520 crate::progress::poll_watch(watch)?;
9521 }
9522 Ok(())
9523}
9524
9525#[allow(clippy::too_many_arguments)]
9536pub fn detect_renames_with_policy(
9537 odb: &Odb,
9538 work_root: Option<&Path>,
9539 entries: Vec<DiffEntry>,
9540 threshold: u32,
9541 config: &ConfigSet,
9542 thread_policy: &ThreadPolicy,
9543 cancel_watch: Option<&std::sync::atomic::AtomicBool>,
9544 detect_opts: DetectRenamesOptions,
9545) -> Result<RenameDetectionResult> {
9546 poll_rename_cancel(cancel_watch)?;
9547 let has_deleted = entries.iter().any(|e| e.status == DiffStatus::Deleted);
9548 let has_added = entries.iter().any(|e| e.status == DiffStatus::Added);
9549 if (!has_deleted || !has_added) && entries_strictly_sorted_by_path(&entries) {
9550 return Ok(RenameDetectionResult {
9552 entries,
9553 inexact_skipped: None,
9554 });
9555 }
9556 let mut deleted: Vec<DiffEntry> = Vec::new();
9558 let mut added: Vec<DiffEntry> = Vec::new();
9559 let mut others: Vec<DiffEntry> = Vec::new();
9560
9561 for entry in entries {
9562 match entry.status {
9563 DiffStatus::Deleted => deleted.push(entry),
9564 DiffStatus::Added => added.push(entry),
9565 _ => others.push(entry),
9566 }
9567 }
9568
9569 if deleted.is_empty() || added.is_empty() {
9570 let mut result = others;
9572 result.extend(deleted);
9573 result.extend(added);
9574 sort_entries_by_path(&mut result);
9575 return Ok(RenameDetectionResult {
9576 entries: result,
9577 inexact_skipped: None,
9578 });
9579 }
9580
9581 fn is_regularish_mode(mode: FileMode) -> bool {
9582 mode == FileMode::REGULAR || mode == FileMode::EXECUTABLE
9583 }
9584
9585 fn same_path_same_blob(del: &DiffEntry, add: &DiffEntry) -> bool {
9586 del.old_path == add.new_path && del.old_oid == add.new_oid && del.old_mode == add.new_mode
9587 }
9588
9589 fn sort_rename_scores(
9590 scores: &mut [(u32, usize, usize)],
9591 deleted: &[DiffEntry],
9592 added: &[DiffEntry],
9593 ) {
9594 scores.sort_by(|a, b| {
9595 let a_noop = same_path_same_blob(&deleted[a.1], &added[a.2]);
9596 let b_noop = same_path_same_blob(&deleted[b.1], &added[b.2]);
9597 let a_same = same_basename(&deleted[a.1], &added[a.2]);
9598 let b_same = same_basename(&deleted[b.1], &added[b.2]);
9599 a_noop
9600 .cmp(&b_noop)
9601 .then_with(|| b_same.cmp(&a_same))
9602 .then_with(|| b.0.cmp(&a.0))
9603 });
9604 }
9605
9606 fn apply_rename_scores(
9607 scores: &[(u32, usize, usize)],
9608 deleted: &[DiffEntry],
9609 added: &[DiffEntry],
9610 used_deleted: &mut [bool],
9611 used_added: &mut [bool],
9612 renames: &mut Vec<DiffEntry>,
9613 ) {
9614 for (score, di, ai) in scores {
9615 if used_deleted[*di] || used_added[*ai] {
9616 continue;
9617 }
9618 used_deleted[*di] = true;
9619 used_added[*ai] = true;
9620
9621 let del = &deleted[*di];
9622 let add = &added[*ai];
9623 if same_path_same_blob(del, add) {
9624 continue;
9625 }
9626
9627 renames.push(DiffEntry {
9628 status: DiffStatus::Renamed,
9629 old_path: del.old_path.clone(),
9630 new_path: add.new_path.clone(),
9631 old_mode: del.old_mode,
9632 new_mode: add.new_mode,
9633 old_oid: del.old_oid,
9634 new_oid: add.new_oid,
9635 score: Some(*score),
9636 });
9637 }
9638 }
9639
9640 let mut used_deleted = vec![false; deleted.len()];
9641 let mut used_added = vec![false; added.len()];
9642 let mut renames: Vec<DiffEntry> = Vec::new();
9643
9644 let mut added_by_oid: std::collections::HashMap<ObjectId, Vec<usize>> =
9646 std::collections::HashMap::new();
9647 for (ai, add) in added.iter().enumerate() {
9648 added_by_oid.entry(add.new_oid).or_default().push(ai);
9649 }
9650 let mut exact_scores: Vec<(u32, usize, usize)> = Vec::new();
9651 for (di, del) in deleted.iter().enumerate() {
9652 poll_rename_cancel(cancel_watch)?;
9653 if let Some(ais) = added_by_oid.get(&del.old_oid) {
9654 for &ai in ais {
9655 exact_scores.push((100, di, ai));
9656 }
9657 }
9658 }
9659 sort_rename_scores(&mut exact_scores, &deleted, &added);
9660 apply_rename_scores(
9661 &exact_scores,
9662 &deleted,
9663 &added,
9664 &mut used_deleted,
9665 &mut used_added,
9666 &mut renames,
9667 );
9668
9669 let mut blob_cache = None;
9670 if !detect_opts.skip_basename_heuristic
9671 && super::basename_rename::basename_needs_similarity_scoring(
9672 &deleted,
9673 &added,
9674 &used_deleted,
9675 &used_added,
9676 )
9677 {
9678 let mut cache = super::rename_blob_cache::RenameBlobCache::new(deleted.len(), added.len());
9679 let par_opts = diff_rename_parallel_options(config, thread_policy);
9680 super::basename_rename::apply_basename_rename_pairs(
9681 odb,
9682 work_root,
9683 &deleted,
9684 &added,
9685 &mut used_deleted,
9686 &mut used_added,
9687 &mut renames,
9688 threshold,
9689 cancel_watch,
9690 &mut cache,
9691 par_opts,
9692 )?;
9693 blob_cache = Some(cache);
9694 }
9695
9696 let remaining_deleted: Vec<usize> = used_deleted
9697 .iter()
9698 .enumerate()
9699 .filter_map(|(i, used)| (!*used).then_some(i))
9700 .collect();
9701 let remaining_added: Vec<usize> = used_added
9702 .iter()
9703 .enumerate()
9704 .filter_map(|(i, used)| (!*used).then_some(i))
9705 .collect();
9706
9707 let rename_limit = parse_rename_limit(config);
9708 let mut inexact_skipped = None;
9709 if !remaining_deleted.is_empty()
9710 && !remaining_added.is_empty()
9711 && inexact_matrix_exceeds_limit(
9712 remaining_added.len(),
9713 remaining_deleted.len(),
9714 rename_limit,
9715 )
9716 {
9717 let suggested = remaining_deleted.len().max(remaining_added.len());
9718 inexact_skipped = Some(InexactRenameDetectionSkipped {
9719 unmatched_sources: remaining_deleted.len(),
9720 unmatched_destinations: remaining_added.len(),
9721 configured_limit: rename_limit.configured_max(),
9722 suggested_limit: suggested,
9723 });
9724 } else if !remaining_deleted.is_empty() && !remaining_added.is_empty() {
9725 let par_opts = diff_rename_parallel_options(config, thread_policy);
9726
9727 let mut prefetch_entries: Vec<DiffEntry> =
9728 Vec::with_capacity(remaining_deleted.len() + remaining_added.len());
9729 for &di in &remaining_deleted {
9730 prefetch_entries.push(deleted[di].clone());
9731 }
9732 for &ai in &remaining_added {
9733 prefetch_entries.push(added[ai].clone());
9734 }
9735 super::tree_content::prefetch_tree_diff_blobs(odb, &prefetch_entries)?;
9736
9737 let deleted_sizes: Vec<Option<usize>> = remaining_deleted
9738 .iter()
9739 .map(|&di| blob_payload_size(odb, &deleted[di].old_oid))
9740 .collect();
9741 let added_sizes: Vec<Option<usize>> = remaining_added
9742 .iter()
9743 .map(|&ai| added_entry_payload_size(odb, &added[ai], work_root))
9744 .collect();
9745
9746 let mut load_deleted = vec![false; remaining_deleted.len()];
9747 let mut load_added = vec![false; remaining_added.len()];
9748 let mut candidate_pairs: Vec<(usize, usize)> = Vec::new();
9749 for (row, &di) in remaining_deleted.iter().enumerate() {
9750 poll_rename_cancel(cancel_watch)?;
9751 let del = &deleted[di];
9752 if !is_regularish_mode(del.old_mode) {
9753 continue;
9754 }
9755 let Some(ds) = deleted_sizes[row] else {
9756 continue;
9757 };
9758 for (col, &ai) in remaining_added.iter().enumerate() {
9759 let add = &added[ai];
9760 if !is_regularish_mode(add.new_mode) {
9761 continue;
9762 }
9763 let Some(asz) = added_sizes[col] else {
9764 continue;
9765 };
9766 if rename_pair_size_reachable(ds, asz, threshold) {
9767 load_deleted[row] = true;
9768 load_added[col] = true;
9769 candidate_pairs.push((row, col));
9770 }
9771 }
9772 }
9773
9774 if !candidate_pairs.is_empty() {
9775 let loads_needed = load_deleted.iter().filter(|&&b| b).count()
9776 + load_added.iter().filter(|&&b| b).count();
9777 let (deleted_contents, added_contents) = if let Some(cache) = blob_cache.as_mut() {
9778 let mut deleted_staging: Vec<Option<Vec<u8>>> = vec![None; remaining_deleted.len()];
9779 for (row, &need) in load_deleted.iter().enumerate() {
9780 if !need {
9781 continue;
9782 }
9783 let di = remaining_deleted[row];
9784 deleted_staging[row] = cache
9785 .take_deleted_blob(di)
9786 .or_else(|| odb.read(&deleted[di].old_oid).ok().map(|obj| obj.data));
9787 }
9788 let mut added_staging: Vec<Option<Vec<u8>>> = vec![None; remaining_added.len()];
9789 for (col, &need) in load_added.iter().enumerate() {
9790 if !need {
9791 continue;
9792 }
9793 let ai = remaining_added[col];
9794 added_staging[col] = cache
9795 .take_added_blob(ai)
9796 .or_else(|| read_added_entry_bytes(odb, &added[ai], work_root));
9797 }
9798 let load_deleted_blob = |i: usize| -> Option<Vec<u8>> {
9799 if !load_deleted[i] {
9800 return None;
9801 }
9802 deleted_staging[i].clone()
9803 };
9804 let load_added_blob = |i: usize| -> Option<Vec<u8>> {
9805 if !load_added[i] {
9806 return None;
9807 }
9808 added_staging[i].clone()
9809 };
9810 if loads_needed <= 32 {
9811 (
9812 (0..remaining_deleted.len())
9813 .map(load_deleted_blob)
9814 .collect(),
9815 (0..remaining_added.len()).map(load_added_blob).collect(),
9816 )
9817 } else {
9818 (
9819 parallel_load_optional_blobs(
9820 odb,
9821 remaining_deleted.len(),
9822 par_opts,
9823 0,
9824 load_deleted_blob,
9825 ),
9826 parallel_load_optional_blobs(
9827 odb,
9828 remaining_added.len(),
9829 par_opts,
9830 0,
9831 load_added_blob,
9832 ),
9833 )
9834 }
9835 } else {
9836 let mut deleted_staging: Vec<Option<Vec<u8>>> = vec![None; remaining_deleted.len()];
9837 for (row, &need) in load_deleted.iter().enumerate() {
9838 if !need {
9839 continue;
9840 }
9841 let di = remaining_deleted[row];
9842 deleted_staging[row] = odb.read(&deleted[di].old_oid).ok().map(|obj| obj.data);
9843 }
9844 let mut added_staging: Vec<Option<Vec<u8>>> = vec![None; remaining_added.len()];
9845 for (col, &need) in load_added.iter().enumerate() {
9846 if !need {
9847 continue;
9848 }
9849 let ai = remaining_added[col];
9850 added_staging[col] = read_added_entry_bytes(odb, &added[ai], work_root);
9851 }
9852 let load_deleted_blob = |i: usize| -> Option<Vec<u8>> {
9853 if !load_deleted[i] {
9854 return None;
9855 }
9856 deleted_staging[i].clone()
9857 };
9858 let load_added_blob = |i: usize| -> Option<Vec<u8>> {
9859 if !load_added[i] {
9860 return None;
9861 }
9862 added_staging[i].clone()
9863 };
9864 if loads_needed <= 32 {
9865 (
9866 (0..remaining_deleted.len())
9867 .map(load_deleted_blob)
9868 .collect(),
9869 (0..remaining_added.len()).map(load_added_blob).collect(),
9870 )
9871 } else {
9872 (
9873 parallel_load_optional_blobs(
9874 odb,
9875 remaining_deleted.len(),
9876 par_opts,
9877 0,
9878 load_deleted_blob,
9879 ),
9880 parallel_load_optional_blobs(
9881 odb,
9882 remaining_added.len(),
9883 par_opts,
9884 0,
9885 load_added_blob,
9886 ),
9887 )
9888 }
9889 };
9890
9891 let est_table_bytes: usize = deleted_contents
9892 .iter()
9893 .chain(added_contents.iter())
9894 .filter_map(|opt| opt.as_ref().map(|b| b.len()))
9895 .sum();
9896 let build_deleted_table = |row: usize| -> std::result::Result<
9897 Option<RenameLineTable>,
9898 std::convert::Infallible,
9899 > {
9900 Ok(deleted_contents[row]
9901 .as_ref()
9902 .map(|d| RenameLineTable::from_blob(d)))
9903 };
9904 let build_added_table = |col: usize| -> std::result::Result<
9905 Option<RenameLineTable>,
9906 std::convert::Infallible,
9907 > {
9908 Ok(added_contents[col]
9909 .as_ref()
9910 .map(|d| RenameLineTable::from_blob(d)))
9911 };
9912 let deleted_tables: Vec<Option<RenameLineTable>> = if loads_needed <= 32 {
9913 (0..remaining_deleted.len())
9914 .map(|row| build_deleted_table(row).unwrap())
9915 .collect()
9916 } else {
9917 parallel_map_indices(
9918 odb,
9919 remaining_deleted.len(),
9920 par_opts,
9921 est_table_bytes,
9922 build_deleted_table,
9923 )
9924 .expect("deleted rename line tables")
9925 };
9926 let added_tables: Vec<Option<RenameLineTable>> = if loads_needed <= 32 {
9927 (0..remaining_added.len())
9928 .map(|col| build_added_table(col).unwrap())
9929 .collect()
9930 } else {
9931 parallel_map_indices(
9932 odb,
9933 remaining_added.len(),
9934 par_opts,
9935 est_table_bytes,
9936 build_added_table,
9937 )
9938 .expect("added rename line tables")
9939 };
9940
9941 let mut inexact_scores: Vec<(u32, usize, usize)> = Vec::new();
9942 for (row, col) in candidate_pairs {
9943 poll_rename_cancel(cancel_watch)?;
9944 let di = remaining_deleted[row];
9945 let ai = remaining_added[col];
9946 let (Some(old_table), Some(new_table)) = (&deleted_tables[row], &added_tables[col])
9947 else {
9948 continue;
9949 };
9950 let score = RenameLineTable::similarity_score(old_table, new_table);
9951 if score >= threshold {
9952 inexact_scores.push((score, di, ai));
9953 }
9954 }
9955 sort_rename_scores(&mut inexact_scores, &deleted, &added);
9956 apply_rename_scores(
9957 &inexact_scores,
9958 &deleted,
9959 &added,
9960 &mut used_deleted,
9961 &mut used_added,
9962 &mut renames,
9963 );
9964 }
9965 }
9966
9967 let mut result = others;
9969 result.extend(renames);
9970 for (i, entry) in deleted.into_iter().enumerate() {
9971 if !used_deleted[i] {
9972 result.push(entry);
9973 }
9974 }
9975 for (i, entry) in added.into_iter().enumerate() {
9976 if !used_added[i] {
9977 result.push(entry);
9978 }
9979 }
9980
9981 result = prune_break_deletions_obviated_by_renames(result);
9982 sort_entries_by_path(&mut result);
9983 Ok(RenameDetectionResult {
9984 entries: result,
9985 inexact_skipped,
9986 })
9987}
9988
9989#[allow(dead_code)]
10004fn detect_copies(
10005 odb: &Odb,
10006 work_root: Option<&Path>,
10007 entries: Vec<DiffEntry>,
10008 threshold: u32,
10009 find_copies_harder: bool,
10010 source_tree_entries: &[(String, FileMode, ObjectId)],
10011) -> Result<Vec<DiffEntry>> {
10012 let config = ConfigSet::new();
10013 let policy = ThreadPolicy::default();
10014 detect_copies_with_policy(
10015 odb,
10016 work_root,
10017 entries,
10018 threshold,
10019 find_copies_harder,
10020 source_tree_entries,
10021 &config,
10022 &policy,
10023 )
10024}
10025
10026#[expect(clippy::too_many_arguments)]
10032pub fn detect_copies_with_policy(
10033 odb: &Odb,
10034 work_root: Option<&Path>,
10035 entries: Vec<DiffEntry>,
10036 threshold: u32,
10037 find_copies_harder: bool,
10038 source_tree_entries: &[(String, FileMode, ObjectId)],
10039 config: &ConfigSet,
10040 thread_policy: &ThreadPolicy,
10041) -> Result<Vec<DiffEntry>> {
10042 use std::collections::{HashMap, HashSet};
10043
10044 let mut deleted: Vec<DiffEntry> = Vec::new();
10046 let mut added: Vec<DiffEntry> = Vec::new();
10047 let mut others: Vec<DiffEntry> = Vec::new();
10048
10049 for entry in entries {
10050 match entry.status {
10051 DiffStatus::Deleted => deleted.push(entry),
10052 DiffStatus::Added => added.push(entry),
10053 _ => others.push(entry),
10054 }
10055 }
10056
10057 let mut sources: Vec<(String, ObjectId, bool)> = Vec::new(); let mut deleted_source_idx: HashMap<String, usize> = HashMap::new();
10061
10062 for entry in &deleted {
10063 if let Some(ref path) = entry.old_path {
10064 deleted_source_idx.insert(path.clone(), sources.len());
10065 sources.push((path.clone(), entry.old_oid, true));
10066 }
10067 }
10068
10069 for entry in &others {
10072 if matches!(entry.status, DiffStatus::Modified | DiffStatus::TypeChanged) {
10073 if let Some(ref old_path) = entry.old_path {
10074 if !sources.iter().any(|(p, _, _)| p == old_path) {
10075 sources.push((old_path.clone(), entry.old_oid, false));
10076 }
10077 }
10078 }
10079 }
10080
10081 if find_copies_harder {
10083 for (path, _mode, oid) in source_tree_entries {
10084 if !sources.iter().any(|(p, _, _)| p == path) {
10085 sources.push((path.clone(), *oid, false));
10086 }
10087 }
10088 }
10089
10090 let mut source_modes: Vec<(String, FileMode, ObjectId)> = source_tree_entries.to_vec();
10093 for entry in deleted.iter().chain(&others) {
10094 if let Some(ref path) = entry.old_path {
10095 if !source_modes.iter().any(|(p, _, _)| p == path) {
10096 source_modes.push((path.clone(), entry.old_mode, entry.old_oid));
10097 }
10098 }
10099 }
10100
10101 if sources.is_empty() {
10102 let mut result = others;
10103 result.extend(deleted);
10104 result.extend(added);
10105 sort_entries_by_path(&mut result);
10106 return Ok(result);
10107 }
10108
10109 let par_opts = diff_rename_parallel_options(config, thread_policy);
10110 let rename_limit = parse_rename_limit(config);
10111
10112 fn apply_copy_score_assignments(
10113 scores: &[(u32, usize, usize)],
10114 sources: &[(String, ObjectId, bool)],
10115 added: &[DiffEntry],
10116 source_tree_entries: &[(String, FileMode, ObjectId)],
10117 used_added2: &mut [bool],
10118 renamed_deleted: &mut HashSet<usize>,
10119 result_entries: &mut Vec<DiffEntry>,
10120 ) {
10121 let mut used_added = vec![false; added.len()];
10122 let mut source_to_added: HashMap<usize, Vec<(usize, u32)>> = HashMap::new();
10123 for &(score, si, ai) in scores {
10124 if used_added[ai] {
10125 continue;
10126 }
10127 used_added[ai] = true;
10128 source_to_added.entry(si).or_default().push((ai, score));
10129 }
10130
10131 for (&si, assignments_for_src) in &source_to_added {
10132 let (_, _, is_deleted) = &sources[si];
10133 if *is_deleted && !assignments_for_src.is_empty() {
10134 let rename_ai = assignments_for_src
10135 .iter()
10136 .max_by_key(|(ai, _score)| added[*ai].path().to_string())
10137 .map(|(ai, _)| *ai);
10138
10139 for &(ai, score) in assignments_for_src {
10140 let (ref src_path, _, _) = sources[si];
10141 let add = &added[ai];
10142 let src_mode = source_tree_entries
10143 .iter()
10144 .find(|(p, _, _)| p == src_path)
10145 .map_or(add.old_mode, |(_, m, _)| *m);
10146
10147 let is_rename = Some(ai) == rename_ai;
10148 result_entries.push(DiffEntry {
10149 status: if is_rename {
10150 DiffStatus::Renamed
10151 } else {
10152 DiffStatus::Copied
10153 },
10154 old_path: Some(src_path.clone()),
10155 new_path: add.new_path.clone(),
10156 old_mode: src_mode,
10157 new_mode: add.new_mode,
10158 old_oid: sources[si].1,
10159 new_oid: add.new_oid,
10160 score: Some(score),
10161 });
10162 used_added2[ai] = true;
10163 }
10164 renamed_deleted.insert(si);
10165 } else {
10166 for &(ai, score) in assignments_for_src {
10167 let (ref src_path, _, _) = sources[si];
10168 let add = &added[ai];
10169 let src_mode = source_tree_entries
10170 .iter()
10171 .find(|(p, _, _)| p == src_path)
10172 .map_or(add.old_mode, |(_, m, _)| *m);
10173
10174 result_entries.push(DiffEntry {
10175 status: DiffStatus::Copied,
10176 old_path: Some(src_path.clone()),
10177 new_path: add.new_path.clone(),
10178 old_mode: src_mode,
10179 new_mode: add.new_mode,
10180 old_oid: sources[si].1,
10181 new_oid: add.new_oid,
10182 score: Some(score),
10183 });
10184 used_added2[ai] = true;
10185 }
10186 }
10187 }
10188 }
10189
10190 fn added_entry_oid(
10191 odb: &Odb,
10192 add: &DiffEntry,
10193 added_contents: &[Option<Vec<u8>>],
10194 ai: usize,
10195 ) -> Result<ObjectId> {
10196 if add.new_oid != zero_oid() {
10197 Ok(add.new_oid)
10198 } else if let Some(ref data) = added_contents[ai] {
10199 odb.hash(ObjectKind::Blob, data)
10200 } else {
10201 Ok(zero_oid())
10202 }
10203 }
10204
10205 let mut result_entries: Vec<DiffEntry> = Vec::new();
10206 let mut renamed_deleted: HashSet<usize> = HashSet::new();
10207 let mut used_added2 = vec![false; added.len()];
10208 let mut source_contents = vec![None; sources.len()];
10209
10210 if !added.is_empty() {
10211 let added_contents = parallel_load_optional_blobs(odb, added.len(), par_opts, 0, |ai| {
10212 read_added_entry_bytes(odb, &added[ai], work_root)
10213 });
10214
10215 let mut exact_scores: Vec<(u32, usize, usize)> = Vec::new();
10217 for (si, (src_path, src_oid, _)) in sources.iter().enumerate() {
10218 for (ai, add) in added.iter().enumerate() {
10219 if add.new_path.as_deref() == Some(src_path.as_str()) {
10220 continue;
10221 }
10222 let add_oid = added_entry_oid(odb, add, &added_contents, ai)?;
10223 if *src_oid == add_oid && add_oid != zero_oid() {
10224 exact_scores.push((100, si, ai));
10225 }
10226 }
10227 }
10228 exact_scores.sort_by_key(|a| std::cmp::Reverse(a.0));
10229 apply_copy_score_assignments(
10230 &exact_scores,
10231 &sources,
10232 &added,
10233 &source_modes,
10234 &mut used_added2,
10235 &mut renamed_deleted,
10236 &mut result_entries,
10237 );
10238
10239 let remaining_added: Vec<usize> = used_added2
10240 .iter()
10241 .enumerate()
10242 .filter_map(|(i, used)| (!*used).then_some(i))
10243 .collect();
10244 let remaining_source_count = sources
10245 .iter()
10246 .enumerate()
10247 .filter(|(si, (_, _, is_deleted))| !*is_deleted || !renamed_deleted.contains(si))
10248 .count();
10249
10250 let skip_inexact = remaining_added.is_empty()
10251 || inexact_matrix_exceeds_limit(
10252 remaining_added.len(),
10253 remaining_source_count,
10254 rename_limit,
10255 );
10256
10257 if !skip_inexact {
10258 source_contents = parallel_load_optional_blobs(odb, sources.len(), par_opts, 0, |si| {
10259 odb.read(&sources[si].1).ok().map(|obj| obj.data)
10260 });
10261
10262 let est_bytes: usize = source_contents
10263 .iter()
10264 .chain(added_contents.iter())
10265 .filter_map(|opt| opt.as_ref().map(|b| b.len()))
10266 .sum();
10267
10268 let source_tables: Vec<Option<RenameLineTable>> = parallel_map_indices(
10269 odb,
10270 sources.len(),
10271 par_opts,
10272 est_bytes,
10273 |si| -> std::result::Result<Option<RenameLineTable>, std::convert::Infallible> {
10274 Ok(source_contents[si]
10275 .as_ref()
10276 .map(|d| RenameLineTable::from_blob(d)))
10277 },
10278 )
10279 .expect("copy source line tables");
10280 let added_tables: Vec<Option<RenameLineTable>> = parallel_map_indices(
10281 odb,
10282 added.len(),
10283 par_opts,
10284 est_bytes,
10285 |ai| -> std::result::Result<Option<RenameLineTable>, std::convert::Infallible> {
10286 Ok(added_contents[ai]
10287 .as_ref()
10288 .map(|d| RenameLineTable::from_blob(d)))
10289 },
10290 )
10291 .expect("copy added line tables");
10292
10293 let score_rows: Vec<Vec<(u32, usize, usize)>> = parallel_map_indices(
10294 odb,
10295 sources.len(),
10296 par_opts,
10297 est_bytes,
10298 |si| -> std::result::Result<Vec<(u32, usize, usize)>, std::convert::Infallible> {
10299 if renamed_deleted.contains(&si) {
10300 return Ok(Vec::new());
10301 }
10302 let (src_path, _, _) = &sources[si];
10303 let mut row: Vec<(u32, usize, usize)> = Vec::new();
10304 for &ai in &remaining_added {
10305 let add = &added[ai];
10306 if add.new_path.as_deref() == Some(src_path.as_str()) {
10307 continue;
10308 }
10309 let score = match (&source_tables[si], &added_tables[ai]) {
10310 (Some(old_table), Some(new_table)) => {
10311 RenameLineTable::similarity_score(old_table, new_table)
10312 }
10313 _ => 0,
10314 };
10315 if score >= threshold {
10316 row.push((score, si, ai));
10317 }
10318 }
10319 Ok(row)
10320 },
10321 )
10322 .expect("copy score rows");
10323 let mut inexact_scores: Vec<(u32, usize, usize)> =
10324 score_rows.into_iter().flatten().collect();
10325 inexact_scores.sort_by_key(|a| std::cmp::Reverse(a.0));
10326 apply_copy_score_assignments(
10327 &inexact_scores,
10328 &sources,
10329 &added,
10330 &source_modes,
10331 &mut used_added2,
10332 &mut renamed_deleted,
10333 &mut result_entries,
10334 );
10335 }
10336 }
10337
10338 for entry in deleted.into_iter() {
10340 if let Some(ref path) = entry.old_path {
10341 if let Some(&si) = deleted_source_idx.get(path) {
10342 if renamed_deleted.contains(&si) {
10343 continue;
10345 }
10346 }
10347 }
10348 result_entries.push(entry);
10349 }
10350
10351 let mut result = others;
10352 result.extend(result_entries);
10353 for (i, entry) in added.into_iter().enumerate() {
10355 if !used_added2[i] {
10356 result.push(entry);
10357 }
10358 }
10359
10360 let mut final_result = result;
10364 sort_entries_by_path(&mut final_result);
10365 Ok(final_result)
10366}
10367
10368#[derive(Debug, Clone, Copy)]
10370pub struct DiffPostprocessOptions {
10371 pub break_score: Option<u64>,
10374 pub rename_threshold: u32,
10376 pub copy_threshold: Option<u32>,
10378 pub find_copies_harder: bool,
10380 pub materialize_break_worktree_new_oid: bool,
10383}
10384
10385impl Default for DiffPostprocessOptions {
10386 fn default() -> Self {
10387 Self {
10388 break_score: None,
10389 rename_threshold: 100,
10390 copy_threshold: None,
10391 find_copies_harder: false,
10392 materialize_break_worktree_new_oid: false,
10393 }
10394 }
10395}
10396
10397#[derive(Debug, Clone, Copy)]
10400pub struct DiffPostprocessWorktree<'a> {
10401 pub repository_git_dir: &'a Path,
10403 pub index: &'a Index,
10405}
10406
10407fn blob_bytes(odb: &Odb, oid: &ObjectId) -> Result<Vec<u8>> {
10408 Ok(odb.read(oid)?.data)
10409}
10410
10411fn read_new_side_bytes(
10416 odb: &Odb,
10417 entry: &DiffEntry,
10418 work_root: Option<&Path>,
10419 worktree: Option<&DiffPostprocessWorktree<'_>>,
10420 config: &ConfigSet,
10421) -> Result<Vec<u8>> {
10422 if entry.new_oid != zero_oid() {
10423 return blob_bytes(odb, &entry.new_oid);
10424 }
10425 let path = entry.new_path.as_deref().ok_or_else(|| {
10426 Error::ObjectNotFound("modified entry missing new_path for worktree read".into())
10427 })?;
10428 let root = work_root.ok_or_else(|| {
10429 Error::ObjectNotFound(format!(
10430 "worktree bytes required for {} but no work_root was provided",
10431 path
10432 ))
10433 })?;
10434 if let Some(wctx) = worktree {
10435 let mode = entry.new_mode.bits();
10436 let index_entry = wctx.index.get(path.as_bytes(), 0);
10437 return read_worktree_side_bytes_for_diff(odb, root, path, mode, config, index_entry, None);
10438 }
10439 fs::read(root.join(path)).map_err(Error::Io)
10440}
10441
10442fn worktree_side_differs_after_normalize(
10444 odb: &Odb,
10445 entry: &DiffEntry,
10446 work_root: &Path,
10447 worktree: &DiffPostprocessWorktree<'_>,
10448 config: &ConfigSet,
10449) -> Result<bool> {
10450 let path = entry.new_path.as_deref().ok_or_else(|| {
10451 Error::ObjectNotFound("modified entry missing new_path for worktree read".into())
10452 })?;
10453 let raw = fs::read(work_root.join(path)).map_err(Error::Io)?;
10454 let canonical = read_new_side_bytes(odb, entry, Some(work_root), Some(worktree), config)?;
10455 Ok(raw != canonical)
10456}
10457
10458fn split_broken_modifications(
10459 odb: &Odb,
10460 work_root: Option<&Path>,
10461 entries: Vec<DiffEntry>,
10462 break_score: u64,
10463 worktree: Option<&DiffPostprocessWorktree<'_>>,
10464 config: &ConfigSet,
10465) -> Result<Vec<DiffEntry>> {
10466 let mut out = Vec::with_capacity(entries.len());
10467 for e in entries {
10468 if e.status != DiffStatus::Modified {
10469 out.push(e);
10470 continue;
10471 }
10472 let old = blob_bytes(odb, &e.old_oid)?;
10473 let new = read_new_side_bytes(odb, &e, work_root, worktree, config)?;
10474 if should_break_rewrite_pair(&old, &new, break_score) {
10475 let path = e.path().to_owned();
10476 out.push(DiffEntry {
10477 status: DiffStatus::Deleted,
10478 old_path: Some(path.clone()),
10479 new_path: None,
10480 old_mode: e.old_mode,
10481 new_mode: FileMode::ABSENT,
10482 old_oid: e.old_oid,
10483 new_oid: zero_oid(),
10484 score: None,
10485 });
10486 out.push(DiffEntry {
10487 status: DiffStatus::Added,
10488 old_path: None,
10489 new_path: Some(path),
10490 old_mode: FileMode::ABSENT,
10491 new_mode: e.new_mode,
10492 old_oid: zero_oid(),
10493 new_oid: e.new_oid,
10494 score: None,
10495 });
10496 } else {
10497 out.push(e);
10498 }
10499 }
10500 Ok(out)
10501}
10502
10503fn annotate_broken_modifications(
10504 odb: &Odb,
10505 work_root: Option<&Path>,
10506 entries: Vec<DiffEntry>,
10507 break_score: u64,
10508 worktree: Option<&DiffPostprocessWorktree<'_>>,
10509 config: &ConfigSet,
10510 materialize_worktree_oid: bool,
10511) -> Result<Vec<DiffEntry>> {
10512 let mut out = Vec::with_capacity(entries.len());
10513 for mut e in entries {
10514 if e.status == DiffStatus::Modified {
10515 let old = blob_bytes(odb, &e.old_oid)?;
10516 let new = read_new_side_bytes(odb, &e, work_root, worktree, config)?;
10517 if should_break_rewrite_pair(&old, &new, break_score) {
10518 let show_score = if e.new_oid != zero_oid() {
10522 true
10523 } else if materialize_worktree_oid {
10524 match (work_root, worktree) {
10525 (Some(root), Some(wctx)) => {
10526 worktree_side_differs_after_normalize(odb, &e, root, wctx, config)?
10527 }
10528 _ => false,
10529 }
10530 } else {
10531 false
10532 };
10533 if show_score {
10534 e.score = rewrite_dissimilarity_index_percent(&old, &new);
10535 }
10536 }
10537 }
10538 out.push(e);
10539 }
10540 Ok(out)
10541}
10542
10543fn materialize_break_worktree_new_oids(
10544 odb: &Odb,
10545 work_root: Option<&Path>,
10546 entries: &mut [DiffEntry],
10547 worktree: Option<&DiffPostprocessWorktree<'_>>,
10548 config: &ConfigSet,
10549) -> Result<()> {
10550 for e in entries {
10551 if e.new_oid != zero_oid() {
10552 continue;
10553 }
10554 if !matches!(e.status, DiffStatus::Modified | DiffStatus::Added) {
10555 continue;
10556 }
10557 let bytes = read_new_side_bytes(odb, e, work_root, worktree, config)?;
10558 e.new_oid = odb.hash(ObjectKind::Blob, &bytes)?;
10559 }
10560 Ok(())
10561}
10562
10563fn prune_break_deletions_obviated_by_renames(entries: Vec<DiffEntry>) -> Vec<DiffEntry> {
10566 use std::collections::HashSet;
10567 let rename_targets: HashSet<String> = entries
10568 .iter()
10569 .filter(|e| e.status == DiffStatus::Renamed)
10570 .filter_map(|e| e.new_path.clone())
10571 .collect();
10572 if rename_targets.is_empty() {
10573 return entries;
10574 }
10575 entries
10576 .into_iter()
10577 .filter(|e| {
10578 if e.status != DiffStatus::Deleted {
10579 return true;
10580 }
10581 e.old_path
10582 .as_ref()
10583 .is_none_or(|p| !rename_targets.contains(p))
10584 })
10585 .collect()
10586}
10587
10588#[allow(clippy::too_many_arguments)]
10598pub fn postprocess_diff_entries(
10599 odb: &Odb,
10600 entries: Vec<DiffEntry>,
10601 source_tree_oid: Option<&ObjectId>,
10602 work_root: Option<&Path>,
10603 worktree: Option<&DiffPostprocessWorktree<'_>>,
10604 config: &ConfigSet,
10605 thread_policy: &ThreadPolicy,
10606 opts: &DiffPostprocessOptions,
10607 inexact_notice: Option<&mut Option<InexactRenameDetectionSkipped>>,
10608) -> Result<Vec<DiffEntry>> {
10609 let mut entries = entries;
10610 if let Some(break_score) = opts.break_score {
10611 let rename_or_copy = opts.rename_threshold < 100 || opts.copy_threshold.is_some();
10612 entries = if rename_or_copy {
10613 split_broken_modifications(odb, work_root, entries, break_score, worktree, config)?
10614 } else {
10615 annotate_broken_modifications(
10616 odb,
10617 work_root,
10618 entries,
10619 break_score,
10620 worktree,
10621 config,
10622 opts.materialize_break_worktree_new_oid,
10623 )?
10624 };
10625 if opts.materialize_break_worktree_new_oid {
10626 materialize_break_worktree_new_oids(odb, work_root, &mut entries, worktree, config)?;
10627 }
10628 }
10629
10630 let source_tree_entries = source_tree_oid
10631 .map(|t| flatten_tree_entries(odb, t))
10632 .transpose()?
10633 .unwrap_or_default();
10634
10635 if opts.copy_threshold.is_some() || opts.find_copies_harder {
10636 let threshold = opts.copy_threshold.unwrap_or(opts.rename_threshold.min(99));
10637 detect_copies_with_policy(
10638 odb,
10639 work_root,
10640 entries,
10641 threshold,
10642 opts.find_copies_harder,
10643 &source_tree_entries,
10644 config,
10645 thread_policy,
10646 )
10647 } else if opts.rename_threshold < 100 {
10648 let skip_basename = opts.break_score.is_some();
10649 let out = detect_renames_with_policy(
10650 odb,
10651 work_root,
10652 entries,
10653 opts.rename_threshold,
10654 config,
10655 thread_policy,
10656 None,
10657 DetectRenamesOptions {
10658 skip_basename_heuristic: skip_basename,
10659 },
10660 )?;
10661 if let Some(slot) = inexact_notice {
10662 if out.inexact_skipped.is_some() {
10663 *slot = out.inexact_skipped;
10664 }
10665 }
10666 Ok(out.entries)
10667 } else {
10668 Ok(entries)
10669 }
10670}
10671
10672pub(crate) fn flatten_tree_entries(
10674 odb: &Odb,
10675 tree_oid: &ObjectId,
10676) -> Result<Vec<(String, FileMode, ObjectId)>> {
10677 fn walk(
10678 odb: &Odb,
10679 tree_oid: &ObjectId,
10680 prefix: &str,
10681 out: &mut Vec<(String, FileMode, ObjectId)>,
10682 ) -> Result<()> {
10683 let obj = odb.read(tree_oid)?;
10684 let oid_len = odb.hash_algo().len();
10685 for e in TreeRefIter::with_oid_len(&obj.data, oid_len) {
10686 let e = e?;
10687 let name = String::from_utf8_lossy(e.name);
10688 let path = if prefix.is_empty() {
10689 name.to_string()
10690 } else {
10691 format!("{prefix}/{name}")
10692 };
10693 if e.mode == 0o040000 {
10694 walk(odb, &e.oid, &path, out)?;
10695 } else {
10696 out.push((path, FileMode::new(e.mode), e.oid));
10697 }
10698 }
10699 Ok(())
10700 }
10701 let mut out = Vec::new();
10702 walk(odb, tree_oid, "", &mut out)?;
10703 Ok(out)
10704}
10705
10706#[allow(clippy::too_many_arguments)]
10716pub(crate) fn status_apply_rename_copy_detection(
10717 odb: &Odb,
10718 unstaged_raw: Vec<DiffEntry>,
10719 threshold: u32,
10720 copies: bool,
10721 head_tree: Option<&ObjectId>,
10722 config: &ConfigSet,
10723 thread_policy: &ThreadPolicy,
10724 cancel_watch: Option<&std::sync::atomic::AtomicBool>,
10725) -> Result<Vec<DiffEntry>> {
10726 if !copies {
10727 return detect_renames_with_policy(
10728 odb,
10729 None,
10730 unstaged_raw,
10731 threshold,
10732 config,
10733 thread_policy,
10734 cancel_watch,
10735 DetectRenamesOptions::default(),
10736 )
10737 .map(|r| r.entries);
10738 }
10739 let source_tree_entries: Vec<(String, FileMode, ObjectId)> = match head_tree {
10740 Some(oid) => flatten_tree(odb, oid, "")?
10741 .into_iter()
10742 .map(|e| (e.path, FileMode::new(e.mode), e.oid))
10743 .collect(),
10744 None => Vec::new(),
10745 };
10746 detect_copies_with_policy(
10747 odb,
10748 None,
10749 unstaged_raw,
10750 threshold,
10751 false,
10752 &source_tree_entries,
10753 config,
10754 thread_policy,
10755 )
10756}
10757
10758fn same_basename(del: &DiffEntry, add: &DiffEntry) -> bool {
10760 let old = del.old_path.as_deref().unwrap_or("");
10761 let new = add.new_path.as_deref().unwrap_or("");
10762 let old_base = old.rsplit('/').next().unwrap_or(old);
10763 let new_base = new.rsplit('/').next().unwrap_or(new);
10764 old_base == new_base && !old_base.is_empty()
10765}
10766
10767const RENAME_SCORE_MAX: u32 = 100;
10768
10769fn blob_payload_size(odb: &Odb, oid: &ObjectId) -> Option<usize> {
10771 if oid.is_zero() {
10772 return None;
10773 }
10774 odb.read_info(oid).ok().map(|info| info.size as usize)
10775}
10776
10777fn added_entry_payload_size(odb: &Odb, add: &DiffEntry, work_root: Option<&Path>) -> Option<usize> {
10779 if !add.new_oid.is_zero() {
10780 if let Some(size) = blob_payload_size(odb, &add.new_oid) {
10781 return Some(size);
10782 }
10783 }
10784 read_added_entry_bytes(odb, add, work_root).map(|data| data.len())
10785}
10786
10787fn rename_pair_size_reachable(src_size: usize, dst_size: usize, minimum_score: u32) -> bool {
10789 let max_size = src_size.max(dst_size);
10790 let min_size = src_size.min(dst_size);
10791 if max_size == 0 {
10792 return minimum_score <= RENAME_SCORE_MAX;
10793 }
10794 (max_size - min_size).saturating_mul(RENAME_SCORE_MAX as usize)
10795 <= max_size.saturating_mul((RENAME_SCORE_MAX - minimum_score) as usize)
10796}
10797
10798fn compute_similarity(old: &[u8], new: &[u8]) -> u32 {
10803 RenameLineTable::similarity_score(
10804 &RenameLineTable::from_blob(old),
10805 &RenameLineTable::from_blob(new),
10806 )
10807}
10808
10809#[must_use]
10814pub fn rename_similarity_score(old: &[u8], new: &[u8]) -> u32 {
10815 compute_similarity(old, new)
10816}
10817
10818pub fn format_raw(entry: &DiffEntry) -> String {
10824 let path = match entry.status {
10825 DiffStatus::Renamed | DiffStatus::Copied => {
10826 format!(
10827 "{}\t{}",
10828 entry.old_path.as_deref().unwrap_or(""),
10829 entry.new_path.as_deref().unwrap_or("")
10830 )
10831 }
10832 _ => entry.path().to_owned(),
10833 };
10834
10835 let status_str = match (entry.status, entry.score) {
10836 (DiffStatus::Renamed, Some(s)) => format!("R{:03}", s),
10837 (DiffStatus::Copied, Some(s)) => format!("C{:03}", s),
10838 (DiffStatus::Modified, Some(s)) => format!("M{s:03}"),
10839 (DiffStatus::TypeChanged, Some(s)) => format!("T{s:03}"),
10840 _ => entry.status.letter().to_string(),
10841 };
10842
10843 let (old_hex, new_hex) = raw_oid_hex_pair(&entry.old_oid, &entry.new_oid);
10844 format!(
10845 ":{} {} {} {} {}\t{}",
10846 entry.old_mode, entry.new_mode, old_hex, new_hex, status_str, path
10847 )
10848}
10849
10850fn raw_oid_hex_pair(old: &ObjectId, new: &ObjectId) -> (String, String) {
10858 let width = if !old.is_zero() {
10859 old.algo().hex_len()
10860 } else if !new.is_zero() {
10861 new.algo().hex_len()
10862 } else {
10863 old.algo().hex_len()
10864 };
10865 let render = |oid: &ObjectId| {
10866 if oid.is_zero() {
10867 "0".repeat(width)
10868 } else {
10869 oid.to_hex()
10870 }
10871 };
10872 (render(old), render(new))
10873}
10874
10875pub fn format_raw_abbrev(entry: &DiffEntry, abbrev_len: usize) -> String {
10877 let old_hex = format!("{}", entry.old_oid);
10878 let new_hex = format!("{}", entry.new_oid);
10879 let old_abbrev = &old_hex[..abbrev_len.min(old_hex.len())];
10880 let new_abbrev = &new_hex[..abbrev_len.min(new_hex.len())];
10881
10882 let path = match entry.status {
10884 DiffStatus::Renamed | DiffStatus::Copied => format!(
10885 "{}\t{}",
10886 entry.old_path.as_deref().unwrap_or(""),
10887 entry.new_path.as_deref().unwrap_or("")
10888 ),
10889 _ => entry.path().to_owned(),
10890 };
10891 let status_str = match (entry.status, entry.score) {
10892 (DiffStatus::Renamed, Some(s)) => format!("R{s:03}"),
10893 (DiffStatus::Copied, Some(s)) => format!("C{s:03}"),
10894 (DiffStatus::Modified, Some(s)) => format!("M{s:03}"),
10895 (DiffStatus::TypeChanged, Some(s)) => format!("T{s:03}"),
10896 _ => entry.status.letter().to_string(),
10897 };
10898
10899 format!(
10900 ":{} {} {} {} {}\t{}",
10901 entry.old_mode, entry.new_mode, old_abbrev, new_abbrev, status_str, path
10902 )
10903}
10904
10905pub(crate) fn unified_diff(
10920 old_content: &str,
10921 new_content: &str,
10922 old_path: &str,
10923 new_path: &str,
10924 context_lines: usize,
10925 indent_heuristic: bool,
10926 quote_path_fully: bool,
10927) -> String {
10928 unified_diff_with_prefix(
10929 old_content,
10930 new_content,
10931 old_path,
10932 new_path,
10933 context_lines,
10934 0,
10935 "a/",
10936 "b/",
10937 indent_heuristic,
10938 quote_path_fully,
10939 )
10940}
10941
10942#[allow(clippy::too_many_arguments)] pub(crate) fn unified_diff_with_prefix(
10948 old_content: &str,
10949 new_content: &str,
10950 old_path: &str,
10951 new_path: &str,
10952 context_lines: usize,
10953 inter_hunk_context: usize,
10954 src_prefix: &str,
10955 dst_prefix: &str,
10956 indent_heuristic: bool,
10957 quote_path_fully: bool,
10958) -> String {
10959 unified_diff_with_prefix_and_funcname(
10960 old_content,
10961 new_content,
10962 old_path,
10963 new_path,
10964 context_lines,
10965 inter_hunk_context,
10966 src_prefix,
10967 dst_prefix,
10968 None,
10969 indent_heuristic,
10970 quote_path_fully,
10971 )
10972}
10973
10974#[allow(clippy::too_many_arguments)]
10977pub fn unified_diff_with_prefix_and_funcname(
10978 old_content: &str,
10979 new_content: &str,
10980 old_path: &str,
10981 new_path: &str,
10982 context_lines: usize,
10983 inter_hunk_context: usize,
10984 src_prefix: &str,
10985 dst_prefix: &str,
10986 funcname_matcher: Option<&FuncnameMatcher>,
10987 indent_heuristic: bool,
10988 quote_path_fully: bool,
10989) -> String {
10990 unified_diff_with_prefix_and_funcname_and_algorithm(
10991 old_content,
10992 new_content,
10993 old_path,
10994 new_path,
10995 context_lines,
10996 inter_hunk_context,
10997 src_prefix,
10998 dst_prefix,
10999 funcname_matcher,
11000 similar::Algorithm::Myers,
11001 false,
11002 false,
11003 indent_heuristic,
11004 quote_path_fully,
11005 )
11006}
11007
11008#[allow(clippy::too_many_arguments)]
11014pub(crate) fn write_unified_diff_with_prefix_and_funcname_and_algorithm<W: std::io::Write>(
11015 out: &mut W,
11016 old_content: &str,
11017 new_content: &str,
11018 old_path: &str,
11019 new_path: &str,
11020 context_lines: usize,
11021 inter_hunk_context: usize,
11022 src_prefix: &str,
11023 dst_prefix: &str,
11024 funcname_matcher: Option<&FuncnameMatcher>,
11025 algorithm: similar::Algorithm,
11026 function_context: bool,
11027 use_git_histogram: bool,
11028 indent_heuristic: bool,
11029 quote_path_fully: bool,
11030) -> Result<()> {
11031 if function_context {
11032 let patch = unified_diff_with_function_context(
11033 old_content,
11034 new_content,
11035 old_path,
11036 new_path,
11037 context_lines,
11038 inter_hunk_context,
11039 src_prefix,
11040 dst_prefix,
11041 funcname_matcher,
11042 algorithm,
11043 indent_heuristic,
11044 quote_path_fully,
11045 );
11046 out.write_all(patch.as_bytes())?;
11047 return Ok(());
11048 }
11049
11050 if use_git_histogram {
11051 let patch = unified_diff_histogram_with_prefix_and_funcname_indent(
11052 old_content,
11053 new_content,
11054 old_path,
11055 new_path,
11056 context_lines,
11057 inter_hunk_context,
11058 src_prefix,
11059 dst_prefix,
11060 funcname_matcher,
11061 indent_heuristic,
11062 quote_path_fully,
11063 );
11064 out.write_all(patch.as_bytes())?;
11065 return Ok(());
11066 }
11067
11068 if algorithm == similar::Algorithm::Myers && !function_context {
11069 let patch = unified_diff_git_myers_with_prefix_and_funcname(
11070 old_content,
11071 new_content,
11072 old_path,
11073 new_path,
11074 context_lines,
11075 inter_hunk_context,
11076 src_prefix,
11077 dst_prefix,
11078 funcname_matcher,
11079 indent_heuristic,
11080 quote_path_fully,
11081 );
11082 out.write_all(patch.as_bytes())?;
11083 return Ok(());
11084 }
11085
11086 if algorithm == similar::Algorithm::Lcs && !function_context {
11087 let patch = unified_diff_git_minimal_with_prefix_and_funcname(
11088 old_content,
11089 new_content,
11090 old_path,
11091 new_path,
11092 context_lines,
11093 inter_hunk_context,
11094 src_prefix,
11095 dst_prefix,
11096 funcname_matcher,
11097 indent_heuristic,
11098 quote_path_fully,
11099 );
11100 out.write_all(patch.as_bytes())?;
11101 return Ok(());
11102 }
11103
11104 if algorithm == similar::Algorithm::Patience && !function_context {
11105 let patch = unified_diff_git_patience_with_prefix_and_funcname(
11106 old_content,
11107 new_content,
11108 old_path,
11109 new_path,
11110 context_lines,
11111 inter_hunk_context,
11112 src_prefix,
11113 dst_prefix,
11114 funcname_matcher,
11115 indent_heuristic,
11116 quote_path_fully,
11117 );
11118 out.write_all(patch.as_bytes())?;
11119 return Ok(());
11120 }
11121
11122 use crate::quote_path::format_diff_path_with_prefix;
11123 use similar::{udiff::UnifiedDiffHunk, TextDiff};
11124
11125 let diff = TextDiff::configure()
11126 .algorithm(algorithm)
11127 .diff_lines(old_content, new_content);
11128 let compacted_ops = diff_indent_heuristic::diff_lines_ops_compacted(
11129 old_content,
11130 new_content,
11131 algorithm,
11132 indent_heuristic,
11133 );
11134
11135 if old_path == "/dev/null" {
11136 out.write_all(b"--- /dev/null\n")?;
11137 } else if src_prefix.is_empty() {
11138 writeln!(out, "--- {old_path}")?;
11139 } else {
11140 out.write_all(b"--- ")?;
11141 out.write_all(
11142 format_diff_path_with_prefix(src_prefix, old_path, quote_path_fully).as_bytes(),
11143 )?;
11144 out.write_all(b"\n")?;
11145 }
11146 if new_path == "/dev/null" {
11147 out.write_all(b"+++ /dev/null\n")?;
11148 } else if dst_prefix.is_empty() {
11149 writeln!(out, "+++ {new_path}")?;
11150 } else {
11151 out.write_all(b"+++ ")?;
11152 out.write_all(
11153 format_diff_path_with_prefix(dst_prefix, new_path, quote_path_fully).as_bytes(),
11154 )?;
11155 out.write_all(b"\n")?;
11156 }
11157
11158 let old_lines: Vec<&str> = old_content.lines().collect();
11159 let max_common_gap = context_lines
11160 .saturating_mul(2)
11161 .saturating_add(inter_hunk_context);
11162 let op_groups = group_diff_ops_gap(compacted_ops, context_lines, max_common_gap);
11163
11164 let mut hunk_buf = String::new();
11165 let mut eof_annot = UnifiedDiffNoNewlineAnnotator::new(old_content, new_content);
11166 for ops in op_groups {
11167 if ops.is_empty() {
11168 continue;
11169 }
11170 let hunk = UnifiedDiffHunk::new(ops, &diff, true);
11171 hunk_buf.clear();
11172 use std::fmt::Write as FmtWrite;
11173 let _ = FmtWrite::write_fmt(&mut hunk_buf, format_args!("{hunk}"));
11174 let mut fragment = hunk_buf.clone();
11175 if let Some(first_newline) = hunk_buf.find('\n') {
11176 let header_line = &hunk_buf[..first_newline];
11177 let rest = &hunk_buf[first_newline..];
11178 if let Some(func_ctx) =
11179 extract_function_context(header_line, &old_lines, funcname_matcher, Some(old_path))
11180 {
11181 fragment.clear();
11182 fragment.push_str(header_line);
11183 fragment.push(' ');
11184 fragment.push_str(&func_ctx);
11185 fragment.push_str(rest);
11186 }
11187 }
11188 eof_annot.write_hunk(out, &fragment)?;
11189 }
11190 Ok(())
11191}
11192
11193#[allow(clippy::too_many_arguments)]
11199pub fn unified_diff_with_prefix_and_funcname_and_algorithm(
11200 old_content: &str,
11201 new_content: &str,
11202 old_path: &str,
11203 new_path: &str,
11204 context_lines: usize,
11205 inter_hunk_context: usize,
11206 src_prefix: &str,
11207 dst_prefix: &str,
11208 funcname_matcher: Option<&FuncnameMatcher>,
11209 algorithm: similar::Algorithm,
11210 function_context: bool,
11211 use_git_histogram: bool,
11212 indent_heuristic: bool,
11213 quote_path_fully: bool,
11214) -> String {
11215 let mut output = Vec::new();
11216 if write_unified_diff_with_prefix_and_funcname_and_algorithm(
11217 &mut output,
11218 old_content,
11219 new_content,
11220 old_path,
11221 new_path,
11222 context_lines,
11223 inter_hunk_context,
11224 src_prefix,
11225 dst_prefix,
11226 funcname_matcher,
11227 algorithm,
11228 function_context,
11229 use_git_histogram,
11230 indent_heuristic,
11231 quote_path_fully,
11232 )
11233 .is_err()
11234 {
11235 output.clear();
11236 }
11237 String::from_utf8_lossy(&output).into_owned()
11238}
11239
11240pub(crate) fn group_diff_ops_gap(
11246 mut ops: Vec<similar::DiffOp>,
11247 context: usize,
11248 max_common_gap: usize,
11249) -> Vec<Vec<similar::DiffOp>> {
11250 use similar::DiffOp;
11251 if ops.is_empty() {
11252 return vec![];
11253 }
11254
11255 let mut pending_group = Vec::new();
11256 let mut rv = Vec::new();
11257
11258 if let Some(DiffOp::Equal {
11259 old_index,
11260 new_index,
11261 len,
11262 }) = ops.first_mut()
11263 {
11264 let offset = (*len).saturating_sub(context);
11265 *old_index += offset;
11266 *new_index += offset;
11267 *len -= offset;
11268 }
11269
11270 if let Some(DiffOp::Equal { len, .. }) = ops.last_mut() {
11271 *len -= (*len).saturating_sub(context);
11272 }
11273
11274 for op in ops.into_iter() {
11275 if let DiffOp::Equal {
11276 old_index,
11277 new_index,
11278 len,
11279 } = op
11280 {
11281 if len > max_common_gap {
11284 pending_group.push(DiffOp::Equal {
11285 old_index,
11286 new_index,
11287 len: context,
11288 });
11289 rv.push(pending_group);
11290 let offset = len.saturating_sub(context);
11291 pending_group = vec![DiffOp::Equal {
11292 old_index: old_index + offset,
11293 new_index: new_index + offset,
11294 len: len - offset,
11295 }];
11296 continue;
11297 }
11298 }
11299 pending_group.push(op);
11300 }
11301
11302 match &pending_group[..] {
11303 &[] | &[similar::DiffOp::Equal { .. }] => {}
11304 _ => rv.push(pending_group),
11305 }
11306
11307 rv
11308}
11309#[expect(clippy::too_many_arguments)]
11310fn unified_diff_with_function_context(
11312 old_content: &str,
11313 new_content: &str,
11314 old_path: &str,
11315 new_path: &str,
11316 context_lines: usize,
11317 inter_hunk_context: usize,
11318 src_prefix: &str,
11319 dst_prefix: &str,
11320 funcname_matcher: Option<&FuncnameMatcher>,
11321 algorithm: similar::Algorithm,
11322 indent_heuristic: bool,
11323 quote_path_fully: bool,
11324) -> String {
11325 use crate::quote_path::format_diff_path_with_prefix;
11326 use similar::{udiff::UnifiedDiffHunk, TextDiff};
11327
11328 let diff = TextDiff::configure()
11329 .algorithm(algorithm)
11330 .diff_lines(old_content, new_content);
11331
11332 let old_lines: Vec<&str> = old_content.lines().collect();
11333 let new_lines: Vec<&str> = new_content.lines().collect();
11334 let n_old = old_lines.len();
11335 let n_new = new_lines.len();
11336
11337 let max_common_gap = context_lines
11344 .saturating_mul(2)
11345 .saturating_add(inter_hunk_context);
11346 let all_ops = diff.ops().to_vec();
11347 let op_groups = group_diff_ops_gap(all_ops.clone(), context_lines, max_common_gap);
11348
11349 let mut ranges: Vec<(usize, usize, usize, usize)> = Vec::new();
11350
11351 for ops in op_groups {
11352 if ops.is_empty() {
11353 continue;
11354 }
11355 let i1_anchor = func_context_old_anchor(&ops, n_old);
11356 let i1_end = hunk_old_change_end_exclusive(&ops);
11357 let skip_preimage_pull =
11358 append_with_whole_function_added(&ops, n_old, n_new, &new_lines, funcname_matcher);
11359 let hunk = UnifiedDiffHunk::new(ops, &diff, true);
11360 let hunk_str = format!("{hunk}");
11361 let header_line = hunk_str
11362 .lines()
11363 .next()
11364 .unwrap_or("")
11365 .trim_end_matches(['\r', '\n']);
11366 let Some((base_s1, _base_e1, _base_s2, _base_e2)) =
11367 parse_unified_hunk_header_ranges(header_line)
11368 else {
11369 continue;
11370 };
11371
11372 let ctx = context_lines;
11373 let (s1, e1, s2, e2) = if skip_preimage_pull {
11374 let s = n_old.saturating_sub(ctx);
11375 let s2 = map_old_line_to_new(&all_ops, s, n_new).min(n_new);
11376 (s, n_old, s2, n_new)
11377 } else {
11378 let mut s1 = base_s1.saturating_sub(ctx);
11379 let mut s2 = map_old_line_to_new(&all_ops, s1, n_new).min(n_new);
11380
11381 let base_pre_s1 = i1_anchor.saturating_sub(ctx);
11382 if base_pre_s1 < s1 {
11383 s1 = base_pre_s1;
11384 s2 = map_old_line_to_new(&all_ops, s1, n_new).min(n_new);
11385 }
11386
11387 let fs1 = expand_func_pre_start(s1, i1_anchor, n_old, &old_lines, funcname_matcher);
11388 if fs1 < s1 {
11389 s1 = fs1;
11390 s2 = map_old_line_to_new(&all_ops, s1, n_new).min(n_new);
11391 }
11392
11393 let mut e1 = (i1_end + ctx).min(n_old);
11399 let mut e2 = map_old_line_to_new(&all_ops, e1, n_new).min(n_new);
11400 let fe1 = expand_func_post_end(e1, i1_end, n_old, &old_lines, funcname_matcher);
11401 if fe1 > e1 {
11402 e1 = fe1;
11403 e2 = map_old_line_to_new(&all_ops, e1, n_new).min(n_new);
11404 }
11405 (s1, e1, s2, e2)
11406 };
11407
11408 ranges.push((s1, e1, s2, e2));
11409 }
11410
11411 ranges.sort_by_key(|r| (r.0, r.2));
11414 let mut merged: Vec<(usize, usize, usize, usize)> = Vec::with_capacity(ranges.len());
11415 for (s1, e1, s2, e2) in ranges {
11416 if let Some(last) = merged.last_mut() {
11417 if s1 < last.1 {
11418 last.1 = last.1.max(e1);
11419 last.3 = last.3.max(e2);
11420 continue;
11421 }
11422 }
11423 merged.push((s1, e1, s2, e2));
11424 }
11425 let ranges = merged;
11426
11427 let mut output = String::new();
11428 if old_path == "/dev/null" {
11429 output.push_str("--- /dev/null\n");
11430 } else if src_prefix.is_empty() {
11431 output.push_str(&format!("--- {old_path}\n"));
11432 } else {
11433 output.push_str("--- ");
11434 output.push_str(&format_diff_path_with_prefix(
11435 src_prefix,
11436 old_path,
11437 quote_path_fully,
11438 ));
11439 output.push('\n');
11440 }
11441 if new_path == "/dev/null" {
11442 output.push_str("+++ /dev/null\n");
11443 } else if dst_prefix.is_empty() {
11444 output.push_str(&format!("+++ {new_path}\n"));
11445 } else {
11446 output.push_str("+++ ");
11447 output.push_str(&format_diff_path_with_prefix(
11448 dst_prefix,
11449 new_path,
11450 quote_path_fully,
11451 ));
11452 output.push('\n');
11453 }
11454
11455 for (s1, e1, s2, e2) in ranges {
11456 if s1 >= e1 && s2 >= e2 {
11457 continue;
11458 }
11459 let old_seg =
11460 line_slice_for_diff_with_eof_nl(&old_lines, s1, e1, old_content.ends_with('\n'));
11461 let new_seg =
11462 line_slice_for_diff_with_eof_nl(&new_lines, s2, e2, new_content.ends_with('\n'));
11463 let inner_ctx = old_seg.lines().count().max(new_seg.lines().count()).max(1);
11464 let piece = unified_diff_with_prefix_and_funcname_and_algorithm(
11465 &old_seg,
11466 &new_seg,
11467 old_path,
11468 new_path,
11469 inner_ctx,
11470 0,
11471 src_prefix,
11472 dst_prefix,
11473 funcname_matcher,
11474 algorithm,
11475 false,
11476 false,
11477 indent_heuristic,
11478 quote_path_fully,
11479 );
11480 let shifted = shift_unified_hunk_headers_to_full_file(&piece, s1, s2);
11481 let with_func =
11482 enrich_unified_hunk_headers_funcname(&shifted, &old_lines, funcname_matcher);
11483 for line in with_func.lines() {
11484 if line.starts_with("--- ") || line.starts_with("+++ ") {
11485 continue;
11486 }
11487 output.push_str(line);
11488 output.push('\n');
11489 }
11490 }
11491
11492 output
11493}
11494
11495fn shift_unified_hunk_headers_to_full_file(
11500 patch: &str,
11501 delta_old: usize,
11502 delta_new: usize,
11503) -> String {
11504 if delta_old == 0 && delta_new == 0 {
11505 return patch.to_owned();
11506 }
11507 let mut out = String::with_capacity(patch.len());
11508 for line in patch.lines() {
11509 if let Some(shifted) = shift_one_unified_hunk_header(line, delta_old, delta_new) {
11510 out.push_str(&shifted);
11511 } else {
11512 out.push_str(line);
11513 }
11514 out.push('\n');
11515 }
11516 out
11517}
11518
11519fn shift_one_unified_hunk_header(line: &str, delta_old: usize, delta_new: usize) -> Option<String> {
11520 let rest = line.strip_prefix("@@ ")?;
11521 let (old_chunk, after_plus) = rest.split_once(" +")?;
11522 let old_spec = old_chunk.strip_prefix('-')?;
11523 let (new_spec, suffix) = after_plus.split_once(" @@")?;
11524 let shifted_old = shift_unified_range_spec(old_spec, delta_old)?;
11525 let shifted_new = shift_unified_range_spec(new_spec, delta_new)?;
11526 Some(format!("@@ -{shifted_old} +{shifted_new} @@{suffix}"))
11527}
11528
11529fn shift_unified_range_spec(spec: &str, delta: usize) -> Option<String> {
11530 let spec = spec.trim();
11531 if let Some((start_s, count_s)) = spec.split_once(',') {
11532 let start: usize = start_s.parse().ok()?;
11533 let count: usize = count_s.parse().ok()?;
11534 Some(format!("{},{}", start.saturating_add(delta), count))
11535 } else {
11536 let start: usize = spec.parse().ok()?;
11537 Some(format!("{}", start.saturating_add(delta)))
11538 }
11539}
11540
11541fn enrich_unified_hunk_headers_funcname(
11543 patch: &str,
11544 full_old_lines: &[&str],
11545 funcname_matcher: Option<&FuncnameMatcher>,
11546) -> String {
11547 let mut out = String::with_capacity(patch.len());
11548 for line in patch.lines() {
11549 if let Some(fixed) = enrich_one_hunk_header_funcname(line, full_old_lines, funcname_matcher)
11550 {
11551 out.push_str(&fixed);
11552 } else {
11553 out.push_str(line);
11554 }
11555 out.push('\n');
11556 }
11557 out
11558}
11559
11560fn enrich_one_hunk_header_funcname(
11561 line: &str,
11562 full_old_lines: &[&str],
11563 funcname_matcher: Option<&FuncnameMatcher>,
11564) -> Option<String> {
11565 let after_at = line.strip_prefix("@@ ")?;
11566 let idx = after_at.find(" @@")?;
11567 let mid = after_at[..idx].trim();
11568 let tail = after_at[idx + 3..].trim_start();
11569 let header_for_parse = format!("@@ {mid} @@");
11570 let func = extract_function_context(&header_for_parse, full_old_lines, funcname_matcher, None);
11571 Some(if let Some(f) = func {
11572 format!("@@ {mid} @@ {f}")
11573 } else if !tail.is_empty() {
11574 format!("@@ {mid} @@ {tail}")
11575 } else {
11576 format!("@@ {mid} @@")
11577 })
11578}
11579
11580fn line_slice_for_diff_with_eof_nl(
11581 lines: &[&str],
11582 start: usize,
11583 end: usize,
11584 full_file_ends_with_newline: bool,
11585) -> String {
11586 if start >= end {
11587 return String::new();
11588 }
11589 let mut s = lines[start..end].join("\n");
11590 let slice_is_suffix_of_file = end == lines.len();
11591 let need_trailing_nl = if slice_is_suffix_of_file {
11592 full_file_ends_with_newline
11593 } else {
11594 true
11595 };
11596 if need_trailing_nl && !s.ends_with('\n') {
11597 s.push('\n');
11598 }
11599 s
11600}
11601
11602fn map_old_line_to_new(ops: &[similar::DiffOp], old_line: usize, n_new: usize) -> usize {
11605 use similar::DiffOp;
11606 let mut n = 0usize;
11607 for op in ops {
11608 match *op {
11609 DiffOp::Equal {
11610 old_index,
11611 new_index,
11612 len,
11613 } => {
11614 if old_index + len <= old_line {
11615 n = new_index + len;
11616 continue;
11617 }
11618 if old_index < old_line {
11619 let take = old_line - old_index;
11620 return (new_index + take).min(n_new);
11621 }
11622 return new_index.min(n_new);
11623 }
11624 DiffOp::Delete {
11625 old_index,
11626 old_len,
11627 new_index,
11628 } => {
11629 if old_index + old_len <= old_line {
11630 n = new_index;
11631 continue;
11632 }
11633 if old_index < old_line {
11634 return new_index.min(n_new);
11635 }
11636 }
11637 DiffOp::Insert {
11638 old_index,
11639 new_index,
11640 new_len,
11641 } => {
11642 if old_index < old_line {
11643 n = new_index + new_len;
11644 continue;
11645 }
11646 if old_index == old_line {
11647 return (new_index + new_len).min(n_new);
11650 }
11651 return new_index.min(n_new);
11652 }
11653 DiffOp::Replace {
11654 old_index,
11655 old_len,
11656 new_index,
11657 new_len,
11658 } => {
11659 if old_index + old_len <= old_line {
11660 n = new_index + new_len;
11661 continue;
11662 }
11663 if old_index < old_line {
11664 let into_old = old_line - old_index;
11665 let mapped = new_index + into_old.min(new_len);
11666 return mapped.min(n_new);
11667 }
11668 return new_index.min(n_new);
11669 }
11670 }
11671 }
11672 n.min(n_new)
11673}
11674
11675fn parse_unified_hunk_header_ranges(header: &str) -> Option<(usize, usize, usize, usize)> {
11677 let rest = header.strip_prefix("@@ ")?;
11678 let (old_tok, rest2) = rest.split_once(" +")?;
11679 let old_tok = old_tok.strip_prefix('-')?;
11680 let new_tok = rest2.split_once(" @@").map(|(a, _)| a)?;
11681
11682 fn parse_side(spec: &str) -> Option<(usize, usize)> {
11683 let spec = spec.trim();
11684 let (start_one_based, count) = if let Some((a, b)) = spec.split_once(',') {
11685 (a.parse::<usize>().ok()?, b.parse::<usize>().ok()?)
11686 } else {
11687 let s = spec.parse::<usize>().ok()?;
11688 (s, 1usize)
11689 };
11690 let s0 = start_one_based.saturating_sub(1);
11691 let e0 = s0.saturating_add(count);
11692 Some((s0, e0))
11693 }
11694
11695 let (os, oe) = parse_side(old_tok)?;
11696 let (ns, ne) = parse_side(new_tok)?;
11697 Some((os, oe, ns, ne))
11698}
11699
11700fn append_with_whole_function_added(
11703 ops: &[similar::DiffOp],
11704 n_old: usize,
11705 n_new: usize,
11706 new_lines: &[&str],
11707 matcher: Option<&FuncnameMatcher>,
11708) -> bool {
11709 use similar::DiffOp;
11710 if n_old == 0 {
11711 return false;
11712 }
11713 let mut only_ins_or_eq = true;
11714 let mut min_new_ins = usize::MAX;
11715 for op in ops {
11716 match *op {
11717 DiffOp::Equal { .. } => {}
11718 DiffOp::Insert {
11719 new_index, new_len, ..
11720 } => {
11721 min_new_ins = min_new_ins.min(new_index);
11722 if new_len == 0 {
11723 only_ins_or_eq = false;
11724 }
11725 }
11726 DiffOp::Delete { .. } | DiffOp::Replace { .. } => {
11727 only_ins_or_eq = false;
11728 }
11729 }
11730 }
11731 let mut insert_at_eof = false;
11732 for op in ops {
11733 if let DiffOp::Insert { old_index, .. } = *op {
11734 if old_index == n_old {
11735 insert_at_eof = true;
11736 break;
11737 }
11738 }
11739 }
11740 let append_at_eof = min_new_ins == n_old || insert_at_eof;
11741 if !only_ins_or_eq || !append_at_eof || min_new_ins == usize::MAX {
11742 return false;
11743 }
11744 let mut j = min_new_ins;
11749 while j < n_new {
11750 let line = new_lines[j];
11751 if line.trim().is_empty() {
11752 j += 1;
11753 continue;
11754 }
11755 if let Some(m) = matcher {
11756 if m.match_line(line).is_some() {
11757 return true;
11758 }
11759 } else if inserted_block_starts_with_c_like_function_definition(line) {
11760 return true;
11761 }
11762 j += 1;
11763 }
11764 false
11765}
11766
11767fn inserted_block_starts_with_c_like_function_definition(line: &str) -> bool {
11768 let t = line.trim_start();
11769 let Some(open_paren) = t.find('(') else {
11770 return false;
11771 };
11772 let head = &t[..open_paren];
11773 let tokens: Vec<&str> = head.split_whitespace().collect();
11774 if tokens.len() < 2 {
11775 return false;
11777 }
11778 let nameish = tokens.last().copied().unwrap_or("");
11779 let name = nameish.trim_end_matches(['*', '&']);
11780 if name.is_empty() || !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
11781 return false;
11782 }
11783 let type_or_modifier = |tok: &str| {
11784 matches!(
11785 tok,
11786 "static"
11787 | "extern"
11788 | "inline"
11789 | "void"
11790 | "int"
11791 | "char"
11792 | "short"
11793 | "long"
11794 | "float"
11795 | "double"
11796 | "unsigned"
11797 | "signed"
11798 | "struct"
11799 | "enum"
11800 | "union"
11801 | "const"
11802 | "volatile"
11803 | "typedef"
11804 )
11805 };
11806 tokens[..tokens.len() - 1]
11807 .iter()
11808 .any(|tok| type_or_modifier(tok))
11809}
11810
11811fn hunk_old_change_end_exclusive(ops: &[similar::DiffOp]) -> usize {
11812 use similar::DiffOp;
11813 let mut max_o = 0usize;
11814 for op in ops {
11815 match *op {
11816 DiffOp::Delete {
11817 old_index, old_len, ..
11818 } => {
11819 max_o = max_o.max(old_index + old_len);
11820 }
11821 DiffOp::Replace {
11822 old_index, old_len, ..
11823 } => {
11824 max_o = max_o.max(old_index + old_len);
11825 }
11826 DiffOp::Insert { old_index, .. } => {
11827 max_o = max_o.max(old_index);
11830 }
11831 DiffOp::Equal { .. } => {}
11832 }
11833 }
11834 max_o
11835}
11836
11837fn func_context_old_anchor(ops: &[similar::DiffOp], n_old: usize) -> usize {
11838 use similar::DiffOp;
11839 let mut has_delete_or_replace = false;
11840 let mut min_del = usize::MAX;
11841 let mut min_ins_old = usize::MAX;
11842
11843 for op in ops {
11844 match *op {
11845 DiffOp::Delete {
11846 old_index, old_len, ..
11847 } => {
11848 has_delete_or_replace = true;
11849 min_del = min_del.min(old_index);
11850 min_del = min_del.min(old_index + old_len.saturating_sub(1));
11851 }
11852 DiffOp::Replace {
11853 old_index, old_len, ..
11854 } => {
11855 has_delete_or_replace = true;
11856 min_del = min_del.min(old_index);
11857 min_del = min_del.min(old_index + old_len.saturating_sub(1));
11858 }
11859 DiffOp::Insert { old_index, .. } => {
11860 min_ins_old = min_ins_old.min(old_index);
11861 }
11862 DiffOp::Equal { .. } => {}
11863 }
11864 }
11865
11866 let mut i1 = if has_delete_or_replace {
11867 min_del
11868 } else if min_ins_old != usize::MAX {
11869 min_ins_old
11870 } else {
11871 0
11872 };
11873
11874 let pure_insert = ops
11875 .iter()
11876 .all(|op| matches!(op, DiffOp::Insert { .. } | DiffOp::Equal { .. }))
11877 && ops.iter().any(|op| matches!(op, DiffOp::Insert { .. }));
11878
11879 if pure_insert && i1 >= n_old && n_old > 0 {
11880 i1 = n_old - 1;
11881 }
11882
11883 i1.min(n_old.saturating_sub(1))
11884}
11885
11886fn expand_func_pre_start(
11887 s1: usize,
11888 i1: usize,
11889 n_old: usize,
11890 old_lines: &[&str],
11891 matcher: Option<&FuncnameMatcher>,
11892) -> usize {
11893 if n_old == 0 {
11894 return s1;
11895 }
11896 let i1 = i1.min(n_old.saturating_sub(1));
11897 let mut fs1 = get_func_line_backward(old_lines, i1, matcher).unwrap_or(i1);
11898 while fs1 > 0
11899 && !is_line_empty_for_func_context(old_lines[fs1 - 1])
11900 && !is_func_line(old_lines[fs1 - 1], matcher)
11901 {
11902 fs1 -= 1;
11903 }
11904 s1.min(fs1)
11905}
11906
11907fn expand_func_post_end(
11908 e1: usize,
11909 i1_end: usize,
11910 n_old: usize,
11911 old_lines: &[&str],
11912 matcher: Option<&FuncnameMatcher>,
11913) -> usize {
11914 let from = i1_end.min(n_old);
11915 let fe1 = get_func_line_forward(old_lines, from, matcher).unwrap_or(n_old);
11916 let mut fe1_adj = fe1;
11917 while fe1_adj > 0 && is_line_empty_for_func_context(old_lines[fe1_adj - 1]) {
11918 fe1_adj -= 1;
11919 }
11920 e1.max(fe1_adj).min(n_old)
11921}
11922
11923fn is_line_empty_for_func_context(line: &str) -> bool {
11924 line.chars().all(|c| c.is_whitespace())
11925}
11926
11927pub(crate) fn is_func_line(line: &str, matcher: Option<&FuncnameMatcher>) -> bool {
11928 if let Some(m) = matcher {
11929 return m.match_line(line).is_some();
11930 }
11931 let t = line.trim_end_matches(['\n', '\r']);
11932 if t.is_empty() {
11933 return false;
11934 }
11935 let b = t.as_bytes()[0];
11936 if !(b.is_ascii_alphabetic() || b == b'_' || b == b'$') {
11937 return false;
11938 }
11939 true
11940}
11941
11942fn get_func_line_backward(
11943 old_lines: &[&str],
11944 start: usize,
11945 matcher: Option<&FuncnameMatcher>,
11946) -> Option<usize> {
11947 let mut l = start.min(old_lines.len().saturating_sub(1));
11948 if old_lines.is_empty() {
11949 return None;
11950 }
11951 loop {
11952 if is_func_line(old_lines[l], matcher) {
11953 return Some(l);
11954 }
11955 if l == 0 {
11956 break;
11957 }
11958 l -= 1;
11959 }
11960 None
11961}
11962
11963fn get_func_line_forward(
11964 old_lines: &[&str],
11965 start: usize,
11966 matcher: Option<&FuncnameMatcher>,
11967) -> Option<usize> {
11968 let mut l = start;
11969 while l < old_lines.len() {
11970 if is_func_line(old_lines[l], matcher) {
11971 return Some(l);
11972 }
11973 l += 1;
11974 }
11975 None
11976}
11977#[expect(clippy::too_many_arguments)]
11978pub fn anchored_unified_diff(
11988 old_content: &str,
11989 new_content: &str,
11990 old_path: &str,
11991 new_path: &str,
11992 context_lines: usize,
11993 anchors: &[String],
11994 algorithm: similar::Algorithm,
11995 use_git_histogram: bool,
11996 indent_heuristic: bool,
11997 quote_path_fully: bool,
11998) -> String {
11999 use crate::quote_path::format_diff_path_with_prefix;
12000 use similar::TextDiff;
12001
12002 let old_lines: Vec<&str> = old_content.lines().collect();
12003 let new_lines: Vec<&str> = new_content.lines().collect();
12004
12005 let mut anchor_pairs: Vec<(usize, usize)> = Vec::new(); for anchor in anchors {
12009 let anchor_str = anchor.as_str();
12010
12011 let old_positions: Vec<usize> = old_lines
12013 .iter()
12014 .enumerate()
12015 .filter(|(_, l)| l.trim_end() == anchor_str)
12016 .map(|(i, _)| i)
12017 .collect();
12018
12019 let new_positions: Vec<usize> = new_lines
12021 .iter()
12022 .enumerate()
12023 .filter(|(_, l)| l.trim_end() == anchor_str)
12024 .map(|(i, _)| i)
12025 .collect();
12026
12027 if old_positions.len() == 1 && new_positions.len() == 1 {
12029 anchor_pairs.push((old_positions[0], new_positions[0]));
12030 }
12031 }
12032
12033 if anchor_pairs.is_empty() {
12035 return unified_diff_with_prefix_and_funcname_and_algorithm(
12036 old_content,
12037 new_content,
12038 old_path,
12039 new_path,
12040 context_lines,
12041 0,
12042 "a/",
12043 "b/",
12044 None,
12045 algorithm,
12046 false,
12047 use_git_histogram,
12048 indent_heuristic,
12049 quote_path_fully,
12050 );
12051 }
12052
12053 anchor_pairs.sort_by_key(|&(old_idx, _)| old_idx);
12055
12056 let mut filtered: Vec<(usize, usize)> = Vec::new();
12059 for &pair in &anchor_pairs {
12060 if filtered.is_empty() || filtered.last().is_some_and(|last| pair.1 > last.1) {
12061 filtered.push(pair);
12062 }
12063 }
12064 let anchor_pairs = filtered;
12065
12066 struct LineDiffOp {
12075 tag: char, line: String,
12077 }
12078
12079 let append_segment_diff =
12080 |ops: &mut Vec<LineDiffOp>, old_seg_input: &str, new_seg_input: &str| {
12081 use similar::ChangeTag;
12082 let old_ls: Vec<&str> = old_seg_input.lines().collect();
12083 let new_ls: Vec<&str> = new_seg_input.lines().collect();
12084 if old_ls.is_empty() && new_ls.is_empty() {
12085 return;
12086 }
12087 let seg_diff = TextDiff::configure()
12088 .algorithm(algorithm)
12089 .diff_slices(&old_ls, &new_ls);
12090 let raw = seg_diff.ops().to_vec();
12091 let compacted = diff_indent_heuristic::apply_change_compact_to_ops(
12092 &raw,
12093 &old_ls,
12094 &new_ls,
12095 indent_heuristic,
12096 );
12097 for op in &compacted {
12098 for ch in op.iter_changes(&old_ls, &new_ls) {
12099 let t = match ch.tag() {
12100 ChangeTag::Equal => ' ',
12101 ChangeTag::Delete => '-',
12102 ChangeTag::Insert => '+',
12103 };
12104 ops.push(LineDiffOp {
12105 tag: t,
12106 line: ch.value().to_string(),
12107 });
12108 }
12109 }
12110 };
12111
12112 let mut ops: Vec<LineDiffOp> = Vec::new();
12113 let mut old_pos = 0usize;
12114 let mut new_pos = 0usize;
12115
12116 for &(old_anchor, new_anchor) in &anchor_pairs {
12117 let old_segment: Vec<&str> = old_lines[old_pos..old_anchor].to_vec();
12119 let new_segment: Vec<&str> = new_lines[new_pos..new_anchor].to_vec();
12120
12121 let old_seg_text = old_segment.join("\n");
12122 let new_seg_text = new_segment.join("\n");
12123
12124 if !old_seg_text.is_empty() || !new_seg_text.is_empty() {
12125 let old_seg_input = if old_seg_text.is_empty() {
12126 String::new()
12127 } else {
12128 format!("{}\n", old_seg_text)
12129 };
12130 let new_seg_input = if new_seg_text.is_empty() {
12131 String::new()
12132 } else {
12133 format!("{}\n", new_seg_text)
12134 };
12135 append_segment_diff(&mut ops, &old_seg_input, &new_seg_input);
12136 }
12137
12138 ops.push(LineDiffOp {
12140 tag: ' ',
12141 line: old_lines[old_anchor].to_string(),
12142 });
12143
12144 old_pos = old_anchor + 1;
12145 new_pos = new_anchor + 1;
12146 }
12147
12148 let old_segment: Vec<&str> = old_lines[old_pos..].to_vec();
12150 let new_segment: Vec<&str> = new_lines[new_pos..].to_vec();
12151 let old_seg_text = old_segment.join("\n");
12152 let new_seg_text = new_segment.join("\n");
12153
12154 if !old_seg_text.is_empty() || !new_seg_text.is_empty() {
12155 let old_seg_input = if old_seg_text.is_empty() {
12156 String::new()
12157 } else {
12158 format!("{}\n", old_seg_text)
12159 };
12160 let new_seg_input = if new_seg_text.is_empty() {
12161 String::new()
12162 } else {
12163 format!("{}\n", new_seg_text)
12164 };
12165 append_segment_diff(&mut ops, &old_seg_input, &new_seg_input);
12166 }
12167
12168 let mut output = String::new();
12170 if old_path == "/dev/null" {
12171 output.push_str("--- /dev/null\n");
12172 } else {
12173 output.push_str("--- ");
12174 output.push_str(&format_diff_path_with_prefix(
12175 "a/",
12176 old_path,
12177 quote_path_fully,
12178 ));
12179 output.push('\n');
12180 }
12181 if new_path == "/dev/null" {
12182 output.push_str("+++ /dev/null\n");
12183 } else {
12184 output.push_str("+++ ");
12185 output.push_str(&format_diff_path_with_prefix(
12186 "b/",
12187 new_path,
12188 quote_path_fully,
12189 ));
12190 output.push('\n');
12191 }
12192
12193 let total_ops = ops.len();
12195 if total_ops == 0 {
12196 return output;
12197 }
12198
12199 let mut hunks: Vec<(usize, usize)> = Vec::new(); let mut i = 0;
12202 while i < total_ops {
12203 if ops[i].tag != ' ' {
12204 let start = i.saturating_sub(context_lines);
12205 let mut end = i;
12206 while end < total_ops {
12208 if ops[end].tag != ' ' {
12209 end += 1;
12210 continue;
12211 }
12212 let mut next_change = end;
12214 while next_change < total_ops && ops[next_change].tag == ' ' {
12215 next_change += 1;
12216 }
12217 if next_change < total_ops && next_change - end <= context_lines * 2 {
12218 end = next_change + 1;
12219 } else {
12220 end = (end + context_lines).min(total_ops);
12221 break;
12222 }
12223 }
12224 if let Some(last) = hunks.last_mut() {
12226 if start <= last.1 {
12227 last.1 = end;
12228 } else {
12229 hunks.push((start, end));
12230 }
12231 } else {
12232 hunks.push((start, end));
12233 }
12234 i = end;
12235 } else {
12236 i += 1;
12237 }
12238 }
12239
12240 for (start, end) in hunks {
12242 let mut old_start = 1usize;
12244 let mut new_start = 1usize;
12245 for op in &ops[..start] {
12247 match op.tag {
12248 ' ' => {
12249 old_start += 1;
12250 new_start += 1;
12251 }
12252 '-' => {
12253 old_start += 1;
12254 }
12255 '+' => {
12256 new_start += 1;
12257 }
12258 _ => {}
12259 }
12260 }
12261 let mut old_count = 0usize;
12262 let mut new_count = 0usize;
12263 for op in &ops[start..end] {
12264 match op.tag {
12265 ' ' => {
12266 old_count += 1;
12267 new_count += 1;
12268 }
12269 '-' => {
12270 old_count += 1;
12271 }
12272 '+' => {
12273 new_count += 1;
12274 }
12275 _ => {}
12276 }
12277 }
12278
12279 output.push_str(&format!(
12280 "@@ -{},{} +{},{} @@\n",
12281 old_start, old_count, new_start, new_count
12282 ));
12283 for op in &ops[start..end] {
12284 output.push(op.tag);
12285 output.push_str(&op.line);
12286 output.push('\n');
12287 }
12288 }
12289
12290 output
12291}
12292
12293fn func_context_line_matches(line: &str, funcname_matcher: Option<&FuncnameMatcher>) -> bool {
12299 if let Some(m) = funcname_matcher {
12300 return m.match_line(line).is_some();
12301 }
12302 is_func_line(line, None)
12303}
12304
12305fn extract_function_context(
12306 header: &str,
12307 old_lines: &[&str],
12308 funcname_matcher: Option<&FuncnameMatcher>,
12309 _source_path: Option<&str>,
12310) -> Option<String> {
12311 let at_pos = header.find("-")?;
12313 let rest = &header[at_pos + 1..];
12314 let comma_or_space = rest.find([',', ' '])?;
12315 let start_str = &rest[..comma_or_space];
12316 let start_line: usize = start_str.parse().ok()?;
12317
12318 let old_token_end = rest.find([' ', '\t']).unwrap_or(rest.len());
12322 let old_token = &rest[..old_token_end];
12323 let old_count: usize = if let Some(comma) = old_token.find(',') {
12324 old_token[comma + 1..].parse().unwrap_or(1)
12325 } else {
12326 1
12327 };
12328
12329 if start_line == 0 {
12330 return None;
12331 }
12332
12333 if start_line == 0 {
12340 return None;
12341 }
12342 let search_end = if old_count == 0 {
12343 start_line.min(old_lines.len())
12344 } else {
12345 if start_line <= 1 {
12346 return None;
12347 }
12348 (start_line - 1).min(old_lines.len())
12349 };
12350 let truncate = |line: &str| {
12351 let mut end = line.len().min(80);
12352 while end > 0 && !line.is_char_boundary(end) {
12353 end -= 1;
12354 }
12355 let mut out = line[..end].to_owned();
12356 let tail = line[end..].trim_start();
12357 if tail.is_empty()
12362 || (!tail.starts_with('"')
12363 && !tail.starts_with('\'')
12364 && !line.starts_with("msgstr")
12365 && !line.starts_with("msgid"))
12366 {
12367 out = out.trim_end().to_owned();
12368 }
12369 out
12370 };
12371
12372 for i in (0..search_end).rev() {
12373 let line = old_lines[i];
12374 if line.is_empty() {
12375 continue;
12376 }
12377 if func_context_line_matches(line, funcname_matcher) {
12378 if let Some(matcher) = funcname_matcher {
12379 if let Some(matched) = matcher.match_line(line) {
12380 return Some(truncate(&matched));
12381 }
12382 continue;
12383 }
12384 return Some(truncate(line));
12385 }
12386 }
12387 None
12388}
12389
12390fn last_line_index_without_trailing_newline(text: &str) -> Option<usize> {
12391 LineSide::from_text(text).incomplete_last_line_index()
12392}
12393
12394fn parse_unified_hunk_starts(header: &str) -> Option<(usize, usize)> {
12395 let rest = header.strip_prefix("@@ ")?;
12396 let (old_part, new_part) = rest.split_once(" +")?;
12397 let old_start = parse_unified_range_start(old_part.strip_prefix('-')?)?;
12398 let new_start = parse_unified_range_start(new_part.split(" @@").next()?)?;
12399 Some((old_start, new_start))
12400}
12401
12402fn parse_unified_range_start(spec: &str) -> Option<usize> {
12403 let spec = spec.trim();
12404 if let Some((start, _)) = spec.split_once(',') {
12405 start.parse().ok()
12406 } else {
12407 spec.parse().ok()
12408 }
12409}
12410
12411struct UnifiedDiffNoNewlineAnnotator {
12413 old_last: Option<usize>,
12414 new_last: Option<usize>,
12415 old_line: usize,
12416 new_line: usize,
12417 passthrough: bool,
12418}
12419
12420impl UnifiedDiffNoNewlineAnnotator {
12421 fn new(old: &str, new: &str) -> Self {
12422 let old_last = last_line_index_without_trailing_newline(old);
12423 let new_last = last_line_index_without_trailing_newline(new);
12424 Self {
12425 old_last,
12426 new_last,
12427 old_line: 1,
12428 new_line: 1,
12429 passthrough: old_last.is_none() && new_last.is_none(),
12430 }
12431 }
12432
12433 fn write_hunk<W: std::io::Write>(&mut self, out: &mut W, patch: &str) -> Result<()> {
12434 if self.passthrough {
12435 out.write_all(patch.as_bytes())?;
12436 return Ok(());
12437 }
12438 let mut lines = patch.lines().peekable();
12439 while let Some(line) = lines.next() {
12440 self.write_annotated_line(out, line, &mut lines)?;
12441 }
12442 Ok(())
12443 }
12444
12445 fn write_annotated_line<W: std::io::Write>(
12446 &mut self,
12447 out: &mut W,
12448 line: &str,
12449 lines: &mut std::iter::Peekable<std::str::Lines<'_>>,
12450 ) -> Result<()> {
12451 if let Some((o, n)) = line
12452 .strip_prefix("@@ ")
12453 .and_then(|_| parse_unified_hunk_starts(line))
12454 {
12455 self.old_line = o;
12456 self.new_line = n;
12457 writeln!(out, "{line}")?;
12458 return Ok(());
12459 }
12460 if line.starts_with("--- ")
12461 || line.starts_with("+++ ")
12462 || line.starts_with("diff --git ")
12463 || line.starts_with("diff --cc ")
12464 || line.starts_with("diff --combined ")
12465 {
12466 writeln!(out, "{line}")?;
12467 return Ok(());
12468 }
12469 if line.starts_with('-') {
12470 writeln!(out, "{line}")?;
12471 let already_marked = lines
12472 .peek()
12473 .is_some_and(|next| *next == "\\ No newline at end of file");
12474 if !already_marked && self.old_last == Some(self.old_line.saturating_sub(1)) {
12475 writeln!(out, "\\ No newline at end of file")?;
12476 }
12477 self.old_line += 1;
12478 return Ok(());
12479 }
12480 if line.starts_with('+') {
12481 writeln!(out, "{line}")?;
12482 let already_marked = lines
12483 .peek()
12484 .is_some_and(|next| *next == "\\ No newline at end of file");
12485 if !already_marked && self.new_last == Some(self.new_line.saturating_sub(1)) {
12486 writeln!(out, "\\ No newline at end of file")?;
12487 }
12488 self.new_line += 1;
12489 return Ok(());
12490 }
12491 if line.starts_with(' ') {
12492 writeln!(out, "{line}")?;
12493 let already_marked = lines
12494 .peek()
12495 .is_some_and(|next| *next == "\\ No newline at end of file");
12496 if !already_marked {
12497 let old_idx = self.old_line.saturating_sub(1);
12498 let new_idx = self.new_line.saturating_sub(1);
12499 if self.old_last == Some(old_idx) || self.new_last == Some(new_idx) {
12500 writeln!(out, "\\ No newline at end of file")?;
12501 }
12502 }
12503 self.old_line += 1;
12504 self.new_line += 1;
12505 return Ok(());
12506 }
12507 writeln!(out, "{line}")?;
12508 Ok(())
12509 }
12510}
12511
12512#[must_use]
12515pub fn annotate_unified_diff_no_newline_at_eof(patch: &str, old: &str, new: &str) -> String {
12516 let mut annotator = UnifiedDiffNoNewlineAnnotator::new(old, new);
12517 if annotator.passthrough {
12518 return patch.to_string();
12519 }
12520 let mut out = Vec::with_capacity(patch.len() + 64);
12521 let mut lines = patch.lines().peekable();
12522 while let Some(line) = lines.next() {
12523 if annotator
12524 .write_annotated_line(&mut out, line, &mut lines)
12525 .is_err()
12526 {
12527 return patch.to_string();
12528 }
12529 }
12530 String::from_utf8(out).unwrap_or_else(|_| patch.to_string())
12531}
12532
12533#[must_use]
12535pub fn normalize_ignore_space_change_line(line: &str) -> String {
12536 let mut result = String::with_capacity(line.len());
12537 let mut in_space = false;
12538 for c in line.chars() {
12539 if c.is_whitespace() {
12540 if !in_space {
12541 result.push(' ');
12542 in_space = true;
12543 }
12544 } else {
12545 result.push(c);
12546 in_space = false;
12547 }
12548 }
12549 while result.ends_with(' ') {
12550 result.pop();
12551 }
12552 result
12553}
12554
12555#[must_use]
12561pub fn normalize_ignore_space_change(content: &str) -> String {
12562 content
12563 .lines()
12564 .map(normalize_ignore_space_change_line)
12565 .collect::<Vec<_>>()
12566 .join("\n")
12567}
12568
12569#[must_use]
12573pub fn count_changes_with_algorithm(
12574 old_content: &str,
12575 new_content: &str,
12576 algorithm: similar::Algorithm,
12577 use_git_histogram: bool,
12578) -> (usize, usize) {
12579 use crate::diff::options::{DiffAlgorithm, DiffOptions};
12580 let diff_algorithm = if use_git_histogram {
12581 DiffAlgorithm::Histogram
12582 } else {
12583 match algorithm {
12584 similar::Algorithm::Myers => DiffAlgorithm::Myers,
12585 similar::Algorithm::Lcs => DiffAlgorithm::Minimal,
12586 similar::Algorithm::Patience => DiffAlgorithm::Patience,
12587 _ => DiffAlgorithm::Myers,
12588 }
12589 };
12590 count_line_changes_with_options(
12591 old_content,
12592 new_content,
12593 &DiffOptions {
12594 algorithm: diff_algorithm,
12595 ..DiffOptions::default()
12596 },
12597 )
12598}
12599
12600#[must_use]
12602#[allow(dead_code)]
12603fn count_changes_git_histogram(old_content: &str, new_content: &str) -> (usize, usize) {
12604 count_changes_with_algorithm(old_content, new_content, similar::Algorithm::Myers, true)
12605}
12606
12607#[must_use]
12612pub fn count_git_lines(data: &[u8]) -> usize {
12613 if data.is_empty() {
12614 return 0;
12615 }
12616 let mut count = 0usize;
12617 let mut nl_just_seen = false;
12618 for &ch in data {
12619 if ch == b'\n' {
12620 count += 1;
12621 nl_just_seen = true;
12622 } else {
12623 nl_just_seen = false;
12624 }
12625 }
12626 if !nl_just_seen {
12627 count += 1;
12628 }
12629 count
12630}
12631
12632pub(crate) const GIT_DIFF_MAX_SCORE: u64 = 60_000;
12634const DIFF_MAX_SCORE: u64 = GIT_DIFF_MAX_SCORE;
12635const DIFF_MINIMUM_BREAK_SIZE: usize = 400;
12636#[allow(dead_code)]
12637const DIFF_DEFAULT_BREAK_SCORE: u64 = 30_000;
12638#[allow(dead_code)]
12640const GIT_DIFF_DEFAULT_BREAK_SCORE: u64 = DIFF_DEFAULT_BREAK_SCORE;
12641const DIFF_HASHBASE: u32 = 107_927;
12642
12643#[derive(Clone, Copy, Default)]
12644struct SpanSlot {
12645 hashval: u32,
12646 cnt: u32,
12647}
12648
12649struct SpanHashTop {
12650 alloc_log2: u8,
12651 free_slots: i32,
12652 data: Vec<SpanSlot>,
12653}
12654
12655impl SpanHashTop {
12656 fn new(initial_log2: u8) -> Self {
12657 let cap = 1usize << initial_log2;
12658 Self {
12659 alloc_log2: initial_log2,
12660 free_slots: initial_free(initial_log2),
12661 data: vec![SpanSlot::default(); cap],
12662 }
12663 }
12664
12665 fn len(&self) -> usize {
12666 1usize << self.alloc_log2
12667 }
12668
12669 fn add_span(&mut self, hashval: u32, cnt: u32) {
12670 loop {
12671 let lim = self.len();
12672 let mut bucket = (hashval as usize) & (lim - 1);
12673 loop {
12674 let h = &mut self.data[bucket];
12675 if h.cnt == 0 {
12676 h.hashval = hashval;
12677 h.cnt = cnt;
12678 self.free_slots -= 1;
12679 if self.free_slots < 0 {
12680 self.rehash();
12681 break;
12682 }
12683 return;
12684 }
12685 if h.hashval == hashval {
12686 h.cnt = h.cnt.saturating_add(cnt);
12687 return;
12688 }
12689 bucket += 1;
12690 if bucket >= lim {
12691 bucket = 0;
12692 }
12693 }
12694 }
12695 }
12696
12697 fn rehash(&mut self) {
12698 let old = std::mem::take(&mut self.data);
12699 let old_log = self.alloc_log2;
12700 self.alloc_log2 = old_log.saturating_add(1);
12701 let new_len = 1usize << self.alloc_log2;
12702 self.free_slots = initial_free(self.alloc_log2);
12703 self.data = vec![SpanSlot::default(); new_len];
12704 let old_sz = 1usize << old_log;
12705 for o in old.iter().take(old_sz) {
12706 let o = *o;
12707 if o.cnt == 0 {
12708 continue;
12709 }
12710 self.add_span_after_rehash(o.hashval, o.cnt);
12711 }
12712 }
12713
12714 fn add_span_after_rehash(&mut self, hashval: u32, cnt: u32) {
12715 loop {
12716 let lim = self.len();
12717 let mut bucket = (hashval as usize) & (lim - 1);
12718 loop {
12719 let h = &mut self.data[bucket];
12720 if h.cnt == 0 {
12721 h.hashval = hashval;
12722 h.cnt = cnt;
12723 self.free_slots -= 1;
12724 if self.free_slots < 0 {
12725 self.rehash();
12726 break;
12727 }
12728 return;
12729 }
12730 if h.hashval == hashval {
12731 h.cnt = h.cnt.saturating_add(cnt);
12732 return;
12733 }
12734 bucket += 1;
12735 if bucket >= lim {
12736 bucket = 0;
12737 }
12738 }
12739 }
12740 }
12741
12742 fn sort_by_hashval(&mut self) {
12743 let sz = self.len();
12744 self.data[..sz].sort_by(|a, b| {
12745 if a.cnt == 0 {
12746 return std::cmp::Ordering::Greater;
12747 }
12748 if b.cnt == 0 {
12749 return std::cmp::Ordering::Less;
12750 }
12751 a.hashval.cmp(&b.hashval)
12752 });
12753 }
12754}
12755
12756fn initial_free(sz_log2: u8) -> i32 {
12757 let sz = sz_log2 as i32;
12758 ((1i32 << sz_log2) * (sz - 3) / sz).max(0)
12759}
12760
12761fn hash_blob_spans(buf: &[u8], is_text: bool) -> SpanHashTop {
12762 let mut hash = SpanHashTop::new(9);
12763 let mut n = 0u32;
12764 let mut accum1: u32 = 0;
12765 let mut accum2: u32 = 0;
12766 let mut i = 0usize;
12767 while i < buf.len() {
12768 let c = buf[i] as u32;
12769 let old_1 = accum1;
12770 i += 1;
12771
12772 if is_text && c == b'\r' as u32 && i < buf.len() && buf[i] == b'\n' {
12773 continue;
12774 }
12775
12776 accum1 = accum1.wrapping_shl(7) ^ accum2.wrapping_shr(25);
12777 accum2 = accum2.wrapping_shl(7) ^ old_1.wrapping_shr(25);
12778 accum1 = accum1.wrapping_add(c);
12779 n += 1;
12780 if n < 64 && c != b'\n' as u32 {
12781 continue;
12782 }
12783 let hashval = (accum1.wrapping_add(accum2.wrapping_mul(0x61))) % DIFF_HASHBASE;
12784 hash.add_span(hashval, n);
12785 n = 0;
12786 accum1 = 0;
12787 accum2 = 0;
12788 }
12789 if n > 0 {
12790 let hashval = (accum1.wrapping_add(accum2.wrapping_mul(0x61))) % DIFF_HASHBASE;
12791 hash.add_span(hashval, n);
12792 }
12793 hash.sort_by_hashval();
12794 hash
12795}
12796
12797#[must_use]
12802pub(crate) fn diffcore_count_changes(old: &[u8], new: &[u8]) -> (u64, u64) {
12803 crate::trace::trace_span!("grit.diff.blob");
12804 let src_is_text = !crate::merge_file::is_binary(old);
12805 let dst_is_text = !crate::merge_file::is_binary(new);
12806 let src_count = hash_blob_spans(old, src_is_text);
12807 let dst_count = hash_blob_spans(new, dst_is_text);
12808 let mut sc: u64 = 0;
12809 let mut la: u64 = 0;
12810 let mut si = 0usize;
12811 let mut di = 0usize;
12812 let src_len = src_count.len();
12813 let dst_len = dst_count.len();
12814 loop {
12815 if si >= src_len || src_count.data[si].cnt == 0 {
12816 break;
12817 }
12818 let s_hash = src_count.data[si].hashval;
12819 let s_cnt = u64::from(src_count.data[si].cnt);
12820 while di < dst_len && dst_count.data[di].cnt != 0 && dst_count.data[di].hashval < s_hash {
12821 la += u64::from(dst_count.data[di].cnt);
12822 di += 1;
12823 }
12824 let mut dst_cnt = 0u64;
12825 if di < dst_len && dst_count.data[di].cnt != 0 && dst_count.data[di].hashval == s_hash {
12826 dst_cnt = u64::from(dst_count.data[di].cnt);
12827 di += 1;
12828 }
12829 if s_cnt < dst_cnt {
12830 la += dst_cnt - s_cnt;
12831 sc += s_cnt;
12832 } else {
12833 sc += dst_cnt;
12834 }
12835 si += 1;
12836 }
12837 while di < dst_len && dst_count.data[di].cnt != 0 {
12838 la += u64::from(dst_count.data[di].cnt);
12839 di += 1;
12840 }
12841 (sc, la)
12842}
12843
12844#[must_use]
12848pub(crate) fn should_break_rewrite_pair(old: &[u8], new: &[u8], break_score: u64) -> bool {
12849 should_break_rewrite_inner(old, new, break_score)
12850}
12851
12852#[must_use]
12854pub(crate) fn apply_diff_filter(
12855 entries: Vec<DiffEntry>,
12856 filter: Option<super::filter::DiffFilter>,
12857) -> Vec<DiffEntry> {
12858 match filter {
12859 Some(f) => f.filter_entries(entries),
12860 None => entries,
12861 }
12862}
12863
12864#[must_use]
12866pub fn parse_diff_rename_threshold_percent(arg: &str) -> Option<u8> {
12867 let token = if arg.is_empty() { "50" } else { arg };
12868 let internal = parse_diff_rename_score_token(token)?;
12869 Some((internal.saturating_mul(100) / GIT_DIFF_MAX_SCORE).min(100) as u8)
12870}
12871
12872pub(crate) fn parse_diff_rename_score_token(arg: &str) -> Option<u64> {
12875 let mut num: u64 = 0;
12876 let mut scale: u64 = 1;
12877 let mut dot = false;
12878 let mut saw_digit = false;
12879 for ch in arg.chars() {
12880 if !dot && ch == '.' {
12881 scale = 1;
12882 dot = true;
12883 continue;
12884 }
12885 if ch == '%' {
12886 scale = if dot { scale.saturating_mul(100) } else { 100 };
12887 break;
12888 }
12889 if ch.is_ascii_digit() {
12890 saw_digit = true;
12891 if scale < 100_000 {
12892 scale = scale.saturating_mul(10);
12893 num = num.saturating_mul(10) + u64::from(ch as u8 - b'0');
12894 }
12895 } else {
12896 break;
12897 }
12898 }
12899 if !saw_digit {
12900 return None;
12901 }
12902 Some(if num >= scale {
12903 GIT_DIFF_MAX_SCORE
12904 } else {
12905 GIT_DIFF_MAX_SCORE * num / scale
12906 })
12907}
12908
12909#[must_use]
12912pub(crate) fn rewrite_merge_score(old: &[u8], new: &[u8]) -> Option<u64> {
12913 if old.is_empty() {
12914 return None;
12915 }
12916 let max_size = old.len().max(new.len());
12917 if max_size < DIFF_MINIMUM_BREAK_SIZE {
12918 return None;
12919 }
12920 let (src_copied, _) = diffcore_count_changes(old, new);
12921 let src_copied = src_copied.min(old.len() as u64);
12922 let src_removed = (old.len() as u64).saturating_sub(src_copied);
12923 Some(src_removed * DIFF_MAX_SCORE / old.len() as u64)
12924}
12925
12926#[must_use]
12928pub(crate) fn rewrite_dissimilarity_index_percent(old: &[u8], new: &[u8]) -> Option<u32> {
12929 let score = rewrite_merge_score(old, new)?;
12930 Some((score * 100 / DIFF_MAX_SCORE).min(100) as u32)
12931}
12932
12933fn should_break_rewrite_inner(src: &[u8], dst: &[u8], break_score: u64) -> bool {
12934 if src.is_empty() {
12935 return false;
12936 }
12937 let max_size = src.len().max(dst.len());
12938 if max_size < DIFF_MINIMUM_BREAK_SIZE {
12939 return false;
12940 }
12941 let (src_copied, literal_added) = diffcore_count_changes(src, dst);
12942 let src_copied = src_copied.min(src.len() as u64);
12943 let mut literal_added = literal_added;
12944 let dst_len = dst.len() as u64;
12945 if src_copied < dst_len && literal_added + src_copied > dst_len {
12946 literal_added = dst_len.saturating_sub(src_copied);
12947 }
12948 let src_removed = (src.len() as u64).saturating_sub(src_copied);
12949 let merge_score = src_removed * DIFF_MAX_SCORE / src.len() as u64;
12950 if merge_score > break_score {
12951 return true;
12952 }
12953 let delta_size = src_removed.saturating_add(literal_added);
12954 if delta_size * DIFF_MAX_SCORE / (max_size as u64) < break_score {
12955 return false;
12956 }
12957 let s = src.len() as u64;
12958 if (s * break_score < src_removed * DIFF_MAX_SCORE)
12959 && (literal_added * 20 < src_removed)
12960 && (literal_added * 20 < src_copied)
12961 {
12962 return false;
12963 }
12964 true
12965}
12966
12967struct FlatEntry {
12971 path: String,
12972 mode: u32,
12973 oid: ObjectId,
12974}
12975
12976fn flatten_tree(odb: &Odb, tree_oid: &ObjectId, prefix: &str) -> Result<Vec<FlatEntry>> {
12977 flatten_tree_with_index(odb, tree_oid, prefix, None)
12978}
12979
12980fn sparse_index_skips_tree_descent(index: &Index, dir_path: &str, tree_oid: &ObjectId) -> bool {
12981 let mut storage = crate::repo_relpath::storage_bytes(dir_path).into_owned();
12982 if !storage.ends_with(b"/") {
12983 storage.push(b'/');
12984 }
12985 index.find_entry_pos(&storage, 0).is_some_and(|pos| {
12986 let entry = &index.entries[pos];
12987 entry.is_sparse_directory_placeholder() && entry.oid == *tree_oid
12988 })
12989}
12990
12991fn subtree_oid_at_path(odb: &Odb, root: &ObjectId, path: &str) -> Result<Option<ObjectId>> {
12993 let mut oid = *root;
12994 for component in path.split('/').filter(|c| !c.is_empty()) {
12995 let entries = read_tree(odb, &oid)?;
12996 let Some(entry) = entries
12997 .iter()
12998 .find(|e| e.name.as_slice() == component.as_bytes())
12999 else {
13000 return Ok(None);
13001 };
13002 if !is_tree_mode(entry.mode) {
13003 return Ok(None);
13004 }
13005 oid = entry.oid;
13006 }
13007 Ok(Some(oid))
13008}
13009
13010fn emit_flattened_subtree_as_added(
13012 odb: &Odb,
13013 subtree: &ObjectId,
13014 prefix: &str,
13015 ignore_submodules: bool,
13016 result: &mut Vec<DiffEntry>,
13017) -> Result<()> {
13018 for entry in flatten_tree(odb, subtree, prefix)? {
13019 if ignore_submodules && entry.mode == 0o160000 {
13020 continue;
13021 }
13022 result.push(DiffEntry {
13023 status: DiffStatus::Added,
13024 old_path: None,
13025 new_path: Some(entry.path),
13026 old_mode: FileMode::ABSENT,
13027 new_mode: FileMode::new(entry.mode),
13028 old_oid: zero_oid(),
13029 new_oid: entry.oid,
13030 score: None,
13031 });
13032 }
13033 Ok(())
13034}
13035
13036fn diff_flattened_subtrees(
13038 head: &[FlatEntry],
13039 index: &[FlatEntry],
13040 ignore_submodules: bool,
13041 result: &mut Vec<DiffEntry>,
13042 stage0_paths: &mut std::collections::BTreeSet<String>,
13043 tree_map: &mut std::collections::BTreeMap<&str, &FlatEntry>,
13044) {
13045 let mut head_map: std::collections::BTreeMap<&str, &FlatEntry> = head
13046 .iter()
13047 .map(|entry| (entry.path.as_str(), entry))
13048 .collect();
13049 for entry in index {
13050 stage0_paths.insert(entry.path.clone());
13051 tree_map.remove(entry.path.as_str());
13052 match head_map.remove(entry.path.as_str()) {
13053 Some(te) => {
13054 if te.oid != entry.oid || te.mode != entry.mode {
13055 result.push(DiffEntry {
13056 status: DiffStatus::for_content_change(te.mode, entry.mode),
13057 old_path: Some(entry.path.clone()),
13058 new_path: Some(entry.path.clone()),
13059 old_mode: FileMode::new(te.mode),
13060 new_mode: FileMode::new(entry.mode),
13061 old_oid: te.oid,
13062 new_oid: entry.oid,
13063 score: None,
13064 });
13065 }
13066 }
13067 None => {
13068 result.push(DiffEntry {
13069 status: DiffStatus::Added,
13070 old_path: None,
13071 new_path: Some(entry.path.clone()),
13072 old_mode: FileMode::ABSENT,
13073 new_mode: FileMode::new(entry.mode),
13074 old_oid: zero_oid(),
13075 new_oid: entry.oid,
13076 score: None,
13077 });
13078 }
13079 }
13080 }
13081 for (path, te) in head_map {
13082 if ignore_submodules && te.mode == 0o160000 {
13083 continue;
13084 }
13085 tree_map.remove(path);
13086 result.push(DiffEntry {
13087 status: DiffStatus::Deleted,
13088 old_path: Some(path.to_owned()),
13089 new_path: None,
13090 old_mode: FileMode::new(te.mode),
13091 new_mode: FileMode::ABSENT,
13092 old_oid: te.oid,
13093 new_oid: zero_oid(),
13094 score: None,
13095 });
13096 }
13097}
13098
13099fn flatten_tree_with_index(
13100 odb: &Odb,
13101 tree_oid: &ObjectId,
13102 prefix: &str,
13103 index: Option<&Index>,
13104) -> Result<Vec<FlatEntry>> {
13105 let entries = read_tree(odb, tree_oid)?;
13106 let mut result = Vec::new();
13107
13108 for entry in entries {
13109 let path = crate::repo_relpath::format_path_stored(prefix, &entry.name);
13110 if is_tree_mode(entry.mode) {
13111 if index.is_some_and(|idx| sparse_index_skips_tree_descent(idx, &path, &entry.oid)) {
13112 continue;
13113 }
13114 let nested = flatten_tree_with_index(odb, &entry.oid, &path, index)?;
13115 result.extend(nested);
13116 } else {
13117 result.push(FlatEntry {
13118 path,
13119 mode: entry.mode,
13120 oid: entry.oid,
13121 });
13122 }
13123 }
13124
13125 Ok(result)
13126}
13127
13128pub(crate) fn head_path_states(
13130 odb: &Odb,
13131 head_tree: Option<&ObjectId>,
13132) -> Result<std::collections::BTreeMap<String, (u32, ObjectId)>> {
13133 let mut m = std::collections::BTreeMap::new();
13134 let Some(t) = head_tree else {
13135 return Ok(m);
13136 };
13137 for fe in flatten_tree(odb, t, "")? {
13138 m.insert(fe.path, (fe.mode, fe.oid));
13139 }
13140 Ok(m)
13141}
13142
13143fn is_tree_mode(mode: u32) -> bool {
13145 crate::index::is_tree_mode(mode)
13146}
13147
13148fn path_order(a: &DiffEntry, b: &DiffEntry) -> std::cmp::Ordering {
13151 use crate::repo_relpath::storage_bytes;
13152 storage_bytes(a.path()).cmp(&storage_bytes(b.path()))
13153}
13154
13155fn entries_sorted_by_path(entries: &[DiffEntry]) -> bool {
13156 entries.is_sorted_by(|a, b| path_order(a, b).is_le())
13157}
13158
13159fn entries_strictly_sorted_by_path(entries: &[DiffEntry]) -> bool {
13160 entries.is_sorted_by(|a, b| path_order(a, b).is_lt())
13161}
13162
13163pub fn sort_entries_by_path(entries: &mut [DiffEntry]) {
13174 if entries_sorted_by_path(entries) {
13175 return;
13176 }
13177 entries.sort_by(path_order);
13178}
13179
13180pub(crate) fn format_mode(mode: u32) -> String {
13182 format!("{mode:06o}")
13183}
13184
13185#[must_use]
13189#[allow(dead_code)]
13190fn read_submodule_head_for_checkout(sub_dir: &Path) -> Option<ObjectId> {
13191 read_submodule_head(sub_dir)
13192}
13193
13194#[must_use]
13199#[allow(dead_code)]
13200fn submodule_commit_subject_line(c: &CommitData) -> String {
13201 let enc = c.encoding.as_deref().unwrap_or("UTF-8");
13202 let is_latin1 = enc.eq_ignore_ascii_case("ISO8859-1")
13203 || enc.eq_ignore_ascii_case("ISO-8859-1")
13204 || enc.eq_ignore_ascii_case("LATIN1")
13205 || enc.eq_ignore_ascii_case("ISO-8859-15");
13206 if let Some(raw) = c.raw_message.as_deref() {
13207 let line = raw.split(|b| *b == b'\n').next().unwrap_or(raw);
13208 if is_latin1 {
13209 return line
13210 .iter()
13211 .map(|&b| b as char)
13212 .collect::<String>()
13213 .trim()
13214 .to_owned();
13215 }
13216 return String::from_utf8_lossy(line).trim().to_string();
13217 }
13218 c.message.lines().next().unwrap_or("").trim().to_owned()
13219}
13220
13221fn uninitialized_gitlink_worktree_matches_index(sub_dir: &Path) -> bool {
13225 submodule_embedded_git_dir(sub_dir).is_none()
13226}
13227
13228fn read_submodule_head(sub_dir: &Path) -> Option<ObjectId> {
13229 read_submodule_head_oid(sub_dir)
13230}
13231
13232#[must_use]
13234pub(crate) fn submodule_embedded_git_dir(sub_dir: &Path) -> Option<PathBuf> {
13235 let sub_meta = fs::symlink_metadata(sub_dir).ok()?;
13236 if !sub_meta.is_dir() || sub_meta.file_type().is_symlink() {
13237 return None;
13238 }
13239 let gitfile = sub_dir.join(".git");
13240 let meta = fs::metadata(&gitfile).ok()?;
13241 if meta.is_file() {
13242 let content = fs::read_to_string(&gitfile).ok()?;
13243 let gitdir = content
13244 .lines()
13245 .find_map(|l| l.strip_prefix("gitdir: "))?
13246 .trim();
13247 Some(if Path::new(gitdir).is_absolute() {
13248 PathBuf::from(gitdir)
13249 } else {
13250 sub_dir.join(gitdir)
13251 })
13252 } else if meta.is_dir() {
13253 Some(gitfile)
13254 } else {
13255 None
13256 }
13257}
13258
13259#[must_use]
13265pub(crate) fn is_unregistered_embedded_git_repo(index: &Index, rel: &str, abs: &Path) -> bool {
13266 if rel.is_empty() || index.has_gitlink_at(rel.as_bytes()) {
13267 return false;
13268 }
13269 let Some(git_dir) = submodule_embedded_git_dir(abs) else {
13270 return false;
13271 };
13272 crate::submodule_gitdir::is_valid_git_dir(&git_dir)
13273}
13274
13275pub(crate) enum EmbeddedGitRepoStage {
13277 NotEmbedded,
13279 Gitlink(crate::objects::ObjectId),
13281}
13282
13283pub(crate) fn classify_embedded_git_repo_for_stage(
13292 _index: &Index,
13293 rel: &str,
13294 abs: &Path,
13295) -> crate::error::Result<EmbeddedGitRepoStage> {
13296 if rel.is_empty() {
13297 return Ok(EmbeddedGitRepoStage::NotEmbedded);
13298 }
13299 let Some(git_dir) = submodule_embedded_git_dir(abs) else {
13300 return Ok(EmbeddedGitRepoStage::NotEmbedded);
13301 };
13302 if !crate::submodule_gitdir::is_valid_git_dir(&git_dir) {
13303 return Ok(EmbeddedGitRepoStage::NotEmbedded);
13304 }
13305 let path = rel.trim_end_matches('/').to_owned();
13306 match read_submodule_head_oid_for_stage(abs) {
13307 Some(oid) => Ok(EmbeddedGitRepoStage::Gitlink(oid)),
13308 None => Err(crate::error::Error::EmbeddedRepoWithoutCommit { path }),
13309 }
13310}
13311
13312fn find_superproject_git(sub_dir: &Path) -> Option<(PathBuf, PathBuf)> {
13314 let mut cur = sub_dir.parent()?;
13315 loop {
13316 let git_path = cur.join(".git");
13317 if git_path.exists() {
13318 let gd = if git_path.is_file() {
13319 let content = fs::read_to_string(&git_path).ok()?;
13320 let line = content
13321 .lines()
13322 .find_map(|l| l.strip_prefix("gitdir: "))?
13323 .trim();
13324 if Path::new(line).is_absolute() {
13325 PathBuf::from(line)
13326 } else {
13327 cur.join(line)
13328 }
13329 } else {
13330 git_path
13331 };
13332 return Some((cur.to_path_buf(), gd));
13333 }
13334 cur = cur.parent()?;
13335 }
13336}
13337
13338fn submodule_git_dir_for_head_read(sub_dir: &Path) -> Option<PathBuf> {
13339 let mut git_dir = submodule_embedded_git_dir(sub_dir)?;
13340 if let Some((super_wt, super_git_dir)) = find_superproject_git(sub_dir) {
13341 let rel = sub_dir.strip_prefix(&super_wt).ok()?;
13342 let rel_str = rel.to_string_lossy().replace('\\', "/");
13343 let local_mod = super_git_dir
13344 .join("modules")
13345 .join(rel_str.trim_start_matches('/'));
13346 if local_mod.join("HEAD").exists() {
13347 let sg = super_git_dir
13348 .canonicalize_simplified()
13349 .unwrap_or(super_git_dir);
13350 let cur = git_dir
13351 .canonicalize_simplified()
13352 .unwrap_or_else(|_| git_dir.clone());
13353 if !cur.starts_with(&sg) {
13354 git_dir = local_mod;
13355 }
13356 }
13357 }
13358 Some(git_dir)
13359}
13360
13361fn read_submodule_head_oid_from_git_dir(
13362 git_dir: &Path,
13363 allow_branch_fallback: bool,
13364) -> Option<ObjectId> {
13365 let head_content = fs::read_to_string(git_dir.join("HEAD")).ok()?;
13366 let head_trimmed = head_content.trim();
13367 if head_trimmed.starts_with("ref: ") {
13368 match crate::refs::resolve_ref(git_dir, "HEAD") {
13369 Ok(oid) => Some(oid),
13370 Err(_) if allow_branch_fallback => {
13371 for branch in ["main", "master"] {
13372 let p = git_dir.join("refs/heads").join(branch);
13373 if let Ok(s) = fs::read_to_string(&p) {
13374 if let Ok(o) = ObjectId::from_hex(s.trim()) {
13375 return Some(o);
13376 }
13377 }
13378 }
13379 None
13380 }
13381 Err(_) => None,
13382 }
13383 } else {
13384 ObjectId::from_hex(head_trimmed).ok()
13385 }
13386}
13387
13388pub(crate) fn read_submodule_head_oid(sub_dir: &Path) -> Option<ObjectId> {
13394 let git_dir = submodule_git_dir_for_head_read(sub_dir)?;
13395 read_submodule_head_oid_from_git_dir(&git_dir, true)
13396}
13397
13398pub(crate) fn read_submodule_head_oid_for_stage(sub_dir: &Path) -> Option<ObjectId> {
13401 let git_dir = submodule_git_dir_for_head_read(sub_dir)?;
13402 read_submodule_head_oid_from_git_dir(&git_dir, false)
13403}
13404
13405#[must_use]
13417pub(crate) fn submodule_head_object_broken(sub_dir: &Path) -> bool {
13418 let Some(sub_git_dir) = submodule_embedded_git_dir(sub_dir) else {
13419 return false;
13420 };
13421 let Some(head_oid) = read_submodule_head_oid(sub_dir) else {
13422 return false;
13423 };
13424 let odb = Odb::new(&sub_git_dir.join("objects"));
13425 match odb.read(&head_oid) {
13426 Ok(obj) => parse_commit(&obj.data).is_err(),
13429 Err(_) => true,
13430 }
13431}
13432
13433#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
13435pub struct SubmodulePorcelainFlags {
13436 pub new_commits: bool,
13438 pub modified: bool,
13440 pub untracked: bool,
13442}
13443
13444pub fn submodule_porcelain_flags(
13450 super_worktree: &Path,
13451 rel_path: &str,
13452 recorded_oid: ObjectId,
13453) -> SubmodulePorcelainFlags {
13454 let sub_dir = super_worktree.join(rel_path);
13455 let Some(sub_git_dir) = submodule_embedded_git_dir(&sub_dir) else {
13456 return SubmodulePorcelainFlags::default();
13457 };
13458 let Some(sub_head) = read_submodule_head_oid(&sub_dir) else {
13459 return SubmodulePorcelainFlags::default();
13460 };
13461
13462 let new_commits = sub_head != recorded_oid;
13463
13464 let index_path = sub_git_dir.join("index");
13465 let mut sub_index = match crate::index::Index::load(&index_path) {
13466 Ok(ix) => ix,
13467 Err(_) => {
13468 return SubmodulePorcelainFlags {
13469 new_commits,
13470 ..Default::default()
13471 }
13472 }
13473 };
13474
13475 let tracked: std::collections::BTreeSet<String> = sub_index
13476 .entries
13477 .iter()
13478 .filter(|e| e.stage() == 0)
13479 .map(|e| String::from_utf8_lossy(&e.path).into_owned())
13480 .collect();
13481 let untracked = submodule_dir_has_untracked_inner(&sub_dir, &sub_dir, &tracked, &sub_index);
13482
13483 let objects_dir = sub_git_dir.join("objects");
13484 let odb = Odb::new(&objects_dir);
13485
13486 let sub_head_tree = (|| -> Option<ObjectId> {
13487 let h = fs::read_to_string(sub_git_dir.join("HEAD")).ok()?;
13488 let h_str = h.trim();
13489 let commit_oid = if let Some(r) = h_str.strip_prefix("ref: ") {
13490 let oid_hex = fs::read_to_string(sub_git_dir.join(r)).ok()?;
13491 ObjectId::from_hex(oid_hex.trim()).ok()?
13492 } else {
13493 ObjectId::from_hex(h_str).ok()?
13494 };
13495 let obj = odb.read(&commit_oid).ok()?;
13496 let commit = parse_commit(&obj.data).ok()?;
13497 Some(commit.tree)
13498 })();
13499
13500 let staged_dirty = sub_head_tree
13501 .as_ref()
13502 .map(|t| diff_index_to_tree(&odb, &sub_index, Some(t), false).map(|v| !v.is_empty()))
13503 .unwrap_or(Ok(false));
13504 let staged_dirty = staged_dirty.unwrap_or(false);
13505
13506 let unstaged_dirty = diff_index_to_worktree(&odb, &mut sub_index, &sub_dir, false, true)
13507 .map(|v| !v.is_empty())
13508 .unwrap_or(false);
13509
13510 let mut modified = staged_dirty || unstaged_dirty;
13511
13512 for e in &sub_index.entries {
13517 if e.stage() != 0 || e.mode != 0o160000 {
13518 continue;
13519 }
13520 let child = String::from_utf8_lossy(&e.path).into_owned();
13521 let full_rel = if rel_path.is_empty() {
13522 child
13523 } else {
13524 format!("{rel_path}/{child}")
13525 };
13526 let nested = submodule_porcelain_flags(super_worktree, &full_rel, e.oid);
13527 modified |= nested.modified;
13528 }
13529
13530 SubmodulePorcelainFlags {
13531 new_commits,
13532 modified,
13533 untracked,
13534 }
13535}
13536
13537fn submodule_dir_has_untracked_inner(
13538 dir: &Path,
13539 root: &Path,
13540 tracked: &std::collections::BTreeSet<String>,
13541 owning_index: &Index,
13542) -> bool {
13543 let entries = match fs::read_dir(dir) {
13544 Ok(e) => e,
13545 Err(_) => return false,
13546 };
13547 let mut sorted: Vec<_> = entries.filter_map(|e| e.ok()).collect();
13548 sorted.sort_by_key(|e| e.file_name());
13549
13550 for entry in sorted {
13551 let name = entry.file_name().to_string_lossy().to_string();
13552 if name == ".git" {
13553 continue;
13554 }
13555 let path = entry.path();
13556 let rel = path
13557 .strip_prefix(root)
13558 .map(|p| p.to_string_lossy().to_string())
13559 .unwrap_or_else(|_| name.clone());
13560
13561 let is_dir = entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false);
13562 if is_dir {
13563 let is_gitlink = owning_index
13564 .get(rel.as_bytes(), 0)
13565 .is_some_and(|e| e.mode == 0o160000);
13566 if is_gitlink {
13567 let Some(nested_git) = submodule_embedded_git_dir(&path) else {
13568 continue;
13569 };
13570 let nested_index_path = nested_git.join("index");
13571 let Ok(nested_ix) = crate::index::Index::load(&nested_index_path) else {
13572 continue;
13573 };
13574 let nested_tracked: std::collections::BTreeSet<String> = nested_ix
13575 .entries
13576 .iter()
13577 .filter(|e| e.stage() == 0)
13578 .map(|e| String::from_utf8_lossy(&e.path).into_owned())
13579 .collect();
13580 if submodule_dir_has_untracked_inner(&path, &path, &nested_tracked, &nested_ix) {
13581 return true;
13582 }
13583 } else if submodule_dir_has_untracked_inner(&path, root, tracked, owning_index) {
13584 return true;
13585 }
13586 } else if !tracked.contains(&rel) {
13587 return true;
13588 }
13589 }
13590 false
13591}
13592
13593#[cfg(test)]
13594mod line_compare_key_tests {
13595 use super::*;
13596 use crate::diff::unified_emit::line_diff_token;
13597 use crate::diff::WhitespaceMode;
13598
13599 #[test]
13600 fn borrowed_keys_match_owned_line_tokens() {
13601 let texts = [
13602 "",
13603 "\n",
13604 "a",
13605 "a\n",
13606 "a\nb",
13607 "a\r\nb\r\n",
13608 "a\r\nb\r",
13609 "\n\n\nlast",
13610 "x\n\r\n\ry\n",
13611 ];
13612 for text in texts {
13613 let side = LineSide::from_text(text);
13614 let mut normalized = Vec::new();
13615 let keys = line_compare_keys(&side, WhitespaceMode::default(), &mut normalized);
13616 let expected: Vec<String> = side.lines().map(|l| line_diff_token(&l)).collect();
13617 assert_eq!(keys, expected, "text {text:?}");
13618 let source = text.as_bytes().as_ptr_range();
13619 assert!(
13620 keys.iter()
13621 .all(|key| source.contains(&key.as_ptr()) || key.is_empty()),
13622 "text {text:?}"
13623 );
13624 }
13625 }
13626
13627 #[test]
13628 fn whitespace_modes_compare_normalized_bodies() {
13629 let text = "a b\nab \n";
13630 let side = LineSide::from_text(text);
13631 let ws = WhitespaceMode {
13632 ignore_all_space: true,
13633 ..WhitespaceMode::default()
13634 };
13635 let mut normalized = Vec::new();
13636 let keys = line_compare_keys(&side, ws, &mut normalized);
13637 assert_eq!(keys, vec!["ab", "ab"]);
13638 }
13639
13640 #[test]
13641 fn xdiff_marks_only_edited_lines_in_large_file() {
13642 let old: String = (0..5_000).map(|i| format!("line{i}\n")).collect();
13643 let new: String = (0..5_000)
13644 .map(|i| {
13645 if i % 100 == 0 {
13646 format!("LINE{i}\n")
13647 } else {
13648 format!("line{i}\n")
13649 }
13650 })
13651 .collect();
13652 let body =
13653 git_xdiff_unified_body_raw(&old, &new, 3, 0, true, false, WhitespaceMode::default());
13654 assert_eq!(body.matches("\n@@ ").count() + 1, 50);
13655 assert_eq!(body.lines().filter(|l| l.starts_with('-')).count(), 50);
13656 assert_eq!(body.lines().filter(|l| l.starts_with('+')).count(), 50);
13657 assert!(body.starts_with("@@ -1,4 +1,4 @@\n-line0\n+LINE0\n line1\n"));
13658 }
13659}
13660
13661#[cfg(test)]
13662mod entry_order_tests {
13663 use super::*;
13664
13665 fn entry(status: DiffStatus, path: &str, score: Option<u32>) -> DiffEntry {
13666 let mode = |present: bool| {
13667 if present {
13668 FileMode::REGULAR
13669 } else {
13670 FileMode::ABSENT
13671 }
13672 };
13673 DiffEntry {
13674 status,
13675 old_path: (status != DiffStatus::Added).then(|| path.to_owned()),
13676 new_path: (status != DiffStatus::Deleted).then(|| path.to_owned()),
13677 old_mode: mode(status != DiffStatus::Added),
13678 new_mode: mode(status != DiffStatus::Deleted),
13679 old_oid: ObjectId::zero(),
13680 new_oid: ObjectId::zero(),
13681 score,
13682 }
13683 }
13684
13685 #[test]
13686 fn sort_entries_by_path_orders_by_path_and_keeps_equal_paths_in_place() {
13687 let mut entries = vec![
13688 entry(DiffStatus::Modified, "b", None),
13689 entry(DiffStatus::Deleted, "a/x", Some(1)),
13690 entry(DiffStatus::Added, "a/x", Some(2)),
13691 entry(DiffStatus::Added, "a", None),
13692 ];
13693 sort_entries_by_path(&mut entries);
13694 let order: Vec<_> = entries.iter().map(|e| (e.path(), e.score)).collect();
13695 assert_eq!(
13696 order,
13697 [("a", None), ("a/x", Some(1)), ("a/x", Some(2)), ("b", None)]
13698 );
13699 let sorted = entries.clone();
13700 sort_entries_by_path(&mut entries);
13701 assert_eq!(entries, sorted);
13702 sort_entries_by_path(&mut []);
13703 }
13704
13705 #[test]
13706 fn sort_entries_by_path_compares_non_utf8_paths_by_raw_bytes() {
13707 let raw = crate::repo_relpath::format_path_stored("", b"raw-\xff");
13708 assert!(raw.starts_with("grit+pathhex:"));
13709 let mut entries = vec![
13710 entry(DiffStatus::Modified, &raw, None),
13711 entry(DiffStatus::Modified, "node/leaf", None),
13712 entry(DiffStatus::Modified, "zeta", None),
13713 ];
13714 sort_entries_by_path(&mut entries);
13715 let paths: Vec<_> = entries.iter().map(DiffEntry::path).collect();
13716 assert_eq!(paths, ["node/leaf", raw.as_str(), "zeta"]);
13717 }
13718
13719 #[test]
13720 fn rename_pass_returns_unpairable_lists_in_path_order() {
13721 let config = ConfigSet::new();
13722 let policy = ThreadPolicy::default();
13723 let odb = Odb::new(Path::new("/nonexistent-objects"));
13724 let run = |entries: Vec<DiffEntry>| {
13725 detect_renames_with_policy(
13726 &odb,
13727 None,
13728 entries,
13729 50,
13730 &config,
13731 &policy,
13732 None,
13733 DetectRenamesOptions::default(),
13734 )
13735 .expect("no blob is read without both adds and deletes")
13736 .entries
13737 };
13738 let sorted = vec![
13739 entry(DiffStatus::Deleted, "a", None),
13740 entry(DiffStatus::Modified, "b", None),
13741 entry(DiffStatus::Deleted, "c", None),
13742 ];
13743 assert_eq!(run(sorted.clone()), sorted);
13744 let unsorted = vec![
13745 entry(DiffStatus::Added, "z", None),
13746 entry(DiffStatus::Modified, "m", None),
13747 entry(DiffStatus::Added, "a", None),
13748 ];
13749 let paths: Vec<_> = run(unsorted).iter().map(|e| e.path().to_owned()).collect();
13750 assert_eq!(paths, ["a", "m", "z"]);
13751 let duplicate = vec![
13753 entry(DiffStatus::Added, "p", None),
13754 entry(DiffStatus::Modified, "p", None),
13755 ];
13756 let statuses: Vec<_> = run(duplicate).iter().map(|e| e.status).collect();
13757 assert_eq!(statuses, [DiffStatus::Modified, DiffStatus::Added]);
13758 }
13759}
13760
13761#[cfg(test)]
13762mod diff_index_to_tree_cache_tree_tests {
13763 use std::rc::Rc;
13764 use std::sync::atomic::{AtomicUsize, Ordering};
13765
13766 use super::*;
13767 use crate::index::{Index, IndexEntry, MODE_EXECUTABLE, MODE_GITLINK, MODE_REGULAR};
13768 use crate::objects::ObjectKind;
13769 use crate::write_tree::{build_cache_tree_from_index, write_tree_from_index};
13770 use tempfile::TempDir;
13771
13772 struct TreeReadCounter {
13773 counter: Rc<AtomicUsize>,
13774 }
13775
13776 impl TreeReadCounter {
13777 fn start() -> Self {
13778 let counter = Rc::new(AtomicUsize::new(0));
13779 set_diff_tree_read_hook_for_tests(Some(counter.clone()));
13780 Self { counter }
13781 }
13782
13783 fn get(&self) -> usize {
13784 self.counter.load(Ordering::Relaxed)
13785 }
13786 }
13787
13788 impl Drop for TreeReadCounter {
13789 fn drop(&mut self) {
13790 set_diff_tree_read_hook_for_tests(None);
13791 }
13792 }
13793
13794 fn diff_index_to_tree_flatten_reference(
13795 odb: &Odb,
13796 index: &Index,
13797 head_tree: &ObjectId,
13798 ignore_submodules: bool,
13799 ) -> Result<Vec<DiffEntry>> {
13800 let mut flat_index = index.clone();
13801 flat_index.clear_cache_tree();
13802 diff_index_to_tree(odb, &flat_index, Some(head_tree), ignore_submodules)
13803 }
13804
13805 fn index_entry(path: &str, mode: u32, oid: ObjectId) -> IndexEntry {
13806 IndexEntry {
13807 ctime_sec: 0,
13808 ctime_nsec: 0,
13809 mtime_sec: 0,
13810 mtime_nsec: 0,
13811 dev: 0,
13812 ino: 0,
13813 mode,
13814 uid: 0,
13815 gid: 0,
13816 size: 0,
13817 oid,
13818 flags: path.len().min(0xfff) as u16,
13819 flags_extended: None,
13820 path: path.as_bytes().to_vec(),
13821 base_index_pos: 0,
13822 }
13823 }
13824
13825 fn repo_with_tree(depth: usize, width: usize) -> (TempDir, Odb, ObjectId, Index) {
13826 let temp = TempDir::new().expect("tempdir");
13827 let odb = Odb::new(temp.path());
13828 let mut index = Index::new();
13829 for d in 0..depth {
13830 for w in 0..width {
13831 let path = format!("unchanged/d{d}/f{w}.txt");
13832 let oid = odb
13833 .write(ObjectKind::Blob, format!("body-{d}-{w}").as_bytes())
13834 .expect("blob");
13835 index.add_or_replace(index_entry(&path, MODE_REGULAR, oid));
13836 }
13837 }
13838 let head_tree = write_tree_from_index(&odb, &index, "").expect("write tree");
13839 let cache = build_cache_tree_from_index(&odb, &index).expect("cache-tree");
13840 index.set_cache_tree(cache);
13841 index.cache_tree_root = Some(head_tree);
13842 (temp, odb, head_tree, index)
13843 }
13844
13845 #[test]
13846 fn diff_index_to_tree_cache_tree_fast_path_matches_slow() {
13847 let (_temp, odb, head_tree, mut index) = repo_with_tree(4, 5);
13848 assert!(index.cache_tree.as_ref().is_some_and(|c| c.is_valid()));
13849
13850 let mut state: u64 = 0xDEAD_BEEF;
13851 for _ in 0..24 {
13852 state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
13853 let d = (state % 4) as usize;
13854 state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
13855 let w = (state % 5) as usize;
13856 let path = format!("unchanged/d{d}/f{w}.txt");
13857 index.invalidate_cache_tree_for_path(path.as_bytes());
13858 assert!(
13859 index.cache_tree_root.is_none(),
13860 "partial invalidation clears cache_tree_root like Index::add"
13861 );
13862 assert!(
13863 index.cache_tree.as_ref().is_some_and(|r| !r.is_valid()),
13864 "root cache node must be invalid after partial invalidation"
13865 );
13866 if let Some(e) = index.get_mut(path.as_bytes(), 0) {
13867 let new_oid = odb
13868 .write(ObjectKind::Blob, format!("new-{d}-{w}").as_bytes())
13869 .expect("blob");
13870 e.oid = new_oid;
13871 }
13872 let fast = diff_index_to_tree(&odb, &index, Some(&head_tree), false).expect("fast");
13873 let flat = diff_index_to_tree_flatten_reference(&odb, &index, &head_tree, false)
13874 .expect("flat");
13875 assert_eq!(fast, flat, "mismatch after mutating {path}");
13876 }
13877 }
13878
13879 #[test]
13880 fn diff_index_to_tree_skips_unchanged_subtrees() {
13881 let (_temp, odb, head_tree, index) = repo_with_tree(5, 6);
13882 let reads = TreeReadCounter::start();
13883 diff_index_to_tree(&odb, &index, Some(&head_tree), false).expect("cached diff");
13884 let cached_reads = reads.get();
13885
13886 let mut no_cache = index.clone();
13887 no_cache.clear_cache_tree();
13888 let reads = TreeReadCounter::start();
13889 diff_index_to_tree(&odb, &no_cache, Some(&head_tree), false).expect("flat diff");
13890 let flat_reads = reads.get();
13891
13892 assert!(
13893 cached_reads < flat_reads,
13894 "expected fewer tree reads with cache-tree (cached={cached_reads}, flat={flat_reads})"
13895 );
13896 assert_eq!(
13897 cached_reads, 1,
13898 "unchanged repo should only read the root tree"
13899 );
13900 }
13901
13902 #[test]
13903 fn diff_index_to_tree_partial_invalidation_skips_sibling_subtree() {
13904 let (_temp, odb, head_tree, mut index) = repo_with_tree(5, 6);
13905 let path = "unchanged/d1/f2.txt";
13906 index.invalidate_cache_tree_for_path(path.as_bytes());
13907 let new_oid = odb.write(ObjectKind::Blob, b"changed").expect("blob");
13908 index.get_mut(path.as_bytes(), 0).expect("entry").oid = new_oid;
13909
13910 let flat =
13911 diff_index_to_tree_flatten_reference(&odb, &index, &head_tree, false).expect("flat");
13912 let reads = TreeReadCounter::start();
13913 let fast = diff_index_to_tree(&odb, &index, Some(&head_tree), false).expect("fast");
13914 let cached_reads = reads.get();
13915 assert_eq!(fast, flat);
13916
13917 let reads = TreeReadCounter::start();
13918 diff_index_to_tree_flatten_reference(&odb, &index, &head_tree, false).expect("flat");
13919 let flat_reads = reads.get();
13920
13921 assert!(
13922 cached_reads < flat_reads,
13923 "partial cache-tree should skip sibling dirs (cached={cached_reads}, flat={flat_reads})"
13924 );
13925 }
13926
13927 fn cache_leaf(name: &str) -> CacheTreeNode {
13928 CacheTreeNode::valid(name.as_bytes().to_vec(), 1, ObjectId::zero(), Vec::new())
13929 }
13930
13931 #[test]
13932 fn cache_children_lookup_matches_linear_scan_on_wide_unsorted_nodes() {
13933 let mut children: Vec<CacheTreeNode> = (0..CACHE_CHILD_LINEAR_SCAN_MAX * 3)
13934 .rev()
13935 .map(|i| cache_leaf(&format!("d{i:03}")))
13936 .collect();
13937 let mut first_dup = cache_leaf("d007");
13938 first_dup.entry_count = 7;
13939 children.insert(3, first_dup);
13940 let root = CacheTreeNode::valid(Vec::new(), 0, ObjectId::zero(), children);
13941
13942 let lookup = CacheChildren::new(Some(&root));
13943 assert!(
13944 !lookup.sorted.is_empty(),
13945 "wide nodes must use the sorted lookup"
13946 );
13947 for name in ["d000", "d007", "d020", "d047", "zzz", "d", ""] {
13948 let linear = root.children.iter().find(|c| c.name == name.as_bytes());
13949 let fast = lookup.get(name.as_bytes());
13950 assert_eq!(
13951 fast.map(|c| (&c.name, c.entry_count)),
13952 linear.map(|c| (&c.name, c.entry_count)),
13953 "lookup of {name:?}"
13954 );
13955 }
13956 assert_eq!(lookup.get(b"d007").map(|c| c.entry_count), Some(7));
13957
13958 let narrow = CacheTreeNode::valid(
13959 Vec::new(),
13960 0,
13961 ObjectId::zero(),
13962 vec![cache_leaf("b"), cache_leaf("a")],
13963 );
13964 let lookup = CacheChildren::new(Some(&narrow));
13965 assert!(lookup.sorted.is_empty());
13966 assert_eq!(lookup.get(b"a").map(|c| c.name.as_slice()), Some(&b"a"[..]));
13967 assert!(lookup.get(b"c").is_none());
13968 assert!(CacheChildren::new(None).get(b"a").is_none());
13969 }
13970
13971 #[test]
13972 fn index_relative_in_dir_requires_a_component_boundary() {
13973 assert_eq!(index_relative_in_dir(b"a/b", b""), Some(&b"a/b"[..]));
13974 assert_eq!(index_relative_in_dir(b"a/b", b"a"), Some(&b"b"[..]));
13975 assert_eq!(index_relative_in_dir(b"a", b"a"), Some(&b""[..]));
13976 assert_eq!(index_relative_in_dir(b"ab/c", b"a"), None);
13977 assert_eq!(index_relative_in_dir(b"a.c", b"a"), None);
13978 assert_eq!(index_relative_in_dir(b"b", b"a"), None);
13979 assert_eq!(join_index_path(b"", b"x"), b"x".to_vec());
13980 assert_eq!(join_index_path(b"a/b", b"x"), b"a/b/x".to_vec());
13981 }
13982
13983 #[test]
13986 fn diff_index_to_tree_wide_unsorted_cache_tree_matches_flatten() {
13987 let temp = TempDir::new().expect("tempdir");
13988 let odb = Odb::new(temp.path());
13989 let mut index = Index::new();
13990 let width = CACHE_CHILD_LINEAR_SCAN_MAX * 4;
13991 for d in 0..width {
13992 for f in 0..2 {
13993 let path = format!("dir{d:03}/sub/f{f}.txt");
13994 let oid = odb.write(ObjectKind::Blob, path.as_bytes()).expect("blob");
13995 index.add_or_replace(index_entry(&path, MODE_REGULAR, oid));
13996 }
13997 }
13998 let head_tree = write_tree_from_index(&odb, &index, "").expect("write tree");
13999 let mut cache = build_cache_tree_from_index(&odb, &index).expect("cache-tree");
14000 cache.children.reverse();
14001 index.set_cache_tree(cache);
14002 index.cache_tree_root = Some(head_tree);
14003
14004 let changed = [
14005 "dir003/sub/f1.txt",
14006 "dir031/sub/f0.txt",
14007 "dir063/sub/f1.txt",
14008 ];
14009 for path in changed {
14010 index.invalidate_cache_tree_for_path(path.as_bytes());
14011 let oid = odb.write(ObjectKind::Blob, b"changed").expect("blob");
14012 index.get_mut(path.as_bytes(), 0).expect("entry").oid = oid;
14013 }
14014 for path in ["dir010/sub/new.txt", "dir999/new.txt", "top.txt"] {
14015 index.invalidate_cache_tree_for_path(path.as_bytes());
14016 let oid = odb.write(ObjectKind::Blob, path.as_bytes()).expect("blob");
14017 index.add_or_replace(index_entry(path, MODE_REGULAR, oid));
14018 }
14019 for path in [
14020 "dir020/sub/f0.txt",
14021 "dir020/sub/f1.txt",
14022 "dir021/sub/f0.txt",
14023 ] {
14024 index.invalidate_cache_tree_for_path(path.as_bytes());
14025 index.remove(path.as_bytes());
14026 }
14027
14028 let flat =
14029 diff_index_to_tree_flatten_reference(&odb, &index, &head_tree, false).expect("flat");
14030 let reads = TreeReadCounter::start();
14031 let fast = diff_index_to_tree(&odb, &index, Some(&head_tree), false).expect("fast");
14032 let cached_reads = reads.get();
14033 assert_eq!(fast, flat);
14034 assert_eq!(fast.len(), changed.len() + 3 + 3);
14035 assert!(
14036 cached_reads <= 1 + 2 * 6,
14037 "unchanged siblings must be skipped through the cache-tree (reads={cached_reads})"
14038 );
14039 }
14040
14041 #[test]
14042 fn diff_index_to_tree_cache_tree_handles_non_utf8_directory_names() {
14043 let temp = TempDir::new().expect("tempdir");
14044 let odb = Odb::new(temp.path());
14045 let mut index = Index::new();
14046 let raw_dirs: [&[u8]; 3] = [b"caf\xe9", b"caf\xef\xbf\xbd", b"plain"];
14047 for dir in raw_dirs {
14048 for f in [&b"a.txt"[..], b"b.txt"] {
14049 let path = [dir, b"/", f].concat();
14050 let oid = odb.write(ObjectKind::Blob, &path).expect("blob");
14051 let mut entry = index_entry("x", MODE_REGULAR, oid);
14052 entry.flags = path.len().min(0xfff) as u16;
14053 entry.path = path;
14054 index.add_or_replace(entry);
14055 }
14056 }
14057 let head_tree = write_tree_from_index(&odb, &index, "").expect("write tree");
14058 let cache = build_cache_tree_from_index(&odb, &index).expect("cache-tree");
14059 index.set_cache_tree(cache);
14060 index.cache_tree_root = Some(head_tree);
14061
14062 let changed: &[u8] = b"caf\xe9/b.txt";
14063 index.invalidate_cache_tree_for_path(changed);
14064 let oid = odb.write(ObjectKind::Blob, b"changed").expect("blob");
14065 index.get_mut(changed, 0).expect("entry").oid = oid;
14066
14067 let fast = diff_index_to_tree(&odb, &index, Some(&head_tree), false).expect("fast");
14070 assert_eq!(fast.len(), 1, "{fast:?}");
14071 assert_eq!(fast[0].status, DiffStatus::Modified);
14072 assert_eq!(
14073 fast[0].old_oid,
14074 odb.write(ObjectKind::Blob, changed).expect("blob")
14075 );
14076 assert_eq!(fast[0].new_oid, oid);
14077 }
14078
14079 #[test]
14080 fn diff_index_to_tree_reports_mode_gitlink_and_intent_to_add() {
14081 let temp = TempDir::new().expect("tempdir");
14082 let odb = Odb::new(temp.path());
14083 let blob = odb.write(ObjectKind::Blob, b"x").expect("blob");
14084 let mut index = Index::new();
14085 index.add_or_replace(index_entry("tracked.txt", MODE_REGULAR, blob));
14086 let head_tree = write_tree_from_index(&odb, &index, "").expect("tree");
14087 let cache = build_cache_tree_from_index(&odb, &index).expect("cache");
14088 index.set_cache_tree(cache);
14089
14090 let mode_entry = index_entry("tracked.txt", MODE_EXECUTABLE, blob);
14091 index.add_or_replace(mode_entry);
14092 index.invalidate_cache_tree_for_path(b"tracked.txt");
14093
14094 let diffs = diff_index_to_tree(&odb, &index, Some(&head_tree), false).expect("diff");
14095 assert_eq!(diffs.len(), 1);
14096 assert_eq!(diffs[0].status, DiffStatus::Modified);
14097 assert_eq!(diffs[0].old_mode, FileMode::new(MODE_REGULAR));
14098 assert_eq!(diffs[0].new_mode, FileMode::new(MODE_EXECUTABLE));
14099
14100 let mut ita = index_entry("new.txt", MODE_REGULAR, blob);
14102 ita.set_intent_to_add(true);
14103 index.add_or_replace(ita);
14104 let diffs = diff_index_to_tree(&odb, &index, Some(&head_tree), false).expect("diff");
14105 assert!(
14106 diffs.iter().all(|d| d.path() != "new.txt"),
14107 "intent-to-add must not appear in staged diff"
14108 );
14109
14110 let gitlink: ObjectId = "855827c583bc30645ba427885caa40c5b81764d2"
14112 .parse()
14113 .expect("oid");
14114 let mut sub_index = Index::new();
14115 sub_index.add_or_replace(index_entry("sub", MODE_GITLINK, gitlink));
14116 let sub_tree = write_tree_from_index(&odb, &sub_index, "").expect("tree");
14117 let sub_cache = build_cache_tree_from_index(&odb, &sub_index).expect("cache");
14118 sub_index.set_cache_tree(sub_cache);
14119 let diffs =
14120 diff_index_to_tree(&odb, &sub_index, Some(&sub_tree), true).expect("gitlink diff");
14121 assert!(
14122 diffs.is_empty(),
14123 "ignored gitlink should not diff: {diffs:?}"
14124 );
14125 }
14126
14127 #[test]
14128 fn ignore_submodules_skips_gitlink_in_added_directory_cache_walk() {
14129 let temp = TempDir::new().expect("tempdir");
14130 let odb = Odb::new(temp.path());
14131 let gitlink: ObjectId = "855827c583bc30645ba427885caa40c5b81764d2"
14132 .parse()
14133 .expect("oid");
14134 let head_tree = write_tree_from_index(&odb, &Index::new(), "").expect("empty tree");
14135 let mut index = Index::new();
14136 index.add_or_replace(index_entry("newdir/sub", MODE_GITLINK, gitlink));
14137 let cache = build_cache_tree_from_index(&odb, &index).expect("cache");
14138 index.set_cache_tree(cache);
14139 index.invalidate_cache_tree_for_path(b"newdir/sub");
14140
14141 let fast = diff_index_to_tree(&odb, &index, Some(&head_tree), true).expect("fast");
14142 let flat =
14143 diff_index_to_tree_flatten_reference(&odb, &index, &head_tree, true).expect("flat");
14144 assert_eq!(fast, flat);
14145 assert!(
14146 fast.is_empty(),
14147 "nested gitlinks in added dirs must be ignored: {fast:?}"
14148 );
14149 }
14150
14151 #[test]
14152 fn ignore_submodules_skips_gitlink_in_deleted_directory_cache_walk() {
14153 let temp = TempDir::new().expect("tempdir");
14154 let odb = Odb::new(temp.path());
14155 let blob = odb.write(ObjectKind::Blob, b"keep").expect("blob");
14156 let gitlink: ObjectId = "855827c583bc30645ba427885caa40c5b81764d2"
14157 .parse()
14158 .expect("oid");
14159 let mut index = Index::new();
14160 index.add_or_replace(index_entry("keep.txt", MODE_REGULAR, blob));
14161 index.add_or_replace(index_entry("olddir/sub", MODE_GITLINK, gitlink));
14162 let head_tree = write_tree_from_index(&odb, &index, "").expect("head");
14163 let cache = build_cache_tree_from_index(&odb, &index).expect("cache");
14164 index.set_cache_tree(cache);
14165 index
14166 .entries
14167 .retain(|e| !String::from_utf8_lossy(&e.path).starts_with("olddir/"));
14168 index.invalidate_cache_tree_for_path(b"olddir/sub");
14169
14170 let fast = diff_index_to_tree(&odb, &index, Some(&head_tree), true).expect("fast");
14171 let flat =
14172 diff_index_to_tree_flatten_reference(&odb, &index, &head_tree, true).expect("flat");
14173 assert_eq!(fast, flat);
14174 assert!(
14175 fast.is_empty(),
14176 "nested gitlinks in deleted dirs must be ignored: {fast:?}"
14177 );
14178 }
14179
14180 #[test]
14181 fn diff_trees_skips_identical_subtree_oids() {
14182 let temp = TempDir::new().expect("tempdir");
14183 let odb = Odb::new(temp.path());
14184 let leaf = odb.write(ObjectKind::Blob, b"leaf").expect("blob");
14185 let mut deep = Index::new();
14186 for i in 0..20 {
14187 deep.add_or_replace(index_entry(&format!("deep/n{i}.txt"), MODE_REGULAR, leaf));
14188 }
14189 let tree = write_tree_from_index(&odb, &deep, "").expect("tree");
14190 let reads = TreeReadCounter::start();
14191 let entries = diff_trees(&odb, Some(&tree), Some(&tree), "").expect("diff");
14192 assert!(entries.is_empty());
14193 assert_eq!(
14194 reads.get(),
14195 0,
14196 "identical tree oids should short-circuit without reading tree objects"
14197 );
14198 }
14199
14200 #[test]
14201 fn diff_tree_path_change_count_matches_diff_trees() {
14202 let temp = TempDir::new().expect("tempdir");
14203 let odb = Odb::new(temp.path());
14204 let a = odb.write(ObjectKind::Blob, b"a").expect("blob");
14205 let b = odb.write(ObjectKind::Blob, b"b").expect("blob");
14206 let mut left = Index::new();
14207 left.add_or_replace(index_entry("f.txt", MODE_REGULAR, a));
14208 let mut right = Index::new();
14209 right.add_or_replace(index_entry("f.txt", MODE_REGULAR, b));
14210 right.add_or_replace(index_entry("g.txt", MODE_REGULAR, a));
14211 let old_tree = write_tree_from_index(&odb, &left, "").expect("old");
14212 let new_tree = write_tree_from_index(&odb, &right, "").expect("new");
14213 let entries = diff_trees(&odb, Some(&old_tree), Some(&new_tree), "").expect("diff");
14214 let count =
14215 diff_tree_path_change_count(&odb, Some(&old_tree), Some(&new_tree), "").expect("count");
14216 assert_eq!(count, entries.len());
14217 assert_eq!(count, 2);
14218 }
14219
14220 #[test]
14221 fn diff_trees_matches_whether_trees_were_cached_parsed_or_raw() {
14222 let temp = TempDir::new().expect("tempdir");
14223 let a = Odb::new(temp.path())
14224 .write(ObjectKind::Blob, b"a")
14225 .expect("blob");
14226 let b = Odb::new(temp.path())
14227 .write(ObjectKind::Blob, b"b")
14228 .expect("blob");
14229 let mut left = Index::new();
14230 let mut right = Index::new();
14231 for i in 0..50 {
14232 left.add_or_replace(index_entry(&format!("same{i:02}.txt"), MODE_REGULAR, a));
14233 right.add_or_replace(index_entry(&format!("same{i:02}.txt"), MODE_REGULAR, a));
14234 }
14235 left.add_or_replace(index_entry("dir/changed.txt", MODE_REGULAR, a));
14236 left.add_or_replace(index_entry("dir/kept.txt", MODE_REGULAR, a));
14237 left.add_or_replace(index_entry("gone/x.txt", MODE_REGULAR, a));
14238 left.add_or_replace(index_entry("mode.sh", MODE_REGULAR, a));
14239 left.add_or_replace(index_entry("swap", MODE_REGULAR, a));
14240 right.add_or_replace(index_entry("dir/changed.txt", MODE_REGULAR, b));
14241 right.add_or_replace(index_entry("dir/kept.txt", MODE_REGULAR, a));
14242 right.add_or_replace(index_entry("mode.sh", MODE_EXECUTABLE, a));
14243 right.add_or_replace(index_entry("new/y.txt", MODE_REGULAR, b));
14244 right.add_or_replace(index_entry("swap/inner.txt", MODE_REGULAR, b));
14245 let setup = Odb::new(temp.path());
14246 let old_tree = write_tree_from_index(&setup, &left, "").expect("old");
14247 let new_tree = write_tree_from_index(&setup, &right, "").expect("new");
14248
14249 let raw = diff_trees(&Odb::new(temp.path()), Some(&old_tree), Some(&new_tree), "")
14250 .expect("raw diff");
14251 let changed: Vec<(DiffStatus, &str)> = raw.iter().map(|e| (e.status, e.path())).collect();
14252 assert_eq!(
14253 changed,
14254 vec![
14255 (DiffStatus::Modified, "dir/changed.txt"),
14256 (DiffStatus::Deleted, "gone/x.txt"),
14257 (DiffStatus::Modified, "mode.sh"),
14258 (DiffStatus::Added, "new/y.txt"),
14259 (DiffStatus::Deleted, "swap"),
14260 (DiffStatus::Added, "swap/inner.txt"),
14261 ]
14262 );
14263
14264 let parsed = Odb::new(temp.path());
14265 parsed.cached_tree_entries(&old_tree).expect("parse old");
14266 parsed.cached_tree_entries(&new_tree).expect("parse new");
14267 assert_eq!(
14268 diff_trees(&parsed, Some(&old_tree), Some(&new_tree), "").expect("parsed diff"),
14269 raw
14270 );
14271
14272 let mixed = Odb::new(temp.path());
14273 mixed.cached_tree_entries(&new_tree).expect("parse new");
14274 assert_eq!(
14275 diff_trees(&mixed, Some(&old_tree), Some(&new_tree), "").expect("mixed diff"),
14276 raw
14277 );
14278 let reparsed = mixed
14279 .cached_tree_entries(&old_tree)
14280 .expect("parse cached raw");
14281 assert_eq!(
14282 reparsed.as_slice(),
14283 crate::objects::parse_tree(&mixed.read(&old_tree).expect("read").data)
14284 .expect("parse")
14285 .as_slice(),
14286 "a tree cached raw by a diff parses to the same entries"
14287 );
14288 }
14289
14290 #[test]
14291 fn diff_trees_reports_a_corrupt_tree() {
14292 let temp = TempDir::new().expect("tempdir");
14293 let odb = Odb::new(temp.path());
14294 let good = write_tree_from_index(&odb, &Index::new(), "").expect("empty tree");
14295 let corrupt = odb
14296 .write(ObjectKind::Tree, b"100644 name-without-oid")
14297 .expect("tree");
14298 let err = diff_trees(&odb, Some(&good), Some(&corrupt), "").expect_err("corrupt");
14299 assert!(matches!(err, Error::CorruptObject(_)), "{err:?}");
14300 }
14301}
14302
14303#[cfg(test)]
14304mod changed_flags_shared_tests {
14305 use super::git_xdiff;
14306 use crate::line_interner::LineInterner;
14307
14308 #[test]
14311 fn shared_counts_stand_in_for_records_left_out() {
14312 let mut old: Vec<String> = Vec::new();
14315 let mut new: Vec<String> = Vec::new();
14316 for i in 0..60 {
14317 for side in [&mut old, &mut new] {
14318 side.push(format!("fn f{i}() {{\n"));
14319 side.push("}\n".to_owned());
14320 }
14321 }
14322 old.extend(
14323 [
14324 "u1\n", "u2\n", "u3\n", "u4\n", "}\n", "u5\n", "u6\n", "u7\n",
14325 ]
14326 .map(str::to_owned),
14327 );
14328 new.extend(
14329 [
14330 "v1\n", "v2\n", "v3\n", "v4\n", "}\n", "v5\n", "v6\n", "v7\n",
14331 ]
14332 .map(str::to_owned),
14333 );
14334 for i in 0..40 {
14335 for side in [&mut old, &mut new] {
14336 side.push(format!("fn g{i}() {{\n"));
14337 side.push("}\n".to_owned());
14338 }
14339 }
14340 let old: Vec<&str> = old.iter().map(String::as_str).collect();
14341 let new: Vec<&str> = new.iter().map(String::as_str).collect();
14342 let (head, tail) = (120, 80);
14343
14344 let mut interner = LineInterner::with_capacity(old.len() + new.len());
14345 let ids1 = interner.intern_all(&old);
14346 let ids2 = interner.intern_all(&new);
14347 let (full1, full2) = git_xdiff::changed_flags(&ids1, &ids2, false);
14348
14349 let mut cut = LineInterner::default();
14350 let cut1 = cut.intern_all(&old[head..old.len() - tail]);
14351 let cut2 = cut.intern_all(&new[head..new.len() - tail]);
14352 let mut shared = vec![0u32; cut.len()];
14353 for line in old[..head].iter().chain(&old[old.len() - tail..]) {
14354 if let Some(id) = cut.get(line) {
14355 shared[id as usize] += 1;
14356 }
14357 }
14358 let (part1, part2) =
14359 git_xdiff::changed_flags_with_shared(&cut1, &cut2, &shared, head + tail, false);
14360 assert_eq!(part1, full1[head..old.len() - tail]);
14361 assert_eq!(part2, full2[head..new.len() - tail]);
14362 assert!(full1[..head]
14363 .iter()
14364 .chain(&full1[old.len() - tail..])
14365 .all(|c| !c));
14366 assert!(full1[head..old.len() - tail].iter().all(|&c| c));
14369 let (alone1, _) = git_xdiff::changed_flags(&cut1, &cut2, false);
14370 assert!(
14371 !alone1[4],
14372 "counted alone, the cut keeps the `}}` as a match"
14373 );
14374 }
14375}
14376
14377#[cfg(test)]
14378mod gitlink_tree_worktree_tests {
14379 use std::fs;
14380
14381 use crate::index::{Index, IndexEntry, MODE_GITLINK};
14382 use crate::objects::{ObjectId, ObjectKind};
14383 use crate::repo::init_repository;
14384
14385 use super::diff_tree_to_worktree;
14386
14387 fn encode_tree_gitlink(name: &str, oid: &ObjectId) -> Vec<u8> {
14388 let mut data = format!("160000 {name}\0").into_bytes();
14389 data.extend_from_slice(oid.as_bytes());
14390 data
14391 }
14392
14393 #[test]
14394 fn unpopulated_gitlink_matching_index_produces_no_diff_entry() {
14395 let dir = tempfile::tempdir().expect("tempdir");
14396 let work_tree = dir.path();
14397 fs::create_dir(work_tree.join("sub")).expect("sub placeholder");
14398
14399 let repo = init_repository(
14400 work_tree,
14401 false,
14402 "main",
14403 None,
14404 crate::RefStorageFormat::Files,
14405 )
14406 .expect("init");
14407 let gitlink: ObjectId = "855827c583bc30645ba427885caa40c5b81764d2"
14408 .parse()
14409 .expect("gitlink oid");
14410 let tree_oid = repo
14411 .odb()
14412 .write(ObjectKind::Tree, &encode_tree_gitlink("sub", &gitlink))
14413 .expect("write tree");
14414
14415 let path = b"sub".to_vec();
14416 let mut index = Index::new();
14417 index.add_or_replace(IndexEntry {
14418 ctime_sec: 0,
14419 ctime_nsec: 0,
14420 mtime_sec: 0,
14421 mtime_nsec: 0,
14422 dev: 0,
14423 ino: 0,
14424 mode: MODE_GITLINK,
14425 uid: 0,
14426 gid: 0,
14427 size: 0,
14428 oid: gitlink,
14429 flags: (path.len().min(0xfff)) as u16,
14430 flags_extended: None,
14431 path,
14432 base_index_pos: 0,
14433 });
14434
14435 let entries = diff_tree_to_worktree(&repo.odb(), Some(&tree_oid), work_tree, &index)
14436 .expect("diff_tree_to_worktree");
14437 assert!(
14438 entries.is_empty(),
14439 "clean uninitialized gitlink should not appear in tree→worktree diff: {entries:?}"
14440 );
14441 }
14442
14443 #[test]
14444 fn unpopulated_gitlink_with_stray_file_produces_no_diff_entry() {
14445 let dir = tempfile::tempdir().expect("tempdir");
14446 let work_tree = dir.path();
14447 let sub = work_tree.join("sub");
14448 fs::create_dir(&sub).expect("sub placeholder");
14449 fs::write(sub.join("x.txt"), b"stray\n").expect("stray file");
14450
14451 let repo = init_repository(
14452 work_tree,
14453 false,
14454 "main",
14455 None,
14456 crate::RefStorageFormat::Files,
14457 )
14458 .expect("init");
14459 let gitlink: ObjectId = "855827c583bc30645ba427885caa40c5b81764d2"
14460 .parse()
14461 .expect("gitlink oid");
14462 let tree_oid = repo
14463 .odb()
14464 .write(ObjectKind::Tree, &encode_tree_gitlink("sub", &gitlink))
14465 .expect("write tree");
14466
14467 let path = b"sub".to_vec();
14468 let mut index = Index::new();
14469 index.add_or_replace(IndexEntry {
14470 ctime_sec: 0,
14471 ctime_nsec: 0,
14472 mtime_sec: 0,
14473 mtime_nsec: 0,
14474 dev: 0,
14475 ino: 0,
14476 mode: MODE_GITLINK,
14477 uid: 0,
14478 gid: 0,
14479 size: 0,
14480 oid: gitlink,
14481 flags: (path.len().min(0xfff)) as u16,
14482 flags_extended: None,
14483 path,
14484 base_index_pos: 0,
14485 });
14486
14487 let tree_entries = diff_tree_to_worktree(&repo.odb(), Some(&tree_oid), work_tree, &index)
14488 .expect("diff_tree_to_worktree");
14489 assert!(
14490 tree_entries.is_empty(),
14491 "uninitialized gitlink with stray files should not diff: {tree_entries:?}"
14492 );
14493
14494 let wt_entries =
14495 super::diff_index_to_worktree(&repo.odb(), &mut index.clone(), work_tree, false, true)
14496 .expect("diff_index_to_worktree");
14497 assert!(
14498 wt_entries.is_empty(),
14499 "uninitialized gitlink with stray files should not diff: {wt_entries:?}"
14500 );
14501
14502 let ie = index.entries()[0].clone();
14503 assert!(
14504 !super::worktree_differs_from_index_entry(
14505 repo.odb(),
14506 repo.git_dir(),
14507 work_tree,
14508 &ie,
14509 false,
14510 None,
14511 None,
14512 )
14513 .expect("differs"),
14514 "stray files in uninitialized submodule should not differ from index"
14515 );
14516 }
14517
14518 #[cfg_attr(
14519 not(feature = "full-tests"),
14520 ignore = "starts system git or is heavy; run with --features full-tests"
14521 )]
14522 #[test]
14523 fn untracked_only_in_checked_out_submodule_produces_no_diff_entry() {
14524 use std::process::Command;
14525
14526 let dir = tempfile::tempdir().expect("tempdir");
14527 let work_tree = dir.path();
14528 fs::create_dir(work_tree.join("sub")).expect("sub dir");
14529
14530 let repo = init_repository(
14531 work_tree,
14532 false,
14533 "main",
14534 None,
14535 crate::RefStorageFormat::Files,
14536 )
14537 .expect("init");
14538 let sub = work_tree.join("sub");
14539 for args in [
14540 &["init", "-q"][..],
14541 &["config", "user.name", "T"][..],
14542 &["config", "user.email", "t@e.com"][..],
14543 ] {
14544 let status = Command::new("git")
14545 .current_dir(&sub)
14546 .args(args)
14547 .status()
14548 .expect("git");
14549 assert!(status.success(), "git {args:?} failed");
14550 }
14551 fs::write(sub.join("tracked.txt"), b"v1\n").expect("write");
14552 for args in [&["add", "tracked.txt"][..], &["commit", "-qm", "sub"][..]] {
14553 let status = Command::new("git")
14554 .current_dir(&sub)
14555 .args(args)
14556 .status()
14557 .expect("git");
14558 assert!(status.success());
14559 }
14560 let head_out = Command::new("git")
14561 .current_dir(&sub)
14562 .args(["rev-parse", "HEAD"])
14563 .output()
14564 .expect("rev-parse");
14565 let gitlink = std::str::from_utf8(&head_out.stdout)
14566 .expect("utf8")
14567 .trim()
14568 .parse::<ObjectId>()
14569 .expect("oid");
14570
14571 let tree_oid = repo
14572 .odb()
14573 .write(ObjectKind::Tree, &encode_tree_gitlink("sub", &gitlink))
14574 .expect("tree");
14575 let path = b"sub".to_vec();
14576 let mut index = Index::new();
14577 index.add_or_replace(IndexEntry {
14578 ctime_sec: 0,
14579 ctime_nsec: 0,
14580 mtime_sec: 0,
14581 mtime_nsec: 0,
14582 dev: 0,
14583 ino: 0,
14584 mode: MODE_GITLINK,
14585 uid: 0,
14586 gid: 0,
14587 size: 0,
14588 oid: gitlink,
14589 flags: (path.len().min(0xfff)) as u16,
14590 flags_extended: None,
14591 path,
14592 base_index_pos: 0,
14593 });
14594
14595 fs::write(sub.join("untracked.txt"), b"extra\n").expect("untracked");
14596
14597 let entries = diff_tree_to_worktree(&repo.odb(), Some(&tree_oid), work_tree, &index)
14598 .expect("diff_tree_to_worktree");
14599 assert!(
14600 entries.is_empty(),
14601 "untracked-only submodule dirt must not appear in default tree→worktree diff: {entries:?}"
14602 );
14603 }
14604}
14605
14606#[cfg(test)]
14607mod racy_index_mtime_diff_tests {
14608 use super::*;
14609 use crate::index::{entry_from_metadata, Index, MODE_REGULAR};
14610 use crate::objects::{HashAlgo, ObjectKind};
14611 use filetime::FileTime;
14612 use std::fs;
14613 use std::path::PathBuf;
14614
14615 const INDEX_MTIME: (u32, u32) = (1_700_000_000, 123_456_789);
14616
14617 fn pin_mtime(path: &Path, sec: u32, nsec: u32) {
14618 filetime::set_file_mtime(path, FileTime::from_unix_time(i64::from(sec), nsec))
14619 .expect("set mtime");
14620 }
14621
14622 fn minimal_sha256_repo(work_tree: &Path) -> (PathBuf, Odb) {
14623 let git_dir = work_tree.join(".git");
14624 fs::create_dir_all(git_dir.join("objects")).expect("objects dir");
14625 fs::write(
14626 git_dir.join("config"),
14627 "[extensions]\n\tobjectformat = sha256\n",
14628 )
14629 .expect("config");
14630 let odb = Odb::new(&git_dir.join("objects")).with_config_git_dir(git_dir.clone());
14631 (git_dir, odb)
14632 }
14633
14634 #[test]
14635 fn detects_same_size_change_when_entry_mtime_matches_index_mtime() {
14636 let td = tempfile::tempdir().expect("tempdir");
14637 let wt = td.path();
14638 let content_a = b"same length!!";
14639 let content_b = b"same length??";
14640 assert_eq!(content_a.len(), content_b.len());
14641
14642 fs::write(wt.join("f.txt"), content_a).expect("write");
14643 pin_mtime(&wt.join("f.txt"), INDEX_MTIME.0, INDEX_MTIME.1);
14644 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
14645 let git_dir = wt.join(".git");
14646 fs::create_dir_all(git_dir.join("objects")).expect("objects dir");
14647 let odb = Odb::new(&git_dir.join("objects"));
14648 let oid_a = odb.hash(ObjectKind::Blob, content_a).expect("hash");
14649 let entry = entry_from_metadata(&meta, b"f.txt", oid_a, MODE_REGULAR);
14650
14651 let mut index = Index::new();
14652 index.source_mtime = Some(INDEX_MTIME);
14653 index.entries.push(entry);
14654
14655 fs::write(wt.join("f.txt"), content_b).expect("rewrite");
14656 pin_mtime(&wt.join("f.txt"), INDEX_MTIME.0, INDEX_MTIME.1);
14657
14658 let diff = diff_index_to_worktree(&odb, &mut index, wt, false, false).expect("diff");
14659 assert_eq!(
14660 diff.len(),
14661 1,
14662 "racy entry must be re-hashed, not stat-trusted"
14663 );
14664 assert_eq!(diff[0].path(), "f.txt");
14665 }
14666
14667 #[test]
14668 #[cfg(unix)]
14669 fn non_utf8_symlink_clean_racy_equal_mtime_reports_no_diff() {
14670 use crate::index::MODE_SYMLINK;
14671 use std::os::unix::ffi::OsStrExt as _;
14672 use std::os::unix::fs::symlink;
14673
14674 let td = tempfile::tempdir().expect("tempdir");
14675 let wt = td.path();
14676 let target = std::ffi::OsStr::from_bytes(b"bad-\xff-target");
14677 let link_path = wt.join("link");
14678 symlink(target, &link_path).expect("symlink");
14679
14680 let ft = FileTime::from_unix_time(i64::from(INDEX_MTIME.0), INDEX_MTIME.1);
14681 filetime::set_symlink_file_times(&link_path, ft, ft).expect("set symlink mtime");
14682 let meta = fs::symlink_metadata(&link_path).expect("stat");
14683 let git_dir = wt.join(".git");
14684 fs::create_dir_all(git_dir.join("objects")).expect("objects dir");
14685 let odb = Odb::new(&git_dir.join("objects"));
14686 let oid = odb.hash(ObjectKind::Blob, target.as_bytes()).expect("hash");
14687 let entry = entry_from_metadata(&meta, b"link", oid, MODE_SYMLINK);
14688
14689 let mut index = Index::new();
14690 index.source_mtime = Some(INDEX_MTIME);
14691 index.entries.push(entry);
14692
14693 let diff = diff_index_to_worktree(&odb, &mut index, wt, false, false).expect("diff");
14694 assert!(
14695 diff.is_empty(),
14696 "clean non-UTF-8 symlink must not be reported modified: {diff:?}"
14697 );
14698 }
14699
14700 #[test]
14701 fn sha256_clean_racy_equal_mtime_reports_no_diff() {
14702 let td = tempfile::tempdir().expect("tempdir");
14703 let wt = td.path();
14704 let (_git_dir, odb) = minimal_sha256_repo(wt);
14705 assert_eq!(odb.hash_algo(), HashAlgo::Sha256);
14706
14707 let bytes = b"sha256 width!!";
14708 fs::write(wt.join("f.txt"), bytes).expect("write");
14709 pin_mtime(&wt.join("f.txt"), INDEX_MTIME.0, INDEX_MTIME.1);
14710 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
14711 let oid = odb.hash(ObjectKind::Blob, bytes).expect("hash");
14712 let entry = entry_from_metadata(&meta, b"f.txt", oid, MODE_REGULAR);
14713
14714 let mut index = Index::new();
14715 index.hash_algo = HashAlgo::Sha256;
14716 index.source_mtime = Some(INDEX_MTIME);
14717 index.entries.push(entry);
14718
14719 let diff = diff_index_to_worktree(&odb, &mut index, wt, false, false).expect("diff");
14720 assert!(
14721 diff.is_empty(),
14722 "clean SHA-256 racy entry must not be reported modified: {diff:?}"
14723 );
14724 }
14725
14726 #[test]
14729 fn racy_entries_hash_with_their_own_attributes() {
14730 let td = tempfile::tempdir().expect("tempdir");
14731 let wt = td.path();
14732 let git_dir = wt.join(".git");
14733 fs::create_dir_all(git_dir.join("objects")).expect("objects dir");
14734 fs::write(git_dir.join("config"), "[core]\n").expect("config");
14735 fs::write(wt.join(".gitattributes"), "*.crlf eol=crlf\n*.bin -text\n").expect("attrs");
14736 let odb = Odb::new(&git_dir.join("objects")).with_config_git_dir(git_dir.clone());
14737
14738 let files: [(&str, &[u8], &[u8]); 5] = [
14739 ("a.crlf", b"one\r\ntwo\r\n", b"one\ntwo\n"),
14740 ("b.bin", b"raw\r\nbytes\r\n", b"raw\r\nbytes\r\n"),
14741 ("c.crlf", b"three\r\n", b"three\n"),
14742 ("d.txt", b"plain\n", b"plain\n"),
14743 ("e.bin", b"more\r\n", b"more\r\n"),
14744 ];
14745 let mut index = Index::new();
14746 index.source_mtime = Some(INDEX_MTIME);
14747 for (path, on_disk, blob) in files {
14748 let file = wt.join(path);
14749 fs::write(&file, on_disk).expect("write");
14750 pin_mtime(&file, INDEX_MTIME.0, INDEX_MTIME.1);
14751 let meta = fs::symlink_metadata(&file).expect("stat");
14752 let oid = odb.hash(ObjectKind::Blob, blob).expect("hash");
14753 index.entries.push(entry_from_metadata(
14754 &meta,
14755 path.as_bytes(),
14756 oid,
14757 MODE_REGULAR,
14758 ));
14759 }
14760
14761 let status_diff = |index: &mut Index| {
14762 diff_index_to_worktree_with_options(
14763 &odb,
14764 index,
14765 wt,
14766 DiffIndexToWorktreeOptions {
14767 index_mtime: Some(INDEX_MTIME),
14768 repository_git_dir: Some(git_dir.clone()),
14769 refresh_index_stat_in_pass: true,
14770 for_status: true,
14771 ..Default::default()
14772 },
14773 )
14774 .expect("diff")
14775 .0
14776 };
14777 let diff = status_diff(&mut index.clone());
14778 assert!(diff.is_empty(), "clean racy entries reported: {diff:?}");
14779
14780 fs::write(wt.join("c.crlf"), b"thrEe\r\n").expect("rewrite");
14781 pin_mtime(&wt.join("c.crlf"), INDEX_MTIME.0, INDEX_MTIME.1);
14782 let diff = status_diff(&mut index);
14783 let paths: Vec<_> = diff.iter().map(DiffEntry::path).collect();
14784 assert_eq!(paths, ["c.crlf"]);
14785 }
14786}
14787
14788#[cfg(test)]
14789mod smudge_racily_clean_tests {
14790 use super::*;
14791 use crate::config::ConfigSet;
14792 use crate::crlf::{self, FileAttrs};
14793 use crate::index::{entry_from_metadata, Index, MODE_REGULAR};
14794 use crate::objects::{HashAlgo, ObjectKind};
14795 use filetime::FileTime;
14796 use std::fs;
14797 use std::path::PathBuf;
14798
14799 const INDEX_MTIME_PIN: (u32, u32) = (1_700_000_000, 123_456_789);
14800
14801 fn mtime_from_metadata(meta: &fs::Metadata) -> (u32, u32) {
14802 #[cfg(unix)]
14803 {
14804 use std::os::unix::fs::MetadataExt;
14805 (meta.mtime() as u32, meta.mtime_nsec() as u32)
14806 }
14807 #[cfg(not(unix))]
14808 {
14809 use std::time::UNIX_EPOCH;
14810 let mtime = meta
14811 .modified()
14812 .ok()
14813 .and_then(|t| t.duration_since(UNIX_EPOCH).ok())
14814 .unwrap_or_default();
14815 (mtime.as_secs() as u32, mtime.subsec_nanos())
14816 }
14817 }
14818
14819 fn pin_mtime_and_sample(path: &Path, sec: u32, nsec: u32) -> (u32, u32) {
14821 filetime::set_file_mtime(path, FileTime::from_unix_time(i64::from(sec), nsec))
14822 .expect("set mtime");
14823 let meta = fs::symlink_metadata(path).expect("stat file");
14824 mtime_from_metadata(&meta)
14825 }
14826
14827 fn setup_index_mtime(work_tree: &Path) -> (u32, u32) {
14829 let probe = work_tree.join(".grit-index-mtime-probe");
14830 fs::write(&probe, b"").expect("probe file");
14831 pin_mtime_and_sample(&probe, INDEX_MTIME_PIN.0, INDEX_MTIME_PIN.1)
14832 }
14833
14834 fn minimal_repo(work_tree: &Path, config: &str) -> (PathBuf, Odb) {
14835 let git_dir = work_tree.join(".git");
14836 fs::create_dir_all(git_dir.join("objects")).expect("objects dir");
14837 fs::write(git_dir.join("config"), config).expect("config");
14838 let odb = Odb::new(&git_dir.join("objects")).with_config_git_dir(git_dir.clone());
14839 (git_dir, odb)
14840 }
14841
14842 fn staged_entry_for_file(
14843 odb: &Odb,
14844 work_tree: &Path,
14845 rel: &str,
14846 bytes: &[u8],
14847 mtime: (u32, u32),
14848 ) -> IndexEntry {
14849 let path = work_tree.join(rel);
14850 fs::write(&path, bytes).expect("write file");
14851 pin_mtime_and_sample(&path, mtime.0, mtime.1);
14852 let meta = fs::symlink_metadata(&path).expect("stat file");
14853 let oid = odb.hash(ObjectKind::Blob, bytes).expect("hash");
14854 entry_from_metadata(&meta, rel.as_bytes(), oid, MODE_REGULAR)
14855 }
14856
14857 fn staged_entry_with_oid(
14858 work_tree: &Path,
14859 rel: &str,
14860 worktree_bytes: &[u8],
14861 oid: crate::objects::ObjectId,
14862 mtime: (u32, u32),
14863 ) -> IndexEntry {
14864 let path = work_tree.join(rel);
14865 fs::write(&path, worktree_bytes).expect("write file");
14866 pin_mtime_and_sample(&path, mtime.0, mtime.1);
14867 let meta = fs::symlink_metadata(&path).expect("stat file");
14868 entry_from_metadata(&meta, rel.as_bytes(), oid, MODE_REGULAR)
14869 }
14870
14871 #[test]
14872 fn index_stat_cache_trustworthy_rejects_unknown_mtime() {
14873 assert!(!super::index_stat_cache_trustworthy(None));
14874 assert!(!super::index_stat_cache_trustworthy(Some((0, 0))));
14875 assert!(super::index_stat_cache_trustworthy(Some((1, 0))));
14876 }
14877
14878 #[test]
14879 fn index_entry_skips_content_hash_after_stat_match_when_trusted_and_not_racy() {
14880 use crate::index::StatMatchPolicy;
14881 let td = tempfile::tempdir().expect("tempdir");
14882 let wt = td.path();
14883 let (git_dir, odb) = minimal_repo(wt, "[core]\n\tfilemode = true\n");
14884 let rel = "trusted.txt";
14885 let file_mtime = (1_700_000_000, 0);
14886 let mut entry = staged_entry_for_file(&odb, wt, rel, b"payload\n", file_mtime);
14887 let index_mtime = (file_mtime.0 + 1, 0);
14888 let policy = StatMatchPolicy::from_config(None);
14889 assert!(
14890 super::index_entry_skips_worktree_content_hash_after_stat_match(
14891 &entry,
14892 Some(index_mtime),
14893 policy,
14894 )
14895 );
14896 entry.size = 0;
14897 entry.mtime_sec = 0;
14898 entry.mtime_nsec = 0;
14899 assert!(
14900 !super::index_entry_skips_worktree_content_hash_after_stat_match(
14901 &entry,
14902 Some(index_mtime),
14903 policy,
14904 )
14905 );
14906 let _ = git_dir;
14907 }
14908
14909 #[cfg(unix)]
14910 #[test]
14911 fn symlink_dir_cache_detects_symlinked_ancestors_across_threads() {
14912 let td = tempfile::tempdir().expect("tempdir");
14913 let wt = td.path();
14914 fs::create_dir_all(wt.join("real/deep")).expect("mkdir");
14915 fs::create_dir_all(wt.join("target/deep")).expect("mkdir");
14916 std::os::unix::fs::symlink(wt.join("target"), wt.join("linked")).expect("symlink");
14917 std::thread::scope(|scope| {
14918 for _ in 0..4 {
14919 scope.spawn(|| {
14920 let mut cache = super::SymlinkDirCache::default();
14921 assert!(!cache.has_symlink_in_path_bytes(wt, b"top.txt", false, false));
14922 assert!(!cache.has_symlink_in_path_bytes(wt, b"linked", false, false));
14923 assert!(!cache.has_symlink_in_path_bytes(wt, b"real/deep/f", false, false));
14924 assert!(cache.has_symlink_in_path_bytes(wt, b"linked/f", false, false));
14925 assert!(cache.has_symlink_in_path_bytes(wt, b"linked/deep/f", false, false));
14926 assert!(!cache.has_symlink_in_path_bytes(wt, b"\xff\xfe", false, false));
14927 });
14928 }
14929 });
14930 }
14931
14932 #[cfg(unix)]
14933 #[test]
14934 fn symlink_dir_cache_accepts_a_work_tree_reached_through_a_symlink() {
14935 let td = tempfile::tempdir().expect("tempdir");
14936 let real = td.path().join("real");
14937 fs::create_dir_all(real.join("a/b/c")).expect("mkdir");
14938 fs::create_dir_all(real.join("t/b/c")).expect("mkdir");
14939 std::os::unix::fs::symlink(real.join("t"), real.join("l")).expect("symlink");
14940 let wt = td.path().join("alias");
14941 std::os::unix::fs::symlink(&real, &wt).expect("symlink");
14942 let mut cache = super::SymlinkDirCache::default();
14943 assert!(!cache.has_symlink_in_path_bytes(&wt, b"a/b/c/f", false, false));
14944 assert!(cache.has_symlink_in_path_bytes(&wt, b"l/b/c/f", false, false));
14945 assert!(!cache.has_symlink_in_path_bytes(&real, b"a/b/c/f", false, false));
14946 assert!(cache.has_symlink_in_path_bytes(&real, b"l/b/c/f", false, false));
14947 }
14948
14949 #[cfg(all(target_os = "linux", target_pointer_width = "64"))]
14950 #[test]
14951 fn real_dir_chain_confirms_only_symlink_free_directories() {
14952 let td = tempfile::tempdir().expect("tempdir");
14953 let wt = td.path();
14954 fs::create_dir_all(wt.join("a/b/c")).expect("mkdir");
14955 fs::create_dir_all(wt.join("t/c")).expect("mkdir");
14956 fs::write(wt.join("a/file"), b"f").expect("write");
14957 std::os::unix::fs::symlink(wt.join("t"), wt.join("a/l")).expect("symlink");
14958 let mut chain = crate::worktree_scan::RealDirChain::open(wt);
14959 assert_eq!(chain.work_tree(), wt);
14960 assert!(chain.confirms(b"a"));
14961 assert!(chain.confirms(b"a/b/c"));
14962 assert!(!chain.confirms(b"a/l"));
14963 assert!(!chain.confirms(b"a/l/c"));
14964 assert!(!chain.confirms(b"a/file"));
14965 assert!(!chain.confirms(b"a/missing/c"));
14966 assert!(!chain.confirms(b""));
14967 assert!(!chain.confirms(b"a/\0b"));
14968 assert!(chain.confirms(b"a/b"));
14969 }
14970
14971 #[test]
14972 fn symlink_dir_cache_probes_each_new_directory_once_in_index_order() {
14973 let td = tempfile::tempdir().expect("tempdir");
14974 let wt = td.path();
14975 let depth = 12;
14976 let chains = 20;
14977 let mut paths = Vec::new();
14978 for chain in 0..chains {
14979 let dir: Vec<String> = (0..depth)
14980 .map(|level| format!("c{chain:02}l{level:02}"))
14981 .collect();
14982 let dir = format!("shared/{}", dir.join("/"));
14983 fs::create_dir_all(wt.join(&dir)).expect("mkdir");
14984 paths.extend((0..3).map(|file| format!("{dir}/f{file}")));
14985 }
14986 paths.sort();
14987 let mut cache = super::SymlinkDirCache::default();
14988 for rel in &paths {
14989 assert!(!cache.has_symlink_in_path_bytes(wt, rel.as_bytes(), false, false));
14990 }
14991 let unique_dirs = 1 + chains * depth;
14993 assert!(
14994 cache.fs_queries <= unique_dirs,
14995 "{} filesystem queries for {unique_dirs} directories",
14996 cache.fs_queries
14997 );
14998 #[cfg(all(target_os = "linux", target_pointer_width = "64"))]
14999 assert!(
15000 cache.fs_queries <= chains + 1,
15001 "one chain walk per new directory: {} queries for {chains} chains",
15002 cache.fs_queries
15003 );
15004 }
15005
15006 #[test]
15007 fn common_dir_prefix_len_stops_at_whole_components() {
15008 use super::{common_dir_prefix_len, path_within_dir};
15009 assert_eq!(common_dir_prefix_len(b"a/b/c", b"a/b/c"), 5);
15010 assert_eq!(common_dir_prefix_len(b"a/b/c", b"a/b"), 3);
15011 assert_eq!(common_dir_prefix_len(b"a/b", b"a/b/c"), 3);
15012 assert_eq!(common_dir_prefix_len(b"a/bc/d", b"a/b"), 1);
15013 assert_eq!(common_dir_prefix_len(b"a/b", b"a/bc/d"), 1);
15014 assert_eq!(common_dir_prefix_len(b"ab", b"a"), 0);
15015 assert_eq!(common_dir_prefix_len(b"a/b", b""), 0);
15016 assert!(path_within_dir(b"a/b", b"a/b"));
15017 assert!(path_within_dir(b"a/b/c", b"a/b"));
15018 assert!(!path_within_dir(b"a/bc", b"a/b"));
15019 assert!(!path_within_dir(b"a", b"a/b"));
15020 }
15021
15022 #[cfg(unix)]
15024 fn naive_has_symlink_ancestor(wt: &Path, rel: &str) -> bool {
15025 let Some((dir, _)) = rel.rsplit_once('/') else {
15026 return false;
15027 };
15028 let mut prefix = wt.to_path_buf();
15029 dir.split('/').any(|component| {
15030 prefix.push(component);
15031 fs::symlink_metadata(&prefix).is_ok_and(|m| m.file_type().is_symlink())
15032 })
15033 }
15034
15035 #[cfg(unix)]
15036 #[test]
15037 fn symlink_dir_cache_matches_uncached_probe_in_any_query_order() {
15038 let td = tempfile::tempdir().expect("tempdir");
15039 let wt = td.path();
15040 fs::create_dir_all(wt.join("a/b/c/d")).expect("mkdir");
15041 fs::create_dir_all(wt.join("a/bb/c")).expect("mkdir");
15042 fs::create_dir_all(wt.join("a/b2")).expect("mkdir");
15043 fs::create_dir_all(wt.join("t/c/d")).expect("mkdir");
15044 fs::create_dir_all(wt.join("x/y")).expect("mkdir");
15045 fs::write(wt.join("a/file"), b"f").expect("write");
15046 fs::write(wt.join("x/y/plain"), b"f").expect("write");
15047 std::os::unix::fs::symlink(wt.join("t"), wt.join("a/b/link")).expect("symlink");
15048 std::os::unix::fs::symlink(wt.join("t"), wt.join("x/l")).expect("symlink");
15049 std::os::unix::fs::symlink(wt.join("t"), wt.join("top")).expect("symlink");
15050
15051 let paths = [
15052 "a/b/c/d/f",
15053 "a/b/c/f",
15054 "a/b/f",
15055 "a/b/link/f",
15056 "a/b/link/c/d/f",
15057 "a/b/linker/f",
15058 "a/b2/f",
15059 "a/bb/c/f",
15060 "a/file/under/f",
15061 "a/missing/deeper/f",
15062 "a/missing/f",
15063 "a/f",
15064 "f",
15065 "top/c/f",
15066 "top/f",
15067 "topper/f",
15068 "x/l/c/d/f",
15069 "x/y/f",
15070 "x/y/plain/f",
15071 ];
15072 let mut orders: Vec<Vec<&str>> = vec![paths.to_vec()];
15073 orders.push(paths.iter().rev().copied().collect());
15074 orders.push(
15075 paths
15076 .iter()
15077 .step_by(2)
15078 .chain(paths.iter().skip(1).step_by(2))
15079 .copied()
15080 .collect(),
15081 );
15082 orders.push(paths.iter().flat_map(|p| [*p, *p]).collect());
15083 for order in orders {
15084 let mut cache = super::SymlinkDirCache::default();
15085 for rel in order {
15086 assert_eq!(
15087 cache.has_symlink_in_path_bytes(wt, rel.as_bytes(), false, false),
15088 naive_has_symlink_ancestor(wt, rel),
15089 "ancestor symlink answer for {rel}"
15090 );
15091 }
15092 }
15093 }
15094
15095 #[test]
15096 fn content_hash_skip_trusts_empty_blob_but_not_smudged_size() {
15097 use crate::index::StatMatchPolicy;
15098 let td = tempfile::tempdir().expect("tempdir");
15099 let wt = td.path();
15100 let (_git_dir, odb) = minimal_repo(wt, "[core]\n\tfilemode = false\n");
15101 let file_mtime = (1_700_000_000, 0);
15102 let index_mtime = Some((file_mtime.0 + 1, 0));
15103 let policy = StatMatchPolicy::from_config(None);
15104
15105 let empty = staged_entry_for_file(&odb, wt, "empty.txt", b"", file_mtime);
15106 assert_eq!(empty.size, 0);
15107 assert!(
15108 super::index_entry_skips_worktree_content_hash_after_stat_match(
15109 &empty,
15110 index_mtime,
15111 policy,
15112 )
15113 );
15114
15115 let smudged_oid = odb.hash(ObjectKind::Blob, b"abc\n").expect("hash");
15116 let smudged = staged_entry_with_oid(wt, "smudged.txt", b"", smudged_oid, file_mtime);
15117 assert_eq!(smudged.size, 0);
15118 assert!(
15119 !super::index_entry_skips_worktree_content_hash_after_stat_match(
15120 &smudged,
15121 index_mtime,
15122 policy,
15123 )
15124 );
15125 }
15126
15127 #[test]
15128 fn status_diff_reports_smudged_entry_whose_file_became_empty() {
15129 let td = tempfile::tempdir().expect("tempdir");
15130 let wt = td.path();
15131 let (git_dir, odb) = minimal_repo(wt, "[core]\n\tfilemode = false\n");
15132 let oid = odb.write(ObjectKind::Blob, b"abc\n").expect("write blob");
15133 let file_mtime = (1_700_000_000, 0);
15134 let mut index = Index::new();
15135 index.entries.push(staged_entry_with_oid(
15136 wt,
15137 "smudged.txt",
15138 b"",
15139 oid,
15140 file_mtime,
15141 ));
15142 let (entries, _) = super::diff_index_to_worktree_with_options(
15143 &odb,
15144 &mut index,
15145 wt,
15146 super::DiffIndexToWorktreeOptions {
15147 repository_git_dir: Some(git_dir),
15148 index_mtime: Some((file_mtime.0 + 1, 0)),
15149 for_status: true,
15150 stat_parallel_threads: Some(1),
15151 ..Default::default()
15152 },
15153 )
15154 .expect("diff");
15155 assert_eq!(entries.len(), 1, "{entries:?}");
15156 assert_eq!(entries[0].status, DiffStatus::Modified);
15157 }
15158
15159 #[test]
15160 fn status_diff_verifies_content_when_index_stat_cache_unset() {
15161 let td = tempfile::tempdir().expect("tempdir");
15162 let wt = td.path();
15163 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
15164 let rel = "unset-stat.txt";
15165 let bytes = b"same bytes\n";
15166 let oid = odb.hash(ObjectKind::Blob, bytes).expect("hash");
15167 fs::write(wt.join(rel), bytes).expect("write");
15168 let mut index = Index::new();
15169 let mut entry = staged_entry_for_file(&odb, wt, rel, bytes, (100, 0));
15170 entry.size = 0;
15171 entry.mtime_sec = 0;
15172 entry.mtime_nsec = 0;
15173 entry.oid = oid;
15174 index.entries.push(entry);
15175 let (entries, _) = super::diff_index_to_worktree_with_options(
15176 &odb,
15177 &mut index,
15178 wt,
15179 super::DiffIndexToWorktreeOptions {
15180 repository_git_dir: Some(git_dir),
15181 for_status: true,
15182 ..Default::default()
15183 },
15184 )
15185 .expect("diff");
15186 assert!(
15187 entries.is_empty(),
15188 "unset stat must not size-mismatch a matching worktree: {entries:?}"
15189 );
15190 }
15191
15192 #[cfg_attr(
15193 not(feature = "full-tests"),
15194 ignore = "starts system git or is heavy; run with --features full-tests"
15195 )]
15196 #[test]
15197 fn status_reports_modified_when_ctime_differs_but_mtime_sec_and_size_match_non_racy() {
15198 let td = tempfile::tempdir().expect("tempdir");
15199 let wt = td.path();
15200 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
15201 let rel = "tracked.txt";
15202 let original = b"original\n";
15203 let modified = b"modified\n";
15204 assert_eq!(original.len(), modified.len());
15205
15206 let entry_mtime = (1_700_000_000u32, 0u32);
15207 let index_file_mtime = (1_700_000_100u32, 0u32);
15208
15209 std::process::Command::new("git")
15210 .args(["-C", &wt.to_string_lossy(), "init", "-q"])
15211 .status()
15212 .expect("git init");
15213 std::process::Command::new("git")
15214 .args([
15215 "-C",
15216 &wt.to_string_lossy(),
15217 "config",
15218 "user.email",
15219 "t@example.com",
15220 ])
15221 .status()
15222 .expect("git config email");
15223 std::process::Command::new("git")
15224 .args(["-C", &wt.to_string_lossy(), "config", "user.name", "T"])
15225 .status()
15226 .expect("git config name");
15227
15228 fs::write(wt.join(rel), original).expect("write original");
15229 pin_mtime_and_sample(&wt.join(rel), entry_mtime.0, entry_mtime.1);
15230 std::process::Command::new("git")
15231 .args(["-C", &wt.to_string_lossy(), "add", rel])
15232 .status()
15233 .expect("git add");
15234 std::process::Command::new("git")
15235 .args([
15236 "-C",
15237 &wt.to_string_lossy(),
15238 "commit",
15239 "-qm",
15240 "seed",
15241 "--no-gpg-sign",
15242 ])
15243 .status()
15244 .expect("git commit");
15245
15246 let mut index = Index::new();
15247 index.source_mtime = Some(index_file_mtime);
15248 let staged = staged_entry_for_file(&odb, wt, rel, original, entry_mtime);
15249 let oid_original = staged.oid;
15250 index.entries.push(staged);
15251
15252 fs::write(wt.join(rel), modified).expect("modify worktree");
15253 let git_dirty = std::process::Command::new("git")
15254 .args([
15255 "-C",
15256 &wt.to_string_lossy(),
15257 "--no-optional-locks",
15258 "status",
15259 "--short",
15260 ])
15261 .output()
15262 .expect("git status");
15263 assert!(
15264 git_dirty.status.success(),
15265 "git status failed: {}",
15266 String::from_utf8_lossy(&git_dirty.stderr)
15267 );
15268 assert!(
15269 String::from_utf8_lossy(&git_dirty.stdout).contains("tracked.txt"),
15270 "system git must see modification before mtime pin: {}",
15271 String::from_utf8_lossy(&git_dirty.stdout)
15272 );
15273
15274 pin_mtime_and_sample(&wt.join(rel), entry_mtime.0, entry_mtime.1);
15275
15276 let meta = fs::symlink_metadata(wt.join(rel)).expect("stat");
15277 let ie = &mut index.entries[0];
15278 assert_eq!(ie.oid, oid_original);
15279 if stat_matches(ie, &meta) {
15280 ie.ctime_sec = ie.ctime_sec.wrapping_sub(3600);
15281 }
15282 assert!(
15283 !stat_matches(ie, &meta),
15284 "ctime/inode change must fail full stat match"
15285 );
15286 let stat_policy = crate::index::StatMatchPolicy::default();
15287 assert!(
15288 !entry_is_racy_with_policy(ie, Some(index_file_mtime), stat_policy),
15289 "fixture must be non-racy"
15290 );
15291
15292 let (entries, index_changed) = super::diff_index_to_worktree_with_options(
15293 &odb,
15294 &mut index,
15295 wt,
15296 super::DiffIndexToWorktreeOptions {
15297 index_mtime: Some(index_file_mtime),
15298 repository_git_dir: Some(git_dir),
15299 refresh_index_stat_in_pass: true,
15300 for_status: true,
15301 ..Default::default()
15302 },
15303 )
15304 .expect("diff");
15305 assert_eq!(
15306 entries.len(),
15307 1,
15308 "must report modified, not refresh stat clean: {entries:?}, index_changed={index_changed}"
15309 );
15310 assert_eq!(entries[0].path(), rel);
15311 }
15312
15313 #[test]
15314 fn diff_name_status_skips_big_worktree_read_on_size_mismatch() {
15315 let td = tempfile::tempdir().expect("tempdir");
15316 let wt = td.path();
15317 let (git_dir, odb) = minimal_repo(wt, "[core]\n\tbigFileThreshold = 4k\n");
15318 let rel = "big.bin";
15319 let indexed = vec![b'x'; 100];
15320 let mut index = Index::new();
15321 index
15322 .entries
15323 .push(staged_entry_for_file(&odb, wt, rel, &indexed, (100, 0)));
15324 fs::write(wt.join(rel), vec![b'y'; 5000]).expect("rewrite bigger");
15325 let metrics = odb.hot_path_metrics();
15326 metrics.reset_blob_content_reads();
15327 metrics.set_blob_counting(true);
15328 let (entries, _) = super::diff_index_to_worktree_with_options(
15329 &odb,
15330 &mut index,
15331 wt,
15332 super::DiffIndexToWorktreeOptions {
15333 repository_git_dir: Some(git_dir),
15334 for_status: false,
15335 ..Default::default()
15336 },
15337 )
15338 .expect("diff");
15339 metrics.set_blob_counting(false);
15340 assert_eq!(entries.len(), 1);
15341 assert_eq!(entries[0].status, super::DiffStatus::Modified);
15342 assert_eq!(entries[0].new_oid, super::zero_oid());
15343 assert_eq!(
15344 metrics.blob_content_reads(),
15345 0,
15346 "large size-only drift must not hash or read the worktree"
15347 );
15348 }
15349
15350 #[test]
15351 fn status_diff_skips_hash_when_worktree_size_differs() {
15352 let td = tempfile::tempdir().expect("tempdir");
15353 let wt = td.path();
15354 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
15355 let rel = "size-change.txt";
15356 let original = b"short\n";
15357 let mut index = Index::new();
15358 index
15359 .entries
15360 .push(staged_entry_for_file(&odb, wt, rel, original, (100, 0)));
15361 fs::write(wt.join(rel), b"longer content\n").expect("rewrite");
15362 let metrics = odb.hot_path_metrics();
15363 metrics.reset_blob_content_reads();
15364 metrics.set_blob_counting(true);
15365 let (entries, _) = super::diff_index_to_worktree_with_options(
15366 &odb,
15367 &mut index,
15368 wt,
15369 super::DiffIndexToWorktreeOptions {
15370 repository_git_dir: Some(git_dir),
15371 for_status: true,
15372 ..Default::default()
15373 },
15374 )
15375 .expect("diff");
15376 metrics.set_blob_counting(false);
15377 assert_eq!(entries.len(), 1);
15378 assert_eq!(entries[0].status, super::DiffStatus::Modified);
15379 assert_eq!(entries[0].path(), rel);
15380 assert_eq!(entries[0].old_oid, index.entries[0].oid);
15381 assert_eq!(entries[0].new_oid, super::zero_oid());
15382 assert_eq!(
15383 metrics.blob_content_reads(),
15384 0,
15385 "identity conversion: a size change alone proves the file modified"
15386 );
15387 }
15388
15389 #[test]
15390 fn status_diff_hashes_size_change_when_eol_conversion_restores_blob() {
15391 for refresh_index_stat_in_pass in [false, true] {
15392 let td = tempfile::tempdir().expect("tempdir");
15393 let wt = td.path();
15394 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
15395 fs::write(wt.join(".gitattributes"), "*.txt text eol=crlf\n").expect("attrs");
15396 let rel = "n.txt";
15397 let blob = odb
15398 .write(ObjectKind::Blob, b"one\ntwo\n")
15399 .expect("write blob");
15400 let mut index = Index::new();
15401 index.entries.push(staged_entry_with_oid(
15402 wt,
15403 rel,
15404 b"one\r\ntwo\r\n",
15405 blob,
15406 (100, 0),
15407 ));
15408 fs::write(wt.join(rel), b"one\ntwo\n").expect("rewrite as LF");
15409 let (entries, index_changed) = super::diff_index_to_worktree_with_options(
15410 &odb,
15411 &mut index,
15412 wt,
15413 super::DiffIndexToWorktreeOptions {
15414 index_mtime: Some((200, 0)),
15415 repository_git_dir: Some(git_dir),
15416 refresh_index_stat_in_pass,
15417 for_status: true,
15418 ..Default::default()
15419 },
15420 )
15421 .expect("diff");
15422 assert!(
15423 entries.is_empty(),
15424 "refresh={refresh_index_stat_in_pass}: LF worktree cleans to the indexed blob: \
15425 {entries:?}"
15426 );
15427 if refresh_index_stat_in_pass {
15428 assert!(index_changed, "verified entry adopts the new stat");
15429 assert_eq!(index.entries[0].size, 8);
15430 }
15431 }
15432 }
15433
15434 #[test]
15435 fn status_diff_reads_size_changed_converted_file_once() {
15436 for refresh_index_stat_in_pass in [false, true] {
15437 let td = tempfile::tempdir().expect("tempdir");
15438 let wt = td.path();
15439 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
15440 fs::write(wt.join(".gitattributes"), "*.txt text eol=crlf\n").expect("attrs");
15441 let rel = "n.txt";
15442 let blob = odb
15443 .write(ObjectKind::Blob, b"one\ntwo\n")
15444 .expect("write blob");
15445 let mut index = Index::new();
15446 index.entries.push(staged_entry_with_oid(
15447 wt,
15448 rel,
15449 b"one\r\ntwo\r\n",
15450 blob,
15451 (100, 0),
15452 ));
15453 fs::write(wt.join(rel), b"one\ntwo\nthree\n").expect("real edit");
15454 let metrics = odb.hot_path_metrics();
15455 metrics.reset_blob_content_reads();
15456 metrics.set_blob_counting(true);
15457 let (entries, _) = super::diff_index_to_worktree_with_options(
15458 &odb,
15459 &mut index,
15460 wt,
15461 super::DiffIndexToWorktreeOptions {
15462 index_mtime: Some((200, 0)),
15463 repository_git_dir: Some(git_dir),
15464 refresh_index_stat_in_pass,
15465 for_status: true,
15466 ..Default::default()
15467 },
15468 )
15469 .expect("diff");
15470 metrics.set_blob_counting(false);
15471 assert_eq!(entries.len(), 1, "refresh={refresh_index_stat_in_pass}");
15472 assert_eq!(entries[0].status, super::DiffStatus::Modified);
15473 assert_eq!(entries[0].new_oid, zero_oid());
15474 assert_eq!(
15475 index.entries[0].size, 10,
15476 "a modified file keeps its stale stat"
15477 );
15478 assert_eq!(
15479 metrics.blob_content_reads(),
15480 1,
15481 "refresh={refresh_index_stat_in_pass}: read once, not again after the refresh"
15482 );
15483 }
15484 }
15485
15486 #[test]
15487 fn stat_matches_ignores_subsecond_nsec_by_default() {
15488 let td = tempfile::tempdir().expect("tempdir");
15489 let wt = td.path();
15490 fs::write(wt.join("f.txt"), b"x\n").expect("write");
15491 pin_mtime_and_sample(&wt.join("f.txt"), 100, 0);
15492 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
15493 let mut entry = entry_from_metadata(&meta, b"f.txt", zero_oid(), MODE_REGULAR);
15494 entry.mtime_nsec = entry.mtime_nsec.wrapping_add(1_000_000);
15495 assert!(stat_matches(&entry, &meta));
15496 }
15497
15498 #[test]
15499 fn classify_add_up_to_date_when_stat_matches() {
15500 let td = tempfile::tempdir().expect("tempdir");
15501 let wt = td.path();
15502 let (_git_dir, odb) = minimal_repo(wt, "[core]\n");
15503 let bytes = b"hello world\n";
15504 let entry = staged_entry_for_file(&odb, wt, "f.txt", bytes, (100, 0));
15505 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
15506 let conv = crlf::ConversionConfig::from_config(&ConfigSet::new());
15507 let attrs = FileAttrs::default();
15508 let params = WorktreeAddRefreshParams {
15509 odb: &odb,
15510 ie: &entry,
15511 meta: &meta,
15512 abs_path: &wt.join("f.txt"),
15513 rel_path: "f.txt",
15514 conv: &conv,
15515 file_attrs: &attrs,
15516 index_mtime: Some((200, 0)),
15517 staged_mode: MODE_REGULAR,
15518 filter_process: None,
15519 };
15520 let refresh = classify_worktree_entry_for_add(¶ms).expect("classify");
15521 assert_eq!(refresh, WorktreeAddRefresh::UpToDate);
15522 }
15523
15524 #[test]
15525 fn classify_add_rehashes_when_index_mtime_unknown_and_stat_matches() {
15526 let td = tempfile::tempdir().expect("tempdir");
15527 let wt = td.path();
15528 let (_git_dir, odb) = minimal_repo(wt, "[core]\n");
15529 let entry = staged_entry_for_file(&odb, wt, "f.txt", b"old content\n", (100, 0));
15530 fs::write(wt.join("f.txt"), b"new content\n").expect("rewrite");
15531 pin_mtime_and_sample(&wt.join("f.txt"), 100, 0);
15532 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
15533 let conv = crlf::ConversionConfig::from_config(&ConfigSet::new());
15534 let attrs = FileAttrs::default();
15535 let params = WorktreeAddRefreshParams {
15536 odb: &odb,
15537 ie: &entry,
15538 meta: &meta,
15539 abs_path: &wt.join("f.txt"),
15540 rel_path: "f.txt",
15541 conv: &conv,
15542 file_attrs: &attrs,
15543 index_mtime: None,
15544 staged_mode: MODE_REGULAR,
15545 filter_process: None,
15546 };
15547 let refresh = classify_worktree_entry_for_add(¶ms).expect("classify");
15548 assert_eq!(refresh, WorktreeAddRefresh::NeedsRestage);
15549 }
15550
15551 #[test]
15552 fn classify_add_needs_restaging_when_content_differs() {
15553 let td = tempfile::tempdir().expect("tempdir");
15554 let wt = td.path();
15555 let (_git_dir, odb) = minimal_repo(wt, "[core]\n");
15556 let entry = staged_entry_for_file(&odb, wt, "f.txt", b"old content\n", (100, 0));
15557 fs::write(wt.join("f.txt"), b"new content\n").expect("rewrite");
15558 pin_mtime_and_sample(&wt.join("f.txt"), 101, 0);
15559 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
15560 let conv = crlf::ConversionConfig::from_config(&ConfigSet::new());
15561 let attrs = FileAttrs::default();
15562 let params = WorktreeAddRefreshParams {
15563 odb: &odb,
15564 ie: &entry,
15565 meta: &meta,
15566 abs_path: &wt.join("f.txt"),
15567 rel_path: "f.txt",
15568 conv: &conv,
15569 file_attrs: &attrs,
15570 index_mtime: Some((200, 0)),
15571 staged_mode: MODE_REGULAR,
15572 filter_process: None,
15573 };
15574 let refresh = classify_worktree_entry_for_add(¶ms).expect("classify");
15575 assert_eq!(refresh, WorktreeAddRefresh::NeedsRestage);
15576 }
15577
15578 #[test]
15579 fn smudges_racily_clean_modified_entry() {
15580 let td = tempfile::tempdir().expect("tempdir");
15581 let wt = td.path();
15582 let index_mtime = setup_index_mtime(wt);
15583 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
15584 let content_a = b"same length!!";
15585 let content_b = b"same length??";
15586 assert_eq!(content_a.len(), content_b.len());
15587
15588 let mut index = Index::new();
15589 let entry = staged_entry_for_file(&odb, wt, "f.txt", content_a, index_mtime);
15590 let stale_oid = entry.oid;
15591
15592 fs::write(wt.join("f.txt"), content_b).expect("rewrite");
15593 pin_mtime_and_sample(&wt.join("f.txt"), index_mtime.0, index_mtime.1);
15594 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
15595 let entry = entry_from_metadata(&meta, b"f.txt", stale_oid, MODE_REGULAR);
15597 index.entries.push(entry.clone());
15598
15599 assert!(stat_matches(&entry, &meta));
15600 assert!(entry_is_racy(&entry, Some(index_mtime)));
15601
15602 let smudged = smudge_racily_clean_entries(
15603 &odb,
15604 &git_dir,
15605 &mut index,
15606 wt,
15607 Some(index_mtime),
15608 None,
15609 None,
15610 );
15611 assert!(smudged);
15612 assert_eq!(index.entries[0].size, 0);
15613
15614 let diff = diff_index_to_worktree(&odb, &mut index, wt, false, false).expect("diff");
15615 assert_eq!(diff.len(), 1);
15616 assert_eq!(diff[0].path(), "f.txt");
15617 }
15618
15619 fn racy_stale_entry(wt: &Path) -> (PathBuf, Odb, Index, (u32, u32), ObjectId) {
15621 let index_mtime = setup_index_mtime(wt);
15622 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
15623 let entry = staged_entry_for_file(&odb, wt, "f.txt", b"same length!!", index_mtime);
15624 let stale_oid = entry.oid;
15625 fs::write(wt.join("f.txt"), b"same length??").expect("rewrite");
15626 pin_mtime_and_sample(&wt.join("f.txt"), index_mtime.0, index_mtime.1);
15627 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
15628 let mut index = Index::new();
15629 index.entries.push(entry_from_metadata(
15630 &meta,
15631 b"f.txt",
15632 stale_oid,
15633 MODE_REGULAR,
15634 ));
15635 (git_dir, odb, index, index_mtime, stale_oid)
15636 }
15637
15638 #[test]
15639 fn content_verified_entry_is_not_reread_and_the_mark_is_consumed() {
15640 let td = tempfile::tempdir().expect("tempdir");
15641 let wt = td.path();
15642 let (git_dir, odb, mut index, index_mtime, stale_oid) = racy_stale_entry(wt);
15643 index.mark_content_verified(b"f.txt", stale_oid);
15644
15645 let smudge = |index: &mut Index| {
15646 smudge_racily_clean_entries(&odb, &git_dir, index, wt, Some(index_mtime), None, None)
15647 };
15648 assert!(!smudge(&mut index), "a verified entry is trusted");
15649 assert_ne!(index.entries[0].size, 0);
15650 assert!(index.content_verified.is_empty());
15651 assert!(smudge(&mut index), "without the mark the entry is checked");
15652 assert_eq!(index.entries[0].size, 0);
15653 }
15654
15655 #[test]
15656 fn content_verified_entry_bit_is_trusted_and_consumed() {
15657 let td = tempfile::tempdir().expect("tempdir");
15658 let wt = td.path();
15659 let (git_dir, odb, mut index, index_mtime, _) = racy_stale_entry(wt);
15660 index.entries[0].mark_content_verified();
15661
15662 let smudge = |index: &mut Index| {
15663 smudge_racily_clean_entries(&odb, &git_dir, index, wt, Some(index_mtime), None, None)
15664 };
15665 assert!(!smudge(&mut index), "a marked entry is trusted");
15666 assert_ne!(index.entries[0].size, 0);
15667 assert_eq!(
15668 index.entries[0].flags_extended, None,
15669 "the mark is consumed"
15670 );
15671 assert!(smudge(&mut index), "without the mark the entry is checked");
15672 assert_eq!(index.entries[0].size, 0);
15673 }
15674
15675 #[test]
15676 fn content_verified_mark_for_another_oid_does_not_skip_the_check() {
15677 let td = tempfile::tempdir().expect("tempdir");
15678 let wt = td.path();
15679 let (git_dir, odb, mut index, index_mtime, _) = racy_stale_entry(wt);
15680 let other = odb.hash(ObjectKind::Blob, b"other").expect("hash");
15681 index.mark_content_verified(b"f.txt", other);
15682 assert!(smudge_racily_clean_entries(
15683 &odb,
15684 &git_dir,
15685 &mut index,
15686 wt,
15687 Some(index_mtime),
15688 None,
15689 None,
15690 ));
15691 assert_eq!(index.entries[0].size, 0);
15692 }
15693
15694 #[test]
15695 fn leaves_non_racy_entry_alone() {
15696 let td = tempfile::tempdir().expect("tempdir");
15697 let wt = td.path();
15698 let index_mtime = setup_index_mtime(wt);
15699 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
15700 let content_a = b"same length!!";
15701 let content_b = b"same length??";
15702 let entry_mtime = (index_mtime.0 - 10, 0);
15703
15704 let mut index = Index::new();
15705 let entry = staged_entry_for_file(&odb, wt, "f.txt", content_a, entry_mtime);
15706 index.entries.push(entry.clone());
15707
15708 fs::write(wt.join("f.txt"), content_b).expect("rewrite");
15709 pin_mtime_and_sample(&wt.join("f.txt"), entry_mtime.0, entry_mtime.1);
15710
15711 assert!(!entry_is_racy(&entry, Some(index_mtime)));
15712 let smudged = smudge_racily_clean_entries(
15713 &odb,
15714 &git_dir,
15715 &mut index,
15716 wt,
15717 Some(index_mtime),
15718 None,
15719 None,
15720 );
15721 assert!(!smudged);
15722 assert_eq!(index.entries[0].size, entry.size);
15723 }
15724
15725 #[test]
15726 fn leaves_matching_content_alone() {
15727 let td = tempfile::tempdir().expect("tempdir");
15728 let wt = td.path();
15729 let index_mtime = setup_index_mtime(wt);
15730 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
15731 let bytes = b"unchanged!!!!";
15732
15733 let mut index = Index::new();
15734 let entry = staged_entry_for_file(&odb, wt, "f.txt", bytes, index_mtime);
15735 index.entries.push(entry.clone());
15736
15737 assert!(entry_is_racy(&entry, Some(index_mtime)));
15738 let smudged = smudge_racily_clean_entries(
15739 &odb,
15740 &git_dir,
15741 &mut index,
15742 wt,
15743 Some(index_mtime),
15744 None,
15745 None,
15746 );
15747 assert!(!smudged);
15748 assert_eq!(index.entries[0].size, entry.size);
15749 }
15750
15751 #[test]
15752 fn status_diff_skips_refresh_rewrite_for_racy_clean_matching_stat() {
15753 let td = tempfile::tempdir().expect("tempdir");
15754 let wt = td.path();
15755 let index_mtime = setup_index_mtime(wt);
15756 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
15757 let bytes = b"racy clean payload\n";
15758
15759 let mut index = Index::new();
15760 index.source_mtime = Some(index_mtime);
15761 let entry = staged_entry_for_file(&odb, wt, "f.txt", bytes, index_mtime);
15762 index.entries.push(entry.clone());
15763 assert!(entry_is_racy(&entry, Some(index_mtime)));
15764
15765 let (entries, index_changed) = super::diff_index_to_worktree_with_options(
15766 &odb,
15767 &mut index,
15768 wt,
15769 super::DiffIndexToWorktreeOptions {
15770 repository_git_dir: Some(git_dir),
15771 index_mtime: Some(index_mtime),
15772 refresh_index_stat_in_pass: true,
15773 for_status: true,
15774 ..Default::default()
15775 },
15776 )
15777 .expect("status diff");
15778 assert!(entries.is_empty(), "unexpected unstaged: {entries:?}");
15779 assert!(
15780 !index_changed,
15781 "matching stat on a racy entry must skip refresh without rewriting the index"
15782 );
15783 }
15784
15785 #[test]
15786 fn leaves_matching_sha256_racy_content_alone() {
15787 let td = tempfile::tempdir().expect("tempdir");
15788 let wt = td.path();
15789 let index_mtime = setup_index_mtime(wt);
15790 let (git_dir, odb) = minimal_repo(wt, "[extensions]\n\tobjectformat = sha256\n");
15791 assert_eq!(odb.hash_algo(), HashAlgo::Sha256);
15792
15793 let bytes = b"sha256 width!!";
15794 let oid = odb.hash(ObjectKind::Blob, bytes).expect("hash");
15795 let mut index = Index::new();
15796 let entry = staged_entry_with_oid(wt, "f.txt", bytes, oid, index_mtime);
15797 index.entries.push(entry.clone());
15798
15799 assert!(entry_is_racy(&entry, Some(index_mtime)));
15800 let smudged = smudge_racily_clean_entries(
15801 &odb,
15802 &git_dir,
15803 &mut index,
15804 wt,
15805 Some(index_mtime),
15806 None,
15807 None,
15808 );
15809 assert!(!smudged, "matching SHA-256 blob must not be smudged");
15810 assert_eq!(index.entries[0].size, entry.size);
15811 }
15812
15813 #[test]
15814 fn materialize_big_binary_under_autocrlf_streams_into_pack() {
15815 use std::io::Write;
15816
15817 let td = tempfile::tempdir().expect("tempdir");
15818 let wt = td.path();
15819 let (git_dir, odb) =
15820 minimal_repo(wt, "[core]\n\tautocrlf = true\n\tbigFileThreshold = 64k\n");
15821 let path = wt.join("big.bin");
15822 {
15823 let mut f = fs::File::create(&path).expect("create");
15824 f.write_all(&[0u8; 9000]).expect("nul prefix");
15825 let chunk = vec![1u8; 256 * 1024];
15826 for _ in 0..20 {
15827 f.write_all(&chunk).expect("chunk");
15828 }
15829 }
15830 let meta = fs::metadata(&path).expect("stat");
15831 let mut conv = crlf::ConversionConfig::from_config(&ConfigSet::new());
15832 conv.autocrlf = crlf::AutoCrlf::True;
15833 let attrs = FileAttrs::default();
15834 let oid =
15835 materialize_worktree_blob(&odb, &path, &meta, &conv, &attrs, "big.bin", None, None)
15836 .expect("materialize");
15837 assert!(
15838 !odb.test_loose_object_path(&oid).exists(),
15839 "big binary under autocrlf should pack, not write loose"
15840 );
15841 let pack_dir = git_dir.join("objects/pack");
15842 let names: Vec<_> = fs::read_dir(&pack_dir)
15843 .expect("pack dir")
15844 .map(|e| e.expect("entry").file_name())
15845 .collect();
15846 assert!(
15847 names.iter().any(|n| n.to_string_lossy().ends_with(".pack")),
15848 "{names:?}"
15849 );
15850 assert!(odb.read(&oid).expect("read blob").data.len() > 9000);
15851 }
15852
15853 #[test]
15854 fn indexed_binary_under_autocrlf_rehash_streams() {
15855 use std::io::Write;
15856
15857 let td = tempfile::tempdir().expect("tempdir");
15858 let wt = td.path();
15859 let (_git_dir, odb) =
15860 minimal_repo(wt, "[core]\n\tautocrlf = true\n\tbigFileThreshold = 1m\n");
15861 let path = wt.join("big.bin");
15862 {
15863 let mut f = fs::File::create(&path).expect("create");
15864 f.write_all(&[0u8; 9000]).expect("nul prefix");
15865 let chunk = vec![1u8; 512 * 1024];
15866 for _ in 0..3 {
15867 f.write_all(&chunk).expect("chunk");
15868 }
15869 }
15870 let meta = fs::symlink_metadata(&path).expect("stat");
15871 let oid = hash_worktree_file(
15872 &odb,
15873 &path,
15874 &meta,
15875 &crlf::ConversionConfig::from_config(&ConfigSet::new()),
15876 &FileAttrs::default(),
15877 "big.bin",
15878 None,
15879 None,
15880 )
15881 .expect("hash");
15882 let entry = entry_from_metadata(&meta, b"big.bin", oid, MODE_REGULAR);
15883 let mut conv = crlf::ConversionConfig::from_config(&ConfigSet::new());
15884 conv.autocrlf = crlf::AutoCrlf::True;
15885 let attrs = FileAttrs::default();
15886 let content = read_regular_worktree_blob(
15887 &odb,
15888 &path,
15889 &conv,
15890 &attrs,
15891 "big.bin",
15892 Some(&entry),
15893 None,
15894 1024 * 1024,
15895 )
15896 .expect("read blob");
15897 assert!(
15898 matches!(content, WorktreeBlobContent::Stream { .. }),
15899 "indexed binary re-hash must stream, not buffer"
15900 );
15901 assert_eq!(content.hash(&odb).expect("hash"), entry.oid);
15902 }
15903
15904 #[test]
15905 fn leaves_matching_crlf_worktree_content_alone() {
15906 let td = tempfile::tempdir().expect("tempdir");
15907 let wt = td.path();
15908 let index_mtime = setup_index_mtime(wt);
15909 let (git_dir, odb) = minimal_repo(wt, "[core]\n\tautocrlf = true\n");
15910 let git_form = b"line one\nline two\n";
15911 let worktree_form = b"line one\r\nline two\r\n";
15912 let oid = odb.hash(ObjectKind::Blob, git_form).expect("hash");
15913
15914 let mut index = Index::new();
15915 let entry = staged_entry_with_oid(wt, "f.txt", worktree_form, oid, index_mtime);
15916 index.entries.push(entry.clone());
15917
15918 assert!(entry_is_racy(&entry, Some(index_mtime)));
15919 let smudged = smudge_racily_clean_entries(
15920 &odb,
15921 &git_dir,
15922 &mut index,
15923 wt,
15924 Some(index_mtime),
15925 None,
15926 None,
15927 );
15928 assert!(
15929 !smudged,
15930 "CRLF worktree matching clean index OID must not be smudged"
15931 );
15932 assert_eq!(index.entries[0].size, entry.size);
15933 }
15934
15935 #[test]
15936 fn skips_skip_worktree_and_intent_to_add() {
15937 let td = tempfile::tempdir().expect("tempdir");
15938 let wt = td.path();
15939 let index_mtime = setup_index_mtime(wt);
15940 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
15941 let content_a = b"same length!!";
15942 let content_b = b"same length??";
15943
15944 let mut index = Index::new();
15945 let mut entry = staged_entry_for_file(&odb, wt, "f.txt", content_a, index_mtime);
15946 entry.set_skip_worktree(true);
15947 index.entries.push(entry);
15948 fs::write(wt.join("f.txt"), content_b).expect("rewrite");
15949 pin_mtime_and_sample(&wt.join("f.txt"), index_mtime.0, index_mtime.1);
15950 assert!(!smudge_racily_clean_entries(
15951 &odb,
15952 &git_dir,
15953 &mut index,
15954 wt,
15955 Some(index_mtime),
15956 None,
15957 None,
15958 ));
15959 assert_ne!(index.entries[0].size, 0);
15960
15961 let mut index = Index::new();
15962 let mut entry = staged_entry_for_file(&odb, wt, "f.txt", content_a, index_mtime);
15963 entry.set_intent_to_add(true);
15964 index.entries.push(entry);
15965 fs::write(wt.join("f.txt"), content_b).expect("rewrite");
15966 pin_mtime_and_sample(&wt.join("f.txt"), index_mtime.0, index_mtime.1);
15967 assert!(!smudge_racily_clean_entries(
15968 &odb,
15969 &git_dir,
15970 &mut index,
15971 wt,
15972 Some(index_mtime),
15973 None,
15974 None,
15975 ));
15976 assert_ne!(index.entries[0].size, 0);
15977 }
15978
15979 #[test]
15980 fn smudges_nothing_without_prior_index_mtime() {
15981 let td = tempfile::tempdir().expect("tempdir");
15982 let wt = td.path();
15983 let index_mtime = setup_index_mtime(wt);
15984 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
15985 let mut index = Index::new();
15986 let entry = staged_entry_for_file(&odb, wt, "f.txt", b"same length!!", index_mtime);
15987 index.entries.push(entry);
15988
15989 fs::write(wt.join("f.txt"), b"same length??").expect("rewrite");
15990 pin_mtime_and_sample(&wt.join("f.txt"), index_mtime.0, index_mtime.1);
15991
15992 let smudged = smudge_racily_clean_entries(&odb, &git_dir, &mut index, wt, None, None, None);
15993 assert!(!smudged);
15994 assert_ne!(index.entries[0].size, 0);
15995 }
15996}
15997
15998#[cfg(all(test, unix))]
15999mod read_worktree_bytes_for_mode_tests {
16000 use super::*;
16001 use crate::index::MODE_SYMLINK;
16002 use std::fs;
16003 use std::os::unix::fs::symlink;
16004
16005 #[test]
16006 fn symlink_mode_reads_link_target_not_followed_file() {
16007 let td = tempfile::tempdir().expect("tempdir");
16008 let wt = td.path();
16009 fs::write(wt.join("payload.txt"), b"secret\n").expect("write");
16010 symlink("payload.txt", wt.join("link")).expect("symlink");
16011 let bytes =
16012 read_worktree_bytes_for_mode(&wt.join("link"), MODE_SYMLINK).expect("read link");
16013 assert_eq!(bytes, b"payload.txt");
16014 }
16015
16016 #[test]
16017 fn symlink_mode_reads_plain_file_holding_the_target() {
16018 let td = tempfile::tempdir().expect("tempdir");
16019 let path = td.path().join("link");
16020 fs::write(&path, b"payload.txt").expect("write");
16021 let bytes = read_worktree_bytes_for_mode(&path, MODE_SYMLINK).expect("read plain link");
16022 assert_eq!(bytes, b"payload.txt");
16023 }
16024
16025 #[test]
16026 fn regular_mode_reads_file_contents() {
16027 let td = tempfile::tempdir().expect("tempdir");
16028 let path = td.path().join("f.txt");
16029 fs::write(&path, b"hello\n").expect("write");
16030 let bytes = read_worktree_bytes_for_mode(&path, crate::index::MODE_REGULAR).expect("read");
16031 assert_eq!(bytes, b"hello\n");
16032 }
16033
16034 #[test]
16035 fn annotate_marks_context_line_when_eof_line_is_unchanged() {
16036 let old = "a\nb\nc";
16037 let new = "a\nx\nc";
16038 let raw = super::unified_diff_histogram_hunks_only(old, new, 3, 0, false);
16039 let out = annotate_unified_diff_no_newline_at_eof(&raw, old, new);
16040 assert!(
16041 out.contains(" c\n\\ No newline at end of file"),
16042 "histogram hunk should mark trailing context line:\n{out}"
16043 );
16044 }
16045
16046 #[test]
16047 fn annotate_does_not_treat_removed_dash_lines_as_file_headers() {
16048 use super::unified_diff_with_prefix_and_funcname_and_algorithm;
16049 use similar::Algorithm;
16050
16051 let old = "--old";
16052 let new = "--new";
16053 let raw = unified_diff_with_prefix_and_funcname_and_algorithm(
16054 old,
16055 new,
16056 "a/x",
16057 "b/x",
16058 3,
16059 0,
16060 "",
16061 "",
16062 None,
16063 Algorithm::Myers,
16064 false,
16065 false,
16066 false,
16067 false,
16068 );
16069 assert_eq!(
16070 raw.matches("\\ No newline at end of file").count(),
16071 2,
16072 "expected two markers like git, got:\n{raw}"
16073 );
16074 }
16075}
16076
16077#[cfg(test)]
16078mod annotate_no_newline_tests {
16079 use super::annotate_unified_diff_no_newline_at_eof;
16080
16081 #[test]
16082 fn annotate_marks_both_sides_without_trailing_newline() {
16083 let raw = "@@ -1 +1 @@\n-foo\n+f o o\n";
16084 let out = annotate_unified_diff_no_newline_at_eof(raw, "foo", "f o o");
16085 assert!(out.contains("\\ No newline at end of file"));
16086 assert_eq!(out.matches("\\ No newline at end of file").count(), 2);
16087 }
16088}
16089
16090#[cfg(test)]
16091mod blob_diff_algorithm_smoke {
16092 use super::{
16093 anchored_unified_diff, count_changes_with_algorithm, unified_diff_histogram_hunks_only,
16094 unified_diff_with_prefix_and_funcname_and_algorithm, unified_hunk_body_for_diff_options,
16095 };
16096 use similar::Algorithm;
16097
16098 const OLD: &str = "alpha\nbeta\ngamma\n";
16099 const NEW: &str = "alpha\nBETA\ngamma\ndelta\n";
16100
16101 #[test]
16102 fn seed15_histogram_u0_body_matches_git() {
16103 use crate::diff::options::{DiffAlgorithm, DiffOptions};
16104 use std::collections::hash_map::DefaultHasher;
16105 use std::hash::{Hash, Hasher};
16106 let seed = 15u64;
16107 let mut hasher = DefaultHasher::new();
16108 seed.hash(&mut hasher);
16109 let h = hasher.finish();
16110 let line_count = 4 + (h as usize % 12);
16111 let mut old = String::new();
16112 let mut new = String::new();
16113 for i in 0..line_count {
16114 let token = (h.wrapping_add(i as u64 * 131) % 7) as u8 + b'a';
16115 let ch = token as char;
16116 old.push(ch);
16117 if (h >> (i % 8)) & 1 == 1 {
16118 new.push(((token as u8).wrapping_add(1)) as char);
16119 } else {
16120 new.push(ch);
16121 }
16122 old.push('\n');
16123 new.push('\n');
16124 }
16125 let opts = DiffOptions {
16126 context_lines: 0,
16127 algorithm: DiffAlgorithm::Histogram,
16128 indent_heuristic: false,
16129 ..DiffOptions::default()
16130 };
16131 let body = unified_hunk_body_for_diff_options(&old, &new, &opts);
16132 assert_eq!(
16133 body.matches("@@ ").count(),
16134 2,
16135 "git 2.55 splits u0 histogram hunks at seed 15: {body}"
16136 );
16137 }
16138
16139 #[test]
16140 fn t4071_histogram_changed_flags_git_myers_completes() {
16141 let old: Vec<&str> = "x\nx\nx\nx\n".lines().collect();
16142 let new: Vec<&str> = "x\nx\nx\nA\nB\nC\nD\nx\nE\nF\nG\n".lines().collect();
16143 let (c1, c2) = super::git_xdiff::histogram_changed_flags(&old, &new);
16144 assert_eq!(c1.len(), 4);
16145 assert_eq!(c2.len(), 11);
16146 let ops = super::changed_flags_to_ops(&c1, &c2, old.len(), new.len());
16147 assert!(!ops.is_empty());
16148 }
16149
16150 #[test]
16151 fn t4071_histogram_compacted_ops_completes() {
16152 use crate::diff::WhitespaceMode;
16153 let old = "x\nx\nx\nx\n";
16154 let new = "x\nx\nx\nA\nB\nC\nD\nx\nE\nF\nG\n";
16155 let ops = super::histogram_compacted_line_ops(old, new, false, WhitespaceMode::default());
16156 assert!(!ops.is_empty(), "expected diff ops");
16157 }
16158
16159 #[test]
16160 fn t4071_histogram_u0_body_completes() {
16161 use crate::diff::options::{DiffAlgorithm, DiffOptions};
16162 let old = "x\nx\nx\nx\n";
16163 let new = "x\nx\nx\nA\nB\nC\nD\nx\nE\nF\nG\n";
16164 let opts = DiffOptions {
16165 context_lines: 0,
16166 algorithm: DiffAlgorithm::Histogram,
16167 indent_heuristic: false,
16168 ..DiffOptions::default()
16169 };
16170 let body = unified_hunk_body_for_diff_options(old, new, &opts);
16171 assert!(body.contains("@@"), "expected hunk header: {body}");
16172 }
16173
16174 #[test]
16175 fn t4071_minimal_u0_matches_git_hunk_headers() {
16176 use crate::diff::options::{DiffAlgorithm, DiffOptions};
16177 let old = "x\nx\nx\nx\n";
16178 let new = "x\nx\nx\nA\nB\nC\nD\nx\nE\nF\nG\n";
16179 let opts = DiffOptions {
16180 context_lines: 0,
16181 algorithm: DiffAlgorithm::Minimal,
16182 indent_heuristic: false,
16183 ..DiffOptions::default()
16184 };
16185 let body = unified_hunk_body_for_diff_options(old, new, &opts);
16186 assert_eq!(
16187 body.matches("@@ ").count(),
16188 2,
16189 "git 2.55+ splits u0 minimal hunks at repeated anchors: {body}"
16190 );
16191 }
16192
16193 #[test]
16194 fn hunks_do_not_slide_onto_final_line_without_newline() {
16195 use crate::diff::options::{DiffAlgorithm, DiffOptions};
16196
16197 let old = "{\n \"a\": 1\n}\n";
16198 let new = "{\n \"a\": 2\n}\n{\n \"b\": 2\n}";
16199 let expected = "@@ -1,3 +1,6 @@\n {\n- \"a\": 1\n+ \"a\": 2\n }\n+{\n+ \"b\": 2\n+}\n\\ No newline at end of file\n";
16200 for algorithm in [
16201 DiffAlgorithm::Myers,
16202 DiffAlgorithm::Minimal,
16203 DiffAlgorithm::Patience,
16204 DiffAlgorithm::Histogram,
16205 ] {
16206 for indent_heuristic in [true, false] {
16207 let options = DiffOptions {
16208 algorithm,
16209 indent_heuristic,
16210 ..DiffOptions::default()
16211 };
16212 assert_eq!(
16213 unified_hunk_body_for_diff_options(old, new, &options),
16214 expected,
16215 "{algorithm:?}, indent heuristic {indent_heuristic}"
16216 );
16217 }
16218 }
16219 }
16220
16221 #[test]
16222 fn myers_patience_histogram_minimal_emit_unified_hunks() {
16223 for algo in [Algorithm::Myers, Algorithm::Patience, Algorithm::Histogram] {
16224 let patch = unified_diff_with_prefix_and_funcname_and_algorithm(
16225 OLD,
16226 NEW,
16227 "a/old.txt",
16228 "b/new.txt",
16229 3,
16230 0,
16231 "a/",
16232 "b/",
16233 None,
16234 algo,
16235 false,
16236 false,
16237 true,
16238 false,
16239 );
16240 assert!(patch.contains("@@"), "algo {algo:?} should emit hunks");
16241 assert!(patch.contains("-beta") || patch.contains("-BETA"));
16242 }
16243 }
16244
16245 #[test]
16246 fn git_histogram_printer_and_anchored_diff_smoke() {
16247 let hist = unified_diff_histogram_hunks_only(OLD, NEW, 3, 0, false);
16248 assert!(!hist.is_empty());
16249 let anchors = vec!["beta".to_string()];
16250 let anchored = anchored_unified_diff(
16251 OLD,
16252 NEW,
16253 "old",
16254 "new",
16255 3,
16256 &anchors,
16257 Algorithm::Myers,
16258 false,
16259 true,
16260 false,
16261 );
16262 assert!(anchored.contains("@@"));
16263 }
16264
16265 #[test]
16266 fn count_changes_git_histogram_vs_similar() {
16267 let hist = count_changes_with_algorithm(OLD, NEW, Algorithm::Myers, true);
16268 let myers = count_changes_with_algorithm(OLD, NEW, Algorithm::Myers, false);
16269 assert!(hist.0 + hist.1 > 0);
16270 assert!(myers.0 + myers.1 > 0);
16271 }
16272}
16273
16274#[cfg(test)]
16275mod line_coverage_tests {
16276 use super::*;
16277 use crate::config::ConfigSet;
16278 use crate::index::StatMatchPolicy;
16279 use crate::index::{Index, IndexEntry, MODE_REGULAR};
16280 use crate::objects::ObjectKind;
16281 use crate::parallel::ThreadPolicy;
16282 use crate::write_tree::write_tree_from_index;
16283 use similar::Algorithm;
16284 use tempfile::TempDir;
16285
16286 fn index_entry(path: &str, mode: u32, oid: ObjectId) -> IndexEntry {
16287 IndexEntry {
16288 ctime_sec: 0,
16289 ctime_nsec: 0,
16290 mtime_sec: 0,
16291 mtime_nsec: 0,
16292 dev: 0,
16293 ino: 0,
16294 mode,
16295 uid: 0,
16296 gid: 0,
16297 size: 0,
16298 oid,
16299 flags: path.len().min(0xfff) as u16,
16300 flags_extended: None,
16301 path: path.as_bytes().to_vec(),
16302 base_index_pos: 0,
16303 }
16304 }
16305
16306 fn write_tree(odb: &Odb, entries: &[(&str, ObjectId)]) -> ObjectId {
16307 let mut body = Vec::new();
16308 for (name, oid) in entries {
16309 body.extend_from_slice(format!("100644 {name}\0").as_bytes());
16310 body.extend_from_slice(oid.as_bytes());
16311 }
16312 odb.write(ObjectKind::Tree, &body).expect("tree")
16313 }
16314
16315 fn pair_of_trees_with_rename() -> (TempDir, Odb, ObjectId, ObjectId) {
16316 let temp = TempDir::new().expect("tempdir");
16317 let odb = Odb::new(temp.path());
16318 let old_blob = odb
16319 .write(ObjectKind::Blob, b"line one\nline two\n")
16320 .expect("old");
16321 let new_blob = odb
16322 .write(ObjectKind::Blob, b"line one\nline TWO\nline three\n")
16323 .expect("new");
16324 let old_tree = write_tree(&odb, &[("old.txt", old_blob)]);
16325 let new_tree = write_tree(&odb, &[("new.txt", new_blob), ("kept.txt", old_blob)]);
16326 (temp, odb, old_tree, new_tree)
16327 }
16328
16329 #[test]
16330 fn detect_copies_reports_source_modes_without_tree_entries() {
16331 let temp = TempDir::new().expect("tempdir");
16332 let odb = Odb::new(temp.path());
16333 let content = odb
16334 .write(ObjectKind::Blob, b"shared content\n")
16335 .expect("blob");
16336 let changed = odb.write(ObjectKind::Blob, b"changed\n").expect("blob");
16337 let old_tree = write_tree(&odb, &[("gone.txt", content), ("mod.txt", content)]);
16338 let new_tree = write_tree(
16339 &odb,
16340 &[
16341 ("copy.txt", content),
16342 ("mod.txt", changed),
16343 ("moved.txt", content),
16344 ],
16345 );
16346 let entries = diff_trees(&odb, Some(&old_tree), Some(&new_tree), "").expect("diff");
16347 let result = detect_copies(&odb, None, entries, 50, false, &[]).expect("copies");
16348 let pairs: Vec<_> = result
16349 .iter()
16350 .filter(|e| matches!(e.status, DiffStatus::Renamed | DiffStatus::Copied))
16351 .map(|e| (e.old_mode, e.new_mode))
16352 .collect();
16353 assert_eq!(pairs.len(), 2, "{result:?}");
16354 assert!(
16355 pairs
16356 .iter()
16357 .all(|p| *p == (FileMode::REGULAR, FileMode::REGULAR)),
16358 "{pairs:?}"
16359 );
16360 }
16361
16362 #[test]
16363 fn exact_rename_detection_pairs_only_equal_blobs_among_many() -> Result<()> {
16364 let temp = TempDir::new().expect("tempdir");
16365 let odb = Odb::new(temp.path());
16366 const N: usize = 200;
16367 let shared = odb.write(ObjectKind::Blob, b"shared\n").expect("blob");
16368 let mut old_names = Vec::with_capacity(N + 1);
16369 let mut old_oids = Vec::with_capacity(N + 1);
16370 for i in 0..N {
16371 old_names.push(format!("old/{i:03}.txt"));
16372 old_oids.push(
16373 odb.write(ObjectKind::Blob, format!("old {i}\n").as_bytes())
16374 .expect("blob"),
16375 );
16376 }
16377 old_names.push("shared-old.txt".to_owned());
16378 old_oids.push(shared);
16379 let old_pairs: Vec<(&str, ObjectId)> = old_names
16380 .iter()
16381 .zip(old_oids.iter().copied())
16382 .map(|(name, oid)| (name.as_str(), oid))
16383 .collect();
16384
16385 let mut new_names = Vec::with_capacity(N + 1);
16386 let mut new_oids = Vec::with_capacity(N + 1);
16387 for i in 0..N {
16388 new_names.push(format!("new/{i:03}.txt"));
16389 new_oids.push(
16390 odb.write(ObjectKind::Blob, format!("new {i}\n").as_bytes())
16391 .expect("blob"),
16392 );
16393 }
16394 new_names.push("shared-new.txt".to_owned());
16395 new_oids.push(shared);
16396 let new_pairs: Vec<(&str, ObjectId)> = new_names
16397 .iter()
16398 .zip(new_oids.iter().copied())
16399 .map(|(name, oid)| (name.as_str(), oid))
16400 .collect();
16401
16402 let old_tree = write_tree(&odb, &old_pairs);
16403 let new_tree = write_tree(&odb, &new_pairs);
16404 let raw = diff_trees(&odb, Some(&old_tree), Some(&new_tree), "")?;
16405 assert_eq!(raw.len(), (N + 1) * 2);
16406 let config = ConfigSet::new();
16407 let policy = ThreadPolicy::default();
16408 let renamed = detect_renames_with_policy(
16409 &odb,
16410 None,
16411 raw,
16412 50,
16413 &config,
16414 &policy,
16415 None,
16416 DetectRenamesOptions::default(),
16417 )?;
16418 assert_eq!(
16419 renamed
16420 .entries
16421 .iter()
16422 .filter(|e| e.status == DiffStatus::Renamed)
16423 .count(),
16424 1
16425 );
16426 Ok(())
16427 }
16428
16429 #[test]
16430 fn pathspec_limited_diff_does_not_pair_outside_pathspec() -> Result<()> {
16431 let temp = TempDir::new().expect("tempdir");
16432 let odb = Odb::new(temp.path());
16433 let shared = odb.write(ObjectKind::Blob, b"payload\n").expect("blob");
16434 let mut old_names = Vec::new();
16435 let mut old_oids = Vec::new();
16436 for i in 0..50 {
16437 old_names.push(format!("bulk/old/{i:02}.txt"));
16438 old_oids.push(
16439 odb.write(ObjectKind::Blob, format!("bulk old {i}\n").as_bytes())
16440 .expect("blob"),
16441 );
16442 }
16443 old_names.push("target.txt".to_owned());
16444 old_oids.push(shared);
16445 let old_pairs: Vec<(&str, ObjectId)> = old_names
16446 .iter()
16447 .zip(old_oids.iter().copied())
16448 .map(|(name, oid)| (name.as_str(), oid))
16449 .collect();
16450
16451 let mut new_names = Vec::new();
16452 let mut new_oids = Vec::new();
16453 for i in 0..50 {
16454 new_names.push(format!("bulk/new/{i:02}.txt"));
16455 new_oids.push(
16456 odb.write(ObjectKind::Blob, format!("bulk new {i}\n").as_bytes())
16457 .expect("blob"),
16458 );
16459 }
16460 new_names.push("elsewhere.txt".to_owned());
16461 new_oids.push(shared);
16462 let new_pairs: Vec<(&str, ObjectId)> = new_names
16463 .iter()
16464 .zip(new_oids.iter().copied())
16465 .map(|(name, oid)| (name.as_str(), oid))
16466 .collect();
16467
16468 let old_tree = write_tree(&odb, &old_pairs);
16469 let new_tree = write_tree(&odb, &new_pairs);
16470 let config = ConfigSet::new();
16471 let policy = ThreadPolicy::default();
16472 let pathspecs = vec!["target.txt".to_owned()];
16473 let entries = diff_trees_with_renames_for_pathspecs_and_policy(
16474 &odb,
16475 Some(&old_tree),
16476 Some(&new_tree),
16477 "",
16478 &pathspecs,
16479 None,
16480 50,
16481 &config,
16482 &policy,
16483 )?;
16484 assert_eq!(entries.len(), 1);
16485 assert_eq!(entries[0].status, DiffStatus::Deleted);
16486 assert_eq!(entries[0].path(), "target.txt");
16487 Ok(())
16488 }
16489
16490 #[test]
16491 fn detect_copies_keeps_modifications_as_modifications() {
16492 let temp = TempDir::new().expect("tempdir");
16495 let odb = Odb::new(temp.path());
16496 let one = odb.write(ObjectKind::Blob, b"1\n").expect("blob");
16497 let two = odb.write(ObjectKind::Blob, b"2\n").expect("blob");
16498 let old_tree = write_tree(&odb, &[("a", one), ("b", two)]);
16499 let new_tree = write_tree(&odb, &[("a", two), ("b", one)]);
16500 let entries = diff_trees(&odb, Some(&old_tree), Some(&new_tree), "").expect("diff");
16501 let result = detect_copies(&odb, None, entries, 50, false, &[]).expect("copies");
16502 let statuses: Vec<_> = result.iter().map(|e| (e.path(), e.status)).collect();
16503 assert_eq!(
16504 statuses,
16505 [("a", DiffStatus::Modified), ("b", DiffStatus::Modified)]
16506 );
16507 }
16508
16509 #[test]
16510 fn diff_helper_and_format_sweep() {
16511 let cfg = ConfigSet::new();
16512 assert!(resolve_indent_heuristic(&cfg, false, false));
16513 assert!(!resolve_indent_heuristic(&cfg, false, true));
16514
16515 let old = "alpha beta\ngamma\n";
16516 let new = "alpha BETA\ngamma delta\n";
16517 let _ = normalize_ignore_space_change(old);
16518 let _ = normalize_ignore_space_change_line("- alpha beta ");
16519 let _ = count_git_lines(old.as_bytes());
16520 let _ = diffcore_count_changes(old.as_bytes(), new.as_bytes());
16521 assert!(parse_diff_rename_score_token("50%").is_some());
16522 let _ = rewrite_merge_score(b"a", b"b");
16523 let _ = rewrite_dissimilarity_index_percent(b"abc", b"abd");
16524 assert_eq!(format_mode(0o100644), "100644");
16525
16526 let old_lines = ["a", "b", "c"];
16527 let new_lines = ["a", "b", "d"];
16528 let compact = diff_slice_ops_compacted(&old_lines, &new_lines, Algorithm::Myers, false);
16529 assert!(!compact.is_empty());
16530 }
16531
16532 #[test]
16533 fn tree_diff_rename_copy_postprocess_matrix() -> Result<()> {
16534 let (_temp, odb, old_tree, new_tree) = pair_of_trees_with_rename();
16535 let raw = diff_trees(&odb, Some(&old_tree), Some(&new_tree), "")?;
16536 assert!(!raw.is_empty());
16537 let with_renames = diff_trees_with_renames(&odb, Some(&old_tree), Some(&new_tree), "", 50)?;
16538 assert!(!with_renames.is_empty());
16539 let config = ConfigSet::new();
16540 let policy = ThreadPolicy::default();
16541 let detected = detect_renames_with_policy(
16542 &odb,
16543 None,
16544 raw.clone(),
16545 50,
16546 &config,
16547 &policy,
16548 None,
16549 DetectRenamesOptions::default(),
16550 )?
16551 .entries;
16552 let source_flat = flatten_tree_entries(&odb, &old_tree)?;
16553 let _ = detect_copies_with_policy(
16554 &odb,
16555 None,
16556 detected.clone(),
16557 50,
16558 true,
16559 &source_flat,
16560 &config,
16561 &policy,
16562 )?;
16563 let opts = DiffPostprocessOptions {
16564 break_score: Some(50),
16565 rename_threshold: 50,
16566 copy_threshold: Some(50),
16567 find_copies_harder: true,
16568 materialize_break_worktree_new_oid: false,
16569 };
16570 let processed = postprocess_diff_entries(
16571 &odb,
16572 raw,
16573 Some(&old_tree),
16574 None,
16575 None,
16576 &config,
16577 &policy,
16578 &opts,
16579 None,
16580 )?;
16581 assert!(!processed.is_empty());
16582 let _ = status_apply_rename_copy_detection(
16583 &odb,
16584 with_renames.clone(),
16585 50,
16586 true,
16587 Some(&old_tree),
16588 &config,
16589 &policy,
16590 None,
16591 )?;
16592 let flat = flatten_tree_entries(&odb, &new_tree)?;
16593 assert!(!flat.is_empty());
16594 let _ = diff_trees_show_tree_entries(&odb, Some(&old_tree), Some(&new_tree), "")?;
16595 Ok(())
16596 }
16597
16598 #[test]
16599 fn unified_diff_flag_matrix() {
16600 let old = "fn main() {\n println!(\"hi\");\n}\n";
16601 let new = "fn main() {\n println!(\"bye\");\n}\n";
16602 for algo in [Algorithm::Myers, Algorithm::Patience, Algorithm::Histogram] {
16603 for (ws, ws_all, fc, ig) in [(true, false, false, false), (false, true, true, true)] {
16604 let patch = unified_diff_with_prefix_and_funcname_and_algorithm(
16605 old,
16606 new,
16607 "a/main.rs",
16608 "b/main.rs",
16609 3,
16610 0,
16611 "a/",
16612 "b/",
16613 None,
16614 algo,
16615 ws,
16616 ws_all,
16617 fc,
16618 ig,
16619 );
16620 assert!(patch.contains("@@"), "algo {algo:?}");
16621 }
16622 }
16623 let hist = unified_diff_histogram_ignore_with_prefix_and_funcname(
16624 old,
16625 new,
16626 "a/x",
16627 "b/x",
16628 3,
16629 0,
16630 "",
16631 "",
16632 None,
16633 false,
16634 false,
16635 false,
16636 |_old, _new| false,
16637 );
16638 assert!(!hist.is_empty());
16639 let raw = format_raw_abbrev(
16640 &DiffEntry {
16641 status: DiffStatus::Modified,
16642 old_path: Some("x".into()),
16643 new_path: Some("x".into()),
16644 old_mode: FileMode::REGULAR,
16645 new_mode: FileMode::REGULAR,
16646 old_oid: empty_blob_oid(),
16647 new_oid: empty_blob_oid(),
16648 score: Some(80),
16649 },
16650 7,
16651 );
16652 assert!(raw.contains('M') || raw.contains('x'));
16653 }
16654
16655 #[test]
16656 fn index_and_commit_diff_paths() -> Result<()> {
16657 let temp = TempDir::new().expect("tempdir");
16658 let odb = Odb::new(temp.path());
16659 let b1 = odb.write(ObjectKind::Blob, b"v1\n").expect("b1");
16660 let b2 = odb.write(ObjectKind::Blob, b"v2\n").expect("b2");
16661 let b3 = odb.write(ObjectKind::Blob, b"v3\n").expect("b3");
16662 let mut index = Index::new();
16663 index.add_or_replace(index_entry("file.txt", MODE_REGULAR, b2));
16664 let head = write_tree_from_index(&odb, &index, "").expect("tree");
16665 index.entries_mut()[0].oid = b1;
16666 let idx_diff = diff_index_to_tree(&odb, &index, Some(&head), false)?;
16667 assert!(!idx_diff.is_empty());
16668 let commit_body = format!(
16669 "tree {head}\nauthor T <t@e.com> 1 +0000\ncommitter T <t@e.com> 1 +0000\n\nm\n"
16670 );
16671 let c1 = odb
16672 .write(ObjectKind::Commit, commit_body.as_bytes())
16673 .expect("c1");
16674 let mut index2 = Index::new();
16675 index2.add_or_replace(index_entry("file.txt", MODE_REGULAR, b3));
16676 let head2 = write_tree_from_index(&odb, &index2, "").expect("head2");
16677 let c2_body = format!(
16678 "tree {head2}\nparent {c1}\nauthor T <t@e.com> 2 +0000\ncommitter T <t@e.com> 2 +0000\n\nm2\n"
16679 );
16680 let c2 = odb
16681 .write(ObjectKind::Commit, c2_body.as_bytes())
16682 .expect("c2");
16683 let commit_diff = diff_commits_with_renames(&odb, &c1, &c2, 50)?;
16684 assert!(!commit_diff.is_empty());
16685 let _ = tree_oid_of_commit(&odb, &c2)?;
16686 let mtime = index_mtime_for_diff(&index, Some((1, 0)));
16687 let _ = entry_is_racy(&index.entries()[0], mtime);
16688 let _ = entry_is_racy_with_policy(&index.entries()[0], mtime, StatMatchPolicy::default());
16689 Ok(())
16690 }
16691
16692 #[test]
16693 fn tree_oid_of_commit_reports_invalid_ref_for_non_commits() {
16694 let temp = TempDir::new().expect("tempdir");
16695 let odb = Odb::new(temp.path());
16696 let tree = odb.write(ObjectKind::Tree, &[]).expect("tree");
16697 let body = format!(
16698 "tree {tree}\nauthor T <t@e.com> 1 +0000\ncommitter T <t@e.com> 1 +0000\n\nm\n"
16699 );
16700 let commit = odb
16701 .write(ObjectKind::Commit, body.as_bytes())
16702 .expect("commit");
16703 let blob = odb.write(ObjectKind::Blob, b"x\n").expect("blob");
16704
16705 assert_eq!(tree_oid_of_commit(&odb, &commit).expect("tree"), tree);
16706 assert!(matches!(
16707 tree_oid_of_commit(&odb, &blob),
16708 Err(Error::InvalidRef(_))
16709 ));
16710 assert!(matches!(
16711 tree_oid_of_commit(&odb, &tree),
16712 Err(Error::InvalidRef(_))
16713 ));
16714 }
16715
16716 #[test]
16717 fn worktree_index_diff_and_rename_score() -> Result<()> {
16718 let temp = TempDir::new().expect("tempdir");
16719 let wt = temp.path();
16720 let git_dir = wt.join(".git");
16721 std::fs::create_dir_all(git_dir.join("objects")).expect("objects");
16722 let objects_dir = git_dir.join("objects");
16723 let odb = Odb::new(&objects_dir);
16724 let content = b"hello world\n";
16725 std::fs::write(wt.join("tracked.txt"), content).expect("write");
16726 let oid = odb.write(ObjectKind::Blob, content).expect("blob");
16727 let mut index = Index::new();
16728 index.add_or_replace(index_entry("tracked.txt", MODE_REGULAR, oid));
16729 let head = write_tree_from_index(&odb, &index, "").expect("tree");
16730 std::fs::write(wt.join("tracked.txt"), b"hello WORLD\n").expect("mutate");
16731 let mut index_mut = index.clone();
16732 let diff = diff_index_to_worktree(&odb, &mut index_mut, wt, false, false)?;
16733 assert!(!diff.is_empty());
16734 let _ = diff_tree_to_worktree(&odb, Some(&head), wt, &index)?;
16735 assert_eq!(rename_similarity_score(b"a", b"a\n"), 0);
16736 assert_eq!(rename_similarity_score(b"a\n", b"a"), 0);
16737 let score = rename_similarity_score(b"line one\nline two\n", b"line one\nline TWO\n");
16738 assert!(score >= 50);
16739 let _ = detect_renames(&odb, Some(wt), diff, 50);
16740 Ok(())
16741 }
16742
16743 #[test]
16744 fn extended_diff_helpers_sweep() -> Result<()> {
16745 use crate::crlf::{ConversionConfig, FileAttrs};
16746 let temp = TempDir::new().expect("tempdir");
16747 let wt = temp.path();
16748 let git_dir = wt.join(".git");
16749 std::fs::create_dir_all(git_dir.join("objects")).expect("objects");
16750 let objects_dir = git_dir.join("objects");
16751 let odb = Odb::new(&objects_dir);
16752 let b_old = odb.write(ObjectKind::Blob, b"alpha\nbeta\n").expect("old");
16753 let b_new = odb
16754 .write(ObjectKind::Blob, b"alpha\nBETA\ngamma\n")
16755 .expect("new");
16756 let old_tree = write_tree(&odb, &[("a.txt", b_old), ("b.txt", b_old)]);
16757 let new_tree = write_tree(
16758 &odb,
16759 &[("a.txt", b_new), ("b.txt", b_new), ("c.txt", b_new)],
16760 );
16761 let entries = diff_trees(&odb, Some(&old_tree), Some(&new_tree), "")?;
16762 assert!(entries.len() >= 2);
16763
16764 let cfg = ConfigSet::new();
16765 assert!(indent_heuristic_from_config(&cfg));
16766 let _ = unified_diff_histogram_hunks_only("a\nb\n", "a\nB\n", 3, 0, false);
16767 let _ = unified_diff_histogram_with_prefix_and_funcname_indent(
16768 "x\n", "y\n", "a/f", "b/f", 3, 0, "", "", None, false, false,
16769 );
16770 let _ = count_changes_with_algorithm("a\n", "b\n", Algorithm::Myers, false);
16771 let _ = should_break_rewrite_pair(b"short", b"totally different body", 50);
16772 let _ = head_path_states(&odb, Some(&new_tree))?;
16773 std::fs::write(wt.join("tracked.txt"), b"hello\n").expect("wt");
16774 let abs = wt.join("tracked.txt");
16775 let meta = std::fs::metadata(&abs).expect("meta");
16776 let mut index = Index::new();
16777 index.add_or_replace(index_entry("tracked.txt", MODE_REGULAR, b_old));
16778 let _ = stat_matches_with_policy(&index.entries()[0], &meta, StatMatchPolicy::default());
16779 let _ = worktree_differs_from_index_entry(
16780 &odb,
16781 &git_dir,
16782 wt,
16783 &index.entries()[0],
16784 false,
16785 None,
16786 None,
16787 )?;
16788 let conv = ConversionConfig::from_config(&cfg);
16789 let attrs = FileAttrs::default();
16790 let params = WorktreeAddRefreshParams {
16791 odb: &odb,
16792 ie: &index.entries()[0],
16793 meta: &meta,
16794 abs_path: &abs,
16795 rel_path: "tracked.txt",
16796 conv: &conv,
16797 file_attrs: &attrs,
16798 index_mtime: None,
16799 staged_mode: MODE_REGULAR,
16800 filter_process: None,
16801 };
16802 let _ = classify_worktree_entry_for_add(¶ms)?;
16803 let _ = hash_worktree_file(
16804 &odb,
16805 &abs,
16806 &meta,
16807 &conv,
16808 &attrs,
16809 "tracked.txt",
16810 Some(&index.entries()[0]),
16811 None,
16812 )?;
16813 let _ = materialize_worktree_blob(
16814 &odb,
16815 &abs,
16816 &meta,
16817 &conv,
16818 &attrs,
16819 "tracked.txt",
16820 Some(&index.entries()[0]),
16821 None,
16822 )?;
16823 let _ = read_worktree_bytes_for_mode(&abs, MODE_REGULAR);
16824 let _ = mode_from_metadata(&meta);
16825 let _ = smudge_racily_clean_entries(&odb, &git_dir, &mut index, wt, None, Some(&cfg), None);
16826 let _ = refresh_index_stat_content_verified(
16827 &odb,
16828 &git_dir,
16829 &mut index,
16830 wt,
16831 None,
16832 Some(&cfg),
16833 None,
16834 )?;
16835 let _ = refresh_index_stat_content_verified_with_rules(
16836 &odb,
16837 &git_dir,
16838 &mut index,
16839 wt,
16840 None,
16841 Some(&cfg),
16842 None,
16843 None,
16844 )?;
16845 let _ = path_checkout_skip_blob_write_when_up_to_date_with_config(
16846 &odb,
16847 &git_dir,
16848 wt,
16849 &index,
16850 &index.entries()[0],
16851 &b_old,
16852 MODE_REGULAR,
16853 "tracked.txt",
16854 None,
16855 None,
16856 )?;
16857 let patch = anchored_unified_diff(
16858 "a\nb\n",
16859 "a\nB\n",
16860 "f",
16861 "f",
16862 3,
16863 &[String::from("b")],
16864 Algorithm::Myers,
16865 false,
16866 true,
16867 false,
16868 );
16869 assert!(patch.contains("@@"));
16870 let _ = annotate_unified_diff_no_newline_at_eof("@@\n+a\n\\ No newline\n", "a", "a\n");
16871 Ok(())
16872 }
16873
16874 #[test]
16875 fn unmerged_only_tree_worktree_diff_and_func_heuristics() -> Result<()> {
16876 use similar::DiffOp;
16877 assert!(inserted_block_starts_with_c_like_function_definition(
16878 "static int foo(int bar) {"
16879 ));
16880 assert!(!inserted_block_starts_with_c_like_function_definition(
16881 "printf(hi);"
16882 ));
16883 let ops = vec![
16884 DiffOp::Equal {
16885 old_index: 0,
16886 new_index: 0,
16887 len: 4,
16888 },
16889 DiffOp::Insert {
16890 old_index: 4,
16891 new_index: 4,
16892 new_len: 2,
16893 },
16894 ];
16895 let new_lines = ["a\n", "b\n", "c\n", "d\n", "void added() {\n", "}\n"];
16896 assert!(append_with_whole_function_added(
16897 &ops, 4, 6, &new_lines, None
16898 ));
16899 let _ = hunk_old_change_end_exclusive(&ops);
16900
16901 let temp = TempDir::new().expect("tempdir");
16902 let wt = temp.path();
16903 let git_dir = wt.join(".git");
16904 let objects_dir = git_dir.join("objects");
16905 std::fs::create_dir_all(&objects_dir).expect("objects");
16906 let odb = Odb::new(&objects_dir);
16907 let tree_blob = odb.write(ObjectKind::Blob, b"base\n").expect("blob");
16908 std::fs::write(wt.join("conflict.txt"), b"worktree\n").expect("wt");
16909 let tree = write_tree(&odb, &[("conflict.txt", tree_blob)]);
16910 let mut index = Index::new();
16911 let mut stage1 = index_entry("conflict.txt", MODE_REGULAR, tree_blob);
16912 stage1.flags = (stage1.flags & 0x0FFF) | (1 << 12);
16913 index.entries_mut().push(stage1);
16914 let cfg = ConfigSet::new();
16915 let diff = diff_tree_to_worktree_with_git_dir(
16916 &odb,
16917 Some(&tree),
16918 wt,
16919 &git_dir,
16920 &index,
16921 Some(&cfg),
16922 )?;
16923 assert!(!diff.is_empty());
16924 Ok(())
16925 }
16926
16927 #[test]
16928 fn high_volume_unified_diff_and_postprocess() -> Result<()> {
16929 let (_temp, odb, old_tree, new_tree) = pair_of_trees_with_rename();
16930 let config = ConfigSet::new();
16931 let policy = ThreadPolicy::default();
16932 for i in 0..600u32 {
16933 let old = format!("seed {i}\nalpha\n");
16934 let new = format!("seed {i}\nalpha\nvoid fn_{i}(int arg) {{\n return arg;\n}}\n");
16935 let _ = unified_diff_with_prefix_and_funcname_and_algorithm(
16936 &old,
16937 &new,
16938 "a/f.c",
16939 "b/f.c",
16940 3,
16941 0,
16942 "a/",
16943 "b/",
16944 None,
16945 Algorithm::Myers,
16946 i % 2 == 0,
16947 i % 3 == 0,
16948 i % 5 == 0,
16949 i % 7 == 0,
16950 );
16951 let threshold = 30 + (i % 40);
16952 let raw = diff_trees(&odb, Some(&old_tree), Some(&new_tree), "")?;
16953 let opts = DiffPostprocessOptions {
16954 rename_threshold: threshold,
16955 copy_threshold: Some(threshold),
16956 find_copies_harder: i % 2 == 0,
16957 break_score: Some(threshold as u64),
16958 materialize_break_worktree_new_oid: i % 4 == 0,
16959 };
16960 let _ = postprocess_diff_entries(
16961 &odb,
16962 raw,
16963 Some(&old_tree),
16964 None,
16965 None,
16966 &config,
16967 &policy,
16968 &opts,
16969 None,
16970 )?;
16971 }
16972 Ok(())
16973 }
16974}
16975
16976#[cfg(test)]
16977mod refresh_checks_tests {
16978 use super::{DistinctFileAttrs, RefreshChecks, RefreshContent};
16979 use crate::crlf::{EolAttr, FileAttrs, TextAttr};
16980 use crate::objects::ObjectId;
16981
16982 fn attrs_with_eol(eol: EolAttr) -> FileAttrs {
16983 FileAttrs {
16984 eol,
16985 ..FileAttrs::default()
16986 }
16987 }
16988
16989 #[test]
16990 fn equal_attributes_share_one_slot() {
16991 let mut attrs = DistinctFileAttrs::default();
16992 assert_eq!(attrs.intern(FileAttrs::default()), 0);
16993 assert_eq!(attrs.intern(attrs_with_eol(EolAttr::Crlf)), 1);
16994 assert_eq!(attrs.intern(FileAttrs::default()), 0);
16995 assert_eq!(attrs.intern(attrs_with_eol(EolAttr::Crlf)), 1);
16996 let binary = FileAttrs {
16997 text: TextAttr::Unset,
16998 ..FileAttrs::default()
16999 };
17000 assert_eq!(attrs.intern(binary.clone()), 2);
17001 assert_eq!(attrs.intern(binary), 2);
17002 assert_eq!(attrs.0.len(), 3);
17003 assert_eq!(attrs.0[1], attrs_with_eol(EolAttr::Crlf));
17004 }
17005
17006 #[test]
17007 fn sets_past_the_search_limit_are_stored_again() {
17008 let mut attrs = DistinctFileAttrs::default();
17009 let first = attrs.intern(FileAttrs::default());
17010 for n in 0..DistinctFileAttrs::SEARCH_LIMIT {
17011 let distinct = FileAttrs {
17012 whitespace: Some(format!("tabwidth={}", n + 1)),
17013 ..FileAttrs::default()
17014 };
17015 assert_eq!(attrs.intern(distinct), n + 1);
17016 }
17017 let again = attrs.intern(FileAttrs::default());
17018 assert_ne!(again, first, "only the most recent sets are searched");
17019 assert_eq!(attrs.0[again], attrs.0[first]);
17020 assert_eq!(attrs.intern(FileAttrs::default()), again);
17021 }
17022
17023 #[test]
17024 fn empty_checks_answer_none_for_every_entry() {
17025 let checks = RefreshChecks::default();
17026 assert!(checks.0.is_empty());
17027 assert!(checks.get(0).is_none());
17028 assert!(checks.get(9_999).is_none());
17029 }
17030
17031 #[test]
17032 fn filled_checks_answer_by_entry_position() {
17033 let oid = ObjectId::from_bytes(&[7u8; 20]).unwrap();
17034 let other = ObjectId::from_bytes(&[8u8; 20]).unwrap();
17035 let checks = RefreshChecks(vec![None, Some(RefreshContent::Hashed(oid)), None]);
17036 assert!(checks.get(0).is_none());
17037 assert!(RefreshContent::matches(checks.get(1), &oid));
17038 assert!(RefreshContent::differs(checks.get(1), &other));
17039 assert!(
17040 checks.get(3).is_none(),
17041 "positions past the end have no check"
17042 );
17043 }
17044}