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};
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 fn indent_heuristic_from_config(config: &ConfigSet) -> bool {
1648 match config.get_bool("diff.indentHeuristic") {
1649 Some(Ok(b)) => b,
1650 Some(Err(_)) | None => true,
1651 }
1652}
1653
1654#[must_use]
1656pub fn resolve_indent_heuristic(
1657 config: &ConfigSet,
1658 cli_indent_heuristic: bool,
1659 cli_no_indent_heuristic: bool,
1660) -> bool {
1661 if cli_no_indent_heuristic {
1662 false
1663 } else if cli_indent_heuristic {
1664 true
1665 } else {
1666 indent_heuristic_from_config(config)
1667 }
1668}
1669
1670mod git_xdiff {
1680 use std::cell::RefCell;
1681
1682 const XDL_MAX_COST_MIN: i64 = 256;
1683 const XDL_HEUR_MIN_COST: i64 = 256;
1684 const XDL_SNAKE_CNT: i64 = 20;
1685 const XDL_K_HEUR: i64 = 4;
1686 const XDL_LINE_MAX: i64 = i64::MAX;
1687 const XDL_KPDIS_RUN: i64 = 4;
1688 const XDL_MAX_EQLIMIT: i64 = 1024;
1689 const XDL_SIMSCAN_WINDOW: i64 = 100;
1690
1691 const DISCARD: u8 = 0;
1693 const KEEP: u8 = 1;
1694 const INVESTIGATE: u8 = 2;
1695
1696 fn bogosqrt(mut n: i64) -> i64 {
1698 let mut i: i64 = 1;
1699 while n > 0 {
1700 i <<= 1;
1701 n >>= 2;
1702 }
1703 i
1704 }
1705
1706 struct XdAlgoEnv {
1707 mxcost: i64,
1708 snake_cnt: i64,
1709 heur_min: i64,
1710 }
1711
1712 struct XdpSplit {
1713 i1: i64,
1714 i2: i64,
1715 min_lo: bool,
1716 min_hi: bool,
1717 }
1718
1719 struct XdFile<'a> {
1722 #[allow(dead_code)]
1723 ids: &'a [u32],
1724 changed: Vec<u8>,
1725 reference_index: Vec<usize>,
1726 effective_ids: Vec<u32>,
1728 dstart: i64,
1729 dend: i64,
1730 nreff: i64,
1731 }
1732
1733 #[inline]
1734 fn effective_id(xdf: &XdFile<'_>, idx: i64) -> u32 {
1735 xdf.effective_ids[idx as usize]
1736 }
1737
1738 struct XdiffScratch {
1739 count1: Vec<u32>,
1740 count2: Vec<u32>,
1741 action1: Vec<u8>,
1742 action2: Vec<u8>,
1743 kvd: Vec<i64>,
1744 }
1745
1746 impl XdiffScratch {
1747 fn prepare_counts(&mut self, nclasses: usize) {
1748 if self.count1.len() < nclasses {
1749 self.count1.resize(nclasses, 0);
1750 self.count2.resize(nclasses, 0);
1751 }
1752 self.count1[..nclasses].fill(0);
1753 self.count2[..nclasses].fill(0);
1754 }
1755
1756 fn prepare_actions(&mut self, nrec1: usize, nrec2: usize) {
1757 let a1 = nrec1 + 1;
1758 let a2 = nrec2 + 1;
1759 if self.action1.len() < a1 {
1760 self.action1.resize(a1, 0);
1761 } else {
1762 self.action1[..a1].fill(0);
1763 }
1764 if self.action2.len() < a2 {
1765 self.action2.resize(a2, 0);
1766 } else {
1767 self.action2[..a2].fill(0);
1768 }
1769 }
1770
1771 fn prepare_kvd(&mut self, kvd_len: usize) {
1772 if self.kvd.len() < kvd_len {
1776 self.kvd = vec![0; kvd_len];
1777 }
1778 }
1779 }
1780
1781 thread_local! { static XDIFF_SCRATCH: RefCell<XdiffScratch> = const {
1783 RefCell::new(XdiffScratch {
1784 count1: Vec::new(),
1785 count2: Vec::new(),
1786 action1: Vec::new(),
1787 action2: Vec::new(),
1788 kvd: Vec::new(),
1789 })
1790 };
1791 }
1792
1793 fn bool_changed(changed: Vec<u8>) -> Vec<bool> {
1794 changed.into_iter().map(|b| b != 0).collect()
1795 }
1796
1797 #[allow(clippy::too_many_arguments)]
1800 fn xdl_split(
1801 xdf1: &XdFile<'_>,
1802 off1: i64,
1803 lim1: i64,
1804 xdf2: &XdFile<'_>,
1805 off2: i64,
1806 lim2: i64,
1807 kvdf: &mut [i64],
1808 kvdb: &mut [i64],
1809 koff: i64,
1810 need_min: bool,
1811 spl: &mut XdpSplit,
1812 xenv: &XdAlgoEnv,
1813 ) -> i64 {
1814 let dmin = off1 - lim2;
1815 let dmax = lim1 - off2;
1816 let fmid = off1 - off2;
1817 let bmid = lim1 - lim2;
1818 let odd = ((fmid - bmid) & 1) != 0;
1819 let mut fmin = fmid;
1820 let mut fmax = fmid;
1821 let mut bmin = bmid;
1822 let mut bmax = bmid;
1823
1824 let kf = |k: i64| -> usize { (k + koff) as usize };
1825
1826 kvdf[kf(fmid)] = off1;
1827 kvdb[kf(bmid)] = lim1;
1828
1829 let mut ec: i64 = 1;
1830 loop {
1831 let mut got_snake = false;
1832
1833 if fmin > dmin {
1834 fmin -= 1;
1835 kvdf[kf(fmin - 1)] = -1;
1836 } else {
1837 fmin += 1;
1838 }
1839 if fmax < dmax {
1840 fmax += 1;
1841 kvdf[kf(fmax + 1)] = -1;
1842 } else {
1843 fmax -= 1;
1844 }
1845
1846 let mut d = fmax;
1847 while d >= fmin {
1848 let mut i1 = if kvdf[kf(d - 1)] >= kvdf[kf(d + 1)] {
1849 kvdf[kf(d - 1)] + 1
1850 } else {
1851 kvdf[kf(d + 1)]
1852 };
1853 let prev1 = i1;
1854 let mut i2 = i1 - d;
1855 while i1 < lim1 && i2 < lim2 && effective_id(xdf1, i1) == effective_id(xdf2, i2) {
1856 i1 += 1;
1857 i2 += 1;
1858 }
1859 if i1 - prev1 > xenv.snake_cnt {
1860 got_snake = true;
1861 }
1862 kvdf[kf(d)] = i1;
1863 if odd && bmin <= d && d <= bmax && kvdb[kf(d)] <= i1 {
1864 spl.i1 = i1;
1865 spl.i2 = i2;
1866 spl.min_lo = true;
1867 spl.min_hi = true;
1868 return ec;
1869 }
1870 d -= 2;
1871 }
1872
1873 if bmin > dmin {
1874 bmin -= 1;
1875 kvdb[kf(bmin - 1)] = XDL_LINE_MAX;
1876 } else {
1877 bmin += 1;
1878 }
1879 if bmax < dmax {
1880 bmax += 1;
1881 kvdb[kf(bmax + 1)] = XDL_LINE_MAX;
1882 } else {
1883 bmax -= 1;
1884 }
1885
1886 let mut d = bmax;
1887 while d >= bmin {
1888 let mut i1 = if kvdb[kf(d - 1)] < kvdb[kf(d + 1)] {
1889 kvdb[kf(d - 1)]
1890 } else {
1891 kvdb[kf(d + 1)] - 1
1892 };
1893 let prev1 = i1;
1894 let mut i2 = i1 - d;
1895 while i1 > off1
1896 && i2 > off2
1897 && effective_id(xdf1, i1 - 1) == effective_id(xdf2, i2 - 1)
1898 {
1899 i1 -= 1;
1900 i2 -= 1;
1901 }
1902 if prev1 - i1 > xenv.snake_cnt {
1903 got_snake = true;
1904 }
1905 kvdb[kf(d)] = i1;
1906 if !odd && fmin <= d && d <= fmax && i1 <= kvdf[kf(d)] {
1907 spl.i1 = i1;
1908 spl.i2 = i2;
1909 spl.min_lo = true;
1910 spl.min_hi = true;
1911 return ec;
1912 }
1913 d -= 2;
1914 }
1915
1916 if need_min {
1917 ec += 1;
1918 continue;
1919 }
1920
1921 if got_snake && ec > xenv.heur_min {
1922 let mut best = 0i64;
1923 let mut d = fmax;
1924 while d >= fmin {
1925 let dd = if d > fmid { d - fmid } else { fmid - d };
1926 let i1 = kvdf[kf(d)];
1927 let i2 = i1 - d;
1928 let v = (i1 - off1) + (i2 - off2) - dd;
1929 if v > XDL_K_HEUR * ec
1930 && v > best
1931 && off1 + xenv.snake_cnt <= i1
1932 && i1 < lim1
1933 && off2 + xenv.snake_cnt <= i2
1934 && i2 < lim2
1935 {
1936 let mut k = 1i64;
1937 while effective_id(xdf1, i1 - k) == effective_id(xdf2, i2 - k) {
1938 if k == xenv.snake_cnt {
1939 best = v;
1940 spl.i1 = i1;
1941 spl.i2 = i2;
1942 break;
1943 }
1944 k += 1;
1945 }
1946 }
1947 d -= 2;
1948 }
1949 if best > 0 {
1950 spl.min_lo = true;
1951 spl.min_hi = false;
1952 return ec;
1953 }
1954
1955 let mut best = 0i64;
1956 let mut d = bmax;
1957 while d >= bmin {
1958 let dd = if d > bmid { d - bmid } else { bmid - d };
1959 let i1 = kvdb[kf(d)];
1960 let i2 = i1 - d;
1961 let v = (lim1 - i1) + (lim2 - i2) - dd;
1962 if v > XDL_K_HEUR * ec
1963 && v > best
1964 && off1 < i1
1965 && i1 <= lim1 - xenv.snake_cnt
1966 && off2 < i2
1967 && i2 <= lim2 - xenv.snake_cnt
1968 {
1969 let mut k = 0i64;
1970 while effective_id(xdf1, i1 + k) == effective_id(xdf2, i2 + k) {
1971 if k == xenv.snake_cnt - 1 {
1972 best = v;
1973 spl.i1 = i1;
1974 spl.i2 = i2;
1975 break;
1976 }
1977 k += 1;
1978 }
1979 }
1980 d -= 2;
1981 }
1982 if best > 0 {
1983 spl.min_lo = false;
1984 spl.min_hi = true;
1985 return ec;
1986 }
1987 }
1988
1989 if ec >= xenv.mxcost {
1990 let mut fbest = -1i64;
1991 let mut fbest1 = -1i64;
1992 let mut d = fmax;
1993 while d >= fmin {
1994 let mut i1 = kvdf[kf(d)].min(lim1);
1995 let mut i2 = i1 - d;
1996 if lim2 < i2 {
1997 i1 = lim2 + d;
1998 i2 = lim2;
1999 }
2000 if fbest < i1 + i2 {
2001 fbest = i1 + i2;
2002 fbest1 = i1;
2003 }
2004 d -= 2;
2005 }
2006
2007 let mut bbest = XDL_LINE_MAX;
2008 let mut bbest1 = XDL_LINE_MAX;
2009 let mut d = bmax;
2010 while d >= bmin {
2011 let mut i1 = off1.max(kvdb[kf(d)]);
2012 let mut i2 = i1 - d;
2013 if i2 < off2 {
2014 i1 = off2 + d;
2015 i2 = off2;
2016 }
2017 if i1 + i2 < bbest {
2018 bbest = i1 + i2;
2019 bbest1 = i1;
2020 }
2021 d -= 2;
2022 }
2023
2024 if (lim1 + lim2) - bbest < fbest - (off1 + off2) {
2025 spl.i1 = fbest1;
2026 spl.i2 = fbest - fbest1;
2027 spl.min_lo = true;
2028 spl.min_hi = false;
2029 } else {
2030 spl.i1 = bbest1;
2031 spl.i2 = bbest - bbest1;
2032 spl.min_lo = false;
2033 spl.min_hi = true;
2034 }
2035 return ec;
2036 }
2037
2038 ec += 1;
2039 }
2040 }
2041
2042 #[allow(clippy::too_many_arguments)]
2044 fn xdl_recs_cmp(
2045 xdf1: &mut XdFile<'_>,
2046 mut off1: i64,
2047 mut lim1: i64,
2048 xdf2: &mut XdFile<'_>,
2049 mut off2: i64,
2050 mut lim2: i64,
2051 kvdf: &mut [i64],
2052 kvdb: &mut [i64],
2053 koff: i64,
2054 need_min: bool,
2055 xenv: &XdAlgoEnv,
2056 ) {
2057 while off1 < lim1 && off2 < lim2 && effective_id(xdf1, off1) == effective_id(xdf2, off2) {
2058 off1 += 1;
2059 off2 += 1;
2060 }
2061 while off1 < lim1
2062 && off2 < lim2
2063 && effective_id(xdf1, lim1 - 1) == effective_id(xdf2, lim2 - 1)
2064 {
2065 lim1 -= 1;
2066 lim2 -= 1;
2067 }
2068
2069 if off1 == lim1 {
2070 while off2 < lim2 {
2071 let r = xdf2.reference_index[off2 as usize];
2072 xdf2.changed[r] = 1;
2073 off2 += 1;
2074 }
2075 } else if off2 == lim2 {
2076 while off1 < lim1 {
2077 let r = xdf1.reference_index[off1 as usize];
2078 xdf1.changed[r] = 1;
2079 off1 += 1;
2080 }
2081 } else {
2082 let mut spl = XdpSplit {
2083 i1: 0,
2084 i2: 0,
2085 min_lo: false,
2086 min_hi: false,
2087 };
2088 xdl_split(
2089 xdf1, off1, lim1, xdf2, off2, lim2, kvdf, kvdb, koff, need_min, &mut spl, xenv,
2090 );
2091 xdl_recs_cmp(
2092 xdf1, off1, spl.i1, xdf2, off2, spl.i2, kvdf, kvdb, koff, spl.min_lo, xenv,
2093 );
2094 xdl_recs_cmp(
2095 xdf1, spl.i1, lim1, xdf2, spl.i2, lim2, kvdf, kvdb, koff, spl.min_hi, xenv,
2096 );
2097 }
2098 }
2099
2100 fn clean_mmatch(action: &[u8], i: i64, mut s: i64, mut e: i64) -> bool {
2102 if i - s > XDL_SIMSCAN_WINDOW {
2103 s = i - XDL_SIMSCAN_WINDOW;
2104 }
2105 if e - i > XDL_SIMSCAN_WINDOW {
2106 e = i + XDL_SIMSCAN_WINDOW;
2107 }
2108
2109 let mut rdis0 = 0i64;
2110 let mut rpdis0 = 1i64;
2111 let mut r = 1i64;
2112 while i - r >= s {
2113 match action[(i - r) as usize] {
2114 DISCARD => rdis0 += 1,
2115 INVESTIGATE => rpdis0 += 1,
2116 _ => break, }
2118 r += 1;
2119 }
2120 if rdis0 == 0 {
2121 return false;
2122 }
2123 let mut rdis1 = 0i64;
2124 let mut rpdis1 = 1i64;
2125 let mut r = 1i64;
2126 while i + r <= e {
2127 match action[(i + r) as usize] {
2128 DISCARD => rdis1 += 1,
2129 INVESTIGATE => rpdis1 += 1,
2130 _ => break, }
2132 r += 1;
2133 }
2134 if rdis1 == 0 {
2135 return false;
2136 }
2137 rdis1 += rdis0;
2138 rpdis1 += rpdis0;
2139 rpdis1 * XDL_KPDIS_RUN < (rpdis1 + rdis1)
2140 }
2141
2142 pub fn changed_flags(ids1: &[u32], ids2: &[u32], need_min: bool) -> (Vec<bool>, Vec<bool>) {
2148 changed_flags_with_shared(ids1, ids2, &[], 0, need_min)
2149 }
2150
2151 pub fn changed_flags_with_shared(
2158 ids1: &[u32],
2159 ids2: &[u32],
2160 shared_counts: &[u32],
2161 shared_records: usize,
2162 need_min: bool,
2163 ) -> (Vec<bool>, Vec<bool>) {
2164 XDIFF_SCRATCH.with(|cell| {
2165 changed_flags_with_shared_inner(
2166 cell,
2167 ids1,
2168 ids2,
2169 shared_counts,
2170 shared_records,
2171 need_min,
2172 )
2173 })
2174 }
2175
2176 fn changed_flags_with_shared_inner(
2177 scratch: &RefCell<XdiffScratch>,
2178 ids1: &[u32],
2179 ids2: &[u32],
2180 shared_counts: &[u32],
2181 shared_records: usize,
2182 need_min: bool,
2183 ) -> (Vec<bool>, Vec<bool>) {
2184 let nrec1 = ids1.len();
2185 let nrec2 = ids2.len();
2186
2187 let nclasses = ids1
2189 .iter()
2190 .chain(ids2)
2191 .max()
2192 .map_or(0, |&max| max as usize + 1);
2193 let mut work = scratch.borrow_mut();
2194 work.prepare_counts(nclasses);
2195 for &id in ids1 {
2196 work.count1[id as usize] += 1;
2197 }
2198 for &id in ids2 {
2199 work.count2[id as usize] += 1;
2200 }
2201 for (id, &shared) in shared_counts.iter().enumerate().take(nclasses) {
2202 work.count1[id] += shared;
2203 work.count2[id] += shared;
2204 }
2205
2206 let mut xdf1 = XdFile {
2207 ids: ids1,
2208 changed: vec![0u8; nrec1],
2209 reference_index: Vec::new(),
2210 effective_ids: Vec::new(),
2211 dstart: 0,
2212 dend: nrec1 as i64 - 1,
2213 nreff: 0,
2214 };
2215 let mut xdf2 = XdFile {
2216 ids: ids2,
2217 changed: vec![0u8; nrec2],
2218 reference_index: Vec::new(),
2219 effective_ids: Vec::new(),
2220 dstart: 0,
2221 dend: nrec2 as i64 - 1,
2222 nreff: 0,
2223 };
2224
2225 let lim = nrec1.min(nrec2) as i64;
2227 let mut i = 0i64;
2228 while i < lim && ids1[i as usize] == ids2[i as usize] {
2229 i += 1;
2230 }
2231 xdf1.dstart = i;
2232 xdf2.dstart = i;
2233 let mut j = 0i64;
2234 let rem = lim - i;
2235 while j < rem && ids1[nrec1 - 1 - j as usize] == ids2[nrec2 - 1 - j as usize] {
2236 j += 1;
2237 }
2238 xdf1.dend = nrec1 as i64 - j - 1;
2239 xdf2.dend = nrec2 as i64 - j - 1;
2240
2241 work.prepare_actions(nrec1, nrec2);
2243 for xdf in [&mut xdf1, &mut xdf2] {
2244 let span = usize::try_from(xdf.dend - xdf.dstart + 1).unwrap_or(0);
2245 xdf.reference_index.reserve_exact(span);
2246 xdf.effective_ids.reserve_exact(span);
2247 }
2248
2249 let mlim1 = if need_min {
2250 i64::MAX
2251 } else {
2252 let mut mlim = bogosqrt((nrec1 + shared_records) as i64);
2253 if mlim > XDL_MAX_EQLIMIT {
2254 mlim = XDL_MAX_EQLIMIT;
2255 }
2256 mlim
2257 };
2258 let mut idx = xdf1.dstart;
2259 while idx <= xdf1.dend {
2260 let id = ids1[idx as usize];
2261 let nm = i64::from(work.count2[id as usize]);
2262 work.action1[idx as usize] = if nm == 0 {
2263 DISCARD
2264 } else if nm >= mlim1 {
2265 INVESTIGATE
2266 } else {
2267 KEEP
2268 };
2269 idx += 1;
2270 }
2271
2272 let mlim2 = if need_min {
2273 i64::MAX
2274 } else {
2275 let mut mlim = bogosqrt((nrec2 + shared_records) as i64);
2276 if mlim > XDL_MAX_EQLIMIT {
2277 mlim = XDL_MAX_EQLIMIT;
2278 }
2279 mlim
2280 };
2281 let mut idx = xdf2.dstart;
2282 while idx <= xdf2.dend {
2283 let id = ids2[idx as usize];
2284 let nm = i64::from(work.count1[id as usize]);
2285 work.action2[idx as usize] = if nm == 0 {
2286 DISCARD
2287 } else if nm >= mlim2 {
2288 INVESTIGATE
2289 } else {
2290 KEEP
2291 };
2292 idx += 1;
2293 }
2294
2295 let mut idx = xdf1.dstart;
2296 while idx <= xdf1.dend {
2297 let id = ids1[idx as usize];
2298 let a = work.action1[idx as usize];
2299 if a == KEEP
2300 || (a == INVESTIGATE
2301 && !clean_mmatch(&work.action1[..nrec1 + 1], idx, xdf1.dstart, xdf1.dend))
2302 {
2303 xdf1.reference_index.push(idx as usize);
2304 xdf1.effective_ids.push(id);
2305 } else {
2306 xdf1.changed[idx as usize] = 1;
2307 }
2308 idx += 1;
2309 }
2310 xdf1.nreff = xdf1.reference_index.len() as i64;
2311
2312 let mut idx = xdf2.dstart;
2313 while idx <= xdf2.dend {
2314 let id = ids2[idx as usize];
2315 let a = work.action2[idx as usize];
2316 if a == KEEP
2317 || (a == INVESTIGATE
2318 && !clean_mmatch(&work.action2[..nrec2 + 1], idx, xdf2.dstart, xdf2.dend))
2319 {
2320 xdf2.reference_index.push(idx as usize);
2321 xdf2.effective_ids.push(id);
2322 } else {
2323 xdf2.changed[idx as usize] = 1;
2324 }
2325 idx += 1;
2326 }
2327 xdf2.nreff = xdf2.reference_index.len() as i64;
2328
2329 let ndiags = xdf1.nreff + xdf2.nreff + 3;
2331 let kvd_len = (2 * ndiags + 2) as usize;
2332 work.prepare_kvd(kvd_len);
2333 let kvd = &mut work.kvd[..kvd_len];
2334 let koff = xdf2.nreff + 1;
2336 let (kvdf_slice, kvdb_slice) = kvd.split_at_mut(ndiags as usize);
2337 let xenv = XdAlgoEnv {
2340 mxcost: bogosqrt(ndiags).max(XDL_MAX_COST_MIN),
2341 snake_cnt: XDL_SNAKE_CNT,
2342 heur_min: XDL_HEUR_MIN_COST,
2343 };
2344
2345 let nreff1 = xdf1.nreff;
2346 let nreff2 = xdf2.nreff;
2347 xdl_recs_cmp(
2348 &mut xdf1, 0, nreff1, &mut xdf2, 0, nreff2, kvdf_slice, kvdb_slice, koff, need_min,
2349 &xenv,
2350 );
2351
2352 (bool_changed(xdf1.changed), bool_changed(xdf2.changed))
2353 }
2354
2355 pub fn patience_changed_flags(lines1: &[&str], lines2: &[&str]) -> (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 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]
2690pub fn diff_slice_ops_compacted(
2691 old_lines: &[&str],
2692 new_lines: &[&str],
2693 algorithm: similar::Algorithm,
2694 indent_heuristic: bool,
2695) -> Vec<similar::DiffOp> {
2696 diff_indent_heuristic::diff_slice_ops_compacted(
2697 old_lines,
2698 new_lines,
2699 algorithm,
2700 indent_heuristic,
2701 )
2702}
2703
2704#[must_use]
2707#[allow(clippy::too_many_arguments)]
2711pub(crate) fn line_diff_ops_git_xdiff_compacted_ids(
2712 old_ids: &[u32],
2713 new_ids: &[u32],
2714 shared_counts: &[u32],
2715 shared_records: usize,
2716 old_lines: &[&str],
2717 new_lines: &[&str],
2718 indent_heuristic: bool,
2719 ws: super::WhitespaceMode,
2720) -> Vec<similar::DiffOp> {
2721 let (changed1, changed2) = git_xdiff::changed_flags_with_shared(
2722 old_ids,
2723 new_ids,
2724 shared_counts,
2725 shared_records,
2726 false,
2727 );
2728 let ops = changed_flags_to_ops(&changed1, &changed2, old_lines.len(), new_lines.len());
2729 diff_indent_heuristic::apply_change_compact_to_ops_with_ws(
2730 &ops,
2731 old_lines,
2732 new_lines,
2733 indent_heuristic,
2734 ws,
2735 )
2736}
2737
2738#[cfg(test)]
2740#[must_use]
2741pub(crate) fn xdiff_new_line_changed_flags(
2742 old_lines: &[&str],
2743 new_lines: &[&str],
2744 indent_heuristic: bool,
2745) -> Vec<bool> {
2746 let mut interner =
2747 crate::line_interner::LineInterner::with_capacity(old_lines.len() + new_lines.len());
2748 let ids1 = interner.intern_all(old_lines);
2749 let ids2 = interner.intern_all(new_lines);
2750 let (changed1, changed2) = git_xdiff::changed_flags(&ids1, &ids2, false);
2751 let ops = changed_flags_to_ops(&changed1, &changed2, old_lines.len(), new_lines.len());
2752 let compacted = diff_indent_heuristic::apply_change_compact_to_ops(
2753 &ops,
2754 old_lines,
2755 new_lines,
2756 indent_heuristic,
2757 );
2758 let mut flags = vec![false; new_lines.len()];
2759 for op in compacted {
2760 match op.tag() {
2761 similar::DiffTag::Insert | similar::DiffTag::Replace => {
2762 for idx in op.new_range() {
2763 if idx < flags.len() {
2764 flags[idx] = true;
2765 }
2766 }
2767 }
2768 similar::DiffTag::Equal | similar::DiffTag::Delete => {}
2769 }
2770 }
2771 flags
2772}
2773
2774#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2776#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
2777#[cfg_attr(feature = "serde-models", serde(rename_all = "snake_case"))]
2778pub enum DiffStatus {
2779 Added,
2781 Deleted,
2783 Modified,
2785 Renamed,
2787 Copied,
2789 TypeChanged,
2791 Unmerged,
2793}
2794
2795impl DiffStatus {
2796 #[must_use]
2798 pub fn letter(&self) -> char {
2799 match self {
2800 Self::Added => 'A',
2801 Self::Deleted => 'D',
2802 Self::Modified => 'M',
2803 Self::Renamed => 'R',
2804 Self::Copied => 'C',
2805 Self::TypeChanged => 'T',
2806 Self::Unmerged => 'U',
2807 }
2808 }
2809
2810 #[must_use]
2817 pub(crate) fn for_content_change(old_mode: u32, new_mode: u32) -> Self {
2818 if old_mode & 0o170000 != new_mode & 0o170000 {
2819 Self::TypeChanged
2820 } else {
2821 Self::Modified
2822 }
2823 }
2824}
2825
2826#[derive(Debug, Clone, PartialEq, Eq)]
2828#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
2829pub struct DiffEntry {
2830 pub status: DiffStatus,
2832 pub old_path: Option<String>,
2834 pub new_path: Option<String>,
2836 pub old_mode: FileMode,
2838 pub new_mode: FileMode,
2840 pub old_oid: ObjectId,
2842 pub new_oid: ObjectId,
2845 pub score: Option<u32>,
2847}
2848
2849impl DiffEntry {
2850 #[must_use]
2852 pub fn path(&self) -> &str {
2853 self.new_path
2854 .as_deref()
2855 .or(self.old_path.as_deref())
2856 .unwrap_or("")
2857 }
2858
2859 #[must_use]
2864 pub fn display_path(&self) -> String {
2865 match self.status {
2866 DiffStatus::Renamed | DiffStatus::Copied => {
2867 let old = self.old_path.as_deref().unwrap_or("");
2868 let new = self.new_path.as_deref().unwrap_or("");
2869 if old.is_empty() || new.is_empty() {
2870 self.path().to_owned()
2871 } else {
2872 format!("{old} -> {new}")
2873 }
2874 }
2875 _ => self.path().to_owned(),
2876 }
2877 }
2878}
2879
2880#[must_use]
2882pub fn zero_oid() -> ObjectId {
2883 ObjectId::from_bytes(&[0u8; 20]).unwrap_or_else(|_| {
2884 panic!("internal error: failed to create zero OID");
2886 })
2887}
2888
2889#[must_use]
2891pub fn empty_blob_oid() -> ObjectId {
2892 ObjectId::from_hex("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391").unwrap_or_else(|_| {
2893 panic!("internal error: failed to create empty blob OID");
2895 })
2896}
2897
2898pub fn diff_trees(
2913 odb: &Odb,
2914 old_tree_oid: Option<&ObjectId>,
2915 new_tree_oid: Option<&ObjectId>,
2916 prefix: &str,
2917) -> Result<Vec<DiffEntry>> {
2918 crate::trace::trace_span!("grit.diff.tree");
2919 diff_trees_opts(odb, old_tree_oid, new_tree_oid, prefix, false, None)
2920}
2921
2922pub(crate) fn diff_trees_with_index(
2925 odb: &Odb,
2926 index: &Index,
2927 old_tree_oid: Option<&ObjectId>,
2928 new_tree_oid: Option<&ObjectId>,
2929 prefix: &str,
2930 trust_index_for_old_tree: bool,
2931) -> Result<Vec<DiffEntry>> {
2932 let hint = (trust_index_for_old_tree
2933 && old_tree_oid.is_some()
2934 && index.entries_sorted
2935 && !index.entries.iter().any(|e| e.stage() != 0))
2936 .then_some(DiffTreesWalkHint {
2937 index: Some(index),
2938 old_tree_matches: true,
2939 ..DiffTreesWalkHint::default()
2940 });
2941 diff_trees_opts(
2942 odb,
2943 old_tree_oid,
2944 new_tree_oid,
2945 prefix,
2946 false,
2947 hint.as_ref(),
2948 )
2949}
2950
2951pub(crate) fn diff_trees_outside_sparse_directories(
2955 odb: &Odb,
2956 index: &Index,
2957 old_tree_oid: Option<&ObjectId>,
2958 new_tree_oid: Option<&ObjectId>,
2959 sparse: Option<&SparseCheckout>,
2960 index_sparse: bool,
2961) -> Result<Vec<DiffEntry>> {
2962 crate::trace::trace_span!("grit.diff.tree");
2963 let hint = index
2964 .has_sparse_directory_placeholders()
2965 .then_some(DiffTreesWalkHint {
2966 index: Some(index),
2967 skip_sparse_directories: true,
2968 sparse,
2969 index_sparse,
2970 ..DiffTreesWalkHint::default()
2971 });
2972 diff_trees_opts(odb, old_tree_oid, new_tree_oid, "", false, hint.as_ref())
2973}
2974
2975pub(crate) fn diff_trees_coalesced_for_checkout(
2978 odb: &Odb,
2979 index: &Index,
2980 old_tree_oid: Option<&ObjectId>,
2981 new_tree_oid: Option<&ObjectId>,
2982 sparse: Option<&SparseCheckout>,
2983 index_sparse: bool,
2984) -> Result<Vec<DiffEntry>> {
2985 crate::trace::trace_span!("grit.diff.tree");
2986 let trust_index_for_old_tree = old_tree_oid.is_some()
2987 && index.entries_sorted
2988 && !index.entries.iter().any(|e| e.stage() != 0);
2989 let hint = DiffTreesWalkHint {
2990 index: trust_index_for_old_tree.then_some(index),
2991 old_tree_matches: trust_index_for_old_tree,
2992 coalesce_subtrees_for_checkout: true,
2993 skip_sparse_directories: index.has_sparse_directory_placeholders(),
2994 sparse,
2995 index_sparse,
2996 ..DiffTreesWalkHint::default()
2997 };
2998 diff_trees_opts(odb, old_tree_oid, new_tree_oid, "", false, Some(&hint))
2999}
3000
3001fn tree_walk_directory_relevant(
3003 dir: &str,
3004 pathspecs: &[String],
3005 pathspec_matcher: Option<&crate::pathspec::PathspecMatcher<'_>>,
3006) -> bool {
3007 if pathspecs.is_empty() {
3008 return true;
3009 }
3010 if let Some(matcher) = pathspec_matcher {
3011 return matcher.matches_entry(dir, 0o040000)
3012 || crate::pathspec::pathspec_wants_descent_for_rev_tree(dir, pathspecs);
3013 }
3014 crate::pathspec::pathspec_entry_is_relevant(dir, true, pathspecs)
3015}
3016
3017fn diff_entry_matches_pathspecs(
3018 entry: &DiffEntry,
3019 pathspecs: &[String],
3020 pathspec_matcher: Option<&crate::pathspec::PathspecMatcher<'_>>,
3021) -> bool {
3022 let path = entry.path();
3023 if path.is_empty() {
3024 return false;
3025 }
3026 if let Some(matcher) = pathspec_matcher {
3027 return matcher.matches_change(path, entry.old_path.as_deref());
3028 }
3029 crate::pathspec::matches_pathspec_list(path, pathspecs)
3030}
3031
3032pub(crate) fn diff_trees_for_pathspecs(
3038 odb: &Odb,
3039 old_tree_oid: Option<&ObjectId>,
3040 new_tree_oid: Option<&ObjectId>,
3041 pathspecs: &[String],
3042 pathspec_matcher: Option<&crate::pathspec::PathspecMatcher<'_>>,
3043) -> Result<Vec<DiffEntry>> {
3044 if pathspecs.is_empty() {
3045 return diff_trees(odb, old_tree_oid, new_tree_oid, "");
3046 }
3047 crate::trace::trace_span!("grit.diff.tree");
3048 let hint = DiffTreesWalkHint {
3049 pathspecs,
3050 pathspec_matcher,
3051 ..DiffTreesWalkHint::default()
3052 };
3053 let mut entries = diff_trees_opts(odb, old_tree_oid, new_tree_oid, "", false, Some(&hint))?;
3054 entries.retain(|entry| diff_entry_matches_pathspecs(entry, pathspecs, pathspec_matcher));
3055 Ok(entries)
3056}
3057
3058#[derive(Clone, Copy, Default)]
3060struct DiffTreesWalkHint<'a> {
3061 index: Option<&'a Index>,
3062 old_tree_matches: bool,
3064 coalesce_subtrees_for_checkout: bool,
3066 skip_sparse_directories: bool,
3068 sparse: Option<&'a SparseCheckout>,
3069 index_sparse: bool,
3070 pathspecs: &'a [String],
3072 pathspec_matcher: Option<&'a crate::pathspec::PathspecMatcher<'a>>,
3073}
3074
3075impl DiffTreesWalkHint<'_> {
3076 fn sparse_excludes_tree(&self, dir: &str) -> bool {
3078 if !self.index_sparse {
3079 return false;
3080 }
3081 let Some(sparse) = self.sparse else {
3082 return false;
3083 };
3084 let mut path = crate::repo_relpath::storage_bytes(dir).into_owned();
3085 if !path.ends_with(b"/") {
3086 path.push(b'/');
3087 }
3088 sparse.excludes_directory(&path)
3089 }
3090
3091 fn skips(&self, dir: &str) -> bool {
3093 if dir.is_empty() {
3094 return false;
3095 }
3096 if !self.pathspecs.is_empty()
3097 && !tree_walk_directory_relevant(dir, self.pathspecs, self.pathspec_matcher)
3098 {
3099 return true;
3100 }
3101 let Some(index) = self.index.filter(|_| self.skip_sparse_directories) else {
3102 return false;
3103 };
3104 let mut row = crate::repo_relpath::storage_bytes(dir).into_owned();
3105 row.push(b'/');
3106 index
3107 .find_entry_pos(&row, 0)
3108 .is_some_and(|pos| index.entries[pos].is_sparse_directory_placeholder())
3109 }
3110}
3111
3112pub fn diff_tree_path_change_count(
3120 odb: &Odb,
3121 old_tree_oid: Option<&ObjectId>,
3122 new_tree_oid: Option<&ObjectId>,
3123 prefix: &str,
3124) -> Result<usize> {
3125 if old_tree_oid == new_tree_oid {
3126 return Ok(0);
3127 }
3128 let old_entries = match old_tree_oid {
3129 Some(oid) => read_tree(odb, oid)?,
3130 None => Vec::new(),
3131 };
3132 let new_entries = match new_tree_oid {
3133 Some(oid) => read_tree(odb, oid)?,
3134 None => Vec::new(),
3135 };
3136 let mut count = 0usize;
3137 diff_tree_entries_count(odb, &old_entries, &new_entries, prefix, false, &mut count)?;
3138 Ok(count)
3139}
3140
3141pub fn diff_trees_with_renames(
3150 odb: &Odb,
3151 old_tree_oid: Option<&ObjectId>,
3152 new_tree_oid: Option<&ObjectId>,
3153 prefix: &str,
3154 rename_threshold: u32,
3155) -> Result<Vec<DiffEntry>> {
3156 let config = ConfigSet::new();
3157 let policy = ThreadPolicy::default();
3158 diff_trees_with_renames_and_policy(
3159 odb,
3160 old_tree_oid,
3161 new_tree_oid,
3162 prefix,
3163 rename_threshold,
3164 &config,
3165 &policy,
3166 )
3167}
3168
3169pub fn diff_trees_with_renames_and_policy(
3175 odb: &Odb,
3176 old_tree_oid: Option<&ObjectId>,
3177 new_tree_oid: Option<&ObjectId>,
3178 prefix: &str,
3179 rename_threshold: u32,
3180 config: &ConfigSet,
3181 thread_policy: &ThreadPolicy,
3182) -> Result<Vec<DiffEntry>> {
3183 Ok(diff_trees_with_renames_and_policy_result(
3184 odb,
3185 old_tree_oid,
3186 new_tree_oid,
3187 prefix,
3188 rename_threshold,
3189 config,
3190 thread_policy,
3191 )?
3192 .entries)
3193}
3194
3195pub fn diff_trees_with_renames_and_policy_result(
3201 odb: &Odb,
3202 old_tree_oid: Option<&ObjectId>,
3203 new_tree_oid: Option<&ObjectId>,
3204 prefix: &str,
3205 rename_threshold: u32,
3206 config: &ConfigSet,
3207 thread_policy: &ThreadPolicy,
3208) -> Result<DiffEntriesResult> {
3209 diff_trees_with_renames_for_pathspecs_and_policy_result(
3210 odb,
3211 old_tree_oid,
3212 new_tree_oid,
3213 prefix,
3214 &[],
3215 None,
3216 rename_threshold,
3217 config,
3218 thread_policy,
3219 )
3220}
3221
3222#[allow(clippy::too_many_arguments)]
3231pub(crate) fn diff_trees_with_renames_for_pathspecs_and_policy(
3232 odb: &Odb,
3233 old_tree_oid: Option<&ObjectId>,
3234 new_tree_oid: Option<&ObjectId>,
3235 prefix: &str,
3236 pathspecs: &[String],
3237 pathspec_matcher: Option<&crate::pathspec::PathspecMatcher<'_>>,
3238 rename_threshold: u32,
3239 config: &ConfigSet,
3240 thread_policy: &ThreadPolicy,
3241) -> Result<Vec<DiffEntry>> {
3242 Ok(diff_trees_with_renames_for_pathspecs_and_policy_result(
3243 odb,
3244 old_tree_oid,
3245 new_tree_oid,
3246 prefix,
3247 pathspecs,
3248 pathspec_matcher,
3249 rename_threshold,
3250 config,
3251 thread_policy,
3252 )?
3253 .entries)
3254}
3255
3256#[allow(clippy::too_many_arguments)]
3258pub(crate) fn diff_trees_with_renames_for_pathspecs_and_policy_result(
3259 odb: &Odb,
3260 old_tree_oid: Option<&ObjectId>,
3261 new_tree_oid: Option<&ObjectId>,
3262 prefix: &str,
3263 pathspecs: &[String],
3264 pathspec_matcher: Option<&crate::pathspec::PathspecMatcher<'_>>,
3265 rename_threshold: u32,
3266 config: &ConfigSet,
3267 thread_policy: &ThreadPolicy,
3268) -> Result<DiffEntriesResult> {
3269 let entries = if pathspecs.is_empty() {
3270 diff_trees(odb, old_tree_oid, new_tree_oid, prefix)?
3271 } else {
3272 diff_trees_for_pathspecs(odb, old_tree_oid, new_tree_oid, pathspecs, pathspec_matcher)?
3273 };
3274 if rename_threshold <= 100 {
3275 let out = detect_renames_with_policy(
3276 odb,
3277 None,
3278 entries,
3279 rename_threshold,
3280 config,
3281 thread_policy,
3282 None,
3283 DetectRenamesOptions::default(),
3284 )?;
3285 return Ok(DiffEntriesResult {
3286 entries: out.entries,
3287 inexact_rename_skipped: out.inexact_skipped,
3288 });
3289 }
3290 Ok(DiffEntriesResult {
3291 entries,
3292 inexact_rename_skipped: None,
3293 })
3294}
3295
3296pub fn tree_oid_of_commit(odb: &Odb, commit_oid: &ObjectId) -> Result<ObjectId> {
3303 odb.commit_tree(commit_oid).map_err(|err| match err {
3304 Error::UnexpectedObjectKind { .. } => {
3305 Error::InvalidRef(format!("invalid ref: {commit_oid} is not a commit"))
3306 }
3307 other => other,
3308 })
3309}
3310
3311pub(crate) fn tree_oid_for_diff_side(odb: &Odb, oid: &ObjectId) -> Result<ObjectId> {
3318 use crate::objects::{CommitRef, ObjectKind};
3319 let object = odb.read(oid)?;
3320 match object.kind {
3321 ObjectKind::Commit => Ok(CommitRef::parse(&object.data)?.tree()),
3322 ObjectKind::Tree => Ok(*oid),
3323 actual => Err(Error::InvalidRef(format!(
3324 "invalid ref: {oid} is not a commit or tree ({actual:?})"
3325 ))),
3326 }
3327}
3328
3329pub fn tree_oid_of_commit_in_repo(
3335 repo: &crate::repo::Repository,
3336 commit_oid: &ObjectId,
3337) -> Result<ObjectId> {
3338 if let Some(chain) = repo.odb().commit_graph_chain() {
3339 if let Some(tree) = chain.graph_commit_tree(commit_oid) {
3340 return Ok(tree);
3341 }
3342 }
3343 tree_oid_of_commit(repo.odb(), commit_oid)
3344}
3345
3346pub fn diff_commits(odb: &Odb, base: &ObjectId, tip: &ObjectId) -> Result<Vec<DiffEntry>> {
3352 let base_tree = tree_oid_of_commit(odb, base)?;
3353 let tip_tree = tree_oid_of_commit(odb, tip)?;
3354 diff_trees(odb, Some(&base_tree), Some(&tip_tree), "")
3355}
3356
3357pub fn diff_commits_with_renames(
3366 odb: &Odb,
3367 base: &ObjectId,
3368 tip: &ObjectId,
3369 rename_threshold: u32,
3370) -> Result<Vec<DiffEntry>> {
3371 let config = ConfigSet::new();
3372 let policy = ThreadPolicy::default();
3373 diff_commits_with_renames_and_policy(odb, base, tip, rename_threshold, &config, &policy)
3374}
3375
3376pub fn diff_commits_with_renames_and_policy(
3382 odb: &Odb,
3383 base: &ObjectId,
3384 tip: &ObjectId,
3385 rename_threshold: u32,
3386 config: &ConfigSet,
3387 thread_policy: &ThreadPolicy,
3388) -> Result<Vec<DiffEntry>> {
3389 let base_tree = tree_oid_of_commit(odb, base)?;
3390 let tip_tree = tree_oid_of_commit(odb, tip)?;
3391 diff_trees_with_renames_and_policy(
3392 odb,
3393 Some(&base_tree),
3394 Some(&tip_tree),
3395 "",
3396 rename_threshold,
3397 config,
3398 thread_policy,
3399 )
3400}
3401
3402pub fn diff_trees_show_tree_entries(
3409 odb: &Odb,
3410 old_tree_oid: Option<&ObjectId>,
3411 new_tree_oid: Option<&ObjectId>,
3412 prefix: &str,
3413) -> Result<Vec<DiffEntry>> {
3414 diff_trees_opts(odb, old_tree_oid, new_tree_oid, prefix, true, None)
3415}
3416
3417fn diff_subtrees_in_place(
3418 odb: &Odb,
3419 old_tree_oid: Option<&ObjectId>,
3420 new_tree_oid: Option<&ObjectId>,
3421 prefix: &str,
3422 show_trees: bool,
3423 index_hint: Option<&DiffTreesWalkHint<'_>>,
3424 result: &mut Vec<DiffEntry>,
3425) -> Result<()> {
3426 if index_hint.is_some_and(|hint| hint.skips(prefix)) {
3427 return Ok(());
3428 }
3429 if !show_trees {
3430 if let (Some(a), Some(b)) = (old_tree_oid, new_tree_oid) {
3431 if a == b {
3432 return Ok(());
3433 }
3434 }
3435 }
3436 let old_tree = old_tree_oid
3439 .map(|oid| odb.cached_tree_for_walk(oid))
3440 .transpose()?;
3441 let new_tree = new_tree_oid
3442 .map(|oid| odb.cached_tree_for_walk(oid))
3443 .transpose()?;
3444 let oid_len = odb.hash_algo().len();
3445 diff_tree_entries_opts(
3446 odb,
3447 WalkEntries::new(old_tree.as_ref(), oid_len),
3448 WalkEntries::new(new_tree.as_ref(), oid_len),
3449 prefix,
3450 show_trees,
3451 index_hint,
3452 result,
3453 )
3454}
3455
3456enum WalkEntries<'a> {
3459 Parsed(std::slice::Iter<'a, TreeEntry>),
3460 Raw(TreeRefIter<'a>),
3461}
3462
3463impl<'a> WalkEntries<'a> {
3464 fn new(tree: Option<&'a SessionTree>, oid_len: usize) -> Self {
3466 match tree {
3467 Some(SessionTree::Parsed(entries)) => Self::Parsed(entries.iter()),
3468 Some(SessionTree::Raw(data)) => Self::Raw(TreeRefIter::with_oid_len(data, oid_len)),
3469 None => Self::Parsed([].iter()),
3470 }
3471 }
3472
3473 fn next_entry(&mut self) -> Result<Option<TreeEntryRef<'a>>> {
3474 match self {
3475 Self::Parsed(entries) => Ok(entries.next().map(|entry| TreeEntryRef {
3476 mode: entry.mode,
3477 name: &entry.name,
3478 oid: entry.oid,
3479 })),
3480 Self::Raw(entries) => entries.next().transpose(),
3481 }
3482 }
3483}
3484
3485fn diff_trees_opts(
3486 odb: &Odb,
3487 old_tree_oid: Option<&ObjectId>,
3488 new_tree_oid: Option<&ObjectId>,
3489 prefix: &str,
3490 show_trees: bool,
3491 index_hint: Option<&DiffTreesWalkHint<'_>>,
3492) -> Result<Vec<DiffEntry>> {
3493 let mut result = Vec::new();
3494 diff_subtrees_in_place(
3495 odb,
3496 old_tree_oid,
3497 new_tree_oid,
3498 prefix,
3499 show_trees,
3500 index_hint,
3501 &mut result,
3502 )?;
3503 Ok(result)
3504}
3505
3506#[cfg(test)]
3507use std::cell::RefCell;
3508#[cfg(test)]
3509use std::rc::Rc;
3510#[cfg(test)]
3511use std::sync::atomic::{AtomicUsize, Ordering};
3512
3513#[cfg(test)]
3514thread_local! {
3515 static DIFF_TREE_READ_HOOK: RefCell<Option<Rc<AtomicUsize>>> = const { RefCell::new(None) };
3516}
3517
3518#[cfg(test)]
3520pub(crate) fn set_diff_tree_read_hook_for_tests(hook: Option<Rc<AtomicUsize>>) {
3521 DIFF_TREE_READ_HOOK.with(|cell| *cell.borrow_mut() = hook);
3522}
3523
3524fn read_tree_from_odb(odb: &Odb, oid: &ObjectId) -> Result<Vec<TreeEntry>> {
3526 #[cfg(test)]
3527 if let Some(counter) = DIFF_TREE_READ_HOOK.with(|cell| cell.borrow().clone()) {
3528 counter.fetch_add(1, Ordering::Relaxed);
3529 }
3530 Ok((*odb.cached_tree_entries(oid)?).clone())
3531}
3532
3533fn read_tree(odb: &Odb, oid: &ObjectId) -> Result<Vec<TreeEntry>> {
3535 read_tree_from_odb(odb, oid)
3536}
3537
3538fn diff_tree_entries_count(
3539 odb: &Odb,
3540 old: &[TreeEntry],
3541 new: &[TreeEntry],
3542 prefix: &str,
3543 show_trees: bool,
3544 count: &mut usize,
3545) -> Result<()> {
3546 let mut oi = 0;
3547 let mut ni = 0;
3548
3549 while oi < old.len() || ni < new.len() {
3550 match (old.get(oi), new.get(ni)) {
3551 (Some(o), Some(n)) => {
3552 let cmp = crate::objects::tree_entry_cmp(
3553 &o.name,
3554 is_tree_mode(o.mode),
3555 &n.name,
3556 is_tree_mode(n.mode),
3557 );
3558 match cmp {
3559 std::cmp::Ordering::Less => {
3560 emit_deleted_count(odb, o, prefix, show_trees, count)?;
3561 oi += 1;
3562 }
3563 std::cmp::Ordering::Greater => {
3564 emit_added_count(odb, n, prefix, show_trees, count)?;
3565 ni += 1;
3566 }
3567 std::cmp::Ordering::Equal => {
3568 if o.oid != n.oid
3569 || crate::index::canon_mode(o.mode) != crate::index::canon_mode(n.mode)
3570 {
3571 let path = crate::repo_relpath::format_path_stored(prefix, &o.name);
3572 if is_tree_mode(o.mode) && is_tree_mode(n.mode) {
3573 if show_trees {
3574 *count += 1;
3575 }
3576 *count += diff_tree_path_change_count(
3577 odb,
3578 Some(&o.oid),
3579 Some(&n.oid),
3580 &path,
3581 )?;
3582 } else if is_tree_mode(o.mode) != is_tree_mode(n.mode) {
3583 emit_deleted_count(odb, o, prefix, show_trees, count)?;
3584 emit_added_count(odb, n, prefix, show_trees, count)?;
3585 } else {
3586 *count += 1;
3587 }
3588 }
3589 oi += 1;
3590 ni += 1;
3591 }
3592 }
3593 }
3594 (Some(o), None) => {
3595 emit_deleted_count(odb, o, prefix, show_trees, count)?;
3596 oi += 1;
3597 }
3598 (None, Some(n)) => {
3599 emit_added_count(odb, n, prefix, show_trees, count)?;
3600 ni += 1;
3601 }
3602 (None, None) => break,
3603 }
3604 }
3605
3606 Ok(())
3607}
3608
3609fn emit_deleted_count(
3610 odb: &Odb,
3611 entry: &TreeEntry,
3612 prefix: &str,
3613 show_trees: bool,
3614 count: &mut usize,
3615) -> Result<()> {
3616 let path = crate::repo_relpath::format_path_stored(prefix, &entry.name);
3617 if is_tree_mode(entry.mode) {
3618 if show_trees {
3619 *count += 1;
3620 }
3621 *count += diff_tree_path_change_count(odb, Some(&entry.oid), None, &path)?;
3622 } else {
3623 *count += 1;
3624 }
3625 Ok(())
3626}
3627
3628fn emit_added_count(
3629 odb: &Odb,
3630 entry: &TreeEntry,
3631 prefix: &str,
3632 show_trees: bool,
3633 count: &mut usize,
3634) -> Result<()> {
3635 let path = crate::repo_relpath::format_path_stored(prefix, &entry.name);
3636 if is_tree_mode(entry.mode) {
3637 if show_trees {
3638 *count += 1;
3639 }
3640 *count += diff_tree_path_change_count(odb, None, Some(&entry.oid), &path)?;
3641 } else {
3642 *count += 1;
3643 }
3644 Ok(())
3645}
3646
3647fn diff_tree_entries_opts(
3649 odb: &Odb,
3650 mut old: WalkEntries<'_>,
3651 mut new: WalkEntries<'_>,
3652 prefix: &str,
3653 show_trees: bool,
3654 index_hint: Option<&DiffTreesWalkHint<'_>>,
3655 result: &mut Vec<DiffEntry>,
3656) -> Result<()> {
3657 let mut old_entry = old.next_entry()?;
3658 let mut new_entry = new.next_entry()?;
3659
3660 loop {
3661 match (&old_entry, &new_entry) {
3662 (Some(o), Some(n)) => {
3663 let cmp = crate::objects::tree_entry_cmp(
3664 o.name,
3665 is_tree_mode(o.mode),
3666 n.name,
3667 is_tree_mode(n.mode),
3668 );
3669 match cmp {
3670 std::cmp::Ordering::Less => {
3671 emit_deleted_opts(odb, o, prefix, show_trees, index_hint, result)?;
3673 old_entry = old.next_entry()?;
3674 }
3675 std::cmp::Ordering::Greater => {
3676 emit_added_opts(odb, n, prefix, show_trees, index_hint, result)?;
3678 new_entry = new.next_entry()?;
3679 }
3680 std::cmp::Ordering::Equal => {
3681 if o.oid != n.oid
3683 || crate::index::canon_mode(o.mode) != crate::index::canon_mode(n.mode)
3684 {
3685 let path = crate::repo_relpath::format_path_stored(prefix, o.name);
3686 if is_tree_mode(o.mode) && is_tree_mode(n.mode) {
3687 if show_trees {
3689 result.push(DiffEntry {
3690 status: DiffStatus::Modified,
3691 old_path: Some(path.clone()),
3692 new_path: Some(path.clone()),
3693 old_mode: FileMode::new(crate::index::canon_mode(o.mode)),
3694 new_mode: FileMode::new(crate::index::canon_mode(n.mode)),
3695 old_oid: o.oid,
3696 new_oid: n.oid,
3697 score: None,
3698 });
3699 }
3700 diff_subtrees_in_place(
3701 odb,
3702 Some(&o.oid),
3703 Some(&n.oid),
3704 &path,
3705 show_trees,
3706 index_hint,
3707 result,
3708 )?;
3709 } else if is_tree_mode(o.mode) && !is_tree_mode(n.mode) {
3710 emit_deleted_opts(odb, o, prefix, show_trees, index_hint, result)?;
3712 emit_added_opts(odb, n, prefix, show_trees, index_hint, result)?;
3713 } else if !is_tree_mode(o.mode) && is_tree_mode(n.mode) {
3714 emit_deleted_opts(odb, o, prefix, show_trees, index_hint, result)?;
3716 emit_added_opts(odb, n, prefix, show_trees, index_hint, result)?;
3717 } else {
3718 result.push(DiffEntry {
3719 status: DiffStatus::for_content_change(o.mode, n.mode),
3720 old_path: Some(path.clone()),
3721 new_path: Some(path),
3722 old_mode: FileMode::new(crate::index::canon_mode(o.mode)),
3723 new_mode: FileMode::new(crate::index::canon_mode(n.mode)),
3724 old_oid: o.oid,
3725 new_oid: n.oid,
3726 score: None,
3727 });
3728 }
3729 }
3730 old_entry = old.next_entry()?;
3731 new_entry = new.next_entry()?;
3732 }
3733 }
3734 }
3735 (Some(o), None) => {
3736 emit_deleted_opts(odb, o, prefix, show_trees, index_hint, result)?;
3737 old_entry = old.next_entry()?;
3738 }
3739 (None, Some(n)) => {
3740 emit_added_opts(odb, n, prefix, show_trees, index_hint, result)?;
3741 new_entry = new.next_entry()?;
3742 }
3743 (None, None) => break,
3744 }
3745 }
3746
3747 Ok(())
3748}
3749
3750fn emit_deleted_tree_from_index(index: &Index, dir_prefix: &str, result: &mut Vec<DiffEntry>) {
3751 let scan_prefix: Vec<u8> = if dir_prefix.is_empty() {
3752 Vec::new()
3753 } else {
3754 format!("{dir_prefix}/").into_bytes()
3755 };
3756 if index.entries_sorted {
3757 let start = if scan_prefix.is_empty() {
3758 0
3759 } else {
3760 index
3761 .entries
3762 .partition_point(|e| e.path.as_slice() < scan_prefix.as_slice())
3763 };
3764 for ie in index.entries[start..].iter() {
3765 if ie.stage() != 0 {
3766 continue;
3767 }
3768 if !scan_prefix.is_empty() && !ie.path.starts_with(&scan_prefix) {
3769 break;
3770 }
3771 if scan_prefix.is_empty() && ie.path.contains(&b'/') {
3772 continue;
3773 }
3774 let path = String::from_utf8_lossy(&ie.path).into_owned();
3775 result.push(DiffEntry {
3776 status: DiffStatus::Deleted,
3777 old_path: Some(path),
3778 new_path: None,
3779 old_mode: FileMode::new(ie.mode),
3780 new_mode: FileMode::ABSENT,
3781 old_oid: ie.oid,
3782 new_oid: zero_oid(),
3783 score: None,
3784 });
3785 }
3786 return;
3787 }
3788 for ie in &index.entries {
3789 if ie.stage() != 0 {
3790 continue;
3791 }
3792 let Ok(path) = std::str::from_utf8(&ie.path) else {
3793 continue;
3794 };
3795 let under = if dir_prefix.is_empty() {
3796 !path.contains('/')
3797 } else if path == dir_prefix {
3798 false
3799 } else {
3800 path.strip_prefix(dir_prefix)
3801 .is_some_and(|rest| rest.starts_with('/'))
3802 };
3803 if !under {
3804 continue;
3805 }
3806 result.push(DiffEntry {
3807 status: DiffStatus::Deleted,
3808 old_path: Some(path.to_owned()),
3809 new_path: None,
3810 old_mode: FileMode::new(ie.mode),
3811 new_mode: FileMode::ABSENT,
3812 old_oid: ie.oid,
3813 new_oid: zero_oid(),
3814 score: None,
3815 });
3816 }
3817}
3818
3819fn emit_deleted_opts(
3820 odb: &Odb,
3821 entry: &TreeEntryRef<'_>,
3822 prefix: &str,
3823 show_trees: bool,
3824 index_hint: Option<&DiffTreesWalkHint<'_>>,
3825 result: &mut Vec<DiffEntry>,
3826) -> Result<()> {
3827 let path = crate::repo_relpath::format_path_stored(prefix, entry.name);
3828 if is_tree_mode(entry.mode) {
3829 if index_hint.is_some_and(|hint| hint.sparse_excludes_tree(&path)) {
3830 return Ok(());
3831 }
3832 if show_trees || index_hint.is_some_and(|hint| hint.coalesce_subtrees_for_checkout) {
3833 result.push(DiffEntry {
3834 status: DiffStatus::Deleted,
3835 old_path: Some(path.clone()),
3836 new_path: None,
3837 old_mode: FileMode::new(entry.mode),
3838 new_mode: FileMode::ABSENT,
3839 old_oid: entry.oid,
3840 new_oid: zero_oid(),
3841 score: None,
3842 });
3843 }
3844 if index_hint.is_some_and(|hint| hint.coalesce_subtrees_for_checkout) {
3845 return Ok(());
3846 }
3847 if let Some(index) = index_hint
3848 .filter(|hint| hint.old_tree_matches)
3849 .and_then(|hint| hint.index)
3850 .filter(|index| !index.has_sparse_directory_placeholders())
3851 {
3852 emit_deleted_tree_from_index(index, &path, result);
3853 return Ok(());
3854 }
3855 diff_subtrees_in_place(
3856 odb,
3857 Some(&entry.oid),
3858 None,
3859 &path,
3860 show_trees,
3861 index_hint,
3862 result,
3863 )?;
3864 } else {
3865 result.push(DiffEntry {
3866 status: DiffStatus::Deleted,
3867 old_path: Some(path.clone()),
3868 new_path: None,
3869 old_mode: FileMode::new(crate::index::canon_mode(entry.mode)),
3870 new_mode: FileMode::ABSENT,
3871 old_oid: entry.oid,
3872 new_oid: zero_oid(),
3873 score: None,
3874 });
3875 }
3876 Ok(())
3877}
3878
3879fn emit_added_opts(
3880 odb: &Odb,
3881 entry: &TreeEntryRef<'_>,
3882 prefix: &str,
3883 show_trees: bool,
3884 index_hint: Option<&DiffTreesWalkHint<'_>>,
3885 result: &mut Vec<DiffEntry>,
3886) -> Result<()> {
3887 let path = crate::repo_relpath::format_path_stored(prefix, entry.name);
3888 if is_tree_mode(entry.mode) {
3889 if index_hint.is_some_and(|hint| hint.sparse_excludes_tree(&path)) {
3890 result.push(DiffEntry {
3891 status: DiffStatus::Added,
3892 old_path: None,
3893 new_path: Some(crate::repo_relpath::sparse_directory_stored_path(&path)),
3894 old_mode: FileMode::ABSENT,
3895 new_mode: FileMode::new(MODE_TREE),
3896 old_oid: zero_oid(),
3897 new_oid: entry.oid,
3898 score: None,
3899 });
3900 return Ok(());
3901 }
3902 if show_trees || index_hint.is_some_and(|hint| hint.coalesce_subtrees_for_checkout) {
3903 result.push(DiffEntry {
3904 status: DiffStatus::Added,
3905 old_path: None,
3906 new_path: Some(path.clone()),
3907 old_mode: FileMode::ABSENT,
3908 new_mode: FileMode::new(entry.mode),
3909 old_oid: zero_oid(),
3910 new_oid: entry.oid,
3911 score: None,
3912 });
3913 }
3914 if index_hint.is_some_and(|hint| hint.coalesce_subtrees_for_checkout) {
3915 return Ok(());
3916 }
3917 diff_subtrees_in_place(
3918 odb,
3919 None,
3920 Some(&entry.oid),
3921 &path,
3922 show_trees,
3923 index_hint,
3924 result,
3925 )?;
3926 } else {
3927 result.push(DiffEntry {
3928 status: DiffStatus::Added,
3929 old_path: None,
3930 new_path: Some(path),
3931 old_mode: FileMode::ABSENT,
3932 new_mode: FileMode::new(crate::index::canon_mode(entry.mode)),
3933 old_oid: zero_oid(),
3934 new_oid: entry.oid,
3935 score: None,
3936 });
3937 }
3938 Ok(())
3939}
3940
3941pub fn diff_index_to_tree(
3961 odb: &Odb,
3962 index: &Index,
3963 tree_oid: Option<&ObjectId>,
3964 ignore_submodules: bool,
3965) -> Result<Vec<DiffEntry>> {
3966 match index_cache_tree_walk(index, tree_oid) {
3967 CacheTreeWalk::Fast {
3968 head_tree,
3969 cache_root,
3970 } => {
3971 diff_index_to_tree_with_cache_tree(odb, index, head_tree, cache_root, ignore_submodules)
3972 }
3973 CacheTreeWalk::Flatten => {
3974 diff_index_to_tree_flatten(odb, index, tree_oid, ignore_submodules)
3975 }
3976 }
3977}
3978
3979pub fn diff_index_to_tree_with_renames(
3988 odb: &Odb,
3989 index: &Index,
3990 tree_oid: Option<&ObjectId>,
3991 ignore_submodules: bool,
3992 rename_threshold: u32,
3993 config: &ConfigSet,
3994 thread_policy: &ThreadPolicy,
3995) -> Result<Vec<DiffEntry>> {
3996 let entries = diff_index_to_tree(odb, index, tree_oid, ignore_submodules)?;
3997 apply_rename_detection_to_diff_entries(
3998 odb,
3999 entries,
4000 None,
4001 rename_threshold,
4002 config,
4003 thread_policy,
4004 )
4005}
4006
4007pub fn apply_rename_detection_to_diff_entries(
4016 odb: &Odb,
4017 entries: Vec<DiffEntry>,
4018 work_root: Option<&Path>,
4019 rename_threshold: u32,
4020 config: &ConfigSet,
4021 thread_policy: &ThreadPolicy,
4022) -> Result<Vec<DiffEntry>> {
4023 if entries.is_empty() || rename_threshold >= 100 {
4024 return Ok(entries);
4025 }
4026 if work_root.is_some() {
4027 let opts = DiffPostprocessOptions {
4028 rename_threshold,
4029 ..DiffPostprocessOptions::default()
4030 };
4031 postprocess_diff_entries(
4032 odb,
4033 entries,
4034 None,
4035 work_root,
4036 None,
4037 config,
4038 thread_policy,
4039 &opts,
4040 None,
4041 )
4042 } else {
4043 detect_renames_with_policy(
4044 odb,
4045 None,
4046 entries,
4047 rename_threshold,
4048 config,
4049 thread_policy,
4050 None,
4051 DetectRenamesOptions::default(),
4052 )
4053 .map(|r| r.entries)
4054 }
4055}
4056
4057pub fn diff_index_to_worktree_with_renames(
4067 repo: &Repository,
4068 index: &mut Index,
4069 work_tree: &Path,
4070 options: DiffIndexToWorktreeOptions,
4071 rename_threshold: u32,
4072) -> Result<(Vec<DiffEntry>, bool)> {
4073 let config = repo.config()?.as_ref().clone();
4074 let thread_policy = repo.thread_policy();
4075 let odb = repo.odb();
4076 let (entries, changed) = diff_index_to_worktree_with_options(odb, index, work_tree, options)?;
4077 let entries = apply_rename_detection_to_diff_entries(
4078 odb,
4079 entries,
4080 Some(work_tree),
4081 rename_threshold,
4082 &config,
4083 &thread_policy,
4084 )?;
4085 Ok((entries, changed))
4086}
4087
4088enum CacheTreeWalk<'a> {
4089 Fast {
4090 head_tree: &'a ObjectId,
4091 cache_root: &'a CacheTreeNode,
4092 },
4093 Flatten,
4094}
4095
4096fn index_cache_tree_walk<'a>(
4099 index: &'a Index,
4100 tree_oid: Option<&'a ObjectId>,
4101) -> CacheTreeWalk<'a> {
4102 let Some(head_tree) = tree_oid else {
4103 return CacheTreeWalk::Flatten;
4104 };
4105 let Some(cache_root) = index.cache_tree.as_ref() else {
4106 return CacheTreeWalk::Flatten;
4107 };
4108 if index.entries.iter().any(|e| e.stage() != 0) {
4109 return CacheTreeWalk::Flatten;
4110 }
4111 CacheTreeWalk::Fast {
4112 head_tree,
4113 cache_root,
4114 }
4115}
4116
4117fn diff_index_to_tree_flatten(
4119 odb: &Odb,
4120 index: &Index,
4121 tree_oid: Option<&ObjectId>,
4122 ignore_submodules: bool,
4123) -> Result<Vec<DiffEntry>> {
4124 let sparse_index = index.has_sparse_directory_placeholders();
4125 let tree_entries = match tree_oid {
4127 Some(oid) if sparse_index => flatten_tree_with_index(odb, oid, "", Some(index))?,
4128 Some(oid) => flatten_tree(odb, oid, "")?,
4129 None => Vec::new(),
4130 };
4131
4132 let mut tree_map: std::collections::BTreeMap<&str, &FlatEntry> =
4134 std::collections::BTreeMap::new();
4135 for entry in &tree_entries {
4136 tree_map.insert(&entry.path, entry);
4137 }
4138
4139 let mut result = Vec::new();
4140 let mut stage0_paths = std::collections::BTreeSet::new();
4141 let mut unmerged_modes: std::collections::BTreeMap<String, (u8, u32)> =
4142 std::collections::BTreeMap::new();
4143
4144 for ie in &index.entries {
4146 let path = String::from_utf8_lossy(&ie.path).to_string();
4147 if ie.stage() == 0 && ie.intent_to_add() {
4148 continue;
4151 }
4152 if ie.stage() == 0 && ie.is_sparse_directory_placeholder() {
4153 let dir = path.strip_suffix('/').unwrap_or(path.as_str());
4154 let index_flat = flatten_tree(odb, &ie.oid, dir)?;
4155 let head_flat = match tree_oid {
4156 None => Vec::new(),
4157 Some(head_root) => match subtree_oid_at_path(odb, head_root, dir)? {
4158 None => Vec::new(),
4159 Some(head_sub) if head_sub == ie.oid => {
4160 continue;
4161 }
4162 Some(head_sub) => flatten_tree(odb, &head_sub, dir)?,
4163 },
4164 };
4165 diff_flattened_subtrees(
4166 &head_flat,
4167 &index_flat,
4168 ignore_submodules,
4169 &mut result,
4170 &mut stage0_paths,
4171 &mut tree_map,
4172 );
4173 continue;
4174 }
4175 if ie.stage() != 0 {
4176 let rank = match ie.stage() {
4177 2 => 0u8,
4178 3 => 1u8,
4179 1 => 2u8,
4180 _ => 3u8,
4181 };
4182 match unmerged_modes.get(&path) {
4183 Some((existing_rank, _)) if *existing_rank <= rank => {}
4184 _ => {
4185 unmerged_modes.insert(path, (rank, ie.mode));
4186 }
4187 }
4188 continue;
4189 }
4190 if ignore_submodules && ie.mode == 0o160000 {
4191 let _ = tree_map.remove(path.as_str());
4192 stage0_paths.insert(path.clone());
4193 continue;
4194 }
4195 stage0_paths.insert(path.clone());
4196 match tree_map.remove(path.as_str()) {
4197 Some(te) => {
4198 if te.oid != ie.oid
4200 || crate::index::canon_mode(te.mode) != crate::index::canon_mode(ie.mode)
4201 {
4202 let tree_mode = crate::index::canon_mode(te.mode);
4203 let index_mode = crate::index::canon_mode(ie.mode);
4204 result.push(DiffEntry {
4205 status: DiffStatus::for_content_change(tree_mode, index_mode),
4206 old_path: Some(path.clone()),
4207 new_path: Some(path),
4208 old_mode: FileMode::new(tree_mode),
4209 new_mode: FileMode::new(index_mode),
4210 old_oid: te.oid,
4211 new_oid: ie.oid,
4212 score: None,
4213 });
4214 }
4215 }
4216 None => {
4217 result.push(DiffEntry {
4219 status: DiffStatus::Added,
4220 old_path: None,
4221 new_path: Some(path),
4222 old_mode: FileMode::ABSENT,
4223 new_mode: FileMode::new(ie.mode),
4224 old_oid: zero_oid(),
4225 new_oid: ie.oid,
4226 score: None,
4227 });
4228 }
4229 }
4230 }
4231
4232 diff_index_to_tree_finish_unmerged_and_deleted(
4233 ignore_submodules,
4234 &mut tree_map,
4235 &unmerged_modes,
4236 &stage0_paths,
4237 &mut result,
4238 );
4239
4240 sort_entries_by_path(&mut result);
4241 Ok(result)
4242}
4243
4244fn diff_index_to_tree_finish_unmerged_and_deleted(
4245 ignore_submodules: bool,
4246 tree_map: &mut std::collections::BTreeMap<&str, &FlatEntry>,
4247 unmerged_modes: &std::collections::BTreeMap<String, (u8, u32)>,
4248 stage0_paths: &std::collections::BTreeSet<String>,
4249 result: &mut Vec<DiffEntry>,
4250) {
4251 for (path, (_, mode)) in unmerged_modes {
4252 if stage0_paths.contains(path) {
4253 continue;
4254 }
4255 tree_map.remove(path.as_str());
4256 result.push(DiffEntry {
4257 status: DiffStatus::Unmerged,
4258 old_path: Some(path.clone()),
4259 new_path: Some(path.clone()),
4260 old_mode: FileMode::ABSENT,
4261 new_mode: FileMode::new(*mode),
4262 old_oid: zero_oid(),
4263 new_oid: zero_oid(),
4264 score: None,
4265 });
4266 }
4267
4268 for (path, te) in std::mem::take(tree_map) {
4270 if ignore_submodules && te.mode == 0o160000 {
4271 continue;
4272 }
4273 result.push(DiffEntry {
4274 status: DiffStatus::Deleted,
4275 old_path: Some(path.to_owned()),
4276 new_path: None,
4277 old_mode: FileMode::new(te.mode),
4278 new_mode: FileMode::ABSENT,
4279 old_oid: te.oid,
4280 new_oid: zero_oid(),
4281 score: None,
4282 });
4283 }
4284}
4285
4286fn diff_index_to_tree_with_cache_tree(
4289 odb: &Odb,
4290 index: &Index,
4291 head_tree: &ObjectId,
4292 cache_root: &CacheTreeNode,
4293 ignore_submodules: bool,
4294) -> Result<Vec<DiffEntry>> {
4295 let mut result = Vec::new();
4296 let mut idx_cursor = 0usize;
4297
4298 let mut walk = DiffIndexTreeWalk {
4299 odb,
4300 index,
4301 idx_cursor: &mut idx_cursor,
4302 ignore_submodules,
4303 result: &mut result,
4304 abort_to_flatten: false,
4305 };
4306 diff_index_tree_at(&mut walk, head_tree, Some(cache_root), b"")?;
4307 if walk.abort_to_flatten {
4308 return diff_index_to_tree_flatten(odb, index, Some(head_tree), ignore_submodules);
4309 }
4310
4311 while idx_cursor < index.entries.len() {
4313 let ie = &index.entries[idx_cursor];
4314 idx_cursor += 1;
4315 let path = String::from_utf8_lossy(&ie.path).to_string();
4316 if ie.stage() == 0 && ie.intent_to_add() {
4317 continue;
4318 }
4319 if ie.stage() != 0 {
4320 continue;
4321 }
4322 if ignore_submodules && ie.mode == 0o160000 {
4323 continue;
4324 }
4325 result.push(DiffEntry {
4326 status: DiffStatus::Added,
4327 old_path: None,
4328 new_path: Some(path),
4329 old_mode: FileMode::ABSENT,
4330 new_mode: FileMode::new(ie.mode),
4331 old_oid: zero_oid(),
4332 new_oid: ie.oid,
4333 score: None,
4334 });
4335 }
4336
4337 sort_entries_by_path(&mut result);
4338 Ok(result)
4339}
4340
4341enum IndexLevelKind<'a> {
4342 Blob {
4343 entry: &'a IndexEntry,
4344 name: &'a [u8],
4345 },
4346 Tree {
4347 name: &'a [u8],
4348 },
4349}
4350
4351fn index_relative_in_dir<'a>(path: &'a [u8], dir_prefix: &[u8]) -> Option<&'a [u8]> {
4354 if dir_prefix.is_empty() {
4355 return Some(path);
4356 }
4357 let rest = path.strip_prefix(dir_prefix)?;
4358 match rest.split_first() {
4359 None => Some(rest),
4360 Some((b'/', tail)) => Some(tail),
4361 Some(_) => None,
4362 }
4363}
4364
4365fn join_index_path(dir_prefix: &[u8], name: &[u8]) -> Vec<u8> {
4366 if dir_prefix.is_empty() {
4367 return name.to_vec();
4368 }
4369 let mut path = Vec::with_capacity(dir_prefix.len() + 1 + name.len());
4370 path.extend_from_slice(dir_prefix);
4371 path.push(b'/');
4372 path.extend_from_slice(name);
4373 path
4374}
4375
4376fn peek_index_level_entry<'a>(
4377 index: &'a Index,
4378 cursor: &mut usize,
4379 dir_prefix: &[u8],
4380 ignore_submodules: bool,
4381) -> Option<IndexLevelKind<'a>> {
4382 while let Some(ie) = index.entries.get(*cursor) {
4383 let rel = index_relative_in_dir(&ie.path, dir_prefix)?;
4384 if ie.stage() != 0 || ie.intent_to_add() || rel.is_empty() {
4385 *cursor += 1;
4386 continue;
4387 }
4388 if let Some(slash) = rel.iter().position(|&b| b == b'/') {
4389 return Some(IndexLevelKind::Tree {
4390 name: &rel[..slash],
4391 });
4392 }
4393 if ignore_submodules && ie.mode == 0o160000 {
4394 *cursor += 1;
4395 continue;
4396 }
4397 return Some(IndexLevelKind::Blob {
4398 entry: ie,
4399 name: rel,
4400 });
4401 }
4402 None
4403}
4404
4405fn skip_index_subtree(index: &Index, cursor: &mut usize, dir_prefix: &[u8]) {
4406 while index
4407 .entries
4408 .get(*cursor)
4409 .is_some_and(|ie| index_relative_in_dir(&ie.path, dir_prefix).is_some())
4410 {
4411 *cursor += 1;
4412 }
4413}
4414
4415const CACHE_CHILD_LINEAR_SCAN_MAX: usize = 16;
4417
4418struct CacheChildren<'a> {
4424 children: &'a [CacheTreeNode],
4425 sorted: Vec<&'a CacheTreeNode>,
4426}
4427
4428impl<'a> CacheChildren<'a> {
4429 fn new(node: Option<&'a CacheTreeNode>) -> Self {
4430 let children = node.map_or(&[][..], |n| n.children.as_slice());
4431 let sorted = if children.len() > CACHE_CHILD_LINEAR_SCAN_MAX {
4432 let mut sorted: Vec<&CacheTreeNode> = children.iter().collect();
4433 sorted.sort_by(|a, b| a.name.cmp(&b.name));
4434 sorted
4435 } else {
4436 Vec::new()
4437 };
4438 Self { children, sorted }
4439 }
4440
4441 fn get(&self, name: &[u8]) -> Option<&'a CacheTreeNode> {
4442 if self.sorted.is_empty() {
4443 return self.children.iter().find(|c| c.name == name);
4444 }
4445 let pos = self.sorted.partition_point(|c| c.name.as_slice() < name);
4446 self.sorted.get(pos).copied().filter(|c| c.name == name)
4447 }
4448}
4449
4450struct DiffIndexTreeWalk<'a, 'b> {
4451 odb: &'a Odb,
4452 index: &'a Index,
4453 idx_cursor: &'b mut usize,
4454 ignore_submodules: bool,
4455 result: &'b mut Vec<DiffEntry>,
4456 abort_to_flatten: bool,
4458}
4459
4460fn tree_entries_have_duplicate_names(entries: &[TreeEntry]) -> bool {
4461 let mut seen = std::collections::BTreeSet::<&[u8]>::new();
4462 for entry in entries {
4463 if !seen.insert(entry.name.as_slice()) {
4464 return true;
4465 }
4466 }
4467 false
4468}
4469
4470fn diff_index_tree_at(
4472 walk: &mut DiffIndexTreeWalk<'_, '_>,
4473 tree_oid: &ObjectId,
4474 cache_node: Option<&CacheTreeNode>,
4475 dir_prefix: &[u8],
4476) -> Result<()> {
4477 if !dir_prefix.is_empty() {
4478 let mut path = dir_prefix.to_vec();
4479 path.push(b'/');
4480 if let Some(entry) = walk
4481 .index
4482 .entries
4483 .get(*walk.idx_cursor)
4484 .filter(|entry| entry.path == path && entry.is_sparse_directory_placeholder())
4485 {
4486 if entry.oid == *tree_oid {
4487 *walk.idx_cursor += 1;
4488 return Ok(());
4489 }
4490 let dir = String::from_utf8_lossy(dir_prefix);
4491 let head_flat = flatten_tree(walk.odb, tree_oid, dir.as_ref())?;
4492 let index_flat = flatten_tree(walk.odb, &entry.oid, dir.as_ref())?;
4493 let mut stage0_paths = std::collections::BTreeSet::new();
4494 let mut tree_map = std::collections::BTreeMap::new();
4495 diff_flattened_subtrees(
4496 &head_flat,
4497 &index_flat,
4498 walk.ignore_submodules,
4499 walk.result,
4500 &mut stage0_paths,
4501 &mut tree_map,
4502 );
4503 *walk.idx_cursor += 1;
4504 return Ok(());
4505 }
4506 }
4507 let tree_entries = read_tree(walk.odb, tree_oid)?;
4508 if tree_entries_have_duplicate_names(&tree_entries) {
4509 walk.abort_to_flatten = true;
4510 return Ok(());
4511 }
4512 let cache_children = CacheChildren::new(cache_node);
4513 let mut ti = 0usize;
4514
4515 loop {
4516 let te = tree_entries.get(ti);
4517 let ie_kind = peek_index_level_entry(
4518 walk.index,
4519 walk.idx_cursor,
4520 dir_prefix,
4521 walk.ignore_submodules,
4522 );
4523
4524 match (te, ie_kind) {
4525 (Some(t_entry), Some(ie)) => {
4526 let t_name = &t_entry.name;
4527 let (i_name, i_is_tree) = match &ie {
4528 IndexLevelKind::Blob { name, .. } => (*name, false),
4529 IndexLevelKind::Tree { name } => (*name, true),
4530 };
4531 let t_is_tree = is_tree_mode(t_entry.mode);
4532 let cmp = crate::objects::tree_entry_cmp(t_name, t_is_tree, i_name, i_is_tree);
4533 match cmp {
4534 std::cmp::Ordering::Less => {
4535 emit_index_tree_deleted(
4536 walk.odb,
4537 t_entry,
4538 dir_prefix,
4539 walk.ignore_submodules,
4540 walk.result,
4541 )?;
4542 ti += 1;
4543 }
4544 std::cmp::Ordering::Greater => {
4545 emit_index_tree_added(walk, dir_prefix)?;
4546 }
4547 std::cmp::Ordering::Equal => {
4548 if t_is_tree && i_is_tree {
4549 let full_path = join_index_path(dir_prefix, t_name);
4550 if diff_index_tree_skip_sparse_placeholder(walk, &full_path, t_entry)? {
4551 ti += 1;
4552 continue;
4553 }
4554 let child_cache = cache_children.get(t_name);
4555 if child_cache
4556 .is_some_and(|c| c.is_valid() && c.oid == Some(t_entry.oid))
4557 {
4558 skip_index_subtree(walk.index, walk.idx_cursor, &full_path);
4559 ti += 1;
4560 continue;
4561 }
4562 diff_index_tree_at(walk, &t_entry.oid, child_cache, &full_path)?;
4563 } else if t_is_tree != i_is_tree {
4564 emit_index_tree_deleted(
4565 walk.odb,
4566 t_entry,
4567 dir_prefix,
4568 walk.ignore_submodules,
4569 walk.result,
4570 )?;
4571 emit_index_tree_added(walk, dir_prefix)?;
4572 } else {
4573 let IndexLevelKind::Blob { entry: ie, .. } = ie else {
4574 ti += 1;
4575 continue;
4576 };
4577 *walk.idx_cursor += 1;
4578 if t_entry.oid != ie.oid
4579 || crate::index::canon_mode(t_entry.mode)
4580 != crate::index::canon_mode(ie.mode)
4581 {
4582 let path = String::from_utf8_lossy(&ie.path).into_owned();
4583 let tree_mode = crate::index::canon_mode(t_entry.mode);
4584 let index_mode = crate::index::canon_mode(ie.mode);
4585 walk.result.push(DiffEntry {
4586 status: DiffStatus::for_content_change(tree_mode, index_mode),
4587 old_path: Some(path.clone()),
4588 new_path: Some(path),
4589 old_mode: FileMode::new(tree_mode),
4590 new_mode: FileMode::new(index_mode),
4591 old_oid: t_entry.oid,
4592 new_oid: ie.oid,
4593 score: None,
4594 });
4595 }
4596 }
4597 ti += 1;
4598 }
4599 }
4600 }
4601 (Some(t_entry), None) => {
4602 emit_index_tree_deleted(
4603 walk.odb,
4604 t_entry,
4605 dir_prefix,
4606 walk.ignore_submodules,
4607 walk.result,
4608 )?;
4609 ti += 1;
4610 }
4611 (None, Some(_)) => {
4612 emit_index_tree_added(walk, dir_prefix)?;
4613 }
4614 (None, None) => break,
4615 }
4616 }
4617
4618 Ok(())
4619}
4620
4621fn diff_index_tree_skip_sparse_placeholder(
4622 walk: &mut DiffIndexTreeWalk<'_, '_>,
4623 full_path: &[u8],
4624 t_entry: &TreeEntry,
4625) -> Result<bool> {
4626 let Some(entry) = walk.index.entries.get(*walk.idx_cursor) else {
4627 return Ok(false);
4628 };
4629 if !entry.is_sparse_directory_placeholder() {
4630 return Ok(false);
4631 }
4632 let mut dir = full_path.to_vec();
4633 if !dir.ends_with(b"/") {
4634 dir.push(b'/');
4635 }
4636 if entry.path != dir || entry.oid != t_entry.oid {
4637 return Ok(false);
4638 }
4639 *walk.idx_cursor += 1;
4640 Ok(true)
4641}
4642
4643fn emit_index_tree_deleted(
4644 odb: &Odb,
4645 entry: &TreeEntry,
4646 dir_prefix: &[u8],
4647 ignore_submodules: bool,
4648 result: &mut Vec<DiffEntry>,
4649) -> Result<()> {
4650 if ignore_submodules && entry.mode == 0o160000 {
4651 return Ok(());
4652 }
4653 let path = String::from_utf8_lossy(&join_index_path(dir_prefix, &entry.name)).into_owned();
4654 if is_tree_mode(entry.mode) {
4655 for fe in flatten_tree(odb, &entry.oid, &path)? {
4656 if ignore_submodules && fe.mode == 0o160000 {
4657 continue;
4658 }
4659 result.push(DiffEntry {
4660 status: DiffStatus::Deleted,
4661 old_path: Some(fe.path.clone()),
4662 new_path: None,
4663 old_mode: FileMode::new(fe.mode),
4664 new_mode: FileMode::ABSENT,
4665 old_oid: fe.oid,
4666 new_oid: zero_oid(),
4667 score: None,
4668 });
4669 }
4670 } else {
4671 result.push(DiffEntry {
4672 status: DiffStatus::Deleted,
4673 old_path: Some(path),
4674 new_path: None,
4675 old_mode: FileMode::new(entry.mode),
4676 new_mode: FileMode::ABSENT,
4677 old_oid: entry.oid,
4678 new_oid: zero_oid(),
4679 score: None,
4680 });
4681 }
4682 Ok(())
4683}
4684
4685fn emit_index_tree_added(walk: &mut DiffIndexTreeWalk<'_, '_>, dir_prefix: &[u8]) -> Result<()> {
4686 let ie_kind = peek_index_level_entry(
4687 walk.index,
4688 walk.idx_cursor,
4689 dir_prefix,
4690 walk.ignore_submodules,
4691 )
4692 .ok_or_else(|| Error::IndexError("missing index entry during tree walk".to_owned()))?;
4693 match ie_kind {
4694 IndexLevelKind::Blob { entry: ie, .. } => {
4695 let path = String::from_utf8_lossy(&ie.path).into_owned();
4696 *walk.idx_cursor += 1;
4697 walk.result.push(DiffEntry {
4698 status: DiffStatus::Added,
4699 old_path: None,
4700 new_path: Some(path),
4701 old_mode: FileMode::ABSENT,
4702 new_mode: FileMode::new(ie.mode),
4703 old_oid: zero_oid(),
4704 new_oid: ie.oid,
4705 score: None,
4706 });
4707 }
4708 IndexLevelKind::Tree { name } => {
4709 let full_path = join_index_path(dir_prefix, name);
4710 let mut path_slash = full_path.clone();
4711 path_slash.push(b'/');
4712 if let Some(entry) =
4713 walk.index.entries.get(*walk.idx_cursor).filter(|entry| {
4714 entry.path == path_slash && entry.is_sparse_directory_placeholder()
4715 })
4716 {
4717 let dir = String::from_utf8_lossy(if dir_prefix.is_empty() {
4718 name
4719 } else {
4720 &full_path
4721 });
4722 emit_flattened_subtree_as_added(
4723 walk.odb,
4724 &entry.oid,
4725 dir.as_ref(),
4726 walk.ignore_submodules,
4727 walk.result,
4728 )?;
4729 *walk.idx_cursor += 1;
4730 return Ok(());
4731 }
4732 let start = *walk.idx_cursor;
4733 skip_index_subtree(walk.index, walk.idx_cursor, &full_path);
4734 for ie in &walk.index.entries[start..*walk.idx_cursor] {
4735 if ie.stage() != 0 || ie.intent_to_add() {
4736 continue;
4737 }
4738 if walk.ignore_submodules && ie.mode == 0o160000 {
4739 continue;
4740 }
4741 let path = String::from_utf8_lossy(&ie.path).into_owned();
4742 walk.result.push(DiffEntry {
4743 status: DiffStatus::Added,
4744 old_path: None,
4745 new_path: Some(path),
4746 old_mode: FileMode::ABSENT,
4747 new_mode: FileMode::new(ie.mode),
4748 old_oid: zero_oid(),
4749 new_oid: ie.oid,
4750 score: None,
4751 });
4752 }
4753 }
4754 }
4755 Ok(())
4756}
4757
4758#[derive(Default)]
4760pub(crate) struct HardResetIndexPlan {
4761 pub remove_paths: Vec<Vec<u8>>,
4762 pub updates: Vec<(Vec<u8>, u32, ObjectId)>,
4763 pub insert_entries: Vec<IndexEntry>,
4764}
4765
4766pub(crate) fn plan_hard_reset_index_align(
4771 odb: &Odb,
4772 index: &Index,
4773 target_tree: &ObjectId,
4774 sparse: Option<&SparseCheckout>,
4775 index_sparse: bool,
4776) -> Result<HardResetIndexPlan> {
4777 let mut plan = HardResetIndexPlan::default();
4778 let mut idx_cursor = 0usize;
4779 let cache_root = index.cache_tree.as_ref();
4780 let use_cache = cache_root.is_some()
4781 && index
4782 .entries
4783 .iter()
4784 .all(|entry| entry.stage() == 0 && !entry.intent_to_add());
4785 let cache_node = if use_cache { cache_root } else { None };
4786 let retry = {
4787 let mut walk = HardResetAlignWalk {
4788 odb,
4789 index,
4790 idx_cursor: &mut idx_cursor,
4791 ignore_submodules: false,
4792 sparse,
4793 index_sparse,
4794 plan: &mut plan,
4795 retry_without_cache: false,
4796 };
4797 hard_reset_align_index_tree_at(&mut walk, target_tree, cache_node, b"")?;
4798 walk.retry_without_cache
4799 };
4800 if retry {
4801 idx_cursor = 0;
4802 plan = HardResetIndexPlan::default();
4803 let mut walk = HardResetAlignWalk {
4804 odb,
4805 index,
4806 idx_cursor: &mut idx_cursor,
4807 ignore_submodules: false,
4808 sparse,
4809 index_sparse,
4810 plan: &mut plan,
4811 retry_without_cache: false,
4812 };
4813 hard_reset_align_index_tree_at(&mut walk, target_tree, None, b"")?;
4814 }
4815 while idx_cursor < index.entries.len() {
4816 let ie = &index.entries[idx_cursor];
4817 idx_cursor += 1;
4818 if ie.stage() != 0 || ie.intent_to_add() {
4819 continue;
4820 }
4821 plan.remove_paths.push(ie.path.clone());
4822 }
4823 Ok(plan)
4824}
4825
4826pub(crate) fn apply_hard_reset_index_plan(index: &mut Index, plan: HardResetIndexPlan) {
4828 let change_count = plan
4829 .remove_paths
4830 .len()
4831 .saturating_add(plan.updates.len())
4832 .saturating_add(plan.insert_entries.len());
4833 const BULK_HARD_RESET_INDEX_MIN: usize = 64;
4834 let bulk = change_count >= BULK_HARD_RESET_INDEX_MIN;
4835 if bulk {
4836 index.drop_derived_index_extensions();
4837 }
4838 for (path, mode, oid) in plan.updates {
4839 if let Some(pos) = index.find_entry_pos(&path, 0) {
4840 let entry = &mut index.entries[pos];
4841 let content_unchanged = entry.oid == oid && entry.mode == mode;
4842 entry.mode = mode;
4843 entry.oid = oid;
4844 if !content_unchanged {
4845 entry.size = 0;
4846 }
4847 if !bulk {
4848 index.invalidate_cache_tree_for_path(&path);
4849 }
4850 }
4851 }
4852 index.defer_untracked_cache_invalidation = bulk;
4853 index.remove_prefixes_paths_and_insert(
4854 &std::collections::BTreeSet::new(),
4855 plan.remove_paths.iter().map(|p| p.as_slice()),
4856 plan.insert_entries,
4857 );
4858 index.defer_untracked_cache_invalidation = false;
4859 if !bulk {
4860 for path in &plan.remove_paths {
4861 index.invalidate_cache_tree_for_path(path);
4862 }
4863 }
4864 index.sort();
4865}
4866
4867pub(crate) fn rebuild_hard_reset_cache_tree(
4870 odb: &Odb,
4871 index: &mut Index,
4872 target_tree: &ObjectId,
4873) -> Result<()> {
4874 let stage0_count = index
4875 .entries
4876 .iter()
4877 .filter(|entry| entry.stage() == 0 && !entry.intent_to_add())
4878 .count();
4879 let seed_root = index.cache_tree.as_ref();
4880 let mut idx_cursor = 0usize;
4881 let root = rebuild_hard_reset_cache_tree_at(
4882 odb,
4883 index,
4884 &mut idx_cursor,
4885 target_tree,
4886 seed_root,
4887 b"",
4888 Vec::new(),
4889 )?;
4890 if usize::try_from(root.entry_count).ok() == Some(stage0_count) {
4891 index.set_cache_tree(root);
4892 Ok(())
4893 } else {
4894 Err(Error::IndexError(
4895 "hard reset cache-tree entry count does not match index".into(),
4896 ))
4897 }
4898}
4899
4900fn rebuild_hard_reset_cache_tree_at(
4901 odb: &Odb,
4902 index: &Index,
4903 idx_cursor: &mut usize,
4904 tree_oid: &ObjectId,
4905 seed_node: Option<&CacheTreeNode>,
4906 dir_prefix: &[u8],
4907 name: Vec<u8>,
4908) -> Result<CacheTreeNode> {
4909 if !dir_prefix.is_empty() {
4910 let mut path = dir_prefix.to_vec();
4911 path.push(b'/');
4912 if let Some(entry) = index.entries.get(*idx_cursor).filter(|entry| {
4913 entry.path == path && entry.is_sparse_directory_placeholder() && entry.oid == *tree_oid
4914 }) {
4915 let _ = entry;
4916 *idx_cursor += 1;
4917 return Ok(CacheTreeNode::valid(name, 1, *tree_oid, Vec::new()));
4918 }
4919 }
4920 let tree_entries = read_tree(odb, tree_oid)?;
4921 let cache_children = CacheChildren::new(seed_node);
4922 let mut entry_count: i32 = 0;
4923 let mut children: Vec<CacheTreeNode> = Vec::new();
4924 let mut ti = 0usize;
4925
4926 loop {
4927 let te = tree_entries.get(ti);
4928 let ie_kind = peek_index_level_entry(index, idx_cursor, dir_prefix, false);
4929 match (te, ie_kind) {
4930 (Some(t_entry), Some(ie)) => {
4931 let t_name = &t_entry.name;
4932 let (i_name, i_is_tree) = match &ie {
4933 IndexLevelKind::Blob { name, .. } => (*name, false),
4934 IndexLevelKind::Tree { name } => (*name, true),
4935 };
4936 let t_is_tree = is_tree_mode(t_entry.mode);
4937 let cmp = crate::objects::tree_entry_cmp(t_name, t_is_tree, i_name, i_is_tree);
4938 if cmp != std::cmp::Ordering::Equal {
4939 return Err(Error::IndexError(
4940 "hard reset cache-tree rebuild: index out of sync with target tree".into(),
4941 ));
4942 }
4943 if t_is_tree && i_is_tree {
4944 let full_path = join_index_path(dir_prefix, t_name);
4945 if hard_reset_skip_sparse_cache_rebuild(index, idx_cursor, &full_path, t_entry)?
4946 {
4947 entry_count = entry_count.saturating_add(1);
4948 ti += 1;
4949 continue;
4950 }
4951 let child = if let Some(child_cache) = cache_children
4952 .get(t_name)
4953 .filter(|node| node.is_valid() && node.oid == Some(t_entry.oid))
4954 {
4955 skip_index_subtree(index, idx_cursor, &full_path);
4956 let mut reused = (*child_cache).clone();
4957 reused.name = t_name.clone();
4958 reused
4959 } else {
4960 rebuild_hard_reset_cache_tree_at(
4961 odb,
4962 index,
4963 idx_cursor,
4964 &t_entry.oid,
4965 cache_children.get(t_name),
4966 &full_path,
4967 t_name.clone(),
4968 )?
4969 };
4970 entry_count = entry_count.saturating_add(child.entry_count.max(0));
4971 children.push(child);
4972 } else if t_is_tree != i_is_tree {
4973 return Err(Error::IndexError(
4974 "hard reset cache-tree rebuild: tree/index type mismatch".into(),
4975 ));
4976 } else {
4977 *idx_cursor += 1;
4978 entry_count = entry_count.saturating_add(1);
4979 }
4980 ti += 1;
4981 }
4982 (None, None) => break,
4983 _ => {
4984 return Err(Error::IndexError(
4985 "hard reset cache-tree rebuild: index out of sync with target tree".into(),
4986 ));
4987 }
4988 }
4989 }
4990 children.sort_by(|a, b| a.name.cmp(&b.name));
4991 Ok(CacheTreeNode::valid(name, entry_count, *tree_oid, children))
4992}
4993
4994fn hard_reset_skip_sparse_cache_rebuild(
4995 index: &Index,
4996 idx_cursor: &mut usize,
4997 full_path: &[u8],
4998 t_entry: &TreeEntry,
4999) -> Result<bool> {
5000 let Some(entry) = index.entries.get(*idx_cursor) else {
5001 return Ok(false);
5002 };
5003 if !entry.is_sparse_directory_placeholder() {
5004 return Ok(false);
5005 }
5006 let mut dir = full_path.to_vec();
5007 if !dir.ends_with(b"/") {
5008 dir.push(b'/');
5009 }
5010 if entry.path != dir || entry.oid != t_entry.oid {
5011 return Ok(false);
5012 }
5013 *idx_cursor += 1;
5014 Ok(true)
5015}
5016
5017struct HardResetAlignWalk<'a, 'b> {
5018 odb: &'a Odb,
5019 index: &'a Index,
5020 idx_cursor: &'b mut usize,
5021 ignore_submodules: bool,
5022 sparse: Option<&'a SparseCheckout>,
5023 index_sparse: bool,
5024 plan: &'b mut HardResetIndexPlan,
5025 retry_without_cache: bool,
5026}
5027
5028fn hard_reset_align_index_tree_at(
5029 walk: &mut HardResetAlignWalk<'_, '_>,
5030 tree_oid: &ObjectId,
5031 cache_node: Option<&CacheTreeNode>,
5032 dir_prefix: &[u8],
5033) -> Result<()> {
5034 let tree_entries = read_tree(walk.odb, tree_oid)?;
5035 if tree_entries_have_duplicate_names(&tree_entries) {
5036 walk.retry_without_cache = true;
5037 return Ok(());
5038 }
5039 let cache_children = CacheChildren::new(cache_node);
5040 let mut ti = 0usize;
5041
5042 loop {
5043 let te = tree_entries.get(ti);
5044 let ie_kind = peek_index_level_entry(
5045 walk.index,
5046 walk.idx_cursor,
5047 dir_prefix,
5048 walk.ignore_submodules,
5049 );
5050
5051 match (te, ie_kind) {
5052 (Some(t_entry), Some(ie)) => {
5053 let t_name = &t_entry.name;
5054 let (i_name, i_is_tree) = match &ie {
5055 IndexLevelKind::Blob { name, .. } => (*name, false),
5056 IndexLevelKind::Tree { name } => (*name, true),
5057 };
5058 let t_is_tree = is_tree_mode(t_entry.mode);
5059 let cmp = crate::objects::tree_entry_cmp(t_name, t_is_tree, i_name, i_is_tree);
5060 match cmp {
5061 std::cmp::Ordering::Less => {
5062 hard_reset_plan_tree_only(walk, t_entry, dir_prefix)?;
5063 ti += 1;
5064 }
5065 std::cmp::Ordering::Greater => {
5066 hard_reset_plan_index_only(walk, dir_prefix)?;
5067 }
5068 std::cmp::Ordering::Equal => {
5069 if t_is_tree && i_is_tree {
5070 let full_path = join_index_path(dir_prefix, t_name);
5071 if hard_reset_skip_sparse_placeholder(walk, &full_path, t_entry)? {
5072 ti += 1;
5073 continue;
5074 }
5075 let child_cache = cache_children.get(t_name);
5076 if child_cache
5077 .is_some_and(|c| c.is_valid() && c.oid == Some(t_entry.oid))
5078 {
5079 skip_index_subtree(walk.index, walk.idx_cursor, &full_path);
5080 ti += 1;
5081 continue;
5082 }
5083 hard_reset_align_index_tree_at(
5084 walk,
5085 &t_entry.oid,
5086 child_cache,
5087 &full_path,
5088 )?;
5089 } else if t_is_tree != i_is_tree {
5090 hard_reset_plan_tree_only(walk, t_entry, dir_prefix)?;
5091 hard_reset_plan_index_only(walk, dir_prefix)?;
5092 } else {
5093 let IndexLevelKind::Blob { entry: ie, .. } = ie else {
5094 ti += 1;
5095 continue;
5096 };
5097 *walk.idx_cursor += 1;
5098 if t_entry.oid != ie.oid
5099 || crate::index::canon_mode(t_entry.mode)
5100 != crate::index::canon_mode(ie.mode)
5101 {
5102 walk.plan.updates.push((
5103 ie.path.clone(),
5104 crate::index::canon_mode(t_entry.mode),
5105 t_entry.oid,
5106 ));
5107 }
5108 }
5109 ti += 1;
5110 }
5111 }
5112 }
5113 (Some(t_entry), None) => {
5114 hard_reset_plan_tree_only(walk, t_entry, dir_prefix)?;
5115 ti += 1;
5116 }
5117 (None, Some(_)) => {
5118 hard_reset_plan_index_only(walk, dir_prefix)?;
5119 }
5120 (None, None) => break,
5121 }
5122 }
5123 Ok(())
5124}
5125
5126fn hard_reset_skip_sparse_placeholder(
5127 walk: &mut HardResetAlignWalk<'_, '_>,
5128 full_path: &[u8],
5129 t_entry: &TreeEntry,
5130) -> Result<bool> {
5131 let Some(ie) = walk.index.entries.get(*walk.idx_cursor) else {
5132 return Ok(false);
5133 };
5134 if !ie.is_sparse_directory_placeholder() {
5135 return Ok(false);
5136 }
5137 let mut dir = full_path.to_vec();
5138 if !dir.ends_with(b"/") {
5139 dir.push(b'/');
5140 }
5141 if ie.path != dir || ie.oid != t_entry.oid {
5142 return Ok(false);
5143 }
5144 *walk.idx_cursor += 1;
5145 Ok(true)
5146}
5147
5148fn hard_reset_plan_index_only(
5149 walk: &mut HardResetAlignWalk<'_, '_>,
5150 dir_prefix: &[u8],
5151) -> Result<()> {
5152 let ie_kind = peek_index_level_entry(
5153 walk.index,
5154 walk.idx_cursor,
5155 dir_prefix,
5156 walk.ignore_submodules,
5157 )
5158 .ok_or_else(|| Error::IndexError("missing index entry during hard reset align".into()))?;
5159 match ie_kind {
5160 IndexLevelKind::Blob { entry: ie, .. } => {
5161 walk.plan.remove_paths.push(ie.path.clone());
5162 *walk.idx_cursor += 1;
5163 }
5164 IndexLevelKind::Tree { name } => {
5165 let full_path = join_index_path(dir_prefix, name);
5166 let start = *walk.idx_cursor;
5167 skip_index_subtree(walk.index, walk.idx_cursor, &full_path);
5168 for ie in &walk.index.entries[start..*walk.idx_cursor] {
5169 if ie.stage() != 0 || ie.intent_to_add() {
5170 continue;
5171 }
5172 walk.plan.remove_paths.push(ie.path.clone());
5173 }
5174 }
5175 }
5176 Ok(())
5177}
5178
5179fn hard_reset_plan_tree_only(
5180 walk: &mut HardResetAlignWalk<'_, '_>,
5181 entry: &TreeEntry,
5182 dir_prefix: &[u8],
5183) -> Result<()> {
5184 if walk.ignore_submodules && entry.mode == 0o160000 {
5185 return Ok(());
5186 }
5187 let path = join_index_path(dir_prefix, &entry.name);
5188 if is_tree_mode(entry.mode) {
5189 if hard_reset_sparse_excludes_tree(walk, &path) {
5190 walk.plan
5191 .insert_entries
5192 .push(hard_reset_sparse_directory_row(path, entry.oid));
5193 } else {
5194 hard_reset_append_entries_from_tree(
5195 walk.odb,
5196 &entry.oid,
5197 &path,
5198 &mut walk.plan.insert_entries,
5199 )?;
5200 }
5201 } else {
5202 walk.plan
5203 .insert_entries
5204 .push(hard_reset_index_tree_row(path, entry.mode, entry.oid));
5205 }
5206 Ok(())
5207}
5208
5209fn hard_reset_sparse_excludes_tree(walk: &HardResetAlignWalk<'_, '_>, path: &[u8]) -> bool {
5210 if !walk.index_sparse {
5211 return false;
5212 }
5213 let Some(sparse) = walk.sparse else {
5214 return false;
5215 };
5216 let mut dir = path.to_vec();
5217 if !dir.ends_with(b"/") {
5218 dir.push(b'/');
5219 }
5220 sparse.excludes_directory(&dir)
5221}
5222
5223fn hard_reset_sparse_directory_row(mut path: Vec<u8>, oid: ObjectId) -> IndexEntry {
5224 if !path.ends_with(b"/") {
5225 path.push(b'/');
5226 }
5227 let mut row = hard_reset_index_tree_row(path, MODE_TREE, oid);
5228 row.set_skip_worktree(true);
5229 row
5230}
5231
5232fn hard_reset_index_tree_row(path: Vec<u8>, mode: u32, oid: ObjectId) -> IndexEntry {
5233 IndexEntry {
5234 ctime_sec: 0,
5235 ctime_nsec: 0,
5236 mtime_sec: 0,
5237 mtime_nsec: 0,
5238 dev: 0,
5239 ino: 0,
5240 mode: crate::index::canon_mode(mode),
5241 uid: 0,
5242 gid: 0,
5243 size: 0,
5244 oid,
5245 flags: path.len().min(0xFFF) as u16,
5246 flags_extended: None,
5247 path,
5248 base_index_pos: 0,
5249 }
5250}
5251
5252fn hard_reset_append_entries_from_tree(
5253 odb: &Odb,
5254 tree_oid: &ObjectId,
5255 prefix: &[u8],
5256 out: &mut Vec<IndexEntry>,
5257) -> Result<()> {
5258 for te in read_tree(odb, tree_oid)? {
5259 let mut path = prefix.to_vec();
5260 if !path.is_empty() {
5261 path.push(b'/');
5262 }
5263 path.extend_from_slice(&te.name);
5264 if is_tree_mode(te.mode) {
5265 hard_reset_append_entries_from_tree(odb, &te.oid, &path, out)?;
5266 } else {
5267 out.push(hard_reset_index_tree_row(path, te.mode, te.oid));
5268 }
5269 }
5270 Ok(())
5271}
5272
5273pub fn diff_index_to_worktree(
5298 odb: &Odb,
5299 index: &mut Index,
5300 work_tree: &Path,
5301 ignore_submodule_untracked: bool,
5302 simplify_gitlinks: bool,
5303) -> Result<Vec<DiffEntry>> {
5304 diff_index_to_worktree_with_options(
5305 odb,
5306 index,
5307 work_tree,
5308 DiffIndexToWorktreeOptions {
5309 ignore_submodule_untracked,
5310 simplify_gitlinks,
5311 ..DiffIndexToWorktreeOptions::default()
5312 },
5313 )
5314 .map(|(entries, _)| entries)
5315}
5316
5317pub type IndexPathSkipPredicate = std::sync::Arc<dyn Fn(&[u8]) -> bool + Send + Sync>;
5319
5320#[derive(Clone)]
5322pub struct DiffIndexToWorktreeOptions {
5323 pub index_mtime: Option<(u32, u32)>,
5329 pub ignore_submodule_untracked: bool,
5331 pub simplify_gitlinks: bool,
5333 pub error_on_broken_gitlinks: bool,
5336 pub repository_git_dir: Option<std::path::PathBuf>,
5340 pub refresh_index_stat_in_pass: bool,
5343 pub config: Option<std::sync::Arc<crate::config::ConfigSet>>,
5345 pub stat_parallel_threads: Option<usize>,
5349 pub thread_policy: Option<crate::parallel::ThreadPolicy>,
5351 pub worktree_rules:
5353 Option<std::sync::Arc<std::sync::Mutex<crate::worktree_rules::WorktreeRules>>>,
5354 pub for_status: bool,
5361 pub size_change_proves_modified: bool,
5369 pub refresh_fsmonitor: bool,
5371 pub command_runner: Option<std::sync::Arc<dyn crate::command_runner::CommandRunner>>,
5373 pub fsmonitor_query_boundary: Option<crate::fsmonitor::FsmonitorQueryBoundary>,
5375 pub restrict_to_paths: Option<std::sync::Arc<std::collections::BTreeSet<String>>>,
5382 pub skip_index_path: Option<IndexPathSkipPredicate>,
5384 pub hash_blobs_without_write: bool,
5386 pub cancel_watch: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
5389 pub diagnostics: Option<crate::diagnostics::DiagnosticsHandle>,
5392 pub skip_inaccessible_paths: bool,
5396}
5397
5398impl Default for DiffIndexToWorktreeOptions {
5399 fn default() -> Self {
5400 Self {
5401 index_mtime: None,
5402 ignore_submodule_untracked: false,
5403 simplify_gitlinks: false,
5404 error_on_broken_gitlinks: false,
5405 repository_git_dir: None,
5406 refresh_index_stat_in_pass: false,
5407 config: None,
5408 stat_parallel_threads: None,
5409 thread_policy: None,
5410 worktree_rules: None,
5411 for_status: false,
5412 size_change_proves_modified: false,
5413 refresh_fsmonitor: true,
5414 command_runner: None,
5415 fsmonitor_query_boundary: None,
5416 restrict_to_paths: None,
5417 skip_index_path: None,
5418 hash_blobs_without_write: false,
5419 cancel_watch: None,
5420 diagnostics: None,
5421 skip_inaccessible_paths: false,
5422 }
5423 }
5424}
5425
5426#[derive(Clone, Default)]
5428pub struct DiffTreeToWorktreeExtras {
5429 pub skip_inaccessible_paths: bool,
5431 pub diagnostics: Option<crate::diagnostics::DiagnosticsHandle>,
5433 pub restrict_to_paths: Option<std::sync::Arc<std::collections::BTreeSet<String>>>,
5435}
5436
5437#[inline]
5438fn worktree_diff_skips_inaccessible_paths(
5439 for_status: bool,
5440 skip_inaccessible_paths: bool,
5441 materialize_dirty_blobs: bool,
5442) -> bool {
5443 (for_status || skip_inaccessible_paths) && !materialize_dirty_blobs
5444}
5445
5446#[inline]
5447fn poll_diff_cancel_watch(
5448 watch: Option<&std::sync::Arc<std::sync::atomic::AtomicBool>>,
5449) -> crate::error::Result<()> {
5450 if let Some(flag) = watch {
5451 crate::progress::poll_watch(flag)?;
5452 }
5453 Ok(())
5454}
5455
5456#[inline]
5457fn poll_diff_cancel(
5458 options: &DiffIndexToWorktreeOptions,
5459 owner_poll: Option<&std::sync::Arc<crate::progress::StackCancelPoll>>,
5460) -> crate::error::Result<()> {
5461 if let Some(poll) = owner_poll {
5462 poll.poll_owner()?;
5463 } else {
5464 poll_diff_cancel_watch(options.cancel_watch.as_ref())?;
5465 }
5466 Ok(())
5467}
5468
5469struct StagingHashJob {
5474 entry_index: usize,
5475 worktree_mode: u32,
5477 file_attrs: Option<Box<crate::crlf::FileAttrs>>,
5479}
5480
5481impl StagingHashJob {
5482 fn new(entry_index: usize, worktree_mode: u32, file_attrs: crate::crlf::FileAttrs) -> Self {
5483 let file_attrs =
5484 (file_attrs != crate::crlf::FileAttrs::default()).then(|| Box::new(file_attrs));
5485 Self {
5486 entry_index,
5487 worktree_mode,
5488 file_attrs,
5489 }
5490 }
5491}
5492
5493struct StagingHashSource<'a> {
5495 index: &'a Index,
5496 work_tree: &'a Path,
5497 precompose_unicode: bool,
5498 ignorecase: bool,
5499}
5500
5501#[allow(clippy::too_many_arguments)]
5502fn parallel_process_blob_worktree_jobs(
5503 odb: &Odb,
5504 source: &StagingHashSource<'_>,
5505 jobs: &[StagingHashJob],
5506 thread_policy: &crate::parallel::ThreadPolicy,
5507 config: &crate::config::ConfigSet,
5508 materialize_dirty_blobs: bool,
5509 hash_blobs_without_write: bool,
5510 for_status: bool,
5511 skip_inaccessible_paths: bool,
5512 diagnostics: Option<&crate::diagnostics::DiagnosticsHandle>,
5513 conv: &crate::crlf::ConversionConfig,
5514 filter_process: Option<&crate::filter_process::FilterProcessState>,
5515 _icase_collisions: Option<&crate::path_icase::IcaseCheckoutCollisionMap>,
5516 result: &mut Vec<DiffEntry>,
5517) -> Result<()> {
5518 use crate::parallel::{try_par_map, ParallelHashError, ParallelOp};
5519
5520 if jobs.is_empty() {
5521 return Ok(());
5522 }
5523 let mut par_opts = thread_policy.resolve(ParallelOp::IndexRefresh, config);
5524 par_opts.min_items = 0;
5525 par_opts.min_total_bytes = None;
5526 let est_bytes = jobs.len().saturating_mul(8192);
5527 let store_dirty_blobs = (materialize_dirty_blobs && !hash_blobs_without_write)
5528 .then(|| odb.bulk_stage_write_options());
5529 let stream_from = if store_dirty_blobs.is_some() {
5530 odb.big_file_threshold()?
5531 } else {
5532 hash_stream_threshold(odb)?
5533 };
5534 let default_attrs = crate::crlf::FileAttrs::default();
5535
5536 let new_oids = try_par_map::<StagingHashJob, Option<ObjectId>, Error, _>(
5539 jobs,
5540 par_opts,
5541 est_bytes,
5542 |job| {
5543 let entry = &source.index.entries[job.entry_index];
5544 let rel_path = crate::repo_relpath::storage_string(&entry.path);
5545 let modified = |new_oid| Ok(Some(new_oid));
5546 let file_path = index_entry_worktree_abs_bytes(
5547 source.work_tree,
5548 &entry.path,
5549 source.precompose_unicode,
5550 source.ignorecase,
5551 );
5552 let content = match read_regular_worktree_blob(
5553 odb,
5554 &file_path,
5555 conv,
5556 job.file_attrs.as_deref().unwrap_or(&default_attrs),
5557 &rel_path,
5558 Some(entry),
5559 filter_process,
5560 stream_from,
5561 ) {
5562 Ok(content) => content,
5563 Err(err)
5564 if status_skips_inaccessible_worktree_error(
5565 &err,
5566 for_status,
5567 skip_inaccessible_paths,
5568 materialize_dirty_blobs,
5569 ) =>
5570 {
5571 warn_and_skip_status_inaccessible(diagnostics, rel_path.as_str(), &err);
5572 return Ok(None);
5573 }
5574 Err(err) if unreadable_worktree_blob_is_modified(&err, materialize_dirty_blobs) => {
5575 return modified(zero_oid());
5576 }
5577 Err(err) => return Err(err),
5578 };
5579 let eff_oid = content.object_id(odb, store_dirty_blobs)?;
5580 if eff_oid != entry.oid || job.worktree_mode != entry.mode {
5581 let new_oid = if eff_oid != entry.oid && !materialize_dirty_blobs {
5582 zero_oid()
5583 } else {
5584 eff_oid
5585 };
5586 modified(new_oid)
5587 } else {
5588 Ok(None)
5589 }
5590 },
5591 )
5592 .map_err(|e| match e {
5593 ParallelHashError::Task(err) => err,
5594 })?;
5595
5596 let mut changed: Vec<(usize, u32, ObjectId)> = jobs
5597 .iter()
5598 .zip(new_oids)
5599 .filter_map(|(job, new_oid)| new_oid.map(|oid| (job.entry_index, job.worktree_mode, oid)))
5600 .collect();
5601 changed.sort_by_key(|(idx, _, _)| *idx);
5602 result.extend(
5603 changed
5604 .into_iter()
5605 .map(|(entry_index, worktree_mode, new_oid)| {
5606 let entry = &source.index.entries[entry_index];
5607 let rel_path = crate::repo_relpath::storage_string(&entry.path);
5608 worktree_content_change_entry(entry, &rel_path, worktree_mode, new_oid)
5609 }),
5610 );
5611 Ok(())
5612}
5613
5614fn worktree_content_change_entry(
5616 ie: &IndexEntry,
5617 path: &str,
5618 worktree_mode: u32,
5619 new_oid: ObjectId,
5620) -> DiffEntry {
5621 DiffEntry {
5622 status: DiffStatus::for_content_change(ie.mode, worktree_mode),
5623 old_path: Some(path.to_owned()),
5624 new_path: Some(path.to_owned()),
5625 old_mode: FileMode::new(ie.mode),
5626 new_mode: FileMode::new(worktree_mode),
5627 old_oid: ie.oid,
5628 new_oid,
5629 score: None,
5630 }
5631}
5632
5633fn unreadable_worktree_blob_is_modified(err: &Error, materialize_dirty_blobs: bool) -> bool {
5640 !materialize_dirty_blobs
5641 && matches!(
5642 err,
5643 Error::WorktreePathIo {
5644 operation: crate::worktree::WorktreePathOperation::Read,
5645 ..
5646 }
5647 )
5648}
5649
5650fn warn_worktree_access_failed(
5651 diagnostics: Option<&crate::diagnostics::DiagnosticsHandle>,
5652 path: &str,
5653 err: &std::io::Error,
5654) {
5655 if let Some(handle) = diagnostics {
5656 crate::diagnostics::warn(
5657 handle,
5658 crate::diagnostics::Warning::WorktreeAccessFailed {
5659 path: path.to_owned(),
5660 detail: err.to_string(),
5661 },
5662 );
5663 }
5664}
5665
5666fn blob_disk_lookup_io_result(
5667 diagnostics: Option<&crate::diagnostics::DiagnosticsHandle>,
5668 path: &str,
5669 err: std::io::Error,
5670 for_status: bool,
5671 skip_inaccessible_paths: bool,
5672 materialize_dirty_blobs: bool,
5673) -> Result<()> {
5674 if worktree_diff_skips_inaccessible_paths(
5675 for_status,
5676 skip_inaccessible_paths,
5677 materialize_dirty_blobs,
5678 ) && io_err_is_permission_denied(&err)
5679 {
5680 warn_worktree_access_failed(diagnostics, path, &err);
5681 return Ok(());
5682 }
5683 Err(Error::WorktreePathIo {
5684 operation: crate::worktree::WorktreePathOperation::Metadata,
5685 path: path.to_owned(),
5686 source: err,
5687 })
5688}
5689
5690fn io_err_is_permission_denied(err: &std::io::Error) -> bool {
5691 err.kind() == std::io::ErrorKind::PermissionDenied
5692}
5693
5694fn status_skips_inaccessible_worktree_error(
5695 err: &Error,
5696 for_status: bool,
5697 skip_inaccessible_paths: bool,
5698 materialize_dirty_blobs: bool,
5699) -> bool {
5700 if !worktree_diff_skips_inaccessible_paths(
5701 for_status,
5702 skip_inaccessible_paths,
5703 materialize_dirty_blobs,
5704 ) {
5705 return false;
5706 }
5707 match err {
5708 Error::Io(e) => io_err_is_permission_denied(e),
5709 Error::WorktreePathIo {
5710 operation: crate::worktree::WorktreePathOperation::Metadata,
5711 source,
5712 ..
5713 } => io_err_is_permission_denied(source),
5714 _ => false,
5715 }
5716}
5717
5718fn warn_and_skip_status_inaccessible(
5719 diagnostics: Option<&crate::diagnostics::DiagnosticsHandle>,
5720 path: &str,
5721 err: &Error,
5722) -> bool {
5723 let detail = match err {
5724 Error::Io(e) => e.to_string(),
5725 Error::WorktreePathIo { source, .. } => source.to_string(),
5726 _ => return false,
5727 };
5728 if let Some(handle) = diagnostics {
5729 crate::diagnostics::warn(
5730 handle,
5731 crate::diagnostics::Warning::WorktreeAccessFailed {
5732 path: path.to_owned(),
5733 detail,
5734 },
5735 );
5736 }
5737 true
5738}
5739
5740enum WorktreeSymlinkMetadata {
5741 Found(fs::Metadata),
5742 Absent,
5743 Inaccessible,
5744}
5745
5746fn probe_worktree_symlink_metadata(
5747 file_path: &Path,
5748 path_str: &str,
5749 diagnostics: Option<&crate::diagnostics::DiagnosticsHandle>,
5750 for_status: bool,
5751 skip_inaccessible_paths: bool,
5752 materialize_dirty_blobs: bool,
5753) -> Result<WorktreeSymlinkMetadata> {
5754 match fs::symlink_metadata(file_path) {
5755 Ok(meta) => Ok(WorktreeSymlinkMetadata::Found(meta)),
5756 Err(e)
5757 if e.kind() == std::io::ErrorKind::NotFound
5758 || e.raw_os_error() == Some(20) =>
5759 {
5760 Ok(WorktreeSymlinkMetadata::Absent)
5761 }
5762 Err(e) => {
5763 if worktree_diff_skips_inaccessible_paths(
5764 for_status,
5765 skip_inaccessible_paths,
5766 materialize_dirty_blobs,
5767 ) && io_err_is_permission_denied(&e)
5768 {
5769 warn_worktree_access_failed(diagnostics, path_str, &e);
5770 Ok(WorktreeSymlinkMetadata::Inaccessible)
5771 } else {
5772 Err(Error::WorktreePathIo {
5773 operation: crate::worktree::WorktreePathOperation::Metadata,
5774 path: path_str.to_owned(),
5775 source: e,
5776 })
5777 }
5778 }
5779 }
5780}
5781
5782fn symlink_metadata_worktree_diff(
5783 file_path: &Path,
5784 path_str: &str,
5785 diagnostics: Option<&crate::diagnostics::DiagnosticsHandle>,
5786 for_status: bool,
5787 skip_inaccessible_paths: bool,
5788 materialize_dirty_blobs: bool,
5789) -> Result<Option<fs::Metadata>> {
5790 Ok(
5791 match probe_worktree_symlink_metadata(
5792 file_path,
5793 path_str,
5794 diagnostics,
5795 for_status,
5796 skip_inaccessible_paths,
5797 materialize_dirty_blobs,
5798 )? {
5799 WorktreeSymlinkMetadata::Found(meta) => Some(meta),
5800 WorktreeSymlinkMetadata::Absent | WorktreeSymlinkMetadata::Inaccessible => None,
5801 },
5802 )
5803}
5804
5805fn status_size_change_proves_modified(
5814 ie: &IndexEntry,
5815 worktree_size: u32,
5816 for_status: bool,
5817 conversion_is_identity: impl FnOnce() -> bool,
5818) -> bool {
5819 for_status && !ie.stat_cache_unset() && worktree_size != ie.size && conversion_is_identity()
5820}
5821
5822#[allow(clippy::too_many_arguments)]
5825fn try_push_status_modified_on_size_mismatch(
5826 odb: &Odb,
5827 ie: &IndexEntry,
5828 path_str_ref: &str,
5829 worktree_size: u32,
5830 worktree_mode: u32,
5831 raw_worktree_len: u64,
5832 for_status: bool,
5833 conversion_is_identity: impl FnOnce() -> bool,
5834 result: &mut Vec<DiffEntry>,
5835) -> Result<bool> {
5836 let identity = conversion_is_identity();
5837 let size_differs = worktree_size != ie.size;
5838 if !size_differs || !identity {
5839 return Ok(false);
5840 }
5841 let threshold = odb.big_file_threshold()?;
5842 let big_file = raw_worktree_len > threshold;
5843 let proves_modified = if big_file {
5844 true
5845 } else {
5846 status_size_change_proves_modified(ie, worktree_size, for_status, || identity)
5847 };
5848 if !proves_modified {
5849 return Ok(false);
5850 }
5851 result.push(worktree_content_change_entry(
5852 ie,
5853 path_str_ref,
5854 worktree_mode,
5855 zero_oid(),
5856 ));
5857 Ok(true)
5858}
5859
5860#[allow(clippy::too_many_arguments)]
5861fn push_index_blob_worktree_diff(
5862 odb: &Odb,
5863 ie: &IndexEntry,
5864 path_str_ref: &str,
5865 file_path: &Path,
5866 meta: fs::Metadata,
5867 index_mtime: Option<(u32, u32)>,
5868 mode_policy: crate::index::WorktreeModePolicy,
5869 stat_policy: crate::index::StatMatchPolicy,
5870 file_attrs_for: &dyn Fn(&str) -> crate::crlf::FileAttrs,
5871 conv: &crate::crlf::ConversionConfig,
5872 materialize_dirty_blobs: bool,
5873 hash_blobs_without_write: bool,
5874 for_status: bool,
5875 size_change_proves_modified: bool,
5876 skip_inaccessible_paths: bool,
5877 diagnostics: Option<&crate::diagnostics::DiagnosticsHandle>,
5878 filter_process: Option<&crate::filter_process::FilterProcessState>,
5879 icase_collisions: Option<&crate::path_icase::IcaseCheckoutCollisionMap>,
5880 result: &mut Vec<DiffEntry>,
5881) -> Result<()> {
5882 if meta.is_dir() {
5883 if file_path.join(".git").exists() {
5884 let head = read_submodule_head_oid(file_path).unwrap_or_else(zero_oid);
5885 let path_owned = path_str_ref.to_owned();
5886 result.push(DiffEntry {
5887 status: DiffStatus::TypeChanged,
5888 old_path: Some(path_owned.clone()),
5889 new_path: Some(path_owned),
5890 old_mode: FileMode::new(ie.mode),
5891 new_mode: FileMode::new(0o160000),
5892 old_oid: ie.oid,
5893 new_oid: head,
5894 score: None,
5895 });
5896 return Ok(());
5897 }
5898 result.push(DiffEntry {
5899 status: DiffStatus::Deleted,
5900 old_path: Some(path_str_ref.to_owned()),
5901 new_path: None,
5902 old_mode: FileMode::new(ie.mode),
5903 new_mode: FileMode::ABSENT,
5904 old_oid: ie.oid,
5905 new_oid: zero_oid(),
5906 score: None,
5907 });
5908 return Ok(());
5909 }
5910
5911 let worktree_mode =
5912 mode_policy.index_mode_for_worktree(mode_from_metadata(&meta), Some(ie.mode));
5913 let stat_same = stat_matches_with_policy(ie, &meta, stat_policy);
5914 if stat_same && worktree_mode != ie.mode {
5915 result.push(worktree_content_change_entry(
5916 ie,
5917 path_str_ref,
5918 worktree_mode,
5919 ie.oid,
5920 ));
5921 return Ok(());
5922 }
5923
5924 if stat_same
5925 && index_entry_skips_worktree_content_hash_after_stat_match(ie, index_mtime, stat_policy)
5926 {
5927 return Ok(());
5928 }
5929
5930 let file_attrs = file_attrs_for(path_str_ref);
5931 if !icase_collisions.is_some_and(|map| map.diff_requires_worktree_hash(ie.path.as_slice()))
5932 && try_push_status_modified_on_size_mismatch(
5933 odb,
5934 ie,
5935 path_str_ref,
5936 crate::index::munge_index_size(meta.len()),
5937 worktree_mode,
5938 meta.len(),
5939 size_change_proves_modified,
5940 || crate::crlf::git_conversion_is_identity(conv, &file_attrs),
5941 result,
5942 )?
5943 {
5944 return Ok(());
5945 }
5946
5947 let store = (materialize_dirty_blobs && !hash_blobs_without_write)
5948 .then(|| odb.bulk_stage_write_options());
5949 let stream_from = if store.is_some() {
5950 odb.big_file_threshold()?
5951 } else {
5952 hash_stream_threshold(odb)?
5953 };
5954 let content = match worktree_blob_content_from(
5955 odb,
5956 file_path,
5957 &meta,
5958 conv,
5959 &file_attrs,
5960 path_str_ref,
5961 Some(ie),
5962 filter_process,
5963 stream_from,
5964 ) {
5965 Ok(content) => content,
5966 Err(err)
5967 if status_skips_inaccessible_worktree_error(
5968 &err,
5969 for_status,
5970 skip_inaccessible_paths,
5971 materialize_dirty_blobs,
5972 ) =>
5973 {
5974 warn_and_skip_status_inaccessible(diagnostics, path_str_ref, &err);
5975 return Ok(());
5976 }
5977 Err(err) if unreadable_worktree_blob_is_modified(&err, materialize_dirty_blobs) => {
5978 result.push(worktree_content_change_entry(
5979 ie,
5980 path_str_ref,
5981 worktree_mode,
5982 zero_oid(),
5983 ));
5984 return Ok(());
5985 }
5986 Err(err) => return Err(err),
5987 };
5988 let eff_oid = content.object_id(odb, store)?;
5989
5990 if eff_oid != ie.oid || worktree_mode != ie.mode {
5991 let new_oid = if eff_oid != ie.oid && !materialize_dirty_blobs {
5994 zero_oid()
5995 } else {
5996 eff_oid
5997 };
5998 result.push(worktree_content_change_entry(
5999 ie,
6000 path_str_ref,
6001 worktree_mode,
6002 new_oid,
6003 ));
6004 }
6005 Ok(())
6006}
6007
6008pub fn diff_index_to_worktree_with_options(
6018 odb: &Odb,
6019 index: &mut Index,
6020 work_tree: &Path,
6021 options: DiffIndexToWorktreeOptions,
6022) -> Result<(Vec<DiffEntry>, bool)> {
6023 let default_git_dir = work_tree.join(".git");
6024 let git_dir = options
6025 .repository_git_dir
6026 .clone()
6027 .unwrap_or(default_git_dir);
6028 diff_index_to_worktree_inner(odb, index, work_tree, &git_dir, options, false, None)
6029 .map(|(entries, changed, _)| (entries, changed))
6030}
6031
6032pub(crate) fn diff_index_to_worktree_with_owner_poll(
6035 odb: &Odb,
6036 index: &mut Index,
6037 work_tree: &Path,
6038 options: DiffIndexToWorktreeOptions,
6039 owner_poll: Option<std::sync::Arc<crate::progress::StackCancelPoll>>,
6040) -> Result<(Vec<DiffEntry>, bool)> {
6041 let default_git_dir = work_tree.join(".git");
6042 let git_dir = options
6043 .repository_git_dir
6044 .clone()
6045 .unwrap_or(default_git_dir);
6046 diff_index_to_worktree_inner(odb, index, work_tree, &git_dir, options, false, owner_poll)
6047 .map(|(entries, changed, _)| (entries, changed))
6048}
6049
6050pub fn diff_index_to_worktree_for_staging(
6056 odb: &Odb,
6057 repository_git_dir: &Path,
6058 index: &mut Index,
6059 work_tree: &Path,
6060 options: DiffIndexToWorktreeOptions,
6061) -> Result<(
6062 Vec<DiffEntry>,
6063 bool,
6064 std::collections::BTreeMap<String, crate::index::IndexStatFields>,
6065)> {
6066 diff_index_to_worktree_inner(
6067 odb,
6068 index,
6069 work_tree,
6070 repository_git_dir,
6071 options,
6072 true,
6073 None,
6074 )
6075}
6076
6077#[allow(clippy::too_many_arguments)]
6079fn worktree_diff_pass1_index_entry(
6080 odb: &Odb,
6081 work_tree: &Path,
6082 ie: &IndexEntry,
6083 path_str_ref: &str,
6084 simplify_gitlinks: bool,
6085 ignore_submodule_untracked: bool,
6086 error_on_broken_gitlinks: bool,
6087 materialize_dirty_blobs: bool,
6088 filter_fp: Option<&crate::filter_process::FilterProcessState>,
6089 conv: &crate::crlf::ConversionConfig,
6090 file_attrs_for: &dyn Fn(&str) -> crate::crlf::FileAttrs,
6091 precompose_unicode: bool,
6092 ignorecase: bool,
6093 result: &mut Vec<DiffEntry>,
6094) -> Result<()> {
6095 if ie.skip_worktree() || ie.assume_unchanged() {
6096 return Ok(());
6097 }
6098 let is_intent_to_add = ie.intent_to_add();
6099
6100 if ie.mode == 0o160000 {
6101 let sub_dir =
6102 index_entry_worktree_abs_bytes(work_tree, &ie.path, precompose_unicode, ignorecase);
6103 match fs::symlink_metadata(&sub_dir) {
6104 Err(e)
6105 if e.kind() == std::io::ErrorKind::NotFound
6106 || e.raw_os_error() == Some(20) =>
6107 {
6108 if !simplify_gitlinks {
6109 let path_owned = path_str_ref.to_owned();
6110 result.push(DiffEntry {
6111 status: DiffStatus::Deleted,
6112 old_path: Some(path_owned.clone()),
6113 new_path: Some(path_owned),
6114 old_mode: FileMode::new(ie.mode),
6115 new_mode: FileMode::ABSENT,
6116 old_oid: ie.oid,
6117 new_oid: zero_oid(),
6118 score: None,
6119 });
6120 }
6121 return Ok(());
6122 }
6123 Err(e) => return Err(Error::Io(e)),
6124 Ok(meta) if !meta.is_dir() => {
6125 let worktree_mode = mode_from_metadata(&meta);
6126 let file_attrs = file_attrs_for(path_str_ref);
6127 let worktree_oid = worktree_file_oid(
6128 odb,
6129 &sub_dir,
6130 &meta,
6131 conv,
6132 &file_attrs,
6133 path_str_ref,
6134 None,
6135 materialize_dirty_blobs,
6136 filter_fp,
6137 )?;
6138 let path_owned = path_str_ref.to_owned();
6139 result.push(DiffEntry {
6140 status: DiffStatus::TypeChanged,
6141 old_path: Some(path_owned.clone()),
6142 new_path: Some(path_owned),
6143 old_mode: FileMode::new(ie.mode),
6144 new_mode: FileMode::new(worktree_mode),
6145 old_oid: ie.oid,
6146 new_oid: worktree_oid,
6147 score: None,
6148 });
6149 return Ok(());
6150 }
6151 Ok(_) => {}
6152 }
6153 let sub_head_oid = read_submodule_head_oid(&sub_dir);
6154 let ref_matches = if let Some(oid) = sub_head_oid {
6155 oid == ie.oid
6156 } else {
6157 if error_on_broken_gitlinks && submodule_embedded_git_dir(&sub_dir).is_some() {
6158 return Err(Error::Config(
6159 format!("could not read submodule HEAD for '{path_str_ref}'").into(),
6160 ));
6161 }
6162 uninitialized_gitlink_worktree_matches_index(&sub_dir)
6163 };
6164 if simplify_gitlinks {
6165 if !ref_matches {
6166 let path_owned = path_str_ref.to_owned();
6167 let new_oid = sub_head_oid.unwrap_or_else(zero_oid);
6168 result.push(DiffEntry {
6169 status: DiffStatus::Modified,
6170 old_path: Some(path_owned.clone()),
6171 new_path: Some(path_owned),
6172 old_mode: FileMode::new(ie.mode),
6173 new_mode: FileMode::new(ie.mode),
6174 old_oid: ie.oid,
6175 new_oid,
6176 score: None,
6177 });
6178 }
6179 return Ok(());
6180 }
6181 if sub_head_oid.is_some() && submodule_head_object_broken(&sub_dir) {
6182 return Err(Error::Config(
6183 format!("'git status --porcelain=2' failed in submodule {path_str_ref}").into(),
6184 ));
6185 }
6186 let mut flags = submodule_porcelain_flags(work_tree, path_str_ref, ie.oid);
6187 if ignore_submodule_untracked {
6188 flags.untracked = false;
6189 }
6190 let inner_dirty = flags.modified || flags.untracked;
6191 if !ref_matches || inner_dirty {
6192 let path_owned = path_str_ref.to_owned();
6193 let new_oid = if !ref_matches {
6194 sub_head_oid.unwrap_or_else(zero_oid)
6195 } else {
6196 zero_oid()
6197 };
6198 result.push(DiffEntry {
6199 status: DiffStatus::Modified,
6200 old_path: Some(path_owned.clone()),
6201 new_path: Some(path_owned),
6202 old_mode: FileMode::new(ie.mode),
6203 new_mode: FileMode::new(ie.mode),
6204 old_oid: ie.oid,
6205 new_oid,
6206 score: None,
6207 });
6208 }
6209 return Ok(());
6210 }
6211
6212 let file_path =
6213 index_entry_worktree_abs_bytes(work_tree, &ie.path, precompose_unicode, ignorecase);
6214 let path_match = crate::repo_relpath::as_utf8_for_matching(path_str_ref);
6215
6216 if is_intent_to_add {
6217 match fs::symlink_metadata(&file_path) {
6218 Ok(meta) => {
6219 let file_attrs = file_attrs_for(path_match.as_ref());
6220 let worktree_oid = worktree_file_oid(
6221 odb,
6222 &file_path,
6223 &meta,
6224 conv,
6225 &file_attrs,
6226 path_match.as_ref(),
6227 None,
6228 materialize_dirty_blobs,
6229 filter_fp,
6230 )?;
6231 let worktree_mode = mode_from_metadata(&meta);
6232 result.push(DiffEntry {
6233 status: DiffStatus::Added,
6234 old_path: None,
6235 new_path: Some(path_str_ref.to_owned()),
6236 old_mode: FileMode::ABSENT,
6237 new_mode: FileMode::new(worktree_mode),
6238 old_oid: zero_oid(),
6239 new_oid: worktree_oid,
6240 score: None,
6241 });
6242 }
6243 Err(e)
6244 if e.kind() == std::io::ErrorKind::NotFound
6245 || e.raw_os_error() == Some(20) =>
6246 {
6247 result.push(DiffEntry {
6248 status: DiffStatus::Deleted,
6249 old_path: Some(path_str_ref.to_owned()),
6250 new_path: None,
6251 old_mode: FileMode::new(ie.mode),
6252 new_mode: FileMode::ABSENT,
6253 old_oid: ie.oid,
6254 new_oid: zero_oid(),
6255 score: None,
6256 });
6257 }
6258 Err(e) => return Err(Error::Io(e)),
6259 }
6260 }
6261 Ok(())
6262}
6263
6264#[inline]
6265fn index_entry_needs_worktree_diff_pass1(ie: &IndexEntry) -> bool {
6266 ie.mode == crate::index::MODE_GITLINK || ie.intent_to_add()
6267}
6268
6269fn diff_index_to_worktree_inner(
6270 odb: &Odb,
6271 index: &mut Index,
6272 work_tree: &Path,
6273 repository_git_dir: &Path,
6274 options: DiffIndexToWorktreeOptions,
6275 materialize_dirty_blobs: bool,
6276 owner_poll: Option<std::sync::Arc<crate::progress::StackCancelPoll>>,
6277) -> Result<(
6278 Vec<DiffEntry>,
6279 bool,
6280 std::collections::BTreeMap<String, crate::index::IndexStatFields>,
6281)> {
6282 use crate::config::ConfigSet;
6283 use crate::crlf;
6284 use std::collections::BTreeMap;
6285
6286 let mut staging_stat_cache = BTreeMap::new();
6287 poll_diff_cancel(&options, owner_poll.as_ref())?;
6288 let index_mtime = index_mtime_for_diff(index, options.index_mtime);
6289
6290 let ignore_submodule_untracked = options.ignore_submodule_untracked;
6291 let simplify_gitlinks = options.simplify_gitlinks;
6292 let size_change_proves_modified =
6294 !materialize_dirty_blobs && (options.for_status || options.size_change_proves_modified);
6295
6296 let rules_lock = options
6297 .worktree_rules
6298 .as_ref()
6299 .map(|shared| {
6300 shared
6301 .lock()
6302 .map_err(|e| Error::Message(format!("worktree rules lock poisoned: {e}")))
6303 })
6304 .transpose()?;
6305 let config = if let Some(rules) = rules_lock.as_ref() {
6306 rules.config().clone()
6307 } else if let Some(c) = options.config.as_ref() {
6308 c.as_ref().clone()
6309 } else {
6310 ConfigSet::load(
6311 &crate::environment::Environment::empty(),
6312 Some(repository_git_dir),
6313 true,
6314 )
6315 .unwrap_or_else(|_| ConfigSet::new())
6316 };
6317 let conv = rules_lock
6318 .as_ref()
6319 .map(|r| r.conversion().clone())
6320 .unwrap_or_else(|| crlf::ConversionConfig::from_config(&config));
6321 let legacy_attrs = if rules_lock.is_none() {
6322 Some(crlf::load_gitattributes_parsed(work_tree))
6323 } else {
6324 None
6325 };
6326 let file_attrs_for = |path: &str| -> crlf::FileAttrs {
6327 if let Some(rules) = rules_lock.as_ref() {
6328 rules.file_attrs(path, false)
6329 } else if let Some(parsed) = legacy_attrs.as_ref() {
6330 crlf::file_attrs_from_gitattributes(&parsed.rules, &parsed.macros, path, false, &config)
6331 } else {
6332 crlf::FileAttrs::default()
6333 }
6334 };
6335 let filter_fp = rules_lock.as_ref().map(|r| r.filter_process());
6336 let precompose_unicode = config
6337 .get_bool("core.precomposeunicode")
6338 .and_then(|r| r.ok())
6339 .unwrap_or(false);
6340 let mode_policy = crate::index::WorktreeModePolicy::from_config(&config);
6341 let stat_policy = crate::index::StatMatchPolicy::from_config(Some(&config));
6342 let ignorecase = config
6343 .get_bool("core.ignorecase")
6344 .and_then(|r| r.ok())
6345 .unwrap_or(false);
6346 let icase_collisions = ignorecase
6347 .then(|| crate::path_icase::IcaseCheckoutCollisionMap::from_index(work_tree, index, true));
6348 let hash_blobs_without_write = options.hash_blobs_without_write;
6349
6350 let mut result = Vec::new();
6351 let refresh_index_stat_in_pass = options.refresh_index_stat_in_pass;
6352 let mut index_stat_refresh_changed = false;
6353 let mut fsmonitor_valid_updated = false;
6354 let mut honor_fsmonitor_valid = false;
6355 if options.refresh_fsmonitor {
6356 let default_runner = crate::command_runner::system_command_runner();
6357 let runner = options
6358 .command_runner
6359 .as_deref()
6360 .unwrap_or(default_runner.as_ref());
6361 let boundary = options.fsmonitor_query_boundary.unwrap_or_else(|| {
6362 crate::fsmonitor::FsmonitorQueryBoundary(
6363 crate::environment::wall_clock_unix_timestamp_nanos(),
6364 )
6365 });
6366 let refresh_opts = crate::fsmonitor::FsmonitorRefreshOptions::with_query_boundary(boundary);
6367 let refresh = crate::fsmonitor::refresh_index(
6368 index,
6369 repository_git_dir,
6370 work_tree,
6371 &config,
6372 Some(runner),
6373 &refresh_opts,
6374 )?;
6375 if refresh.index_changed {
6376 index_stat_refresh_changed = true;
6377 }
6378 honor_fsmonitor_valid = refresh.honor_valid_bits;
6379 }
6380 let use_fsmonitor_stat_skip = honor_fsmonitor_valid;
6381 let mut unmerged_base: std::collections::BTreeMap<String, (u8, IndexEntry)> =
6382 std::collections::BTreeMap::new();
6383
6384 let error_on_broken_gitlinks = options.error_on_broken_gitlinks;
6385 if options.restrict_to_paths.is_none() {
6386 for ie in &index.entries {
6387 poll_diff_cancel(&options, owner_poll.as_ref())?;
6388 if ie.stage() != 0 {
6389 let path = String::from_utf8_lossy(&ie.path).to_string();
6390 let rank = match ie.stage() {
6391 2 => 0u8,
6392 3 => 1u8,
6393 1 => 2u8,
6394 _ => 3u8,
6395 };
6396 match unmerged_base.get(&path) {
6397 Some((existing_rank, _)) if *existing_rank <= rank => {}
6398 _ => {
6399 unmerged_base.insert(path, (rank, ie.clone()));
6400 }
6401 }
6402 }
6403 }
6404 }
6405
6406 if let Some(limit) = options.restrict_to_paths.as_ref() {
6407 let mut cursor = 0;
6408 for path in limit.iter() {
6409 let Some(pos) = index.find_stage0_pos_from(path.as_bytes(), &mut cursor) else {
6410 continue;
6411 };
6412 let ie = &index.entries[pos];
6413 if options
6414 .skip_index_path
6415 .as_ref()
6416 .is_some_and(|skip| skip(&ie.path))
6417 {
6418 continue;
6419 }
6420 if !index_entry_needs_worktree_diff_pass1(ie) {
6421 continue;
6422 }
6423 worktree_diff_pass1_index_entry(
6424 odb,
6425 work_tree,
6426 ie,
6427 path.as_str(),
6428 simplify_gitlinks,
6429 ignore_submodule_untracked,
6430 error_on_broken_gitlinks,
6431 materialize_dirty_blobs,
6432 filter_fp,
6433 &conv,
6434 &file_attrs_for,
6435 precompose_unicode,
6436 ignorecase,
6437 &mut result,
6438 )?;
6439 }
6440 } else {
6441 for ie in &index.entries {
6442 poll_diff_cancel(&options, owner_poll.as_ref())?;
6443 if ie.stage() != 0 {
6444 continue;
6445 }
6446 if options
6447 .skip_index_path
6448 .as_ref()
6449 .is_some_and(|skip| skip(&ie.path))
6450 {
6451 continue;
6452 }
6453 if !index_entry_needs_worktree_diff_pass1(ie) {
6454 continue;
6455 }
6456 let path_stored = crate::repo_relpath::storage_string(&ie.path);
6457 worktree_diff_pass1_index_entry(
6458 odb,
6459 work_tree,
6460 ie,
6461 &path_stored,
6462 simplify_gitlinks,
6463 ignore_submodule_untracked,
6464 error_on_broken_gitlinks,
6465 materialize_dirty_blobs,
6466 filter_fp,
6467 &conv,
6468 &file_attrs_for,
6469 precompose_unicode,
6470 ignorecase,
6471 &mut result,
6472 )?;
6473 }
6474 }
6475
6476 use crate::worktree_scan::{
6477 collect_blob_lookup_batches_with_literal_paths, group_blob_entries_by_dir,
6478 stat_scan_thread_count, visit_blob_lookup_batches, BlobDiskLookup, BlobScanSparseMode,
6479 WorktreeBlobScanOptions, PARALLEL_STAT_MIN_ENTRIES,
6480 };
6481 let preload_index = config
6482 .get_bool("core.preloadIndex")
6483 .and_then(|r| r.ok())
6484 .unwrap_or(true);
6485 let blob_scan = WorktreeBlobScanOptions {
6486 preload_index,
6487 stat_parallel_threads: options.stat_parallel_threads,
6488 ..WorktreeBlobScanOptions::default()
6489 };
6490 let blob_dirs = if let Some(limit) = options.restrict_to_paths.as_ref() {
6491 if let Some(skip) = options.skip_index_path.as_ref() {
6492 let mut positions = Vec::new();
6493 let mut cursor = 0;
6494 for path in limit.iter() {
6495 let Some(pos) = index.find_stage0_pos_from(path.as_bytes(), &mut cursor) else {
6496 continue;
6497 };
6498 let ie = &index.entries[pos];
6499 if skip(&ie.path) {
6500 continue;
6501 }
6502 if !crate::worktree_scan::index_blob_scan_eligible(ie, index, honor_fsmonitor_valid)
6503 {
6504 continue;
6505 }
6506 positions.push(pos);
6507 }
6508 crate::worktree_scan::plan_blob_entries_by_dir_for_positions(
6509 index,
6510 &positions,
6511 honor_fsmonitor_valid,
6512 )
6513 } else {
6514 crate::worktree_scan::group_blob_entries_by_dir_for_paths(index, limit)
6515 }
6516 } else if let Some(skip) = options.skip_index_path.as_ref() {
6517 let positions: Vec<usize> = index
6518 .entries
6519 .iter()
6520 .enumerate()
6521 .filter(|(_, ie)| {
6522 ie.stage() == 0
6523 && !skip(&ie.path)
6524 && crate::worktree_scan::index_blob_scan_eligible(
6525 ie,
6526 index,
6527 honor_fsmonitor_valid,
6528 )
6529 })
6530 .map(|(pos, _)| pos)
6531 .collect();
6532 crate::worktree_scan::plan_blob_entries_by_dir_for_positions(
6533 index,
6534 &positions,
6535 honor_fsmonitor_valid,
6536 )
6537 } else {
6538 group_blob_entries_by_dir(index, honor_fsmonitor_valid)
6539 };
6540 let work_tree_owned = work_tree.to_path_buf();
6541 let dir_abs = move |dir: &[u8]| {
6542 if dir.is_empty() {
6543 work_tree_owned.clone()
6544 } else {
6545 index_entry_worktree_abs_bytes(&work_tree_owned, dir, precompose_unicode, ignorecase)
6546 }
6547 };
6548 let work_tree_for_files = work_tree.to_path_buf();
6549 let file_abs = move |rel: &[u8]| {
6550 index_entry_worktree_abs_bytes(&work_tree_for_files, rel, precompose_unicode, ignorecase)
6551 };
6552 let blob_count = blob_dirs.len();
6553 let thread_policy = options.thread_policy.unwrap_or_default();
6554 let hash_threads = stat_scan_thread_count(&thread_policy, &config, blob_scan, blob_count);
6555 let parallel_blob_work = blob_count >= PARALLEL_STAT_MIN_ENTRIES && hash_threads > 1;
6556 let mut staging_hash_jobs: Vec<StagingHashJob> = Vec::new();
6557 let mut refresh_content_verified = vec![false; index.entries.len()];
6559 let sparse = if parallel_blob_work {
6564 BlobScanSparseMode::StatusDiff {
6565 stat_policy,
6566 index_mtime,
6567 mode_policy,
6568 for_status: options.for_status,
6569 refresh_index_stat_in_pass,
6570 parallel_blob_work,
6571 honor_fsmonitor_stat_skip: use_fsmonitor_stat_skip,
6572 }
6573 } else {
6574 BlobScanSparseMode::Dense
6575 };
6576 let blob_stat_batches = collect_blob_lookup_batches_with_literal_paths(
6577 index,
6578 &blob_dirs,
6579 blob_scan,
6580 &thread_policy,
6581 &config,
6582 dir_abs,
6583 file_abs,
6584 !precompose_unicode && !ignorecase,
6585 || {
6586 let mut dir_symlinks = SymlinkDirCache::default();
6587 move |rel_path: &[u8]| {
6588 dir_symlinks.has_symlink_in_path_bytes(
6589 work_tree,
6590 rel_path,
6591 precompose_unicode,
6592 ignorecase,
6593 )
6594 }
6595 },
6596 sparse,
6597 options.cancel_watch.clone(),
6598 owner_poll.clone(),
6599 )?;
6600 let identity_by_entry = std::cell::RefCell::new(std::collections::HashMap::new());
6603 let conversion_is_identity = |entry_index: usize, path: &str| -> bool {
6604 *identity_by_entry
6605 .borrow_mut()
6606 .entry(entry_index)
6607 .or_insert_with(|| crlf::git_conversion_is_identity(&conv, &file_attrs_for(path)))
6608 };
6609 let refresh_checks = if refresh_index_stat_in_pass {
6610 let par_opts =
6611 refresh_hash_parallel_options(&thread_policy, &config, options.stat_parallel_threads);
6612 refresh_content_checks(
6613 odb,
6614 index,
6615 &blob_stat_batches,
6616 index_mtime,
6617 stat_policy,
6618 &|entry_index, ie, stat| {
6619 !(use_fsmonitor_stat_skip && crate::fsmonitor::entry_skips_worktree_stat(ie, index))
6620 && worktree_diff_refreshes_entry(
6621 ie,
6622 stat,
6623 index_mtime,
6624 stat_policy,
6625 options.for_status,
6626 || {
6627 conversion_is_identity(
6628 entry_index,
6629 &crate::repo_relpath::storage_string(&ie.path),
6630 )
6631 },
6632 )
6633 },
6634 &WorktreeHashContext {
6635 work_tree,
6636 precompose_unicode,
6637 ignorecase,
6638 conv: &conv,
6639 file_attrs_for: &file_attrs_for,
6640 filter_process: filter_fp,
6641 },
6642 par_opts,
6643 options.cancel_watch.as_ref(),
6644 )?
6645 } else {
6646 RefreshChecks::default()
6647 };
6648 poll_diff_cancel(&options, owner_poll.as_ref())?;
6649 visit_blob_lookup_batches(blob_stat_batches, |entry_index, lookup| {
6650 poll_diff_cancel(&options, owner_poll.as_ref())?;
6651 let path_owned = crate::repo_relpath::storage_string(&index.entries[entry_index].path);
6652 let path_str_ref = path_owned.as_str();
6653 if use_fsmonitor_stat_skip {
6654 let ie_skip = &index.entries[entry_index];
6655 if crate::fsmonitor::entry_skips_worktree_stat(ie_skip, index) {
6656 return Ok(());
6657 }
6658 }
6659 match lookup {
6660 BlobDiskLookup::Missing => {
6661 if icase_collisions.as_ref().is_some_and(|map| {
6662 map.preserve_missing_collision_loser(&index.entries[entry_index].path)
6663 }) {
6664 return Ok(());
6665 }
6666 let ie = &index.entries[entry_index];
6667 result.push(DiffEntry {
6668 status: DiffStatus::Deleted,
6669 old_path: Some(path_str_ref.to_owned()),
6670 new_path: None,
6671 old_mode: FileMode::new(ie.mode),
6672 new_mode: FileMode::ABSENT,
6673 old_oid: ie.oid,
6674 new_oid: zero_oid(),
6675 score: None,
6676 });
6677 }
6678 BlobDiskLookup::Io(e) => {
6679 blob_disk_lookup_io_result(
6680 options.diagnostics.as_ref(),
6681 path_str_ref,
6682 e,
6683 options.for_status,
6684 options.skip_inaccessible_paths,
6685 materialize_dirty_blobs,
6686 )?;
6687 }
6688 BlobDiskLookup::Present(stat) => {
6689 if materialize_dirty_blobs {
6690 staging_stat_cache.insert(path_str_ref.to_owned(), stat.fields);
6691 }
6692 if refresh_index_stat_in_pass
6693 && worktree_diff_refreshes_entry(
6694 &index.entries[entry_index],
6695 &stat,
6696 index_mtime,
6697 stat_policy,
6698 options.for_status,
6699 || conversion_is_identity(entry_index, path_str_ref),
6700 )
6701 {
6702 let content_matches = RefreshContent::matches(
6703 refresh_checks.get(entry_index),
6704 &index.entries[entry_index].oid,
6705 );
6706 let refresh = refresh_index_blob_stat_from_scan(
6707 &mut index.entries[entry_index],
6708 &stat,
6709 index_mtime,
6710 stat_policy,
6711 content_matches,
6712 );
6713 if refresh.index_changed {
6714 index_stat_refresh_changed = true;
6715 }
6716 if refresh.content_verified {
6717 refresh_content_verified[entry_index] = true;
6718 let path = index.entries[entry_index].path.clone();
6719 let oid = index.entries[entry_index].oid;
6720 index.mark_content_verified(&path, oid);
6721 }
6722 }
6723 let ie = &index.entries[entry_index];
6724 let worktree_mode =
6725 mode_policy.index_mode_for_worktree(stat.git_mode, Some(ie.mode));
6726 if !materialize_dirty_blobs
6729 && !stat.is_dir
6730 && stat.git_mode != crate::index::MODE_SYMLINK
6731 && RefreshContent::differs(refresh_checks.get(entry_index), &ie.oid)
6732 {
6733 result.push(worktree_content_change_entry(
6734 ie,
6735 path_str_ref,
6736 worktree_mode,
6737 zero_oid(),
6738 ));
6739 return Ok(());
6740 }
6741 if parallel_blob_work && !stat.is_dir && stat.git_mode != crate::index::MODE_SYMLINK
6745 {
6746 let stat_same = stat_matches_blob_stat(ie, &stat, stat_policy);
6747 if stat_same
6748 && worktree_mode == ie.mode
6749 && index_entry_skips_worktree_content_hash_after_stat_match(
6750 ie,
6751 index_mtime,
6752 stat_policy,
6753 )
6754 {
6755 if crate::fsmonitor::mark_fsmonitor_valid_proven_clean(index, entry_index) {
6756 fsmonitor_valid_updated = true;
6757 }
6758 return Ok(());
6759 }
6760 if stat_same && worktree_mode != ie.mode {
6761 result.push(worktree_content_change_entry(
6762 ie,
6763 path_str_ref,
6764 worktree_mode,
6765 ie.oid,
6766 ));
6767 return Ok(());
6768 }
6769 if !icase_collisions.as_ref().is_some_and(|map| {
6770 map.diff_requires_worktree_hash(&index.entries[entry_index].path)
6771 }) && try_push_status_modified_on_size_mismatch(
6772 odb,
6773 ie,
6774 path_str_ref,
6775 stat.worktree_size(),
6776 worktree_mode,
6777 stat.raw_size,
6778 size_change_proves_modified,
6779 || conversion_is_identity(entry_index, path_str_ref),
6780 &mut result,
6781 )? {
6782 return Ok(());
6783 }
6784 if refresh_content_verified[entry_index] {
6785 if worktree_mode == ie.mode {
6786 if crate::fsmonitor::mark_fsmonitor_valid_proven_clean(
6787 index,
6788 entry_index,
6789 ) {
6790 fsmonitor_valid_updated = true;
6791 }
6792 } else {
6793 result.push(worktree_content_change_entry(
6794 ie,
6795 path_str_ref,
6796 worktree_mode,
6797 ie.oid,
6798 ));
6799 }
6800 } else {
6801 staging_hash_jobs.push(StagingHashJob::new(
6802 entry_index,
6803 worktree_mode,
6804 file_attrs_for(path_str_ref),
6805 ));
6806 }
6807 } else {
6808 let same_file_type = (worktree_mode == crate::index::MODE_SYMLINK)
6812 == (ie.mode == crate::index::MODE_SYMLINK);
6813 if !stat.is_dir
6814 && worktree_mode == ie.mode
6815 && ((refresh_content_verified[entry_index] && same_file_type)
6816 || (stat_matches_blob_stat(ie, &stat, stat_policy)
6817 && index_entry_skips_worktree_content_hash_after_stat_match(
6818 ie,
6819 index_mtime,
6820 stat_policy,
6821 )))
6822 {
6823 if crate::fsmonitor::mark_fsmonitor_valid_proven_clean(index, entry_index) {
6824 fsmonitor_valid_updated = true;
6825 }
6826 return Ok(());
6827 }
6828 let file_path = index_entry_worktree_abs_bytes(
6829 work_tree,
6830 &index.entries[entry_index].path,
6831 precompose_unicode,
6832 ignorecase,
6833 );
6834 let Some(meta) = symlink_metadata_worktree_diff(
6835 &file_path,
6836 path_str_ref,
6837 options.diagnostics.as_ref(),
6838 options.for_status,
6839 options.skip_inaccessible_paths,
6840 materialize_dirty_blobs,
6841 )?
6842 else {
6843 return Ok(());
6844 };
6845 push_index_blob_worktree_diff(
6846 odb,
6847 ie,
6848 path_str_ref,
6849 &file_path,
6850 meta,
6851 index_mtime,
6852 mode_policy,
6853 stat_policy,
6854 &file_attrs_for,
6855 &conv,
6856 materialize_dirty_blobs,
6857 hash_blobs_without_write,
6858 options.for_status,
6859 size_change_proves_modified,
6860 options.skip_inaccessible_paths,
6861 options.diagnostics.as_ref(),
6862 filter_fp,
6863 icase_collisions.as_ref(),
6864 &mut result,
6865 )?;
6866 }
6867 }
6868 }
6869 Ok(())
6870 })?;
6871 if parallel_blob_work && !staging_hash_jobs.is_empty() {
6872 parallel_process_blob_worktree_jobs(
6873 odb,
6874 &StagingHashSource {
6875 index,
6876 work_tree,
6877 precompose_unicode,
6878 ignorecase,
6879 },
6880 &staging_hash_jobs,
6881 &thread_policy,
6882 &config,
6883 materialize_dirty_blobs,
6884 hash_blobs_without_write,
6885 options.for_status,
6886 options.skip_inaccessible_paths,
6887 options.diagnostics.as_ref(),
6888 &conv,
6889 filter_fp,
6890 icase_collisions.as_ref(),
6891 &mut result,
6892 )?;
6893 }
6894
6895 for (path, (_, base_entry)) in unmerged_base {
6896 let file_path = index_entry_worktree_abs(work_tree, &path, precompose_unicode, ignorecase);
6897 let wt_meta = symlink_metadata_worktree_diff(
6898 &file_path,
6899 &path,
6900 options.diagnostics.as_ref(),
6901 options.for_status,
6902 options.skip_inaccessible_paths,
6903 materialize_dirty_blobs,
6904 )?;
6905
6906 let new_mode = wt_meta.as_ref().map_or_else(
6907 || FileMode::ABSENT,
6908 |meta| FileMode::new(mode_from_metadata(meta)),
6909 );
6910 result.push(DiffEntry {
6911 status: DiffStatus::Unmerged,
6912 old_path: Some(path.clone()),
6913 new_path: Some(path.clone()),
6914 old_mode: FileMode::ABSENT,
6915 new_mode,
6916 old_oid: zero_oid(),
6917 new_oid: zero_oid(),
6918 score: None,
6919 });
6920
6921 if !options.for_status {
6922 if let Some(meta) = wt_meta {
6923 let file_attrs = file_attrs_for(&path);
6924 let wt_oid = worktree_file_oid(
6925 odb,
6926 &file_path,
6927 &meta,
6928 &conv,
6929 &file_attrs,
6930 &path,
6931 Some(&base_entry),
6932 materialize_dirty_blobs,
6933 filter_fp,
6934 )?;
6935 let wt_mode = mode_from_metadata(&meta);
6936 if wt_oid != base_entry.oid || wt_mode != base_entry.mode {
6937 result.push(DiffEntry {
6938 status: DiffStatus::for_content_change(base_entry.mode, wt_mode),
6939 old_path: Some(path.clone()),
6940 new_path: Some(path),
6941 old_mode: FileMode::new(base_entry.mode),
6942 new_mode: FileMode::new(wt_mode),
6943 old_oid: base_entry.oid,
6944 new_oid: wt_oid,
6945 score: None,
6946 });
6947 }
6948 }
6949 }
6950 }
6951
6952 if fsmonitor_valid_updated {
6953 index.fsmonitor_validity_changed();
6954 index_stat_refresh_changed = true;
6955 }
6956
6957 Ok((result, index_stat_refresh_changed, staging_stat_cache))
6958}
6959
6960#[derive(Default)]
6967pub(crate) struct SymlinkDirCache {
6968 real_dirs: Vec<u8>,
6971 blocked: Option<(Vec<u8>, bool)>,
6974 #[cfg(all(target_os = "linux", target_pointer_width = "64"))]
6976 chain: Option<crate::worktree_scan::RealDirChain>,
6977 #[cfg(test)]
6979 fs_queries: usize,
6980}
6981
6982fn path_within_dir(path: &[u8], dir: &[u8]) -> bool {
6984 path.starts_with(dir) && path.get(dir.len()).is_none_or(|&b| b == b'/')
6985}
6986
6987fn common_dir_prefix_len(a: &[u8], b: &[u8]) -> usize {
6989 let same = a.iter().zip(b).take_while(|(x, y)| x == y).count();
6990 let ends_component = |p: &[u8]| p.get(same).is_none_or(|&c| c == b'/');
6991 if ends_component(a) && ends_component(b) {
6992 return same;
6993 }
6994 a[..same].iter().rposition(|&c| c == b'/').unwrap_or(0)
6995}
6996
6997pub(crate) fn index_entry_worktree_abs(
6998 work_tree: &Path,
6999 index_relpath: &str,
7000 precompose_unicode: bool,
7001 ignorecase: bool,
7002) -> PathBuf {
7003 index_entry_worktree_abs_bytes(
7004 work_tree,
7005 index_relpath.as_bytes(),
7006 precompose_unicode,
7007 ignorecase,
7008 )
7009}
7010
7011pub(crate) fn index_entry_worktree_abs_bytes(
7012 work_tree: &Path,
7013 index_relpath: &[u8],
7014 precompose_unicode: bool,
7015 ignorecase: bool,
7016) -> PathBuf {
7017 if precompose_unicode || ignorecase {
7018 if let Ok(s) = std::str::from_utf8(index_relpath) {
7019 if precompose_unicode {
7020 return crate::unicode_normalization::resolve_worktree_path_for_staging(
7021 work_tree, s, true,
7022 )
7023 .abs;
7024 }
7025 return crate::path_icase::worktree_path_for_index_entry(work_tree, s, ignorecase);
7026 }
7027 }
7028 crate::index::worktree_path_from_index_rel(work_tree, index_relpath)
7029}
7030
7031#[must_use]
7036pub fn index_mtime_for_diff(index: &Index, explicit: Option<(u32, u32)>) -> Option<(u32, u32)> {
7037 explicit.or(index.source_mtime)
7038}
7039
7040impl SymlinkDirCache {
7041 pub(crate) fn has_symlink_in_path_bytes(
7046 &mut self,
7047 work_tree: &Path,
7048 rel_path: &[u8],
7049 precompose_unicode: bool,
7050 ignorecase: bool,
7051 ) -> bool {
7052 use crate::worktree_scan::{ancestor_kind, AncestorKind};
7053
7054 let Some(dir_len) = rel_path.iter().rposition(|&b| b == b'/') else {
7055 return false;
7056 };
7057 let dir = &rel_path[..dir_len];
7058 if let Some((blocked, is_symlink)) = &self.blocked {
7059 if path_within_dir(dir, blocked) {
7060 return *is_symlink;
7061 }
7062 }
7063 let verified = common_dir_prefix_len(dir, &self.real_dirs);
7064 self.real_dirs.truncate(verified);
7065 #[cfg(all(target_os = "linux", target_pointer_width = "64"))]
7066 if !precompose_unicode
7067 && !ignorecase
7068 && dir[verified..]
7069 .split(|&b| b == b'/')
7070 .filter(|component| !component.is_empty())
7071 .nth(1)
7072 .is_some()
7073 {
7074 let chain = match &mut self.chain {
7075 Some(chain) if chain.work_tree() == work_tree => chain,
7076 slot => slot.insert(crate::worktree_scan::RealDirChain::open(work_tree)),
7077 };
7078 #[cfg(test)]
7079 {
7080 self.fs_queries += 1;
7081 }
7082 if chain.confirms(dir) {
7083 self.real_dirs.clear();
7084 self.real_dirs.extend_from_slice(dir);
7085 return false;
7086 }
7087 }
7088 let mut start = verified;
7089 while start < dir.len() {
7090 if dir[start] == b'/' {
7091 start += 1;
7092 continue;
7093 }
7094 let end = dir[start..]
7095 .iter()
7096 .position(|&b| b == b'/')
7097 .map_or(dir.len(), |i| start + i);
7098 let prefix = &dir[..end];
7099 let abs =
7100 index_entry_worktree_abs_bytes(work_tree, prefix, precompose_unicode, ignorecase);
7101 #[cfg(test)]
7102 {
7103 self.fs_queries += 1;
7104 }
7105 match ancestor_kind(&abs) {
7106 AncestorKind::Directory => {
7107 let known = self.real_dirs.len();
7108 self.real_dirs.extend_from_slice(&prefix[known..]);
7109 }
7110 AncestorKind::Symlink => {
7111 self.blocked = Some((prefix.to_vec(), true));
7112 return true;
7113 }
7114 AncestorKind::Other => {
7115 self.blocked = Some((prefix.to_vec(), false));
7116 return false;
7117 }
7118 }
7119 start = end;
7120 }
7121 false
7122 }
7123}
7124
7125#[must_use]
7131pub fn entry_is_racy(ie: &IndexEntry, index_mtime: Option<(u32, u32)>) -> bool {
7132 entry_is_racy_with_policy(ie, index_mtime, crate::index::StatMatchPolicy::default())
7133}
7134
7135#[must_use]
7141pub fn entry_is_racy_with_policy(
7142 ie: &IndexEntry,
7143 index_mtime: Option<(u32, u32)>,
7144 policy: crate::index::StatMatchPolicy,
7145) -> bool {
7146 let Some((index_mtime_sec, index_mtime_nsec)) = index_mtime else {
7147 return false;
7148 };
7149 if index_mtime_sec == 0 {
7150 return false;
7151 }
7152 if policy.use_nanosec && policy.check_stat {
7153 index_mtime_sec < ie.mtime_sec
7154 || (index_mtime_sec == ie.mtime_sec && index_mtime_nsec <= ie.mtime_nsec)
7155 } else {
7156 index_mtime_sec <= ie.mtime_sec
7157 }
7158}
7159
7160pub fn smudge_racily_clean_entries(
7177 odb: &Odb,
7178 git_dir: &Path,
7179 index: &mut Index,
7180 work_tree: &Path,
7181 index_mtime: Option<(u32, u32)>,
7182 config: Option<&ConfigSet>,
7183 filter_process: Option<&crate::filter_process::FilterProcessState>,
7184) -> bool {
7185 use crate::crlf;
7186 use crate::index::{MODE_EXECUTABLE, MODE_REGULAR, MODE_SYMLINK};
7187
7188 let Some(index_mtime) = index_mtime else {
7189 return false;
7190 };
7191
7192 let config = config.cloned().unwrap_or_else(|| {
7193 ConfigSet::load(
7194 &crate::environment::Environment::empty(),
7195 Some(git_dir),
7196 true,
7197 )
7198 .unwrap_or_default()
7199 });
7200 let conv = crlf::ConversionConfig::from_config(&config);
7201 let attrs = crlf::load_gitattributes(work_tree);
7202 let verified = std::mem::take(&mut index.content_verified);
7203
7204 let mut changed = false;
7205 for ie in &mut index.entries {
7206 let marked_verified = ie.take_content_verified();
7207 if ie.stage() != 0 || ie.skip_worktree() || ie.assume_unchanged() || ie.intent_to_add() {
7208 continue;
7209 }
7210 if ie.mode != MODE_REGULAR && ie.mode != MODE_EXECUTABLE && ie.mode != MODE_SYMLINK {
7211 continue;
7212 }
7213 if ie.size == 0 {
7214 continue;
7215 }
7216 if !entry_is_racy(ie, Some(index_mtime)) {
7217 continue;
7218 }
7219 if marked_verified || verified.get(&ie.path) == Some(&ie.oid) {
7220 continue;
7221 }
7222 let Ok(path) = std::str::from_utf8(&ie.path) else {
7223 continue;
7224 };
7225 let abs = work_tree.join(path);
7226 let Ok(meta) = fs::symlink_metadata(&abs) else {
7227 continue;
7228 };
7229 if !stat_matches(ie, &meta) {
7230 continue;
7231 }
7232 let file_attrs = crlf::get_file_attrs(&attrs, path, false, &config);
7233 let content_matches = hash_worktree_file(
7234 odb,
7235 &abs,
7236 &meta,
7237 &conv,
7238 &file_attrs,
7239 path,
7240 Some(ie),
7241 filter_process,
7242 )
7243 .map(|oid| oid == ie.oid)
7244 .unwrap_or(false);
7245 if content_matches {
7246 continue;
7247 }
7248 ie.size = 0;
7249 changed = true;
7250 }
7251 changed
7252}
7253pub fn worktree_differs_from_index_entry(
7261 odb: &Odb,
7262 repository_git_dir: &Path,
7263 work_tree: &Path,
7264 ie: &IndexEntry,
7265 ignore_submodule_untracked: bool,
7266 config: Option<&ConfigSet>,
7267 filter_process: Option<&crate::filter_process::FilterProcessState>,
7268) -> Result<bool> {
7269 WorktreeEntryComparer::new(repository_git_dir, work_tree, config).differs(
7270 odb,
7271 ie,
7272 ignore_submodule_untracked,
7273 filter_process,
7274 )
7275}
7276
7277pub(crate) struct WorktreeEntryComparer<'a> {
7281 work_tree: &'a Path,
7282 config: ConfigSet,
7283 mode_policy: crate::index::WorktreeModePolicy,
7284 precompose_unicode: bool,
7285 ignorecase: bool,
7286 conv: std::cell::OnceCell<crate::crlf::ConversionConfig>,
7287 attrs: std::cell::OnceCell<Vec<crate::crlf::AttrRule>>,
7288}
7289
7290impl<'a> WorktreeEntryComparer<'a> {
7291 pub(crate) fn new(
7293 repository_git_dir: &Path,
7294 work_tree: &'a Path,
7295 config: Option<&ConfigSet>,
7296 ) -> Self {
7297 let config = config.cloned().unwrap_or_else(|| {
7298 ConfigSet::load(
7299 &crate::environment::Environment::empty(),
7300 Some(repository_git_dir),
7301 true,
7302 )
7303 .unwrap_or_else(|_| ConfigSet::new())
7304 });
7305 let flag =
7306 |key: &str, default: bool| config.get_bool(key).and_then(|r| r.ok()).unwrap_or(default);
7307 Self {
7308 work_tree,
7309 mode_policy: crate::index::WorktreeModePolicy::from_config(&config),
7310 precompose_unicode: flag("core.precomposeunicode", false),
7311 ignorecase: flag("core.ignorecase", false),
7312 config,
7313 conv: std::cell::OnceCell::new(),
7314 attrs: std::cell::OnceCell::new(),
7315 }
7316 }
7317
7318 pub(crate) fn differs(
7320 &self,
7321 odb: &Odb,
7322 ie: &IndexEntry,
7323 ignore_submodule_untracked: bool,
7324 filter_process: Option<&crate::filter_process::FilterProcessState>,
7325 ) -> Result<bool> {
7326 use crate::crlf;
7327
7328 let work_tree = self.work_tree;
7329 let path_stored = crate::repo_relpath::storage_string(&ie.path);
7330 let path_str_ref = crate::repo_relpath::as_utf8_for_matching(&path_stored);
7331 let file_path = index_entry_worktree_abs_bytes(
7332 work_tree,
7333 &ie.path,
7334 self.precompose_unicode,
7335 self.ignorecase,
7336 );
7337
7338 if ie.mode == 0o160000 {
7339 let sub_head_oid = read_submodule_head(&file_path);
7340 let ref_matches = match sub_head_oid {
7341 Some(oid) => oid == ie.oid,
7342 None => uninitialized_gitlink_worktree_matches_index(&file_path),
7343 };
7344 let mut flags = submodule_porcelain_flags(work_tree, path_str_ref.as_ref(), ie.oid);
7345 if ignore_submodule_untracked {
7346 flags.untracked = false;
7347 }
7348 return Ok(!ref_matches || flags.modified || flags.untracked);
7349 }
7350
7351 let meta = match fs::symlink_metadata(&file_path) {
7352 Ok(m) => m,
7353 Err(e)
7354 if e.kind() == std::io::ErrorKind::NotFound
7355 || e.raw_os_error() == Some(20) =>
7356 {
7357 return Ok(true);
7358 }
7359 Err(e) => return Err(Error::Io(e)),
7360 };
7361
7362 if meta.is_dir() {
7363 return Ok(true);
7364 }
7365
7366 if !self
7367 .mode_policy
7368 .worktree_mode_matches(mode_from_metadata(&meta), ie.mode)
7369 {
7370 return Ok(true);
7371 }
7372
7373 let conv = self
7374 .conv
7375 .get_or_init(|| crlf::ConversionConfig::from_config(&self.config));
7376 let attrs = self
7377 .attrs
7378 .get_or_init(|| crlf::load_gitattributes(work_tree));
7379 let file_attrs = crlf::get_file_attrs(attrs, path_str_ref.as_ref(), false, &self.config);
7380 let worktree_oid = hash_worktree_file(
7381 odb,
7382 &file_path,
7383 &meta,
7384 conv,
7385 &file_attrs,
7386 path_str_ref.as_ref(),
7387 Some(ie),
7388 filter_process,
7389 )?;
7390
7391 let mut eff_oid = worktree_oid;
7392 if eff_oid != ie.oid {
7393 if let Ok(raw) = fs::read(&file_path) {
7394 let raw_oid = odb.hash(ObjectKind::Blob, &raw)?;
7395 if raw_oid == ie.oid {
7396 eff_oid = ie.oid;
7397 }
7398 }
7399 }
7400
7401 Ok(eff_oid != ie.oid)
7402 }
7403}
7404
7405pub struct WorktreeAddRefreshParams<'a> {
7407 pub odb: &'a Odb,
7409 pub ie: &'a IndexEntry,
7411 pub meta: &'a fs::Metadata,
7413 pub abs_path: &'a Path,
7415 pub rel_path: &'a str,
7417 pub conv: &'a crate::crlf::ConversionConfig,
7419 pub file_attrs: &'a crate::crlf::FileAttrs,
7421 pub index_mtime: Option<(u32, u32)>,
7423 pub staged_mode: u32,
7426 pub filter_process: Option<&'a crate::filter_process::FilterProcessState>,
7428}
7429
7430#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7432pub enum WorktreeAddRefresh {
7433 UpToDate,
7435 ModeOnly {
7437 mode: u32,
7439 },
7440 StatOnly,
7442 NeedsRestage,
7444}
7445
7446#[must_use]
7448pub(crate) fn index_stat_cache_trustworthy(index_mtime: Option<(u32, u32)>) -> bool {
7449 match index_mtime {
7450 None => false,
7451 Some((0, _)) => false,
7452 Some(_) => true,
7453 }
7454}
7455
7456#[must_use]
7465pub(crate) fn index_entry_skips_worktree_content_hash_after_stat_match(
7466 ie: &IndexEntry,
7467 index_mtime: Option<(u32, u32)>,
7468 stat_policy: crate::index::StatMatchPolicy,
7469) -> bool {
7470 if ie.stat_cache_unset() || (ie.size == 0 && !ie.oid.is_empty_blob()) {
7471 return false;
7472 }
7473 if !index_stat_cache_trustworthy(index_mtime) {
7474 return false;
7475 }
7476 !entry_is_racy_with_policy(ie, index_mtime, stat_policy)
7477}
7478
7479pub fn classify_worktree_entry_for_add(
7493 params: &WorktreeAddRefreshParams<'_>,
7494) -> Result<WorktreeAddRefresh> {
7495 use crate::index::{MODE_EXECUTABLE, MODE_REGULAR, MODE_SYMLINK};
7496
7497 let ie = params.ie;
7498 let meta = params.meta;
7499 let staged_mode = params.staged_mode;
7500
7501 if ie.intent_to_add() {
7502 return Ok(WorktreeAddRefresh::NeedsRestage);
7503 }
7504
7505 let racy = entry_is_racy(ie, params.index_mtime);
7506 let stat_same = stat_matches(ie, meta);
7507 let trust_stat = index_stat_cache_trustworthy(params.index_mtime);
7508
7509 if ie.mode == MODE_SYMLINK {
7510 if !meta.file_type().is_symlink() && staged_mode != MODE_SYMLINK {
7513 return Ok(WorktreeAddRefresh::NeedsRestage);
7514 }
7515 if stat_same && !racy && trust_stat && staged_mode == ie.mode {
7516 return Ok(WorktreeAddRefresh::UpToDate);
7517 }
7518 if stat_same && !racy && trust_stat && staged_mode != ie.mode {
7519 return Ok(WorktreeAddRefresh::ModeOnly { mode: staged_mode });
7520 }
7521 return worktree_effective_oid_matches_index(params);
7522 }
7523
7524 if ie.mode != MODE_REGULAR && ie.mode != MODE_EXECUTABLE {
7525 return Ok(WorktreeAddRefresh::NeedsRestage);
7526 }
7527
7528 if meta.file_type().is_symlink() || meta.is_dir() {
7529 return Ok(WorktreeAddRefresh::NeedsRestage);
7530 }
7531
7532 if stat_same && !racy && trust_stat {
7533 if staged_mode == ie.mode {
7534 return Ok(WorktreeAddRefresh::UpToDate);
7535 }
7536 return Ok(WorktreeAddRefresh::ModeOnly { mode: staged_mode });
7537 }
7538
7539 worktree_effective_oid_matches_index(params)
7540}
7541
7542fn worktree_effective_oid_matches_index(
7543 params: &WorktreeAddRefreshParams<'_>,
7544) -> Result<WorktreeAddRefresh> {
7545 let WorktreeAddRefreshParams {
7546 odb,
7547 ie,
7548 meta,
7549 abs_path,
7550 rel_path,
7551 conv,
7552 file_attrs,
7553 staged_mode,
7554 ..
7555 } = params;
7556 let worktree_oid = hash_worktree_file(
7557 odb,
7558 abs_path,
7559 meta,
7560 conv,
7561 file_attrs,
7562 rel_path,
7563 Some(ie),
7564 params.filter_process,
7565 )?;
7566 let mut eff_oid = worktree_oid;
7567 if eff_oid != ie.oid {
7568 if let Ok(raw) = fs::read(abs_path) {
7569 let raw_oid = odb.hash(ObjectKind::Blob, &raw)?;
7570 if raw_oid == ie.oid {
7571 eff_oid = ie.oid;
7572 }
7573 }
7574 }
7575 if eff_oid != ie.oid {
7576 return Ok(WorktreeAddRefresh::NeedsRestage);
7577 }
7578 if *staged_mode != ie.mode {
7579 return Ok(WorktreeAddRefresh::ModeOnly { mode: *staged_mode });
7580 }
7581 if stat_matches(ie, meta) {
7582 Ok(WorktreeAddRefresh::UpToDate)
7583 } else {
7584 Ok(WorktreeAddRefresh::StatOnly)
7585 }
7586}
7587
7588#[must_use]
7590pub fn stat_matches(ie: &IndexEntry, meta: &fs::Metadata) -> bool {
7591 stat_matches_with_policy(ie, meta, crate::index::StatMatchPolicy::default())
7592}
7593
7594pub(crate) fn stat_matches_blob_stat(
7596 ie: &IndexEntry,
7597 stat: &crate::worktree_scan::BlobDiskStat,
7598 policy: crate::index::StatMatchPolicy,
7599) -> bool {
7600 stat.fields.matches_index_entry(ie, policy)
7601}
7602
7603#[must_use]
7605pub fn stat_matches_with_policy(
7606 ie: &IndexEntry,
7607 meta: &fs::Metadata,
7608 policy: crate::index::StatMatchPolicy,
7609) -> bool {
7610 if crate::index::munge_index_size(meta.len()) != ie.size {
7611 return false;
7612 }
7613 #[cfg(unix)]
7614 {
7615 use std::os::unix::fs::MetadataExt;
7616 if meta.mtime() as u32 != ie.mtime_sec {
7617 return false;
7618 }
7619 if policy.use_nanosec && policy.check_stat && meta.mtime_nsec() as u32 != ie.mtime_nsec {
7620 return false;
7621 }
7622 if policy.trust_ctime && policy.check_stat {
7623 if meta.ctime() as u32 != ie.ctime_sec {
7624 return false;
7625 }
7626 if policy.use_nanosec && meta.ctime_nsec() as u32 != ie.ctime_nsec {
7627 return false;
7628 }
7629 }
7630 if policy.check_stat && meta.ino() as u32 != ie.ino {
7631 return false;
7632 }
7633 }
7634 #[cfg(not(unix))]
7635 {
7636 let ((ctime_sec, ctime_nsec), (mtime_sec, mtime_nsec)) = crate::index::metadata_times(meta);
7637 if mtime_sec != ie.mtime_sec {
7638 return false;
7639 }
7640 if policy.use_nanosec && policy.check_stat && mtime_nsec != ie.mtime_nsec {
7641 return false;
7642 }
7643 if policy.trust_ctime && policy.check_stat {
7644 if ctime_sec != ie.ctime_sec {
7645 return false;
7646 }
7647 if policy.use_nanosec && ctime_nsec != ie.ctime_nsec {
7648 return false;
7649 }
7650 }
7651 }
7652 true
7653}
7654
7655pub fn refresh_index_stat_content_verified(
7678 odb: &Odb,
7679 git_dir: &Path,
7680 index: &mut Index,
7681 work_tree: &Path,
7682 index_mtime: Option<(u32, u32)>,
7683 config: Option<&ConfigSet>,
7684 _stat_parallel_threads: Option<usize>,
7685) -> Result<bool> {
7686 let _ = _stat_parallel_threads;
7687 refresh_index_stat_content_verified_with_rules(
7688 odb,
7689 git_dir,
7690 index,
7691 work_tree,
7692 index_mtime,
7693 config,
7694 _stat_parallel_threads,
7695 None,
7696 )
7697}
7698
7699#[allow(clippy::too_many_arguments)]
7707pub fn refresh_index_stat_content_verified_with_rules(
7708 odb: &Odb,
7709 git_dir: &Path,
7710 index: &mut Index,
7711 work_tree: &Path,
7712 index_mtime: Option<(u32, u32)>,
7713 config: Option<&ConfigSet>,
7714 stat_parallel_threads: Option<usize>,
7715 worktree_rules: Option<&std::sync::Arc<std::sync::Mutex<crate::worktree_rules::WorktreeRules>>>,
7716) -> Result<bool> {
7717 use crate::config::ConfigSet;
7718 use crate::crlf;
7719 use crate::worktree_scan::{
7720 collect_blob_lookup_batches, group_blob_entries_by_dir, visit_blob_lookup_batches,
7721 BlobDiskLookup, BlobScanSparseMode, WorktreeBlobScanOptions,
7722 };
7723
7724 let index_mtime = index_mtime_for_diff(index, index_mtime);
7725 let rules_lock = worktree_rules
7726 .map(|shared| {
7727 shared
7728 .lock()
7729 .map_err(|e| Error::Message(format!("worktree rules lock poisoned: {e}")))
7730 })
7731 .transpose()?;
7732 let config = rules_lock
7733 .as_ref()
7734 .map(|r| r.config().clone())
7735 .unwrap_or_else(|| {
7736 config.cloned().unwrap_or_else(|| {
7737 ConfigSet::load(
7738 &crate::environment::Environment::empty(),
7739 Some(git_dir),
7740 true,
7741 )
7742 .unwrap_or_default()
7743 })
7744 });
7745 let preload_index = config
7746 .get_bool("core.preloadIndex")
7747 .and_then(|r| r.ok())
7748 .unwrap_or(true);
7749 let blob_scan = WorktreeBlobScanOptions {
7750 preload_index,
7751 stat_parallel_threads,
7752 ..WorktreeBlobScanOptions::default()
7753 };
7754 let conv = rules_lock
7755 .as_ref()
7756 .map(|r| r.conversion().clone())
7757 .unwrap_or_else(|| crlf::ConversionConfig::from_config(&config));
7758 let legacy_attrs = if rules_lock.is_none() {
7759 Some(crlf::load_gitattributes_parsed(work_tree))
7760 } else {
7761 None
7762 };
7763 let file_attrs_for = |path: &str| -> crlf::FileAttrs {
7764 if let Some(rules) = rules_lock.as_ref() {
7765 rules.file_attrs(path, false)
7766 } else if let Some(parsed) = legacy_attrs.as_ref() {
7767 crlf::file_attrs_from_gitattributes(&parsed.rules, &parsed.macros, path, false, &config)
7768 } else {
7769 crlf::FileAttrs::default()
7770 }
7771 };
7772 let precompose_unicode = config
7773 .get_bool("core.precomposeunicode")
7774 .and_then(|r| r.ok())
7775 .unwrap_or(false);
7776 let ignorecase = config
7777 .get_bool("core.ignorecase")
7778 .and_then(|r| r.ok())
7779 .unwrap_or(false);
7780 let stat_policy = crate::index::StatMatchPolicy::from_config(Some(&config));
7781
7782 let blob_dirs = group_blob_entries_by_dir(index, false);
7783 let work_tree_owned = work_tree.to_path_buf();
7784 let dir_abs = move |dir: &[u8]| {
7785 if dir.is_empty() {
7786 work_tree_owned.clone()
7787 } else {
7788 index_entry_worktree_abs_bytes(&work_tree_owned, dir, precompose_unicode, ignorecase)
7789 }
7790 };
7791 let work_tree_for_files = work_tree.to_path_buf();
7792 let file_abs = move |rel: &[u8]| {
7793 index_entry_worktree_abs_bytes(&work_tree_for_files, rel, precompose_unicode, ignorecase)
7794 };
7795 let mut changed = false;
7796 let filter_fp = rules_lock.as_ref().map(|r| r.filter_process());
7797 let thread_policy = crate::parallel::ThreadPolicy::default();
7798 let refresh_batches = collect_blob_lookup_batches(
7800 index,
7801 &blob_dirs,
7802 blob_scan,
7803 &thread_policy,
7804 &config,
7805 dir_abs,
7806 file_abs,
7807 || {
7808 let mut dir_symlinks = SymlinkDirCache::default();
7809 move |rel_path: &[u8]| {
7810 dir_symlinks.has_symlink_in_path_bytes(
7811 work_tree,
7812 rel_path,
7813 precompose_unicode,
7814 ignorecase,
7815 )
7816 }
7817 },
7818 BlobScanSparseMode::StatRefresh {
7819 stat_policy,
7820 index_mtime,
7821 },
7822 )?;
7823 let par_opts = refresh_hash_parallel_options(&thread_policy, &config, stat_parallel_threads);
7824 let refresh_checks = refresh_content_checks(
7825 odb,
7826 index,
7827 &refresh_batches,
7828 index_mtime,
7829 stat_policy,
7830 &|_, _, _| true,
7831 &WorktreeHashContext {
7832 work_tree,
7833 precompose_unicode,
7834 ignorecase,
7835 conv: &conv,
7836 file_attrs_for: &file_attrs_for,
7837 filter_process: filter_fp,
7838 },
7839 par_opts,
7840 None,
7841 )?;
7842 visit_blob_lookup_batches(refresh_batches, |entry_index, lookup| {
7843 let BlobDiskLookup::Present(stat) = lookup else {
7844 return Ok(());
7845 };
7846 let content_matches = RefreshContent::matches(
7847 refresh_checks.get(entry_index),
7848 &index.entries[entry_index].oid,
7849 );
7850 let refresh = refresh_index_blob_stat_from_scan(
7851 &mut index.entries[entry_index],
7852 &stat,
7853 index_mtime,
7854 stat_policy,
7855 content_matches,
7856 );
7857 if refresh.index_changed {
7858 changed = true;
7859 }
7860 if refresh.content_verified {
7861 let path = index.entries[entry_index].path.clone();
7862 let oid = index.entries[entry_index].oid;
7863 index.mark_content_verified(&path, oid);
7864 }
7865 Ok(())
7866 })?;
7867 Ok(changed)
7868}
7869
7870struct BlobStatRefreshOutcome {
7872 index_changed: bool,
7874 content_verified: bool,
7876}
7877
7878fn refresh_index_entry_eligible(ie: &IndexEntry) -> bool {
7880 use crate::index::{MODE_EXECUTABLE, MODE_REGULAR, MODE_SYMLINK};
7881 ie.stage() == 0
7882 && !ie.skip_worktree()
7883 && !ie.assume_unchanged()
7884 && !ie.intent_to_add()
7885 && matches!(ie.mode, MODE_REGULAR | MODE_EXECUTABLE | MODE_SYMLINK)
7886}
7887
7888fn refresh_needs_content_check(
7891 ie: &IndexEntry,
7892 stat: &crate::worktree_scan::BlobDiskStat,
7893 index_mtime: Option<(u32, u32)>,
7894 stat_policy: crate::index::StatMatchPolicy,
7895) -> bool {
7896 refresh_index_entry_eligible(ie)
7897 && !stat.is_dir
7898 && (!stat_matches_blob_stat(ie, stat, stat_policy)
7899 || entry_is_racy_with_policy(ie, index_mtime, stat_policy))
7900}
7901
7902pub(crate) fn worktree_diff_refreshes_entry(
7909 ie: &IndexEntry,
7910 stat: &crate::worktree_scan::BlobDiskStat,
7911 index_mtime: Option<(u32, u32)>,
7912 stat_policy: crate::index::StatMatchPolicy,
7913 for_status: bool,
7914 conversion_is_identity: impl FnOnce() -> bool,
7915) -> bool {
7916 if !for_status {
7917 return true;
7918 }
7919 if stat_matches_blob_stat(ie, stat, stat_policy) {
7920 return entry_is_racy_with_policy(ie, index_mtime, stat_policy);
7921 }
7922 !status_size_change_proves_modified(
7923 ie,
7924 stat.worktree_size(),
7925 for_status,
7926 conversion_is_identity,
7927 )
7928}
7929
7930fn refresh_hash_parallel_options(
7937 thread_policy: &crate::parallel::ThreadPolicy,
7938 config: &ConfigSet,
7939 stat_parallel_threads: Option<usize>,
7940) -> crate::parallel::ParallelOptions {
7941 let policy = crate::parallel::ThreadPolicy {
7942 min_items_override: thread_policy.min_items_override.or(Some(0)),
7943 min_total_bytes_override: thread_policy.min_total_bytes_override.or(Some(0)),
7944 ..*thread_policy
7945 };
7946 let mut par_opts = policy.resolve(crate::parallel::ParallelOp::IndexRefresh, config);
7947 if let Some(cap) = stat_parallel_threads {
7948 par_opts.threads = par_opts
7949 .threads
7950 .min(std::num::NonZeroUsize::new(cap).unwrap_or(std::num::NonZeroUsize::MIN));
7951 }
7952 par_opts.min_items = 0;
7953 par_opts.min_total_bytes = None;
7954 par_opts
7955}
7956
7957struct WorktreeHashContext<'a> {
7959 work_tree: &'a Path,
7960 precompose_unicode: bool,
7961 ignorecase: bool,
7962 conv: &'a crate::crlf::ConversionConfig,
7963 file_attrs_for: &'a dyn Fn(&str) -> crate::crlf::FileAttrs,
7964 filter_process: Option<&'a crate::filter_process::FilterProcessState>,
7965}
7966
7967#[allow(clippy::too_many_arguments)]
7977fn refresh_content_checks(
7978 odb: &Odb,
7979 index: &Index,
7980 batches: &[Vec<(usize, crate::worktree_scan::BlobDiskLookup)>],
7981 index_mtime: Option<(u32, u32)>,
7982 stat_policy: crate::index::StatMatchPolicy,
7983 include: &dyn Fn(usize, &IndexEntry, &crate::worktree_scan::BlobDiskStat) -> bool,
7984 ctx: &WorktreeHashContext<'_>,
7985 par_opts: crate::parallel::ParallelOptions,
7986 cancel_watch: Option<&std::sync::Arc<std::sync::atomic::AtomicBool>>,
7987) -> Result<RefreshChecks> {
7988 use crate::progress::{poll_watch, CANCEL_POLL_STRIDE};
7989 use crate::worktree_scan::BlobDiskLookup;
7990
7991 struct ContentCheck {
7993 entry_index: usize,
7994 worktree_mode: u32,
7995 attrs_index: usize,
7997 }
7998
7999 let mut attrs = DistinctFileAttrs::default();
8000 let mut jobs = Vec::new();
8001 let mut total_bytes = 0usize;
8002 let mut cancel_stride = 0usize;
8003 for (entry_index, lookup) in batches.iter().flatten() {
8004 cancel_stride = cancel_stride.wrapping_add(1);
8005 if cancel_stride.is_multiple_of(CANCEL_POLL_STRIDE) {
8006 if let Some(w) = cancel_watch {
8007 poll_watch(w)?;
8008 }
8009 }
8010 let BlobDiskLookup::Present(stat) = lookup else {
8011 continue;
8012 };
8013 let ie = &index.entries[*entry_index];
8014 if !refresh_needs_content_check(ie, stat, index_mtime, stat_policy)
8015 || !include(*entry_index, ie, stat)
8016 {
8017 continue;
8018 }
8019 total_bytes = total_bytes.saturating_add(ie.size as usize);
8020 let file_attrs = (ctx.file_attrs_for)(&crate::repo_relpath::storage_string(&ie.path));
8021 jobs.push(ContentCheck {
8022 entry_index: *entry_index,
8023 worktree_mode: stat.git_mode,
8024 attrs_index: attrs.intern(file_attrs),
8025 });
8026 }
8027 if jobs.is_empty() {
8028 return Ok(RefreshChecks::default());
8029 }
8030 let attrs = attrs.0;
8031 let mut checks = vec![None; index.entries.len()];
8032 let (work_tree, precompose_unicode, ignorecase) =
8033 (ctx.work_tree, ctx.precompose_unicode, ctx.ignorecase);
8034 let (conv, filter_process) = (ctx.conv, ctx.filter_process);
8035 let entries = index.entries.as_slice();
8036 let watch_for_hash = cancel_watch.cloned();
8037 let hash_one = |job: &ContentCheck| -> Result<RefreshContent> {
8038 if job.entry_index.is_multiple_of(CANCEL_POLL_STRIDE) {
8039 if let Some(w) = watch_for_hash.as_ref() {
8040 poll_watch(w)?;
8041 }
8042 }
8043 let ie = &entries[job.entry_index];
8044 let file_path =
8045 index_entry_worktree_abs_bytes(work_tree, &ie.path, precompose_unicode, ignorecase);
8046 Ok(RefreshContent::from(hash_worktree_file_with_mode(
8047 odb,
8048 &file_path,
8049 job.worktree_mode,
8050 conv,
8051 &attrs[job.attrs_index],
8052 &crate::repo_relpath::storage_string(&ie.path),
8053 Some(ie),
8054 filter_process,
8055 )))
8056 };
8057 let matches = if par_opts.threads.get() > 1 && jobs.len() > 1 {
8058 crate::parallel::try_par_map_force(&jobs, par_opts, hash_one).map_err(|e| match e {
8059 crate::parallel::ParallelHashError::Task(err) => err,
8060 })?
8061 } else {
8062 let mut out = Vec::with_capacity(jobs.len());
8063 let mut serial_stride = 0usize;
8064 for job in &jobs {
8065 serial_stride = serial_stride.wrapping_add(1);
8066 if serial_stride.is_multiple_of(CANCEL_POLL_STRIDE) {
8067 if let Some(w) = cancel_watch {
8068 poll_watch(w)?;
8069 }
8070 }
8071 out.push(hash_one(job)?);
8072 }
8073 out
8074 };
8075 for (job, content) in jobs.iter().zip(matches) {
8076 checks[job.entry_index] = Some(content);
8077 }
8078 Ok(RefreshChecks(checks))
8079}
8080
8081#[derive(Default)]
8088struct DistinctFileAttrs(Vec<crate::crlf::FileAttrs>);
8089
8090impl DistinctFileAttrs {
8091 const SEARCH_LIMIT: usize = 16;
8092
8093 fn intern(&mut self, attrs: crate::crlf::FileAttrs) -> usize {
8095 let len = self.0.len();
8096 let found = self
8097 .0
8098 .iter()
8099 .rev()
8100 .take(Self::SEARCH_LIMIT)
8101 .position(|seen| *seen == attrs);
8102 if let Some(back) = found {
8103 return len - 1 - back;
8104 }
8105 self.0.push(attrs);
8106 len
8107 }
8108}
8109
8110#[derive(Default)]
8115struct RefreshChecks(Vec<Option<RefreshContent>>);
8116
8117impl RefreshChecks {
8118 fn get(&self, entry_index: usize) -> Option<RefreshContent> {
8120 self.0.get(entry_index).copied().flatten()
8121 }
8122}
8123
8124#[derive(Clone, Copy)]
8126enum RefreshContent {
8127 Failed,
8129 Hashed(ObjectId),
8131}
8132
8133impl From<Result<ObjectId>> for RefreshContent {
8134 fn from(hashed: Result<ObjectId>) -> Self {
8135 hashed.map_or(Self::Failed, Self::Hashed)
8136 }
8137}
8138
8139impl RefreshContent {
8140 fn matches(check: Option<Self>, oid: &ObjectId) -> bool {
8142 matches!(check, Some(Self::Hashed(hashed)) if hashed == *oid)
8143 }
8144
8145 fn differs(check: Option<Self>, oid: &ObjectId) -> bool {
8147 matches!(check, Some(Self::Hashed(hashed)) if hashed != *oid)
8148 }
8149}
8150
8151fn refresh_index_blob_stat_from_scan(
8157 ie: &mut IndexEntry,
8158 stat: &crate::worktree_scan::BlobDiskStat,
8159 index_mtime: Option<(u32, u32)>,
8160 stat_policy: crate::index::StatMatchPolicy,
8161 content_matches: bool,
8162) -> BlobStatRefreshOutcome {
8163 const UNCHANGED: BlobStatRefreshOutcome = BlobStatRefreshOutcome {
8164 index_changed: false,
8165 content_verified: false,
8166 };
8167 if !refresh_index_entry_eligible(ie) || stat.is_dir {
8168 return UNCHANGED;
8169 }
8170 let cached = crate::index::IndexStatFields::of_entry(ie);
8171 let stat_same = cached.equivalent_to(&stat.fields, stat_policy);
8172 let stat_matches = stat_matches_blob_stat(ie, stat, stat_policy);
8173 if !stat_matches || entry_is_racy_with_policy(ie, index_mtime, stat_policy) {
8174 if !content_matches {
8175 if stat_matches {
8176 invalidate_index_stat_cache(ie);
8177 return BlobStatRefreshOutcome {
8178 index_changed: true,
8179 content_verified: false,
8180 };
8181 }
8182 return UNCHANGED;
8183 }
8184 if !stat_same {
8185 stat.fields.store_in(ie);
8186 }
8187 return BlobStatRefreshOutcome {
8188 index_changed: !stat_same,
8189 content_verified: true,
8190 };
8191 }
8192 if stat_same {
8193 return BlobStatRefreshOutcome {
8194 index_changed: false,
8195 content_verified: true,
8196 };
8197 }
8198 stat.fields.store_in(ie);
8199 BlobStatRefreshOutcome {
8200 index_changed: true,
8201 content_verified: false,
8202 }
8203}
8204
8205#[allow(clippy::too_many_arguments)]
8217pub(crate) fn path_checkout_skip_blob_write_when_up_to_date_with_config(
8218 odb: &Odb,
8219 git_dir: &Path,
8220 work_tree: &Path,
8221 index: &Index,
8222 entry: &IndexEntry,
8223 expected_oid: &ObjectId,
8224 expected_mode: u32,
8225 rel_path: &str,
8226 filter_process: Option<&crate::filter_process::FilterProcessState>,
8227 preloaded_config: Option<&crate::config::ConfigSet>,
8228) -> Result<bool> {
8229 use crate::config::ConfigSet;
8230 use crate::crlf;
8231 use crate::index::{MODE_EXECUTABLE, MODE_REGULAR};
8232
8233 if entry.oid != *expected_oid || entry.mode != expected_mode {
8234 return Ok(false);
8235 }
8236 if expected_mode != MODE_REGULAR && expected_mode != MODE_EXECUTABLE {
8237 return Ok(false);
8238 }
8239 let abs_path = crate::repo_relpath::worktree_abs(work_tree, rel_path);
8240 let meta = match fs::symlink_metadata(&abs_path) {
8241 Ok(m) => m,
8242 Err(_) => return Ok(false),
8243 };
8244 if !meta.is_file() || !stat_matches(entry, &meta) {
8245 return Ok(false);
8246 }
8247 let owned_config;
8248 let config = match preloaded_config {
8249 Some(cfg) => cfg,
8250 None => {
8251 owned_config = ConfigSet::load(
8252 &crate::environment::Environment::empty(),
8253 Some(git_dir),
8254 true,
8255 )
8256 .unwrap_or_default();
8257 &owned_config
8258 }
8259 };
8260 if !crate::index::WorktreeModePolicy::from_config(config)
8261 .worktree_mode_matches(mode_from_metadata(&meta), expected_mode)
8262 {
8263 return Ok(false);
8264 }
8265 let index_mtime = index_mtime_for_diff(index, None);
8266 if !entry_is_racy(entry, index_mtime) {
8267 return Ok(true);
8268 }
8269 let conv = crlf::ConversionConfig::from_config(config);
8270 let attrs = crlf::load_gitattributes(work_tree);
8271 let file_attrs = crlf::get_file_attrs(&attrs, rel_path, false, config);
8272 let wt_oid = hash_worktree_file(
8273 odb,
8274 &abs_path,
8275 &meta,
8276 &conv,
8277 &file_attrs,
8278 rel_path,
8279 Some(entry),
8280 filter_process,
8281 )?;
8282 Ok(wt_oid == *expected_oid)
8283}
8284
8285fn invalidate_index_stat_cache(ie: &mut IndexEntry) {
8287 ie.ctime_sec = 0;
8288 ie.ctime_nsec = 0;
8289 ie.mtime_sec = 0;
8290 ie.mtime_nsec = 0;
8291 ie.dev = 0;
8292 ie.ino = 0;
8293 ie.size = 0;
8294}
8295
8296pub fn read_worktree_bytes_for_mode(path: &Path, mode: u32) -> std::io::Result<Vec<u8>> {
8307 if mode == MODE_SYMLINK && fs::symlink_metadata(path)?.file_type().is_symlink() {
8308 let target = fs::read_link(path)?;
8309 return Ok(symlink_target_bytes(&target));
8310 }
8311 fs::read(path)
8312}
8313
8314pub fn read_worktree_side_bytes_for_diff(
8324 odb: &Odb,
8325 work_tree: &Path,
8326 rel_path: &str,
8327 mode: u32,
8328 config: &crate::config::ConfigSet,
8329 index_entry: Option<&IndexEntry>,
8330 worktree_rules: Option<&crate::worktree_rules::WorktreeRules>,
8331) -> Result<Vec<u8>> {
8332 Ok(load_worktree_side_for_diff(
8333 odb,
8334 work_tree,
8335 rel_path,
8336 mode,
8337 config,
8338 index_entry,
8339 worktree_rules,
8340 false,
8341 )?
8342 .0
8343 .data)
8344}
8345
8346#[allow(clippy::too_many_arguments)]
8356pub fn load_worktree_side_for_diff(
8357 odb: &Odb,
8358 work_tree: &Path,
8359 rel_path: &str,
8360 mode: u32,
8361 config: &crate::config::ConfigSet,
8362 index_entry: Option<&IndexEntry>,
8363 worktree_rules: Option<&crate::worktree_rules::WorktreeRules>,
8364 force_read_content: bool,
8365) -> Result<(super::structured::SideBytes, bool)> {
8366 use super::tree_content::{side_bytes_after_blob_read, treat_side_as_binary_without_reading};
8367 let file_path = crate::repo_relpath::worktree_abs(work_tree, rel_path);
8368 let rel_match = crate::repo_relpath::as_utf8_for_matching(rel_path);
8369 let meta = fs::symlink_metadata(&file_path).map_err(|source| {
8370 crate::worktree::map_worktree_io(
8371 rel_match.as_ref(),
8372 crate::worktree::WorktreePathOperation::Metadata,
8373 source,
8374 )
8375 })?;
8376 if mode == MODE_SYMLINK && meta.file_type().is_symlink() {
8377 let target = fs::read_link(&file_path).map_err(|source| {
8378 crate::worktree::map_worktree_io(
8379 rel_match.as_ref(),
8380 crate::worktree::WorktreePathOperation::Read,
8381 source,
8382 )
8383 })?;
8384 return Ok((
8385 super::structured::SideBytes::from_bytes(symlink_target_bytes(&target)),
8386 false,
8387 ));
8388 }
8389 if !force_read_content
8390 && treat_side_as_binary_without_reading(odb, meta.len(), Some(rel_match.as_ref()), false)?
8391 {
8392 return Ok((super::structured::SideBytes::from_bytes(Vec::new()), true));
8393 }
8394 let data = if let Some(rules) = worktree_rules {
8395 worktree_blob_bytes(
8396 odb,
8397 &file_path,
8398 &meta,
8399 rules.conversion(),
8400 &rules.file_attrs(rel_match.as_ref(), meta.is_dir()),
8401 rel_match.as_ref(),
8402 index_entry,
8403 Some(rules.filter_process()),
8404 )?
8405 } else {
8406 let conv = crate::crlf::ConversionConfig::from_config(config);
8407 let attrs = crate::crlf::load_gitattributes(work_tree);
8408 let file_attrs =
8409 crate::crlf::get_file_attrs(&attrs, rel_match.as_ref(), meta.is_dir(), config);
8410 worktree_blob_bytes(
8411 odb,
8412 &file_path,
8413 &meta,
8414 &conv,
8415 &file_attrs,
8416 rel_match.as_ref(),
8417 index_entry,
8418 None,
8419 )?
8420 };
8421 Ok(side_bytes_after_blob_read(data, force_read_content))
8422}
8423
8424pub(crate) fn symlink_target_bytes(target: &Path) -> Vec<u8> {
8430 #[cfg(unix)]
8431 {
8432 use std::os::unix::ffi::OsStrExt as _;
8433 target.as_os_str().as_bytes().to_vec()
8434 }
8435 #[cfg(not(unix))]
8436 {
8437 target.as_os_str().to_string_lossy().as_bytes().to_vec()
8438 }
8439}
8440
8441pub(crate) enum WorktreeBlobContent {
8448 Bytes(Vec<u8>),
8450 Stream {
8452 file: fs::File,
8454 size: u64,
8456 },
8457}
8458
8459impl WorktreeBlobContent {
8460 pub(crate) fn hash(self, odb: &Odb) -> Result<ObjectId> {
8462 match self {
8463 Self::Bytes(data) => odb.hash(ObjectKind::Blob, &data),
8464 Self::Stream { mut file, size } => odb.hash_stream(ObjectKind::Blob, size, &mut file),
8465 }
8466 }
8467
8468 pub(crate) fn store(self, odb: &Odb, options: crate::odb::WriteOptions) -> Result<ObjectId> {
8470 match self {
8471 Self::Bytes(data) => odb.write_with_options(ObjectKind::Blob, &data, options),
8472 Self::Stream { mut file, size } => {
8473 odb.write_stream(ObjectKind::Blob, size, &mut file, options)
8474 }
8475 }
8476 }
8477
8478 fn object_id(self, odb: &Odb, store: Option<crate::odb::WriteOptions>) -> Result<ObjectId> {
8480 match store {
8481 Some(options) => self.store(odb, options),
8482 None => self.hash(odb),
8483 }
8484 }
8485}
8486
8487#[allow(clippy::too_many_arguments)]
8489fn read_regular_worktree_bytes(
8490 odb: &Odb,
8491 mut file: fs::File,
8492 size: u64,
8493 conv: &crate::crlf::ConversionConfig,
8494 file_attrs: &crate::crlf::FileAttrs,
8495 rel_path: &str,
8496 index_entry: Option<&IndexEntry>,
8497 filter_process: Option<&crate::filter_process::FilterProcessState>,
8498) -> Result<Vec<u8>> {
8499 let needs_index_blob =
8500 index_entry.is_some_and(|_| crate::crlf::clean_uses_autocrlf_index_guard(file_attrs, conv));
8501 let prior_blob: Option<Vec<u8>> = if needs_index_blob {
8502 index_entry
8503 .filter(|e| e.oid != zero_oid())
8504 .and_then(|e| odb.read(&e.oid).ok().map(|o| o.data))
8505 } else {
8506 None
8507 };
8508 let mut raw = Vec::with_capacity(usize::try_from(size).unwrap_or_default());
8509 std::io::Read::read_to_end(&mut file, &mut raw).map_err(|source| {
8510 crate::worktree::map_worktree_io(
8511 rel_path,
8512 crate::worktree::WorktreePathOperation::Read,
8513 source,
8514 )
8515 })?;
8516 #[cfg(test)]
8517 odb.hot_path_metrics().record_blob_content_read();
8518 let opts = crate::crlf::ConvertToGitOpts {
8519 index_blob: prior_blob.as_deref(),
8520 renormalize: false,
8521 check_safecrlf: false,
8522 filter_process,
8523 command_runner: None,
8524 };
8525 Ok(crate::crlf::convert_to_git_owned(
8526 raw, rel_path, conv, file_attrs, opts,
8527 )?)
8528}
8529
8530fn open_with_size(path: &Path, rel_path: &str) -> Result<(fs::File, u64)> {
8531 let file = fs::File::open(path).map_err(|source| {
8532 crate::worktree::map_worktree_io(
8533 rel_path,
8534 crate::worktree::WorktreePathOperation::Read,
8535 source,
8536 )
8537 })?;
8538 let size = file
8539 .metadata()
8540 .map_err(|source| {
8541 crate::worktree::map_worktree_io(
8542 rel_path,
8543 crate::worktree::WorktreePathOperation::Metadata,
8544 source,
8545 )
8546 })?
8547 .len();
8548 Ok((file, size))
8549}
8550
8551#[allow(clippy::too_many_arguments)]
8554fn read_regular_worktree_blob(
8555 odb: &Odb,
8556 path: &Path,
8557 conv: &crate::crlf::ConversionConfig,
8558 file_attrs: &crate::crlf::FileAttrs,
8559 rel_path: &str,
8560 index_entry: Option<&IndexEntry>,
8561 filter_process: Option<&crate::filter_process::FilterProcessState>,
8562 stream_from: u64,
8563) -> Result<WorktreeBlobContent> {
8564 let (mut file, size) = open_with_size(path, rel_path)?;
8565 if size >= stream_from
8566 && crate::crlf::worktree_allows_raw_byte_stream(
8567 conv,
8568 file_attrs,
8569 index_entry,
8570 &mut file,
8571 size,
8572 )?
8573 {
8574 #[cfg(test)]
8575 odb.hot_path_metrics().record_blob_content_read();
8576 return Ok(WorktreeBlobContent::Stream { file, size });
8577 }
8578 read_regular_worktree_bytes(
8579 odb,
8580 file,
8581 size,
8582 conv,
8583 file_attrs,
8584 rel_path,
8585 index_entry,
8586 filter_process,
8587 )
8588 .map(WorktreeBlobContent::Bytes)
8589}
8590
8591#[allow(clippy::too_many_arguments)]
8594pub(crate) fn worktree_blob_content(
8595 odb: &Odb,
8596 path: &Path,
8597 meta: &fs::Metadata,
8598 conv: &crate::crlf::ConversionConfig,
8599 file_attrs: &crate::crlf::FileAttrs,
8600 rel_path: &str,
8601 index_entry: Option<&IndexEntry>,
8602 filter_process: Option<&crate::filter_process::FilterProcessState>,
8603) -> Result<WorktreeBlobContent> {
8604 worktree_blob_content_from(
8605 odb,
8606 path,
8607 meta,
8608 conv,
8609 file_attrs,
8610 rel_path,
8611 index_entry,
8612 filter_process,
8613 hash_stream_threshold(odb)?,
8614 )
8615}
8616
8617#[allow(clippy::too_many_arguments)]
8619fn worktree_blob_content_from(
8620 odb: &Odb,
8621 path: &Path,
8622 meta: &fs::Metadata,
8623 conv: &crate::crlf::ConversionConfig,
8624 file_attrs: &crate::crlf::FileAttrs,
8625 rel_path: &str,
8626 index_entry: Option<&IndexEntry>,
8627 filter_process: Option<&crate::filter_process::FilterProcessState>,
8628 stream_from: u64,
8629) -> Result<WorktreeBlobContent> {
8630 if meta.file_type().is_symlink() {
8631 let target = fs::read_link(path).map_err(|source| {
8632 crate::worktree::map_worktree_io(
8633 rel_path,
8634 crate::worktree::WorktreePathOperation::Read,
8635 source,
8636 )
8637 })?;
8638 return Ok(WorktreeBlobContent::Bytes(symlink_target_bytes(&target)));
8639 }
8640 if meta.is_dir() {
8641 return Ok(WorktreeBlobContent::Bytes(Vec::new()));
8642 }
8643 read_regular_worktree_blob(
8644 odb,
8645 path,
8646 conv,
8647 file_attrs,
8648 rel_path,
8649 index_entry,
8650 filter_process,
8651 stream_from,
8652 )
8653}
8654
8655#[allow(clippy::too_many_arguments)]
8658pub(crate) fn worktree_blob_bytes(
8659 odb: &Odb,
8660 path: &Path,
8661 meta: &fs::Metadata,
8662 conv: &crate::crlf::ConversionConfig,
8663 file_attrs: &crate::crlf::FileAttrs,
8664 rel_path: &str,
8665 index_entry: Option<&IndexEntry>,
8666 filter_process: Option<&crate::filter_process::FilterProcessState>,
8667) -> Result<Vec<u8>> {
8668 if meta.file_type().is_symlink() {
8669 let target = fs::read_link(path).map_err(|source| {
8670 crate::worktree::map_worktree_io(
8671 rel_path,
8672 crate::worktree::WorktreePathOperation::Read,
8673 source,
8674 )
8675 })?;
8676 return Ok(symlink_target_bytes(&target));
8677 }
8678 if meta.is_dir() {
8679 return Ok(Vec::new());
8680 }
8681 let (file, size) = open_with_size(path, rel_path)?;
8682 read_regular_worktree_bytes(
8683 odb,
8684 file,
8685 size,
8686 conv,
8687 file_attrs,
8688 rel_path,
8689 index_entry,
8690 filter_process,
8691 )
8692}
8693
8694#[allow(clippy::too_many_arguments)]
8695fn worktree_file_oid(
8696 odb: &Odb,
8697 path: &Path,
8698 meta: &fs::Metadata,
8699 conv: &crate::crlf::ConversionConfig,
8700 file_attrs: &crate::crlf::FileAttrs,
8701 rel_path: &str,
8702 index_entry: Option<&IndexEntry>,
8703 materialize: bool,
8704 filter_process: Option<&crate::filter_process::FilterProcessState>,
8705) -> Result<ObjectId> {
8706 let stream_from = hash_stream_threshold(odb)?;
8707 worktree_blob_content_from(
8708 odb,
8709 path,
8710 meta,
8711 conv,
8712 file_attrs,
8713 rel_path,
8714 index_entry,
8715 filter_process,
8716 stream_from,
8717 )?
8718 .object_id(odb, materialize.then(|| odb.bulk_stage_write_options()))
8719}
8720
8721#[allow(clippy::too_many_arguments)]
8732pub fn hash_worktree_file(
8733 odb: &Odb,
8734 path: &Path,
8735 meta: &fs::Metadata,
8736 conv: &crate::crlf::ConversionConfig,
8737 file_attrs: &crate::crlf::FileAttrs,
8738 rel_path: &str,
8739 index_entry: Option<&IndexEntry>,
8740 filter_process: Option<&crate::filter_process::FilterProcessState>,
8741) -> Result<ObjectId> {
8742 worktree_file_oid(
8743 odb,
8744 path,
8745 meta,
8746 conv,
8747 file_attrs,
8748 rel_path,
8749 index_entry,
8750 false,
8751 filter_process,
8752 )
8753}
8754
8755const STREAMED_HASH_MIN_SIZE: u64 = 1024 * 1024;
8758
8759fn hash_stream_threshold(odb: &Odb) -> Result<u64> {
8761 Ok(odb.big_file_threshold()?.min(STREAMED_HASH_MIN_SIZE))
8762}
8763
8764#[allow(clippy::too_many_arguments)]
8768pub(crate) fn hash_worktree_file_with_mode(
8769 odb: &Odb,
8770 path: &Path,
8771 worktree_mode: u32,
8772 conv: &crate::crlf::ConversionConfig,
8773 file_attrs: &crate::crlf::FileAttrs,
8774 rel_path: &str,
8775 index_entry: Option<&IndexEntry>,
8776 filter_process: Option<&crate::filter_process::FilterProcessState>,
8777) -> Result<ObjectId> {
8778 if worktree_mode == MODE_SYMLINK {
8779 let data = read_worktree_bytes_for_mode(path, worktree_mode)?;
8780 return odb.hash(ObjectKind::Blob, &data);
8781 }
8782 read_regular_worktree_blob(
8783 odb,
8784 path,
8785 conv,
8786 file_attrs,
8787 rel_path,
8788 index_entry,
8789 filter_process,
8790 hash_stream_threshold(odb)?,
8791 )?
8792 .hash(odb)
8793}
8794
8795#[allow(clippy::too_many_arguments)]
8803pub fn materialize_worktree_blob(
8804 odb: &Odb,
8805 path: &Path,
8806 meta: &fs::Metadata,
8807 conv: &crate::crlf::ConversionConfig,
8808 file_attrs: &crate::crlf::FileAttrs,
8809 rel_path: &str,
8810 index_entry: Option<&IndexEntry>,
8811 filter_process: Option<&crate::filter_process::FilterProcessState>,
8812) -> Result<ObjectId> {
8813 worktree_file_oid(
8814 odb,
8815 path,
8816 meta,
8817 conv,
8818 file_attrs,
8819 rel_path,
8820 index_entry,
8821 true,
8822 filter_process,
8823 )
8824}
8825
8826pub fn mode_from_metadata(meta: &fs::Metadata) -> u32 {
8828 if meta.file_type().is_symlink() {
8829 0o120000
8830 } else {
8831 #[cfg(unix)]
8832 {
8833 if meta.mode() & 0o111 != 0 {
8834 return 0o100755;
8835 }
8836 }
8837 0o100644
8838 }
8839}
8840
8841pub fn diff_tree_to_worktree(
8858 odb: &Odb,
8859 tree_oid: Option<&ObjectId>,
8860 work_tree: &Path,
8861 index: &Index,
8862) -> Result<Vec<DiffEntry>> {
8863 diff_tree_to_worktree_with_git_dir(
8864 odb,
8865 tree_oid,
8866 work_tree,
8867 &work_tree.join(".git"),
8868 index,
8869 None,
8870 )
8871}
8872
8873pub fn diff_tree_to_worktree_with_git_dir(
8877 odb: &Odb,
8878 tree_oid: Option<&ObjectId>,
8879 work_tree: &Path,
8880 repository_git_dir: &Path,
8881 index: &Index,
8882 config: Option<&ConfigSet>,
8883) -> Result<Vec<DiffEntry>> {
8884 diff_tree_to_worktree_with_git_dir_and_rules(
8885 odb,
8886 tree_oid,
8887 work_tree,
8888 repository_git_dir,
8889 index,
8890 config,
8891 None,
8892 )
8893}
8894
8895pub fn diff_tree_to_worktree_with_git_dir_and_rules(
8897 odb: &Odb,
8898 tree_oid: Option<&ObjectId>,
8899 work_tree: &Path,
8900 repository_git_dir: &Path,
8901 index: &Index,
8902 config: Option<&ConfigSet>,
8903 worktree_rules: Option<&crate::worktree_rules::WorktreeRules>,
8904) -> Result<Vec<DiffEntry>> {
8905 diff_tree_to_worktree_with_git_dir_and_rules_extras(
8906 odb,
8907 tree_oid,
8908 work_tree,
8909 repository_git_dir,
8910 index,
8911 config,
8912 worktree_rules,
8913 DiffTreeToWorktreeExtras::default(),
8914 )
8915}
8916
8917#[allow(clippy::too_many_arguments)]
8919pub fn diff_tree_to_worktree_with_git_dir_and_rules_extras(
8920 odb: &Odb,
8921 tree_oid: Option<&ObjectId>,
8922 work_tree: &Path,
8923 repository_git_dir: &Path,
8924 index: &Index,
8925 config: Option<&ConfigSet>,
8926 worktree_rules: Option<&crate::worktree_rules::WorktreeRules>,
8927 extras: DiffTreeToWorktreeExtras,
8928) -> Result<Vec<DiffEntry>> {
8929 use crate::config::ConfigSet;
8930 use crate::crlf;
8931
8932 if worktree_rules.is_none() {
8933 if let Some(tree_oid) = tree_oid {
8934 if index.cache_tree_root.as_ref() == Some(tree_oid)
8935 && crate::write_tree::cache_tree_fully_valid(odb, index.cache_tree.as_ref())
8936 {
8937 let mut idx = index.clone();
8938 let options = DiffIndexToWorktreeOptions {
8939 repository_git_dir: Some(repository_git_dir.to_path_buf()),
8940 refresh_index_stat_in_pass: false,
8941 for_status: false,
8945 config: config.cloned().map(std::sync::Arc::new),
8946 ..DiffIndexToWorktreeOptions::default()
8947 };
8948 let (entries, _, _) = diff_index_to_worktree_inner(
8949 odb,
8950 &mut idx,
8951 work_tree,
8952 repository_git_dir,
8953 options,
8954 false,
8955 None,
8956 )?;
8957 return Ok(entries);
8958 }
8959 }
8960 }
8961
8962 let config = worktree_rules
8963 .map(|r| r.config().clone())
8964 .unwrap_or_else(|| {
8965 config.cloned().unwrap_or_else(|| {
8966 ConfigSet::load(
8967 &crate::environment::Environment::empty(),
8968 Some(repository_git_dir),
8969 true,
8970 )
8971 .unwrap_or_else(|_| ConfigSet::new())
8972 })
8973 });
8974 let conv = worktree_rules
8975 .map(|r| r.conversion().clone())
8976 .unwrap_or_else(|| crlf::ConversionConfig::from_config(&config));
8977 let legacy_attrs = if worktree_rules.is_none() {
8978 Some(crlf::load_gitattributes_parsed(work_tree))
8979 } else {
8980 None
8981 };
8982 let file_attrs_for = |path: &str| -> crlf::FileAttrs {
8983 if let Some(rules) = worktree_rules {
8984 rules.file_attrs(path, false)
8985 } else if let Some(parsed) = legacy_attrs.as_ref() {
8986 crlf::file_attrs_from_gitattributes(&parsed.rules, &parsed.macros, path, false, &config)
8987 } else {
8988 crlf::FileAttrs::default()
8989 }
8990 };
8991 let filter_fp = worktree_rules.map(|r| r.filter_process());
8992
8993 let sparse_index = index.has_sparse_directory_placeholders();
8994 let tree_flat = match tree_oid {
8996 Some(oid) if sparse_index => flatten_tree_with_index(odb, oid, "", Some(index))?,
8997 Some(oid) => flatten_tree(odb, oid, "")?,
8998 None => Vec::new(),
8999 };
9000 let tree_map: std::collections::BTreeMap<String, &FlatEntry> =
9001 tree_flat.iter().map(|e| (e.path.clone(), e)).collect();
9002
9003 let mut index_entries: std::collections::BTreeMap<&[u8], &IndexEntry> =
9005 std::collections::BTreeMap::new();
9006 let mut index_paths: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
9007 let mut stage0_paths: std::collections::BTreeSet<Vec<u8>> = std::collections::BTreeSet::new();
9008 for ie in &index.entries {
9009 if ie.stage() != 0 {
9010 continue;
9011 }
9012 let path = String::from_utf8_lossy(&ie.path).to_string();
9013 index_entries.insert(&ie.path, ie);
9014 index_paths.insert(path);
9015 stage0_paths.insert(ie.path.clone());
9016 }
9017
9018 let mut unmerged_only_paths: std::collections::BTreeSet<String> =
9021 std::collections::BTreeSet::new();
9022 for ie in &index.entries {
9023 if !(1..=3).contains(&ie.stage()) {
9024 continue;
9025 }
9026 if stage0_paths.contains(&ie.path) {
9027 continue;
9028 }
9029 unmerged_only_paths.insert(String::from_utf8_lossy(&ie.path).into_owned());
9030 }
9031
9032 let mut all_paths: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
9034 all_paths.extend(tree_map.keys().cloned());
9035 all_paths.extend(index_paths.iter().cloned());
9036 all_paths.extend(unmerged_only_paths.iter().cloned());
9037
9038 let index_mtime = index_mtime_for_diff(index, None);
9039 let stat_policy = crate::index::StatMatchPolicy::from_config(Some(&config));
9040 let mode_policy = crate::index::WorktreeModePolicy::from_config(&config);
9041 let mut result = Vec::new();
9042
9043 for path in &all_paths {
9044 if extras
9045 .restrict_to_paths
9046 .as_ref()
9047 .is_some_and(|limit| !limit.contains(path))
9048 {
9049 continue;
9050 }
9051 if index_entries
9052 .get(path.as_bytes())
9053 .is_some_and(|ie| ie.skip_worktree())
9054 {
9055 continue;
9058 }
9059 if index_entries
9060 .get(path.as_bytes())
9061 .is_some_and(|ie| ie.assume_unchanged())
9062 {
9063 continue;
9064 }
9065
9066 let tree_entry = tree_map.get(path.as_str());
9067
9068 let is_gitlink = tree_entry.is_some_and(|te| te.mode == 0o160000)
9070 || index_entries
9071 .get(path.as_bytes())
9072 .is_some_and(|ie| ie.mode == 0o160000);
9073 if is_gitlink {
9074 let sub_dir = work_tree.join(path);
9075 let index_gitlink_oid = index_entries
9076 .get(path.as_bytes())
9077 .filter(|ie| ie.mode == 0o160000)
9078 .map(|ie| ie.oid);
9079 match (tree_entry, index_gitlink_oid) {
9080 (Some(te), _) => {
9081 let sub_head = read_submodule_head_oid(&sub_dir);
9082 if sub_head.is_none() && !sub_dir.exists() {
9086 result.push(DiffEntry {
9087 status: DiffStatus::Deleted,
9088 old_path: Some(path.clone()),
9089 new_path: Some(path.clone()),
9090 old_mode: FileMode::new(te.mode),
9091 new_mode: FileMode::ABSENT,
9092 old_oid: te.oid,
9093 new_oid: zero_oid(),
9094 score: None,
9095 });
9096 continue;
9097 }
9098 let ref_matches = if let Some(head) = sub_head {
9099 head == te.oid
9100 } else {
9101 uninitialized_gitlink_worktree_matches_index(&sub_dir)
9102 };
9103 let index_matches_tree = index_gitlink_oid.is_some_and(|oid| oid == te.oid);
9104 let sub_flags = submodule_porcelain_flags(work_tree, path, te.oid);
9107 let dirty_while_aligned =
9108 index_matches_tree && ref_matches && sub_flags.modified;
9109 if !ref_matches || dirty_while_aligned {
9110 let new_oid = if !ref_matches {
9114 sub_head.unwrap_or_else(zero_oid)
9115 } else {
9116 zero_oid()
9117 };
9118 result.push(DiffEntry {
9119 status: DiffStatus::Modified,
9120 old_path: Some(path.clone()),
9121 new_path: Some(path.clone()),
9122 old_mode: FileMode::new(te.mode),
9123 new_mode: FileMode::new(te.mode),
9124 old_oid: te.oid,
9125 new_oid,
9126 score: None,
9127 });
9128 }
9129 }
9130 (None, Some(idx_oid)) => {
9131 let new_oid = read_submodule_head_oid(&sub_dir).unwrap_or(idx_oid);
9136 result.push(DiffEntry {
9137 status: DiffStatus::Added,
9138 old_path: Some(path.clone()),
9139 new_path: Some(path.clone()),
9140 old_mode: FileMode::ABSENT,
9141 new_mode: FileMode::new(0o160000),
9142 old_oid: zero_oid(),
9143 new_oid,
9144 score: None,
9145 });
9146 }
9147 (None, None) => {}
9148 }
9149 continue;
9150 }
9151
9152 let file_path = work_tree.join(path);
9153
9154 let wt_meta = match probe_worktree_symlink_metadata(
9155 &file_path,
9156 path,
9157 extras.diagnostics.as_ref(),
9158 false,
9159 extras.skip_inaccessible_paths,
9160 false,
9161 )? {
9162 WorktreeSymlinkMetadata::Found(m) => Some(m),
9163 WorktreeSymlinkMetadata::Inaccessible => continue,
9164 WorktreeSymlinkMetadata::Absent => None,
9165 };
9166
9167 if unmerged_only_paths.contains(path) {
9168 if let (Some(te), Some(meta)) = (tree_entry, wt_meta.as_ref()) {
9169 let tree_mode = crate::index::canon_mode(te.mode);
9170 let file_attrs = file_attrs_for(path);
9171 let wt_oid = hash_worktree_file(
9172 odb,
9173 &file_path,
9174 meta,
9175 &conv,
9176 &file_attrs,
9177 path,
9178 None,
9179 filter_fp,
9180 )?;
9181 let wt_mode =
9182 mode_policy.index_mode_for_worktree(mode_from_metadata(meta), Some(tree_mode));
9183 if wt_oid != te.oid || wt_mode != tree_mode {
9184 result.push(DiffEntry {
9185 status: DiffStatus::for_content_change(tree_mode, wt_mode),
9186 old_path: Some(path.clone()),
9187 new_path: Some(path.clone()),
9188 old_mode: FileMode::new(tree_mode),
9189 new_mode: FileMode::new(wt_mode),
9190 old_oid: te.oid,
9191 new_oid: wt_oid,
9192 score: None,
9193 });
9194 }
9195 }
9196 continue;
9197 }
9198
9199 match (tree_entry, wt_meta) {
9200 (Some(te), Some(ref meta)) => {
9201 let tree_mode = crate::index::canon_mode(te.mode);
9202 let Some(ie) = index_entries.get(path.as_bytes()) else {
9203 continue;
9204 };
9205 let wt_mode =
9206 mode_policy.index_mode_for_worktree(mode_from_metadata(meta), Some(ie.mode));
9207
9208 let index_matches_tree = ie.oid == te.oid && ie.mode == tree_mode;
9209
9210 let stat_trusted = stat_matches_with_policy(ie, meta, stat_policy)
9212 && !entry_is_racy_with_policy(ie, index_mtime, stat_policy);
9213 if index_matches_tree && stat_trusted && wt_mode == tree_mode {
9214 continue;
9215 }
9216
9217 let file_attrs = file_attrs_for(path);
9218 let idx_ent = index_entries.get(path.as_bytes()).copied();
9219
9220 if ie.oid == te.oid && ie.mode != tree_mode && stat_trusted && wt_mode == ie.mode {
9225 result.push(DiffEntry {
9226 status: DiffStatus::for_content_change(tree_mode, ie.mode),
9227 old_path: Some(path.clone()),
9228 new_path: Some(path.clone()),
9229 old_mode: FileMode::new(tree_mode),
9230 new_mode: FileMode::new(ie.mode),
9231 old_oid: te.oid,
9232 new_oid: te.oid,
9233 score: None,
9234 });
9235 continue;
9236 }
9237
9238 if index_matches_tree {
9241 let wt_oid = hash_worktree_file(
9242 odb,
9243 &file_path,
9244 meta,
9245 &conv,
9246 &file_attrs,
9247 path,
9248 None,
9249 filter_fp,
9250 )?;
9251 if wt_oid != te.oid {
9252 result.push(DiffEntry {
9253 status: DiffStatus::for_content_change(tree_mode, wt_mode),
9254 old_path: Some(path.clone()),
9255 new_path: Some(path.clone()),
9256 old_mode: FileMode::new(tree_mode),
9257 new_mode: FileMode::new(wt_mode),
9258 old_oid: te.oid,
9259 new_oid: zero_oid(),
9262 score: None,
9263 });
9264 } else if wt_mode != tree_mode {
9265 result.push(DiffEntry {
9266 status: DiffStatus::for_content_change(tree_mode, wt_mode),
9267 old_path: Some(path.clone()),
9268 new_path: Some(path.clone()),
9269 old_mode: FileMode::new(tree_mode),
9270 new_mode: FileMode::new(wt_mode),
9271 old_oid: te.oid,
9272 new_oid: te.oid,
9273 score: None,
9274 });
9275 }
9276 continue;
9277 }
9278
9279 let wt_oid = hash_worktree_file(
9281 odb,
9282 &file_path,
9283 meta,
9284 &conv,
9285 &file_attrs,
9286 path,
9287 idx_ent,
9288 filter_fp,
9289 )?;
9290 if wt_oid != te.oid || wt_mode != tree_mode {
9291 let new_oid = if wt_oid != te.oid { zero_oid() } else { te.oid };
9292 result.push(DiffEntry {
9293 status: DiffStatus::for_content_change(tree_mode, wt_mode),
9294 old_path: Some(path.clone()),
9295 new_path: Some(path.clone()),
9296 old_mode: FileMode::new(tree_mode),
9297 new_mode: FileMode::new(wt_mode),
9298 old_oid: te.oid,
9299 new_oid,
9300 score: None,
9301 });
9302 }
9303 }
9304 (Some(te), None) => {
9305 let tree_mode = crate::index::canon_mode(te.mode);
9306 result.push(DiffEntry {
9308 status: DiffStatus::Deleted,
9309 old_path: Some(path.clone()),
9310 new_path: None,
9311 old_mode: FileMode::new(tree_mode),
9312 new_mode: FileMode::ABSENT,
9313 old_oid: te.oid,
9314 new_oid: zero_oid(),
9315 score: None,
9316 });
9317 }
9318 (None, Some(ref meta)) => {
9319 let file_attrs = file_attrs_for(path);
9321 let idx_ent = index_entries.get(path.as_bytes()).copied();
9322 let wt_oid = materialize_worktree_blob(
9323 odb,
9324 &file_path,
9325 meta,
9326 &conv,
9327 &file_attrs,
9328 path,
9329 idx_ent,
9330 filter_fp,
9331 )?;
9332 let wt_mode = mode_from_metadata(meta);
9333 result.push(DiffEntry {
9334 status: DiffStatus::Added,
9335 old_path: None,
9336 new_path: Some(path.clone()),
9337 old_mode: FileMode::ABSENT,
9338 new_mode: FileMode::new(wt_mode),
9339 old_oid: zero_oid(),
9340 new_oid: wt_oid,
9341 score: None,
9342 });
9343 }
9344 (None, None) => {
9345 }
9347 }
9348 }
9349
9350 sort_entries_by_path(&mut result);
9351 Ok(result)
9352}
9353
9354#[derive(Debug, Clone, PartialEq, Eq)]
9358pub struct InexactRenameDetectionSkipped {
9359 pub unmatched_sources: usize,
9361 pub unmatched_destinations: usize,
9363 pub configured_limit: Option<usize>,
9365 pub suggested_limit: usize,
9367}
9368
9369#[derive(Debug, Clone, Default, PartialEq, Eq)]
9371pub struct DiffEntriesResult {
9372 pub entries: Vec<DiffEntry>,
9374 pub inexact_rename_skipped: Option<InexactRenameDetectionSkipped>,
9376}
9377
9378#[derive(Debug, Clone, PartialEq, Eq)]
9380pub struct RenameDetectionResult {
9381 pub entries: Vec<DiffEntry>,
9383 pub inexact_skipped: Option<InexactRenameDetectionSkipped>,
9385}
9386
9387#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
9389pub struct DetectRenamesOptions {
9390 pub skip_basename_heuristic: bool,
9392}
9393
9394fn read_added_entry_bytes(
9395 odb: &Odb,
9396 entry: &DiffEntry,
9397 work_root: Option<&Path>,
9398) -> Option<Vec<u8>> {
9399 if entry.new_oid != zero_oid() {
9400 if let Ok(obj) = odb.read(&entry.new_oid) {
9401 return Some(obj.data);
9402 }
9403 }
9404 let path = entry.new_path.as_deref()?;
9405 let root = work_root?;
9406 fs::read(root.join(path)).ok()
9407}
9408
9409#[allow(clippy::expect_used)]
9421pub fn detect_renames(
9422 odb: &Odb,
9423 work_root: Option<&Path>,
9424 entries: Vec<DiffEntry>,
9425 threshold: u32,
9426) -> Vec<DiffEntry> {
9427 let config = ConfigSet::new();
9428 let policy = ThreadPolicy::default();
9429 detect_renames_with_policy(
9430 odb,
9431 work_root,
9432 entries,
9433 threshold,
9434 &config,
9435 &policy,
9436 None,
9437 DetectRenamesOptions::default(),
9438 )
9439 .expect("rename detection")
9440 .entries
9441}
9442
9443#[inline]
9444fn poll_rename_cancel(
9445 cancel_watch: Option<&std::sync::atomic::AtomicBool>,
9446) -> crate::error::Result<()> {
9447 if let Some(watch) = cancel_watch {
9448 crate::progress::poll_watch(watch)?;
9449 }
9450 Ok(())
9451}
9452
9453#[allow(clippy::too_many_arguments)]
9464pub fn detect_renames_with_policy(
9465 odb: &Odb,
9466 work_root: Option<&Path>,
9467 entries: Vec<DiffEntry>,
9468 threshold: u32,
9469 config: &ConfigSet,
9470 thread_policy: &ThreadPolicy,
9471 cancel_watch: Option<&std::sync::atomic::AtomicBool>,
9472 detect_opts: DetectRenamesOptions,
9473) -> Result<RenameDetectionResult> {
9474 poll_rename_cancel(cancel_watch)?;
9475 let has_deleted = entries.iter().any(|e| e.status == DiffStatus::Deleted);
9476 let has_added = entries.iter().any(|e| e.status == DiffStatus::Added);
9477 if (!has_deleted || !has_added) && entries_strictly_sorted_by_path(&entries) {
9478 return Ok(RenameDetectionResult {
9480 entries,
9481 inexact_skipped: None,
9482 });
9483 }
9484 let mut deleted: Vec<DiffEntry> = Vec::new();
9486 let mut added: Vec<DiffEntry> = Vec::new();
9487 let mut others: Vec<DiffEntry> = Vec::new();
9488
9489 for entry in entries {
9490 match entry.status {
9491 DiffStatus::Deleted => deleted.push(entry),
9492 DiffStatus::Added => added.push(entry),
9493 _ => others.push(entry),
9494 }
9495 }
9496
9497 if deleted.is_empty() || added.is_empty() {
9498 let mut result = others;
9500 result.extend(deleted);
9501 result.extend(added);
9502 sort_entries_by_path(&mut result);
9503 return Ok(RenameDetectionResult {
9504 entries: result,
9505 inexact_skipped: None,
9506 });
9507 }
9508
9509 fn is_regularish_mode(mode: FileMode) -> bool {
9510 mode == FileMode::REGULAR || mode == FileMode::EXECUTABLE
9511 }
9512
9513 fn same_path_same_blob(del: &DiffEntry, add: &DiffEntry) -> bool {
9514 del.old_path == add.new_path && del.old_oid == add.new_oid && del.old_mode == add.new_mode
9515 }
9516
9517 fn sort_rename_scores(
9518 scores: &mut [(u32, usize, usize)],
9519 deleted: &[DiffEntry],
9520 added: &[DiffEntry],
9521 ) {
9522 scores.sort_by(|a, b| {
9523 let a_noop = same_path_same_blob(&deleted[a.1], &added[a.2]);
9524 let b_noop = same_path_same_blob(&deleted[b.1], &added[b.2]);
9525 let a_same = same_basename(&deleted[a.1], &added[a.2]);
9526 let b_same = same_basename(&deleted[b.1], &added[b.2]);
9527 a_noop
9528 .cmp(&b_noop)
9529 .then_with(|| b_same.cmp(&a_same))
9530 .then_with(|| b.0.cmp(&a.0))
9531 });
9532 }
9533
9534 fn apply_rename_scores(
9535 scores: &[(u32, usize, usize)],
9536 deleted: &[DiffEntry],
9537 added: &[DiffEntry],
9538 used_deleted: &mut [bool],
9539 used_added: &mut [bool],
9540 renames: &mut Vec<DiffEntry>,
9541 ) {
9542 for (score, di, ai) in scores {
9543 if used_deleted[*di] || used_added[*ai] {
9544 continue;
9545 }
9546 used_deleted[*di] = true;
9547 used_added[*ai] = true;
9548
9549 let del = &deleted[*di];
9550 let add = &added[*ai];
9551 if same_path_same_blob(del, add) {
9552 continue;
9553 }
9554
9555 renames.push(DiffEntry {
9556 status: DiffStatus::Renamed,
9557 old_path: del.old_path.clone(),
9558 new_path: add.new_path.clone(),
9559 old_mode: del.old_mode,
9560 new_mode: add.new_mode,
9561 old_oid: del.old_oid,
9562 new_oid: add.new_oid,
9563 score: Some(*score),
9564 });
9565 }
9566 }
9567
9568 let mut used_deleted = vec![false; deleted.len()];
9569 let mut used_added = vec![false; added.len()];
9570 let mut renames: Vec<DiffEntry> = Vec::new();
9571
9572 let mut added_by_oid: std::collections::HashMap<ObjectId, Vec<usize>> =
9574 std::collections::HashMap::new();
9575 for (ai, add) in added.iter().enumerate() {
9576 added_by_oid.entry(add.new_oid).or_default().push(ai);
9577 }
9578 let mut exact_scores: Vec<(u32, usize, usize)> = Vec::new();
9579 for (di, del) in deleted.iter().enumerate() {
9580 poll_rename_cancel(cancel_watch)?;
9581 if let Some(ais) = added_by_oid.get(&del.old_oid) {
9582 for &ai in ais {
9583 exact_scores.push((100, di, ai));
9584 }
9585 }
9586 }
9587 sort_rename_scores(&mut exact_scores, &deleted, &added);
9588 apply_rename_scores(
9589 &exact_scores,
9590 &deleted,
9591 &added,
9592 &mut used_deleted,
9593 &mut used_added,
9594 &mut renames,
9595 );
9596
9597 let mut blob_cache = None;
9598 if !detect_opts.skip_basename_heuristic
9599 && super::basename_rename::basename_needs_similarity_scoring(
9600 &deleted,
9601 &added,
9602 &used_deleted,
9603 &used_added,
9604 )
9605 {
9606 let mut cache = super::rename_blob_cache::RenameBlobCache::new(deleted.len(), added.len());
9607 let par_opts = diff_rename_parallel_options(config, thread_policy);
9608 super::basename_rename::apply_basename_rename_pairs(
9609 odb,
9610 work_root,
9611 &deleted,
9612 &added,
9613 &mut used_deleted,
9614 &mut used_added,
9615 &mut renames,
9616 threshold,
9617 cancel_watch,
9618 &mut cache,
9619 par_opts,
9620 )?;
9621 blob_cache = Some(cache);
9622 }
9623
9624 let remaining_deleted: Vec<usize> = used_deleted
9625 .iter()
9626 .enumerate()
9627 .filter_map(|(i, used)| (!*used).then_some(i))
9628 .collect();
9629 let remaining_added: Vec<usize> = used_added
9630 .iter()
9631 .enumerate()
9632 .filter_map(|(i, used)| (!*used).then_some(i))
9633 .collect();
9634
9635 let rename_limit = parse_rename_limit(config);
9636 let mut inexact_skipped = None;
9637 if !remaining_deleted.is_empty()
9638 && !remaining_added.is_empty()
9639 && inexact_matrix_exceeds_limit(
9640 remaining_added.len(),
9641 remaining_deleted.len(),
9642 rename_limit,
9643 )
9644 {
9645 let suggested = remaining_deleted.len().max(remaining_added.len());
9646 inexact_skipped = Some(InexactRenameDetectionSkipped {
9647 unmatched_sources: remaining_deleted.len(),
9648 unmatched_destinations: remaining_added.len(),
9649 configured_limit: rename_limit.configured_max(),
9650 suggested_limit: suggested,
9651 });
9652 } else if !remaining_deleted.is_empty() && !remaining_added.is_empty() {
9653 let par_opts = diff_rename_parallel_options(config, thread_policy);
9654
9655 let mut prefetch_entries: Vec<DiffEntry> =
9656 Vec::with_capacity(remaining_deleted.len() + remaining_added.len());
9657 for &di in &remaining_deleted {
9658 prefetch_entries.push(deleted[di].clone());
9659 }
9660 for &ai in &remaining_added {
9661 prefetch_entries.push(added[ai].clone());
9662 }
9663 super::tree_content::prefetch_tree_diff_blobs(odb, &prefetch_entries)?;
9664
9665 let deleted_sizes: Vec<Option<usize>> = remaining_deleted
9666 .iter()
9667 .map(|&di| blob_payload_size(odb, &deleted[di].old_oid))
9668 .collect();
9669 let added_sizes: Vec<Option<usize>> = remaining_added
9670 .iter()
9671 .map(|&ai| added_entry_payload_size(odb, &added[ai], work_root))
9672 .collect();
9673
9674 let mut load_deleted = vec![false; remaining_deleted.len()];
9675 let mut load_added = vec![false; remaining_added.len()];
9676 let mut candidate_pairs: Vec<(usize, usize)> = Vec::new();
9677 for (row, &di) in remaining_deleted.iter().enumerate() {
9678 poll_rename_cancel(cancel_watch)?;
9679 let del = &deleted[di];
9680 if !is_regularish_mode(del.old_mode) {
9681 continue;
9682 }
9683 let Some(ds) = deleted_sizes[row] else {
9684 continue;
9685 };
9686 for (col, &ai) in remaining_added.iter().enumerate() {
9687 let add = &added[ai];
9688 if !is_regularish_mode(add.new_mode) {
9689 continue;
9690 }
9691 let Some(asz) = added_sizes[col] else {
9692 continue;
9693 };
9694 if rename_pair_size_reachable(ds, asz, threshold) {
9695 load_deleted[row] = true;
9696 load_added[col] = true;
9697 candidate_pairs.push((row, col));
9698 }
9699 }
9700 }
9701
9702 if !candidate_pairs.is_empty() {
9703 let loads_needed = load_deleted.iter().filter(|&&b| b).count()
9704 + load_added.iter().filter(|&&b| b).count();
9705 let (deleted_contents, added_contents) = if let Some(cache) = blob_cache.as_mut() {
9706 let mut deleted_staging: Vec<Option<Vec<u8>>> = vec![None; remaining_deleted.len()];
9707 for (row, &need) in load_deleted.iter().enumerate() {
9708 if !need {
9709 continue;
9710 }
9711 let di = remaining_deleted[row];
9712 deleted_staging[row] = cache
9713 .take_deleted_blob(di)
9714 .or_else(|| odb.read(&deleted[di].old_oid).ok().map(|obj| obj.data));
9715 }
9716 let mut added_staging: Vec<Option<Vec<u8>>> = vec![None; remaining_added.len()];
9717 for (col, &need) in load_added.iter().enumerate() {
9718 if !need {
9719 continue;
9720 }
9721 let ai = remaining_added[col];
9722 added_staging[col] = cache
9723 .take_added_blob(ai)
9724 .or_else(|| read_added_entry_bytes(odb, &added[ai], work_root));
9725 }
9726 let load_deleted_blob = |i: usize| -> Option<Vec<u8>> {
9727 if !load_deleted[i] {
9728 return None;
9729 }
9730 deleted_staging[i].clone()
9731 };
9732 let load_added_blob = |i: usize| -> Option<Vec<u8>> {
9733 if !load_added[i] {
9734 return None;
9735 }
9736 added_staging[i].clone()
9737 };
9738 if loads_needed <= 32 {
9739 (
9740 (0..remaining_deleted.len())
9741 .map(load_deleted_blob)
9742 .collect(),
9743 (0..remaining_added.len()).map(load_added_blob).collect(),
9744 )
9745 } else {
9746 (
9747 parallel_load_optional_blobs(
9748 odb,
9749 remaining_deleted.len(),
9750 par_opts,
9751 0,
9752 load_deleted_blob,
9753 ),
9754 parallel_load_optional_blobs(
9755 odb,
9756 remaining_added.len(),
9757 par_opts,
9758 0,
9759 load_added_blob,
9760 ),
9761 )
9762 }
9763 } else {
9764 let mut deleted_staging: Vec<Option<Vec<u8>>> = vec![None; remaining_deleted.len()];
9765 for (row, &need) in load_deleted.iter().enumerate() {
9766 if !need {
9767 continue;
9768 }
9769 let di = remaining_deleted[row];
9770 deleted_staging[row] = odb.read(&deleted[di].old_oid).ok().map(|obj| obj.data);
9771 }
9772 let mut added_staging: Vec<Option<Vec<u8>>> = vec![None; remaining_added.len()];
9773 for (col, &need) in load_added.iter().enumerate() {
9774 if !need {
9775 continue;
9776 }
9777 let ai = remaining_added[col];
9778 added_staging[col] = read_added_entry_bytes(odb, &added[ai], work_root);
9779 }
9780 let load_deleted_blob = |i: usize| -> Option<Vec<u8>> {
9781 if !load_deleted[i] {
9782 return None;
9783 }
9784 deleted_staging[i].clone()
9785 };
9786 let load_added_blob = |i: usize| -> Option<Vec<u8>> {
9787 if !load_added[i] {
9788 return None;
9789 }
9790 added_staging[i].clone()
9791 };
9792 if loads_needed <= 32 {
9793 (
9794 (0..remaining_deleted.len())
9795 .map(load_deleted_blob)
9796 .collect(),
9797 (0..remaining_added.len()).map(load_added_blob).collect(),
9798 )
9799 } else {
9800 (
9801 parallel_load_optional_blobs(
9802 odb,
9803 remaining_deleted.len(),
9804 par_opts,
9805 0,
9806 load_deleted_blob,
9807 ),
9808 parallel_load_optional_blobs(
9809 odb,
9810 remaining_added.len(),
9811 par_opts,
9812 0,
9813 load_added_blob,
9814 ),
9815 )
9816 }
9817 };
9818
9819 let est_table_bytes: usize = deleted_contents
9820 .iter()
9821 .chain(added_contents.iter())
9822 .filter_map(|opt| opt.as_ref().map(|b| b.len()))
9823 .sum();
9824 let build_deleted_table = |row: usize| -> std::result::Result<
9825 Option<RenameLineTable>,
9826 std::convert::Infallible,
9827 > {
9828 Ok(deleted_contents[row]
9829 .as_ref()
9830 .map(|d| RenameLineTable::from_blob(d)))
9831 };
9832 let build_added_table = |col: usize| -> std::result::Result<
9833 Option<RenameLineTable>,
9834 std::convert::Infallible,
9835 > {
9836 Ok(added_contents[col]
9837 .as_ref()
9838 .map(|d| RenameLineTable::from_blob(d)))
9839 };
9840 let deleted_tables: Vec<Option<RenameLineTable>> = if loads_needed <= 32 {
9841 (0..remaining_deleted.len())
9842 .map(|row| build_deleted_table(row).unwrap())
9843 .collect()
9844 } else {
9845 parallel_map_indices(
9846 odb,
9847 remaining_deleted.len(),
9848 par_opts,
9849 est_table_bytes,
9850 build_deleted_table,
9851 )
9852 .expect("deleted rename line tables")
9853 };
9854 let added_tables: Vec<Option<RenameLineTable>> = if loads_needed <= 32 {
9855 (0..remaining_added.len())
9856 .map(|col| build_added_table(col).unwrap())
9857 .collect()
9858 } else {
9859 parallel_map_indices(
9860 odb,
9861 remaining_added.len(),
9862 par_opts,
9863 est_table_bytes,
9864 build_added_table,
9865 )
9866 .expect("added rename line tables")
9867 };
9868
9869 let mut inexact_scores: Vec<(u32, usize, usize)> = Vec::new();
9870 for (row, col) in candidate_pairs {
9871 poll_rename_cancel(cancel_watch)?;
9872 let di = remaining_deleted[row];
9873 let ai = remaining_added[col];
9874 let (Some(old_table), Some(new_table)) = (&deleted_tables[row], &added_tables[col])
9875 else {
9876 continue;
9877 };
9878 let score = RenameLineTable::similarity_score(old_table, new_table);
9879 if score >= threshold {
9880 inexact_scores.push((score, di, ai));
9881 }
9882 }
9883 sort_rename_scores(&mut inexact_scores, &deleted, &added);
9884 apply_rename_scores(
9885 &inexact_scores,
9886 &deleted,
9887 &added,
9888 &mut used_deleted,
9889 &mut used_added,
9890 &mut renames,
9891 );
9892 }
9893 }
9894
9895 let mut result = others;
9897 result.extend(renames);
9898 for (i, entry) in deleted.into_iter().enumerate() {
9899 if !used_deleted[i] {
9900 result.push(entry);
9901 }
9902 }
9903 for (i, entry) in added.into_iter().enumerate() {
9904 if !used_added[i] {
9905 result.push(entry);
9906 }
9907 }
9908
9909 result = prune_break_deletions_obviated_by_renames(result);
9910 sort_entries_by_path(&mut result);
9911 Ok(RenameDetectionResult {
9912 entries: result,
9913 inexact_skipped,
9914 })
9915}
9916
9917pub fn detect_copies(
9932 odb: &Odb,
9933 work_root: Option<&Path>,
9934 entries: Vec<DiffEntry>,
9935 threshold: u32,
9936 find_copies_harder: bool,
9937 source_tree_entries: &[(String, FileMode, ObjectId)],
9938) -> Result<Vec<DiffEntry>> {
9939 let config = ConfigSet::new();
9940 let policy = ThreadPolicy::default();
9941 detect_copies_with_policy(
9942 odb,
9943 work_root,
9944 entries,
9945 threshold,
9946 find_copies_harder,
9947 source_tree_entries,
9948 &config,
9949 &policy,
9950 )
9951}
9952
9953#[expect(clippy::too_many_arguments)]
9959pub fn detect_copies_with_policy(
9960 odb: &Odb,
9961 work_root: Option<&Path>,
9962 entries: Vec<DiffEntry>,
9963 threshold: u32,
9964 find_copies_harder: bool,
9965 source_tree_entries: &[(String, FileMode, ObjectId)],
9966 config: &ConfigSet,
9967 thread_policy: &ThreadPolicy,
9968) -> Result<Vec<DiffEntry>> {
9969 use std::collections::{HashMap, HashSet};
9970
9971 let mut deleted: Vec<DiffEntry> = Vec::new();
9973 let mut added: Vec<DiffEntry> = Vec::new();
9974 let mut others: Vec<DiffEntry> = Vec::new();
9975
9976 for entry in entries {
9977 match entry.status {
9978 DiffStatus::Deleted => deleted.push(entry),
9979 DiffStatus::Added => added.push(entry),
9980 _ => others.push(entry),
9981 }
9982 }
9983
9984 let mut sources: Vec<(String, ObjectId, bool)> = Vec::new(); let mut deleted_source_idx: HashMap<String, usize> = HashMap::new();
9988
9989 for entry in &deleted {
9990 if let Some(ref path) = entry.old_path {
9991 deleted_source_idx.insert(path.clone(), sources.len());
9992 sources.push((path.clone(), entry.old_oid, true));
9993 }
9994 }
9995
9996 for entry in &others {
9999 if matches!(entry.status, DiffStatus::Modified | DiffStatus::TypeChanged) {
10000 if let Some(ref old_path) = entry.old_path {
10001 if !sources.iter().any(|(p, _, _)| p == old_path) {
10002 sources.push((old_path.clone(), entry.old_oid, false));
10003 }
10004 }
10005 }
10006 }
10007
10008 if find_copies_harder {
10010 for (path, _mode, oid) in source_tree_entries {
10011 if !sources.iter().any(|(p, _, _)| p == path) {
10012 sources.push((path.clone(), *oid, false));
10013 }
10014 }
10015 }
10016
10017 let mut source_modes: Vec<(String, FileMode, ObjectId)> = source_tree_entries.to_vec();
10020 for entry in deleted.iter().chain(&others) {
10021 if let Some(ref path) = entry.old_path {
10022 if !source_modes.iter().any(|(p, _, _)| p == path) {
10023 source_modes.push((path.clone(), entry.old_mode, entry.old_oid));
10024 }
10025 }
10026 }
10027
10028 if sources.is_empty() {
10029 let mut result = others;
10030 result.extend(deleted);
10031 result.extend(added);
10032 sort_entries_by_path(&mut result);
10033 return Ok(result);
10034 }
10035
10036 let par_opts = diff_rename_parallel_options(config, thread_policy);
10037 let rename_limit = parse_rename_limit(config);
10038
10039 fn apply_copy_score_assignments(
10040 scores: &[(u32, usize, usize)],
10041 sources: &[(String, ObjectId, bool)],
10042 added: &[DiffEntry],
10043 source_tree_entries: &[(String, FileMode, ObjectId)],
10044 used_added2: &mut [bool],
10045 renamed_deleted: &mut HashSet<usize>,
10046 result_entries: &mut Vec<DiffEntry>,
10047 ) {
10048 let mut used_added = vec![false; added.len()];
10049 let mut source_to_added: HashMap<usize, Vec<(usize, u32)>> = HashMap::new();
10050 for &(score, si, ai) in scores {
10051 if used_added[ai] {
10052 continue;
10053 }
10054 used_added[ai] = true;
10055 source_to_added.entry(si).or_default().push((ai, score));
10056 }
10057
10058 for (&si, assignments_for_src) in &source_to_added {
10059 let (_, _, is_deleted) = &sources[si];
10060 if *is_deleted && !assignments_for_src.is_empty() {
10061 let rename_ai = assignments_for_src
10062 .iter()
10063 .max_by_key(|(ai, _score)| added[*ai].path().to_string())
10064 .map(|(ai, _)| *ai);
10065
10066 for &(ai, score) in assignments_for_src {
10067 let (ref src_path, _, _) = sources[si];
10068 let add = &added[ai];
10069 let src_mode = source_tree_entries
10070 .iter()
10071 .find(|(p, _, _)| p == src_path)
10072 .map_or(add.old_mode, |(_, m, _)| *m);
10073
10074 let is_rename = Some(ai) == rename_ai;
10075 result_entries.push(DiffEntry {
10076 status: if is_rename {
10077 DiffStatus::Renamed
10078 } else {
10079 DiffStatus::Copied
10080 },
10081 old_path: Some(src_path.clone()),
10082 new_path: add.new_path.clone(),
10083 old_mode: src_mode,
10084 new_mode: add.new_mode,
10085 old_oid: sources[si].1,
10086 new_oid: add.new_oid,
10087 score: Some(score),
10088 });
10089 used_added2[ai] = true;
10090 }
10091 renamed_deleted.insert(si);
10092 } else {
10093 for &(ai, score) in assignments_for_src {
10094 let (ref src_path, _, _) = sources[si];
10095 let add = &added[ai];
10096 let src_mode = source_tree_entries
10097 .iter()
10098 .find(|(p, _, _)| p == src_path)
10099 .map_or(add.old_mode, |(_, m, _)| *m);
10100
10101 result_entries.push(DiffEntry {
10102 status: DiffStatus::Copied,
10103 old_path: Some(src_path.clone()),
10104 new_path: add.new_path.clone(),
10105 old_mode: src_mode,
10106 new_mode: add.new_mode,
10107 old_oid: sources[si].1,
10108 new_oid: add.new_oid,
10109 score: Some(score),
10110 });
10111 used_added2[ai] = true;
10112 }
10113 }
10114 }
10115 }
10116
10117 fn added_entry_oid(
10118 odb: &Odb,
10119 add: &DiffEntry,
10120 added_contents: &[Option<Vec<u8>>],
10121 ai: usize,
10122 ) -> Result<ObjectId> {
10123 if add.new_oid != zero_oid() {
10124 Ok(add.new_oid)
10125 } else if let Some(ref data) = added_contents[ai] {
10126 odb.hash(ObjectKind::Blob, data)
10127 } else {
10128 Ok(zero_oid())
10129 }
10130 }
10131
10132 let mut result_entries: Vec<DiffEntry> = Vec::new();
10133 let mut renamed_deleted: HashSet<usize> = HashSet::new();
10134 let mut used_added2 = vec![false; added.len()];
10135 let mut source_contents = vec![None; sources.len()];
10136
10137 if !added.is_empty() {
10138 let added_contents = parallel_load_optional_blobs(odb, added.len(), par_opts, 0, |ai| {
10139 read_added_entry_bytes(odb, &added[ai], work_root)
10140 });
10141
10142 let mut exact_scores: Vec<(u32, usize, usize)> = Vec::new();
10144 for (si, (src_path, src_oid, _)) in sources.iter().enumerate() {
10145 for (ai, add) in added.iter().enumerate() {
10146 if add.new_path.as_deref() == Some(src_path.as_str()) {
10147 continue;
10148 }
10149 let add_oid = added_entry_oid(odb, add, &added_contents, ai)?;
10150 if *src_oid == add_oid && add_oid != zero_oid() {
10151 exact_scores.push((100, si, ai));
10152 }
10153 }
10154 }
10155 exact_scores.sort_by_key(|a| std::cmp::Reverse(a.0));
10156 apply_copy_score_assignments(
10157 &exact_scores,
10158 &sources,
10159 &added,
10160 &source_modes,
10161 &mut used_added2,
10162 &mut renamed_deleted,
10163 &mut result_entries,
10164 );
10165
10166 let remaining_added: Vec<usize> = used_added2
10167 .iter()
10168 .enumerate()
10169 .filter_map(|(i, used)| (!*used).then_some(i))
10170 .collect();
10171 let remaining_source_count = sources
10172 .iter()
10173 .enumerate()
10174 .filter(|(si, (_, _, is_deleted))| !*is_deleted || !renamed_deleted.contains(si))
10175 .count();
10176
10177 let skip_inexact = remaining_added.is_empty()
10178 || inexact_matrix_exceeds_limit(
10179 remaining_added.len(),
10180 remaining_source_count,
10181 rename_limit,
10182 );
10183
10184 if !skip_inexact {
10185 source_contents = parallel_load_optional_blobs(odb, sources.len(), par_opts, 0, |si| {
10186 odb.read(&sources[si].1).ok().map(|obj| obj.data)
10187 });
10188
10189 let est_bytes: usize = source_contents
10190 .iter()
10191 .chain(added_contents.iter())
10192 .filter_map(|opt| opt.as_ref().map(|b| b.len()))
10193 .sum();
10194
10195 let source_tables: Vec<Option<RenameLineTable>> = parallel_map_indices(
10196 odb,
10197 sources.len(),
10198 par_opts,
10199 est_bytes,
10200 |si| -> std::result::Result<Option<RenameLineTable>, std::convert::Infallible> {
10201 Ok(source_contents[si]
10202 .as_ref()
10203 .map(|d| RenameLineTable::from_blob(d)))
10204 },
10205 )
10206 .expect("copy source line tables");
10207 let added_tables: Vec<Option<RenameLineTable>> = parallel_map_indices(
10208 odb,
10209 added.len(),
10210 par_opts,
10211 est_bytes,
10212 |ai| -> std::result::Result<Option<RenameLineTable>, std::convert::Infallible> {
10213 Ok(added_contents[ai]
10214 .as_ref()
10215 .map(|d| RenameLineTable::from_blob(d)))
10216 },
10217 )
10218 .expect("copy added line tables");
10219
10220 let score_rows: Vec<Vec<(u32, usize, usize)>> = parallel_map_indices(
10221 odb,
10222 sources.len(),
10223 par_opts,
10224 est_bytes,
10225 |si| -> std::result::Result<Vec<(u32, usize, usize)>, std::convert::Infallible> {
10226 if renamed_deleted.contains(&si) {
10227 return Ok(Vec::new());
10228 }
10229 let (src_path, _, _) = &sources[si];
10230 let mut row: Vec<(u32, usize, usize)> = Vec::new();
10231 for &ai in &remaining_added {
10232 let add = &added[ai];
10233 if add.new_path.as_deref() == Some(src_path.as_str()) {
10234 continue;
10235 }
10236 let score = match (&source_tables[si], &added_tables[ai]) {
10237 (Some(old_table), Some(new_table)) => {
10238 RenameLineTable::similarity_score(old_table, new_table)
10239 }
10240 _ => 0,
10241 };
10242 if score >= threshold {
10243 row.push((score, si, ai));
10244 }
10245 }
10246 Ok(row)
10247 },
10248 )
10249 .expect("copy score rows");
10250 let mut inexact_scores: Vec<(u32, usize, usize)> =
10251 score_rows.into_iter().flatten().collect();
10252 inexact_scores.sort_by_key(|a| std::cmp::Reverse(a.0));
10253 apply_copy_score_assignments(
10254 &inexact_scores,
10255 &sources,
10256 &added,
10257 &source_modes,
10258 &mut used_added2,
10259 &mut renamed_deleted,
10260 &mut result_entries,
10261 );
10262 }
10263 }
10264
10265 for entry in deleted.into_iter() {
10267 if let Some(ref path) = entry.old_path {
10268 if let Some(&si) = deleted_source_idx.get(path) {
10269 if renamed_deleted.contains(&si) {
10270 continue;
10272 }
10273 }
10274 }
10275 result_entries.push(entry);
10276 }
10277
10278 let mut result = others;
10279 result.extend(result_entries);
10280 for (i, entry) in added.into_iter().enumerate() {
10282 if !used_added2[i] {
10283 result.push(entry);
10284 }
10285 }
10286
10287 let mut final_result = result;
10291 sort_entries_by_path(&mut final_result);
10292 Ok(final_result)
10293}
10294
10295#[derive(Debug, Clone, Copy)]
10297pub struct DiffPostprocessOptions {
10298 pub break_score: Option<u64>,
10301 pub rename_threshold: u32,
10303 pub copy_threshold: Option<u32>,
10305 pub find_copies_harder: bool,
10307 pub materialize_break_worktree_new_oid: bool,
10310}
10311
10312impl Default for DiffPostprocessOptions {
10313 fn default() -> Self {
10314 Self {
10315 break_score: None,
10316 rename_threshold: 100,
10317 copy_threshold: None,
10318 find_copies_harder: false,
10319 materialize_break_worktree_new_oid: false,
10320 }
10321 }
10322}
10323
10324#[derive(Debug, Clone, Copy)]
10327pub struct DiffPostprocessWorktree<'a> {
10328 pub repository_git_dir: &'a Path,
10330 pub index: &'a Index,
10332}
10333
10334fn blob_bytes(odb: &Odb, oid: &ObjectId) -> Result<Vec<u8>> {
10335 Ok(odb.read(oid)?.data)
10336}
10337
10338fn read_new_side_bytes(
10343 odb: &Odb,
10344 entry: &DiffEntry,
10345 work_root: Option<&Path>,
10346 worktree: Option<&DiffPostprocessWorktree<'_>>,
10347 config: &ConfigSet,
10348) -> Result<Vec<u8>> {
10349 if entry.new_oid != zero_oid() {
10350 return blob_bytes(odb, &entry.new_oid);
10351 }
10352 let path = entry.new_path.as_deref().ok_or_else(|| {
10353 Error::ObjectNotFound("modified entry missing new_path for worktree read".into())
10354 })?;
10355 let root = work_root.ok_or_else(|| {
10356 Error::ObjectNotFound(format!(
10357 "worktree bytes required for {} but no work_root was provided",
10358 path
10359 ))
10360 })?;
10361 if let Some(wctx) = worktree {
10362 let mode = entry.new_mode.bits();
10363 let index_entry = wctx.index.get(path.as_bytes(), 0);
10364 return read_worktree_side_bytes_for_diff(odb, root, path, mode, config, index_entry, None);
10365 }
10366 fs::read(root.join(path)).map_err(Error::Io)
10367}
10368
10369fn worktree_side_differs_after_normalize(
10371 odb: &Odb,
10372 entry: &DiffEntry,
10373 work_root: &Path,
10374 worktree: &DiffPostprocessWorktree<'_>,
10375 config: &ConfigSet,
10376) -> Result<bool> {
10377 let path = entry.new_path.as_deref().ok_or_else(|| {
10378 Error::ObjectNotFound("modified entry missing new_path for worktree read".into())
10379 })?;
10380 let raw = fs::read(work_root.join(path)).map_err(Error::Io)?;
10381 let canonical = read_new_side_bytes(odb, entry, Some(work_root), Some(worktree), config)?;
10382 Ok(raw != canonical)
10383}
10384
10385fn split_broken_modifications(
10386 odb: &Odb,
10387 work_root: Option<&Path>,
10388 entries: Vec<DiffEntry>,
10389 break_score: u64,
10390 worktree: Option<&DiffPostprocessWorktree<'_>>,
10391 config: &ConfigSet,
10392) -> Result<Vec<DiffEntry>> {
10393 let mut out = Vec::with_capacity(entries.len());
10394 for e in entries {
10395 if e.status != DiffStatus::Modified {
10396 out.push(e);
10397 continue;
10398 }
10399 let old = blob_bytes(odb, &e.old_oid)?;
10400 let new = read_new_side_bytes(odb, &e, work_root, worktree, config)?;
10401 if should_break_rewrite_pair(&old, &new, break_score) {
10402 let path = e.path().to_owned();
10403 out.push(DiffEntry {
10404 status: DiffStatus::Deleted,
10405 old_path: Some(path.clone()),
10406 new_path: None,
10407 old_mode: e.old_mode,
10408 new_mode: FileMode::ABSENT,
10409 old_oid: e.old_oid,
10410 new_oid: zero_oid(),
10411 score: None,
10412 });
10413 out.push(DiffEntry {
10414 status: DiffStatus::Added,
10415 old_path: None,
10416 new_path: Some(path),
10417 old_mode: FileMode::ABSENT,
10418 new_mode: e.new_mode,
10419 old_oid: zero_oid(),
10420 new_oid: e.new_oid,
10421 score: None,
10422 });
10423 } else {
10424 out.push(e);
10425 }
10426 }
10427 Ok(out)
10428}
10429
10430fn annotate_broken_modifications(
10431 odb: &Odb,
10432 work_root: Option<&Path>,
10433 entries: Vec<DiffEntry>,
10434 break_score: u64,
10435 worktree: Option<&DiffPostprocessWorktree<'_>>,
10436 config: &ConfigSet,
10437 materialize_worktree_oid: bool,
10438) -> Result<Vec<DiffEntry>> {
10439 let mut out = Vec::with_capacity(entries.len());
10440 for mut e in entries {
10441 if e.status == DiffStatus::Modified {
10442 let old = blob_bytes(odb, &e.old_oid)?;
10443 let new = read_new_side_bytes(odb, &e, work_root, worktree, config)?;
10444 if should_break_rewrite_pair(&old, &new, break_score) {
10445 let show_score = if e.new_oid != zero_oid() {
10449 true
10450 } else if materialize_worktree_oid {
10451 match (work_root, worktree) {
10452 (Some(root), Some(wctx)) => {
10453 worktree_side_differs_after_normalize(odb, &e, root, wctx, config)?
10454 }
10455 _ => false,
10456 }
10457 } else {
10458 false
10459 };
10460 if show_score {
10461 e.score = rewrite_dissimilarity_index_percent(&old, &new);
10462 }
10463 }
10464 }
10465 out.push(e);
10466 }
10467 Ok(out)
10468}
10469
10470fn materialize_break_worktree_new_oids(
10471 odb: &Odb,
10472 work_root: Option<&Path>,
10473 entries: &mut [DiffEntry],
10474 worktree: Option<&DiffPostprocessWorktree<'_>>,
10475 config: &ConfigSet,
10476) -> Result<()> {
10477 for e in entries {
10478 if e.new_oid != zero_oid() {
10479 continue;
10480 }
10481 if !matches!(e.status, DiffStatus::Modified | DiffStatus::Added) {
10482 continue;
10483 }
10484 let bytes = read_new_side_bytes(odb, e, work_root, worktree, config)?;
10485 e.new_oid = odb.hash(ObjectKind::Blob, &bytes)?;
10486 }
10487 Ok(())
10488}
10489
10490fn prune_break_deletions_obviated_by_renames(entries: Vec<DiffEntry>) -> Vec<DiffEntry> {
10493 use std::collections::HashSet;
10494 let rename_targets: HashSet<String> = entries
10495 .iter()
10496 .filter(|e| e.status == DiffStatus::Renamed)
10497 .filter_map(|e| e.new_path.clone())
10498 .collect();
10499 if rename_targets.is_empty() {
10500 return entries;
10501 }
10502 entries
10503 .into_iter()
10504 .filter(|e| {
10505 if e.status != DiffStatus::Deleted {
10506 return true;
10507 }
10508 e.old_path
10509 .as_ref()
10510 .is_none_or(|p| !rename_targets.contains(p))
10511 })
10512 .collect()
10513}
10514
10515#[allow(clippy::too_many_arguments)]
10525pub fn postprocess_diff_entries(
10526 odb: &Odb,
10527 entries: Vec<DiffEntry>,
10528 source_tree_oid: Option<&ObjectId>,
10529 work_root: Option<&Path>,
10530 worktree: Option<&DiffPostprocessWorktree<'_>>,
10531 config: &ConfigSet,
10532 thread_policy: &ThreadPolicy,
10533 opts: &DiffPostprocessOptions,
10534 inexact_notice: Option<&mut Option<InexactRenameDetectionSkipped>>,
10535) -> Result<Vec<DiffEntry>> {
10536 let mut entries = entries;
10537 if let Some(break_score) = opts.break_score {
10538 let rename_or_copy = opts.rename_threshold < 100 || opts.copy_threshold.is_some();
10539 entries = if rename_or_copy {
10540 split_broken_modifications(odb, work_root, entries, break_score, worktree, config)?
10541 } else {
10542 annotate_broken_modifications(
10543 odb,
10544 work_root,
10545 entries,
10546 break_score,
10547 worktree,
10548 config,
10549 opts.materialize_break_worktree_new_oid,
10550 )?
10551 };
10552 if opts.materialize_break_worktree_new_oid {
10553 materialize_break_worktree_new_oids(odb, work_root, &mut entries, worktree, config)?;
10554 }
10555 }
10556
10557 let source_tree_entries = source_tree_oid
10558 .map(|t| flatten_tree_entries(odb, t))
10559 .transpose()?
10560 .unwrap_or_default();
10561
10562 if opts.copy_threshold.is_some() || opts.find_copies_harder {
10563 let threshold = opts.copy_threshold.unwrap_or(opts.rename_threshold.min(99));
10564 detect_copies_with_policy(
10565 odb,
10566 work_root,
10567 entries,
10568 threshold,
10569 opts.find_copies_harder,
10570 &source_tree_entries,
10571 config,
10572 thread_policy,
10573 )
10574 } else if opts.rename_threshold < 100 {
10575 let skip_basename = opts.break_score.is_some();
10576 let out = detect_renames_with_policy(
10577 odb,
10578 work_root,
10579 entries,
10580 opts.rename_threshold,
10581 config,
10582 thread_policy,
10583 None,
10584 DetectRenamesOptions {
10585 skip_basename_heuristic: skip_basename,
10586 },
10587 )?;
10588 if let Some(slot) = inexact_notice {
10589 if out.inexact_skipped.is_some() {
10590 *slot = out.inexact_skipped;
10591 }
10592 }
10593 Ok(out.entries)
10594 } else {
10595 Ok(entries)
10596 }
10597}
10598
10599pub fn flatten_tree_entries(
10601 odb: &Odb,
10602 tree_oid: &ObjectId,
10603) -> Result<Vec<(String, FileMode, ObjectId)>> {
10604 fn walk(
10605 odb: &Odb,
10606 tree_oid: &ObjectId,
10607 prefix: &str,
10608 out: &mut Vec<(String, FileMode, ObjectId)>,
10609 ) -> Result<()> {
10610 let obj = odb.read(tree_oid)?;
10611 let oid_len = odb.hash_algo().len();
10612 for e in TreeRefIter::with_oid_len(&obj.data, oid_len) {
10613 let e = e?;
10614 let name = String::from_utf8_lossy(e.name);
10615 let path = if prefix.is_empty() {
10616 name.to_string()
10617 } else {
10618 format!("{prefix}/{name}")
10619 };
10620 if e.mode == 0o040000 {
10621 walk(odb, &e.oid, &path, out)?;
10622 } else {
10623 out.push((path, FileMode::new(e.mode), e.oid));
10624 }
10625 }
10626 Ok(())
10627 }
10628 let mut out = Vec::new();
10629 walk(odb, tree_oid, "", &mut out)?;
10630 Ok(out)
10631}
10632
10633#[allow(clippy::too_many_arguments)]
10643pub fn status_apply_rename_copy_detection(
10644 odb: &Odb,
10645 unstaged_raw: Vec<DiffEntry>,
10646 threshold: u32,
10647 copies: bool,
10648 head_tree: Option<&ObjectId>,
10649 config: &ConfigSet,
10650 thread_policy: &ThreadPolicy,
10651 cancel_watch: Option<&std::sync::atomic::AtomicBool>,
10652) -> Result<Vec<DiffEntry>> {
10653 if !copies {
10654 return detect_renames_with_policy(
10655 odb,
10656 None,
10657 unstaged_raw,
10658 threshold,
10659 config,
10660 thread_policy,
10661 cancel_watch,
10662 DetectRenamesOptions::default(),
10663 )
10664 .map(|r| r.entries);
10665 }
10666 let source_tree_entries: Vec<(String, FileMode, ObjectId)> = match head_tree {
10667 Some(oid) => flatten_tree(odb, oid, "")?
10668 .into_iter()
10669 .map(|e| (e.path, FileMode::new(e.mode), e.oid))
10670 .collect(),
10671 None => Vec::new(),
10672 };
10673 detect_copies_with_policy(
10674 odb,
10675 None,
10676 unstaged_raw,
10677 threshold,
10678 false,
10679 &source_tree_entries,
10680 config,
10681 thread_policy,
10682 )
10683}
10684
10685fn same_basename(del: &DiffEntry, add: &DiffEntry) -> bool {
10687 let old = del.old_path.as_deref().unwrap_or("");
10688 let new = add.new_path.as_deref().unwrap_or("");
10689 let old_base = old.rsplit('/').next().unwrap_or(old);
10690 let new_base = new.rsplit('/').next().unwrap_or(new);
10691 old_base == new_base && !old_base.is_empty()
10692}
10693
10694const RENAME_SCORE_MAX: u32 = 100;
10695
10696fn blob_payload_size(odb: &Odb, oid: &ObjectId) -> Option<usize> {
10698 if oid.is_zero() {
10699 return None;
10700 }
10701 odb.read_info(oid).ok().map(|info| info.size as usize)
10702}
10703
10704fn added_entry_payload_size(odb: &Odb, add: &DiffEntry, work_root: Option<&Path>) -> Option<usize> {
10706 if !add.new_oid.is_zero() {
10707 if let Some(size) = blob_payload_size(odb, &add.new_oid) {
10708 return Some(size);
10709 }
10710 }
10711 read_added_entry_bytes(odb, add, work_root).map(|data| data.len())
10712}
10713
10714fn rename_pair_size_reachable(src_size: usize, dst_size: usize, minimum_score: u32) -> bool {
10716 let max_size = src_size.max(dst_size);
10717 let min_size = src_size.min(dst_size);
10718 if max_size == 0 {
10719 return minimum_score <= RENAME_SCORE_MAX;
10720 }
10721 (max_size - min_size).saturating_mul(RENAME_SCORE_MAX as usize)
10722 <= max_size.saturating_mul((RENAME_SCORE_MAX - minimum_score) as usize)
10723}
10724
10725fn compute_similarity(old: &[u8], new: &[u8]) -> u32 {
10730 RenameLineTable::similarity_score(
10731 &RenameLineTable::from_blob(old),
10732 &RenameLineTable::from_blob(new),
10733 )
10734}
10735
10736#[must_use]
10741pub fn rename_similarity_score(old: &[u8], new: &[u8]) -> u32 {
10742 compute_similarity(old, new)
10743}
10744
10745pub fn format_raw(entry: &DiffEntry) -> String {
10751 let path = match entry.status {
10752 DiffStatus::Renamed | DiffStatus::Copied => {
10753 format!(
10754 "{}\t{}",
10755 entry.old_path.as_deref().unwrap_or(""),
10756 entry.new_path.as_deref().unwrap_or("")
10757 )
10758 }
10759 _ => entry.path().to_owned(),
10760 };
10761
10762 let status_str = match (entry.status, entry.score) {
10763 (DiffStatus::Renamed, Some(s)) => format!("R{:03}", s),
10764 (DiffStatus::Copied, Some(s)) => format!("C{:03}", s),
10765 (DiffStatus::Modified, Some(s)) => format!("M{s:03}"),
10766 (DiffStatus::TypeChanged, Some(s)) => format!("T{s:03}"),
10767 _ => entry.status.letter().to_string(),
10768 };
10769
10770 let (old_hex, new_hex) = raw_oid_hex_pair(&entry.old_oid, &entry.new_oid);
10771 format!(
10772 ":{} {} {} {} {}\t{}",
10773 entry.old_mode, entry.new_mode, old_hex, new_hex, status_str, path
10774 )
10775}
10776
10777fn raw_oid_hex_pair(old: &ObjectId, new: &ObjectId) -> (String, String) {
10785 let width = if !old.is_zero() {
10786 old.algo().hex_len()
10787 } else if !new.is_zero() {
10788 new.algo().hex_len()
10789 } else {
10790 old.algo().hex_len()
10791 };
10792 let render = |oid: &ObjectId| {
10793 if oid.is_zero() {
10794 "0".repeat(width)
10795 } else {
10796 oid.to_hex()
10797 }
10798 };
10799 (render(old), render(new))
10800}
10801
10802pub fn format_raw_abbrev(entry: &DiffEntry, abbrev_len: usize) -> String {
10804 let old_hex = format!("{}", entry.old_oid);
10805 let new_hex = format!("{}", entry.new_oid);
10806 let old_abbrev = &old_hex[..abbrev_len.min(old_hex.len())];
10807 let new_abbrev = &new_hex[..abbrev_len.min(new_hex.len())];
10808
10809 let path = match entry.status {
10811 DiffStatus::Renamed | DiffStatus::Copied => format!(
10812 "{}\t{}",
10813 entry.old_path.as_deref().unwrap_or(""),
10814 entry.new_path.as_deref().unwrap_or("")
10815 ),
10816 _ => entry.path().to_owned(),
10817 };
10818 let status_str = match (entry.status, entry.score) {
10819 (DiffStatus::Renamed, Some(s)) => format!("R{s:03}"),
10820 (DiffStatus::Copied, Some(s)) => format!("C{s:03}"),
10821 (DiffStatus::Modified, Some(s)) => format!("M{s:03}"),
10822 (DiffStatus::TypeChanged, Some(s)) => format!("T{s:03}"),
10823 _ => entry.status.letter().to_string(),
10824 };
10825
10826 format!(
10827 ":{} {} {} {} {}\t{}",
10828 entry.old_mode, entry.new_mode, old_abbrev, new_abbrev, status_str, path
10829 )
10830}
10831
10832pub fn unified_diff(
10847 old_content: &str,
10848 new_content: &str,
10849 old_path: &str,
10850 new_path: &str,
10851 context_lines: usize,
10852 indent_heuristic: bool,
10853 quote_path_fully: bool,
10854) -> String {
10855 unified_diff_with_prefix(
10856 old_content,
10857 new_content,
10858 old_path,
10859 new_path,
10860 context_lines,
10861 0,
10862 "a/",
10863 "b/",
10864 indent_heuristic,
10865 quote_path_fully,
10866 )
10867}
10868
10869#[allow(clippy::too_many_arguments)] pub fn unified_diff_with_prefix(
10875 old_content: &str,
10876 new_content: &str,
10877 old_path: &str,
10878 new_path: &str,
10879 context_lines: usize,
10880 inter_hunk_context: usize,
10881 src_prefix: &str,
10882 dst_prefix: &str,
10883 indent_heuristic: bool,
10884 quote_path_fully: bool,
10885) -> String {
10886 unified_diff_with_prefix_and_funcname(
10887 old_content,
10888 new_content,
10889 old_path,
10890 new_path,
10891 context_lines,
10892 inter_hunk_context,
10893 src_prefix,
10894 dst_prefix,
10895 None,
10896 indent_heuristic,
10897 quote_path_fully,
10898 )
10899}
10900
10901#[allow(clippy::too_many_arguments)]
10904pub fn unified_diff_with_prefix_and_funcname(
10905 old_content: &str,
10906 new_content: &str,
10907 old_path: &str,
10908 new_path: &str,
10909 context_lines: usize,
10910 inter_hunk_context: usize,
10911 src_prefix: &str,
10912 dst_prefix: &str,
10913 funcname_matcher: Option<&FuncnameMatcher>,
10914 indent_heuristic: bool,
10915 quote_path_fully: bool,
10916) -> String {
10917 unified_diff_with_prefix_and_funcname_and_algorithm(
10918 old_content,
10919 new_content,
10920 old_path,
10921 new_path,
10922 context_lines,
10923 inter_hunk_context,
10924 src_prefix,
10925 dst_prefix,
10926 funcname_matcher,
10927 similar::Algorithm::Myers,
10928 false,
10929 false,
10930 indent_heuristic,
10931 quote_path_fully,
10932 )
10933}
10934
10935#[allow(clippy::too_many_arguments)]
10941pub fn write_unified_diff_with_prefix_and_funcname_and_algorithm<W: std::io::Write>(
10942 out: &mut W,
10943 old_content: &str,
10944 new_content: &str,
10945 old_path: &str,
10946 new_path: &str,
10947 context_lines: usize,
10948 inter_hunk_context: usize,
10949 src_prefix: &str,
10950 dst_prefix: &str,
10951 funcname_matcher: Option<&FuncnameMatcher>,
10952 algorithm: similar::Algorithm,
10953 function_context: bool,
10954 use_git_histogram: bool,
10955 indent_heuristic: bool,
10956 quote_path_fully: bool,
10957) -> Result<()> {
10958 if function_context {
10959 let patch = unified_diff_with_function_context(
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 funcname_matcher,
10969 algorithm,
10970 indent_heuristic,
10971 quote_path_fully,
10972 );
10973 out.write_all(patch.as_bytes())?;
10974 return Ok(());
10975 }
10976
10977 if use_git_histogram {
10978 let patch = unified_diff_histogram_with_prefix_and_funcname_indent(
10979 old_content,
10980 new_content,
10981 old_path,
10982 new_path,
10983 context_lines,
10984 inter_hunk_context,
10985 src_prefix,
10986 dst_prefix,
10987 funcname_matcher,
10988 indent_heuristic,
10989 quote_path_fully,
10990 );
10991 out.write_all(patch.as_bytes())?;
10992 return Ok(());
10993 }
10994
10995 if algorithm == similar::Algorithm::Myers && !function_context {
10996 let patch = unified_diff_git_myers_with_prefix_and_funcname(
10997 old_content,
10998 new_content,
10999 old_path,
11000 new_path,
11001 context_lines,
11002 inter_hunk_context,
11003 src_prefix,
11004 dst_prefix,
11005 funcname_matcher,
11006 indent_heuristic,
11007 quote_path_fully,
11008 );
11009 out.write_all(patch.as_bytes())?;
11010 return Ok(());
11011 }
11012
11013 if algorithm == similar::Algorithm::Lcs && !function_context {
11014 let patch = unified_diff_git_minimal_with_prefix_and_funcname(
11015 old_content,
11016 new_content,
11017 old_path,
11018 new_path,
11019 context_lines,
11020 inter_hunk_context,
11021 src_prefix,
11022 dst_prefix,
11023 funcname_matcher,
11024 indent_heuristic,
11025 quote_path_fully,
11026 );
11027 out.write_all(patch.as_bytes())?;
11028 return Ok(());
11029 }
11030
11031 if algorithm == similar::Algorithm::Patience && !function_context {
11032 let patch = unified_diff_git_patience_with_prefix_and_funcname(
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 indent_heuristic,
11043 quote_path_fully,
11044 );
11045 out.write_all(patch.as_bytes())?;
11046 return Ok(());
11047 }
11048
11049 use crate::quote_path::format_diff_path_with_prefix;
11050 use similar::{udiff::UnifiedDiffHunk, TextDiff};
11051
11052 let diff = TextDiff::configure()
11053 .algorithm(algorithm)
11054 .diff_lines(old_content, new_content);
11055 let compacted_ops = diff_indent_heuristic::diff_lines_ops_compacted(
11056 old_content,
11057 new_content,
11058 algorithm,
11059 indent_heuristic,
11060 );
11061
11062 if old_path == "/dev/null" {
11063 out.write_all(b"--- /dev/null\n")?;
11064 } else if src_prefix.is_empty() {
11065 writeln!(out, "--- {old_path}")?;
11066 } else {
11067 out.write_all(b"--- ")?;
11068 out.write_all(
11069 format_diff_path_with_prefix(src_prefix, old_path, quote_path_fully).as_bytes(),
11070 )?;
11071 out.write_all(b"\n")?;
11072 }
11073 if new_path == "/dev/null" {
11074 out.write_all(b"+++ /dev/null\n")?;
11075 } else if dst_prefix.is_empty() {
11076 writeln!(out, "+++ {new_path}")?;
11077 } else {
11078 out.write_all(b"+++ ")?;
11079 out.write_all(
11080 format_diff_path_with_prefix(dst_prefix, new_path, quote_path_fully).as_bytes(),
11081 )?;
11082 out.write_all(b"\n")?;
11083 }
11084
11085 let old_lines: Vec<&str> = old_content.lines().collect();
11086 let max_common_gap = context_lines
11087 .saturating_mul(2)
11088 .saturating_add(inter_hunk_context);
11089 let op_groups = group_diff_ops_gap(compacted_ops, context_lines, max_common_gap);
11090
11091 let mut hunk_buf = String::new();
11092 let mut eof_annot = UnifiedDiffNoNewlineAnnotator::new(old_content, new_content);
11093 for ops in op_groups {
11094 if ops.is_empty() {
11095 continue;
11096 }
11097 let hunk = UnifiedDiffHunk::new(ops, &diff, true);
11098 hunk_buf.clear();
11099 use std::fmt::Write as FmtWrite;
11100 let _ = FmtWrite::write_fmt(&mut hunk_buf, format_args!("{hunk}"));
11101 let mut fragment = hunk_buf.clone();
11102 if let Some(first_newline) = hunk_buf.find('\n') {
11103 let header_line = &hunk_buf[..first_newline];
11104 let rest = &hunk_buf[first_newline..];
11105 if let Some(func_ctx) =
11106 extract_function_context(header_line, &old_lines, funcname_matcher, Some(old_path))
11107 {
11108 fragment.clear();
11109 fragment.push_str(header_line);
11110 fragment.push(' ');
11111 fragment.push_str(&func_ctx);
11112 fragment.push_str(rest);
11113 }
11114 }
11115 eof_annot.write_hunk(out, &fragment)?;
11116 }
11117 Ok(())
11118}
11119
11120#[allow(clippy::too_many_arguments)]
11126pub fn unified_diff_with_prefix_and_funcname_and_algorithm(
11127 old_content: &str,
11128 new_content: &str,
11129 old_path: &str,
11130 new_path: &str,
11131 context_lines: usize,
11132 inter_hunk_context: usize,
11133 src_prefix: &str,
11134 dst_prefix: &str,
11135 funcname_matcher: Option<&FuncnameMatcher>,
11136 algorithm: similar::Algorithm,
11137 function_context: bool,
11138 use_git_histogram: bool,
11139 indent_heuristic: bool,
11140 quote_path_fully: bool,
11141) -> String {
11142 let mut output = Vec::new();
11143 if write_unified_diff_with_prefix_and_funcname_and_algorithm(
11144 &mut output,
11145 old_content,
11146 new_content,
11147 old_path,
11148 new_path,
11149 context_lines,
11150 inter_hunk_context,
11151 src_prefix,
11152 dst_prefix,
11153 funcname_matcher,
11154 algorithm,
11155 function_context,
11156 use_git_histogram,
11157 indent_heuristic,
11158 quote_path_fully,
11159 )
11160 .is_err()
11161 {
11162 output.clear();
11163 }
11164 String::from_utf8_lossy(&output).into_owned()
11165}
11166
11167pub(crate) fn group_diff_ops_gap(
11173 mut ops: Vec<similar::DiffOp>,
11174 context: usize,
11175 max_common_gap: usize,
11176) -> Vec<Vec<similar::DiffOp>> {
11177 use similar::DiffOp;
11178 if ops.is_empty() {
11179 return vec![];
11180 }
11181
11182 let mut pending_group = Vec::new();
11183 let mut rv = Vec::new();
11184
11185 if let Some(DiffOp::Equal {
11186 old_index,
11187 new_index,
11188 len,
11189 }) = ops.first_mut()
11190 {
11191 let offset = (*len).saturating_sub(context);
11192 *old_index += offset;
11193 *new_index += offset;
11194 *len -= offset;
11195 }
11196
11197 if let Some(DiffOp::Equal { len, .. }) = ops.last_mut() {
11198 *len -= (*len).saturating_sub(context);
11199 }
11200
11201 for op in ops.into_iter() {
11202 if let DiffOp::Equal {
11203 old_index,
11204 new_index,
11205 len,
11206 } = op
11207 {
11208 if len > max_common_gap {
11211 pending_group.push(DiffOp::Equal {
11212 old_index,
11213 new_index,
11214 len: context,
11215 });
11216 rv.push(pending_group);
11217 let offset = len.saturating_sub(context);
11218 pending_group = vec![DiffOp::Equal {
11219 old_index: old_index + offset,
11220 new_index: new_index + offset,
11221 len: len - offset,
11222 }];
11223 continue;
11224 }
11225 }
11226 pending_group.push(op);
11227 }
11228
11229 match &pending_group[..] {
11230 &[] | &[similar::DiffOp::Equal { .. }] => {}
11231 _ => rv.push(pending_group),
11232 }
11233
11234 rv
11235}
11236#[expect(clippy::too_many_arguments)]
11237fn unified_diff_with_function_context(
11239 old_content: &str,
11240 new_content: &str,
11241 old_path: &str,
11242 new_path: &str,
11243 context_lines: usize,
11244 inter_hunk_context: usize,
11245 src_prefix: &str,
11246 dst_prefix: &str,
11247 funcname_matcher: Option<&FuncnameMatcher>,
11248 algorithm: similar::Algorithm,
11249 indent_heuristic: bool,
11250 quote_path_fully: bool,
11251) -> String {
11252 use crate::quote_path::format_diff_path_with_prefix;
11253 use similar::{udiff::UnifiedDiffHunk, TextDiff};
11254
11255 let diff = TextDiff::configure()
11256 .algorithm(algorithm)
11257 .diff_lines(old_content, new_content);
11258
11259 let old_lines: Vec<&str> = old_content.lines().collect();
11260 let new_lines: Vec<&str> = new_content.lines().collect();
11261 let n_old = old_lines.len();
11262 let n_new = new_lines.len();
11263
11264 let max_common_gap = context_lines
11271 .saturating_mul(2)
11272 .saturating_add(inter_hunk_context);
11273 let all_ops = diff.ops().to_vec();
11274 let op_groups = group_diff_ops_gap(all_ops.clone(), context_lines, max_common_gap);
11275
11276 let mut ranges: Vec<(usize, usize, usize, usize)> = Vec::new();
11277
11278 for ops in op_groups {
11279 if ops.is_empty() {
11280 continue;
11281 }
11282 let i1_anchor = func_context_old_anchor(&ops, n_old);
11283 let i1_end = hunk_old_change_end_exclusive(&ops);
11284 let skip_preimage_pull =
11285 append_with_whole_function_added(&ops, n_old, n_new, &new_lines, funcname_matcher);
11286 let hunk = UnifiedDiffHunk::new(ops, &diff, true);
11287 let hunk_str = format!("{hunk}");
11288 let header_line = hunk_str
11289 .lines()
11290 .next()
11291 .unwrap_or("")
11292 .trim_end_matches(['\r', '\n']);
11293 let Some((base_s1, _base_e1, _base_s2, _base_e2)) =
11294 parse_unified_hunk_header_ranges(header_line)
11295 else {
11296 continue;
11297 };
11298
11299 let ctx = context_lines;
11300 let (s1, e1, s2, e2) = if skip_preimage_pull {
11301 let s = n_old.saturating_sub(ctx);
11302 let s2 = map_old_line_to_new(&all_ops, s, n_new).min(n_new);
11303 (s, n_old, s2, n_new)
11304 } else {
11305 let mut s1 = base_s1.saturating_sub(ctx);
11306 let mut s2 = map_old_line_to_new(&all_ops, s1, n_new).min(n_new);
11307
11308 let base_pre_s1 = i1_anchor.saturating_sub(ctx);
11309 if base_pre_s1 < s1 {
11310 s1 = base_pre_s1;
11311 s2 = map_old_line_to_new(&all_ops, s1, n_new).min(n_new);
11312 }
11313
11314 let fs1 = expand_func_pre_start(s1, i1_anchor, n_old, &old_lines, funcname_matcher);
11315 if fs1 < s1 {
11316 s1 = fs1;
11317 s2 = map_old_line_to_new(&all_ops, s1, n_new).min(n_new);
11318 }
11319
11320 let mut e1 = (i1_end + ctx).min(n_old);
11326 let mut e2 = map_old_line_to_new(&all_ops, e1, n_new).min(n_new);
11327 let fe1 = expand_func_post_end(e1, i1_end, n_old, &old_lines, funcname_matcher);
11328 if fe1 > e1 {
11329 e1 = fe1;
11330 e2 = map_old_line_to_new(&all_ops, e1, n_new).min(n_new);
11331 }
11332 (s1, e1, s2, e2)
11333 };
11334
11335 ranges.push((s1, e1, s2, e2));
11336 }
11337
11338 ranges.sort_by_key(|r| (r.0, r.2));
11341 let mut merged: Vec<(usize, usize, usize, usize)> = Vec::with_capacity(ranges.len());
11342 for (s1, e1, s2, e2) in ranges {
11343 if let Some(last) = merged.last_mut() {
11344 if s1 < last.1 {
11345 last.1 = last.1.max(e1);
11346 last.3 = last.3.max(e2);
11347 continue;
11348 }
11349 }
11350 merged.push((s1, e1, s2, e2));
11351 }
11352 let ranges = merged;
11353
11354 let mut output = String::new();
11355 if old_path == "/dev/null" {
11356 output.push_str("--- /dev/null\n");
11357 } else if src_prefix.is_empty() {
11358 output.push_str(&format!("--- {old_path}\n"));
11359 } else {
11360 output.push_str("--- ");
11361 output.push_str(&format_diff_path_with_prefix(
11362 src_prefix,
11363 old_path,
11364 quote_path_fully,
11365 ));
11366 output.push('\n');
11367 }
11368 if new_path == "/dev/null" {
11369 output.push_str("+++ /dev/null\n");
11370 } else if dst_prefix.is_empty() {
11371 output.push_str(&format!("+++ {new_path}\n"));
11372 } else {
11373 output.push_str("+++ ");
11374 output.push_str(&format_diff_path_with_prefix(
11375 dst_prefix,
11376 new_path,
11377 quote_path_fully,
11378 ));
11379 output.push('\n');
11380 }
11381
11382 for (s1, e1, s2, e2) in ranges {
11383 if s1 >= e1 && s2 >= e2 {
11384 continue;
11385 }
11386 let old_seg =
11387 line_slice_for_diff_with_eof_nl(&old_lines, s1, e1, old_content.ends_with('\n'));
11388 let new_seg =
11389 line_slice_for_diff_with_eof_nl(&new_lines, s2, e2, new_content.ends_with('\n'));
11390 let inner_ctx = old_seg.lines().count().max(new_seg.lines().count()).max(1);
11391 let piece = unified_diff_with_prefix_and_funcname_and_algorithm(
11392 &old_seg,
11393 &new_seg,
11394 old_path,
11395 new_path,
11396 inner_ctx,
11397 0,
11398 src_prefix,
11399 dst_prefix,
11400 funcname_matcher,
11401 algorithm,
11402 false,
11403 false,
11404 indent_heuristic,
11405 quote_path_fully,
11406 );
11407 let shifted = shift_unified_hunk_headers_to_full_file(&piece, s1, s2);
11408 let with_func =
11409 enrich_unified_hunk_headers_funcname(&shifted, &old_lines, funcname_matcher);
11410 for line in with_func.lines() {
11411 if line.starts_with("--- ") || line.starts_with("+++ ") {
11412 continue;
11413 }
11414 output.push_str(line);
11415 output.push('\n');
11416 }
11417 }
11418
11419 output
11420}
11421
11422fn shift_unified_hunk_headers_to_full_file(
11427 patch: &str,
11428 delta_old: usize,
11429 delta_new: usize,
11430) -> String {
11431 if delta_old == 0 && delta_new == 0 {
11432 return patch.to_owned();
11433 }
11434 let mut out = String::with_capacity(patch.len());
11435 for line in patch.lines() {
11436 if let Some(shifted) = shift_one_unified_hunk_header(line, delta_old, delta_new) {
11437 out.push_str(&shifted);
11438 } else {
11439 out.push_str(line);
11440 }
11441 out.push('\n');
11442 }
11443 out
11444}
11445
11446fn shift_one_unified_hunk_header(line: &str, delta_old: usize, delta_new: usize) -> Option<String> {
11447 let rest = line.strip_prefix("@@ ")?;
11448 let (old_chunk, after_plus) = rest.split_once(" +")?;
11449 let old_spec = old_chunk.strip_prefix('-')?;
11450 let (new_spec, suffix) = after_plus.split_once(" @@")?;
11451 let shifted_old = shift_unified_range_spec(old_spec, delta_old)?;
11452 let shifted_new = shift_unified_range_spec(new_spec, delta_new)?;
11453 Some(format!("@@ -{shifted_old} +{shifted_new} @@{suffix}"))
11454}
11455
11456fn shift_unified_range_spec(spec: &str, delta: usize) -> Option<String> {
11457 let spec = spec.trim();
11458 if let Some((start_s, count_s)) = spec.split_once(',') {
11459 let start: usize = start_s.parse().ok()?;
11460 let count: usize = count_s.parse().ok()?;
11461 Some(format!("{},{}", start.saturating_add(delta), count))
11462 } else {
11463 let start: usize = spec.parse().ok()?;
11464 Some(format!("{}", start.saturating_add(delta)))
11465 }
11466}
11467
11468fn enrich_unified_hunk_headers_funcname(
11470 patch: &str,
11471 full_old_lines: &[&str],
11472 funcname_matcher: Option<&FuncnameMatcher>,
11473) -> String {
11474 let mut out = String::with_capacity(patch.len());
11475 for line in patch.lines() {
11476 if let Some(fixed) = enrich_one_hunk_header_funcname(line, full_old_lines, funcname_matcher)
11477 {
11478 out.push_str(&fixed);
11479 } else {
11480 out.push_str(line);
11481 }
11482 out.push('\n');
11483 }
11484 out
11485}
11486
11487fn enrich_one_hunk_header_funcname(
11488 line: &str,
11489 full_old_lines: &[&str],
11490 funcname_matcher: Option<&FuncnameMatcher>,
11491) -> Option<String> {
11492 let after_at = line.strip_prefix("@@ ")?;
11493 let idx = after_at.find(" @@")?;
11494 let mid = after_at[..idx].trim();
11495 let tail = after_at[idx + 3..].trim_start();
11496 let header_for_parse = format!("@@ {mid} @@");
11497 let func = extract_function_context(&header_for_parse, full_old_lines, funcname_matcher, None);
11498 Some(if let Some(f) = func {
11499 format!("@@ {mid} @@ {f}")
11500 } else if !tail.is_empty() {
11501 format!("@@ {mid} @@ {tail}")
11502 } else {
11503 format!("@@ {mid} @@")
11504 })
11505}
11506
11507fn line_slice_for_diff_with_eof_nl(
11508 lines: &[&str],
11509 start: usize,
11510 end: usize,
11511 full_file_ends_with_newline: bool,
11512) -> String {
11513 if start >= end {
11514 return String::new();
11515 }
11516 let mut s = lines[start..end].join("\n");
11517 let slice_is_suffix_of_file = end == lines.len();
11518 let need_trailing_nl = if slice_is_suffix_of_file {
11519 full_file_ends_with_newline
11520 } else {
11521 true
11522 };
11523 if need_trailing_nl && !s.ends_with('\n') {
11524 s.push('\n');
11525 }
11526 s
11527}
11528
11529fn map_old_line_to_new(ops: &[similar::DiffOp], old_line: usize, n_new: usize) -> usize {
11532 use similar::DiffOp;
11533 let mut n = 0usize;
11534 for op in ops {
11535 match *op {
11536 DiffOp::Equal {
11537 old_index,
11538 new_index,
11539 len,
11540 } => {
11541 if old_index + len <= old_line {
11542 n = new_index + len;
11543 continue;
11544 }
11545 if old_index < old_line {
11546 let take = old_line - old_index;
11547 return (new_index + take).min(n_new);
11548 }
11549 return new_index.min(n_new);
11550 }
11551 DiffOp::Delete {
11552 old_index,
11553 old_len,
11554 new_index,
11555 } => {
11556 if old_index + old_len <= old_line {
11557 n = new_index;
11558 continue;
11559 }
11560 if old_index < old_line {
11561 return new_index.min(n_new);
11562 }
11563 }
11564 DiffOp::Insert {
11565 old_index,
11566 new_index,
11567 new_len,
11568 } => {
11569 if old_index < old_line {
11570 n = new_index + new_len;
11571 continue;
11572 }
11573 if old_index == old_line {
11574 return (new_index + new_len).min(n_new);
11577 }
11578 return new_index.min(n_new);
11579 }
11580 DiffOp::Replace {
11581 old_index,
11582 old_len,
11583 new_index,
11584 new_len,
11585 } => {
11586 if old_index + old_len <= old_line {
11587 n = new_index + new_len;
11588 continue;
11589 }
11590 if old_index < old_line {
11591 let into_old = old_line - old_index;
11592 let mapped = new_index + into_old.min(new_len);
11593 return mapped.min(n_new);
11594 }
11595 return new_index.min(n_new);
11596 }
11597 }
11598 }
11599 n.min(n_new)
11600}
11601
11602fn parse_unified_hunk_header_ranges(header: &str) -> Option<(usize, usize, usize, usize)> {
11604 let rest = header.strip_prefix("@@ ")?;
11605 let (old_tok, rest2) = rest.split_once(" +")?;
11606 let old_tok = old_tok.strip_prefix('-')?;
11607 let new_tok = rest2.split_once(" @@").map(|(a, _)| a)?;
11608
11609 fn parse_side(spec: &str) -> Option<(usize, usize)> {
11610 let spec = spec.trim();
11611 let (start_one_based, count) = if let Some((a, b)) = spec.split_once(',') {
11612 (a.parse::<usize>().ok()?, b.parse::<usize>().ok()?)
11613 } else {
11614 let s = spec.parse::<usize>().ok()?;
11615 (s, 1usize)
11616 };
11617 let s0 = start_one_based.saturating_sub(1);
11618 let e0 = s0.saturating_add(count);
11619 Some((s0, e0))
11620 }
11621
11622 let (os, oe) = parse_side(old_tok)?;
11623 let (ns, ne) = parse_side(new_tok)?;
11624 Some((os, oe, ns, ne))
11625}
11626
11627fn append_with_whole_function_added(
11630 ops: &[similar::DiffOp],
11631 n_old: usize,
11632 n_new: usize,
11633 new_lines: &[&str],
11634 matcher: Option<&FuncnameMatcher>,
11635) -> bool {
11636 use similar::DiffOp;
11637 if n_old == 0 {
11638 return false;
11639 }
11640 let mut only_ins_or_eq = true;
11641 let mut min_new_ins = usize::MAX;
11642 for op in ops {
11643 match *op {
11644 DiffOp::Equal { .. } => {}
11645 DiffOp::Insert {
11646 new_index, new_len, ..
11647 } => {
11648 min_new_ins = min_new_ins.min(new_index);
11649 if new_len == 0 {
11650 only_ins_or_eq = false;
11651 }
11652 }
11653 DiffOp::Delete { .. } | DiffOp::Replace { .. } => {
11654 only_ins_or_eq = false;
11655 }
11656 }
11657 }
11658 let mut insert_at_eof = false;
11659 for op in ops {
11660 if let DiffOp::Insert { old_index, .. } = *op {
11661 if old_index == n_old {
11662 insert_at_eof = true;
11663 break;
11664 }
11665 }
11666 }
11667 let append_at_eof = min_new_ins == n_old || insert_at_eof;
11668 if !only_ins_or_eq || !append_at_eof || min_new_ins == usize::MAX {
11669 return false;
11670 }
11671 let mut j = min_new_ins;
11676 while j < n_new {
11677 let line = new_lines[j];
11678 if line.trim().is_empty() {
11679 j += 1;
11680 continue;
11681 }
11682 if let Some(m) = matcher {
11683 if m.match_line(line).is_some() {
11684 return true;
11685 }
11686 } else if inserted_block_starts_with_c_like_function_definition(line) {
11687 return true;
11688 }
11689 j += 1;
11690 }
11691 false
11692}
11693
11694fn inserted_block_starts_with_c_like_function_definition(line: &str) -> bool {
11695 let t = line.trim_start();
11696 let Some(open_paren) = t.find('(') else {
11697 return false;
11698 };
11699 let head = &t[..open_paren];
11700 let tokens: Vec<&str> = head.split_whitespace().collect();
11701 if tokens.len() < 2 {
11702 return false;
11704 }
11705 let nameish = tokens.last().copied().unwrap_or("");
11706 let name = nameish.trim_end_matches(['*', '&']);
11707 if name.is_empty() || !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
11708 return false;
11709 }
11710 let type_or_modifier = |tok: &str| {
11711 matches!(
11712 tok,
11713 "static"
11714 | "extern"
11715 | "inline"
11716 | "void"
11717 | "int"
11718 | "char"
11719 | "short"
11720 | "long"
11721 | "float"
11722 | "double"
11723 | "unsigned"
11724 | "signed"
11725 | "struct"
11726 | "enum"
11727 | "union"
11728 | "const"
11729 | "volatile"
11730 | "typedef"
11731 )
11732 };
11733 tokens[..tokens.len() - 1]
11734 .iter()
11735 .any(|tok| type_or_modifier(tok))
11736}
11737
11738fn hunk_old_change_end_exclusive(ops: &[similar::DiffOp]) -> usize {
11739 use similar::DiffOp;
11740 let mut max_o = 0usize;
11741 for op in ops {
11742 match *op {
11743 DiffOp::Delete {
11744 old_index, old_len, ..
11745 } => {
11746 max_o = max_o.max(old_index + old_len);
11747 }
11748 DiffOp::Replace {
11749 old_index, old_len, ..
11750 } => {
11751 max_o = max_o.max(old_index + old_len);
11752 }
11753 DiffOp::Insert { old_index, .. } => {
11754 max_o = max_o.max(old_index);
11757 }
11758 DiffOp::Equal { .. } => {}
11759 }
11760 }
11761 max_o
11762}
11763
11764fn func_context_old_anchor(ops: &[similar::DiffOp], n_old: usize) -> usize {
11765 use similar::DiffOp;
11766 let mut has_delete_or_replace = false;
11767 let mut min_del = usize::MAX;
11768 let mut min_ins_old = usize::MAX;
11769
11770 for op in ops {
11771 match *op {
11772 DiffOp::Delete {
11773 old_index, old_len, ..
11774 } => {
11775 has_delete_or_replace = true;
11776 min_del = min_del.min(old_index);
11777 min_del = min_del.min(old_index + old_len.saturating_sub(1));
11778 }
11779 DiffOp::Replace {
11780 old_index, old_len, ..
11781 } => {
11782 has_delete_or_replace = true;
11783 min_del = min_del.min(old_index);
11784 min_del = min_del.min(old_index + old_len.saturating_sub(1));
11785 }
11786 DiffOp::Insert { old_index, .. } => {
11787 min_ins_old = min_ins_old.min(old_index);
11788 }
11789 DiffOp::Equal { .. } => {}
11790 }
11791 }
11792
11793 let mut i1 = if has_delete_or_replace {
11794 min_del
11795 } else if min_ins_old != usize::MAX {
11796 min_ins_old
11797 } else {
11798 0
11799 };
11800
11801 let pure_insert = ops
11802 .iter()
11803 .all(|op| matches!(op, DiffOp::Insert { .. } | DiffOp::Equal { .. }))
11804 && ops.iter().any(|op| matches!(op, DiffOp::Insert { .. }));
11805
11806 if pure_insert && i1 >= n_old && n_old > 0 {
11807 i1 = n_old - 1;
11808 }
11809
11810 i1.min(n_old.saturating_sub(1))
11811}
11812
11813fn expand_func_pre_start(
11814 s1: usize,
11815 i1: usize,
11816 n_old: usize,
11817 old_lines: &[&str],
11818 matcher: Option<&FuncnameMatcher>,
11819) -> usize {
11820 if n_old == 0 {
11821 return s1;
11822 }
11823 let i1 = i1.min(n_old.saturating_sub(1));
11824 let mut fs1 = get_func_line_backward(old_lines, i1, matcher).unwrap_or(i1);
11825 while fs1 > 0
11826 && !is_line_empty_for_func_context(old_lines[fs1 - 1])
11827 && !is_func_line(old_lines[fs1 - 1], matcher)
11828 {
11829 fs1 -= 1;
11830 }
11831 s1.min(fs1)
11832}
11833
11834fn expand_func_post_end(
11835 e1: usize,
11836 i1_end: usize,
11837 n_old: usize,
11838 old_lines: &[&str],
11839 matcher: Option<&FuncnameMatcher>,
11840) -> usize {
11841 let from = i1_end.min(n_old);
11842 let fe1 = get_func_line_forward(old_lines, from, matcher).unwrap_or(n_old);
11843 let mut fe1_adj = fe1;
11844 while fe1_adj > 0 && is_line_empty_for_func_context(old_lines[fe1_adj - 1]) {
11845 fe1_adj -= 1;
11846 }
11847 e1.max(fe1_adj).min(n_old)
11848}
11849
11850fn is_line_empty_for_func_context(line: &str) -> bool {
11851 line.chars().all(|c| c.is_whitespace())
11852}
11853
11854pub(crate) fn is_func_line(line: &str, matcher: Option<&FuncnameMatcher>) -> bool {
11855 if let Some(m) = matcher {
11856 return m.match_line(line).is_some();
11857 }
11858 let t = line.trim_end_matches(['\n', '\r']);
11859 if t.is_empty() {
11860 return false;
11861 }
11862 let b = t.as_bytes()[0];
11863 if !(b.is_ascii_alphabetic() || b == b'_' || b == b'$') {
11864 return false;
11865 }
11866 true
11867}
11868
11869fn get_func_line_backward(
11870 old_lines: &[&str],
11871 start: usize,
11872 matcher: Option<&FuncnameMatcher>,
11873) -> Option<usize> {
11874 let mut l = start.min(old_lines.len().saturating_sub(1));
11875 if old_lines.is_empty() {
11876 return None;
11877 }
11878 loop {
11879 if is_func_line(old_lines[l], matcher) {
11880 return Some(l);
11881 }
11882 if l == 0 {
11883 break;
11884 }
11885 l -= 1;
11886 }
11887 None
11888}
11889
11890fn get_func_line_forward(
11891 old_lines: &[&str],
11892 start: usize,
11893 matcher: Option<&FuncnameMatcher>,
11894) -> Option<usize> {
11895 let mut l = start;
11896 while l < old_lines.len() {
11897 if is_func_line(old_lines[l], matcher) {
11898 return Some(l);
11899 }
11900 l += 1;
11901 }
11902 None
11903}
11904#[expect(clippy::too_many_arguments)]
11905pub fn anchored_unified_diff(
11915 old_content: &str,
11916 new_content: &str,
11917 old_path: &str,
11918 new_path: &str,
11919 context_lines: usize,
11920 anchors: &[String],
11921 algorithm: similar::Algorithm,
11922 use_git_histogram: bool,
11923 indent_heuristic: bool,
11924 quote_path_fully: bool,
11925) -> String {
11926 use crate::quote_path::format_diff_path_with_prefix;
11927 use similar::TextDiff;
11928
11929 let old_lines: Vec<&str> = old_content.lines().collect();
11930 let new_lines: Vec<&str> = new_content.lines().collect();
11931
11932 let mut anchor_pairs: Vec<(usize, usize)> = Vec::new(); for anchor in anchors {
11936 let anchor_str = anchor.as_str();
11937
11938 let old_positions: Vec<usize> = old_lines
11940 .iter()
11941 .enumerate()
11942 .filter(|(_, l)| l.trim_end() == anchor_str)
11943 .map(|(i, _)| i)
11944 .collect();
11945
11946 let new_positions: Vec<usize> = new_lines
11948 .iter()
11949 .enumerate()
11950 .filter(|(_, l)| l.trim_end() == anchor_str)
11951 .map(|(i, _)| i)
11952 .collect();
11953
11954 if old_positions.len() == 1 && new_positions.len() == 1 {
11956 anchor_pairs.push((old_positions[0], new_positions[0]));
11957 }
11958 }
11959
11960 if anchor_pairs.is_empty() {
11962 return unified_diff_with_prefix_and_funcname_and_algorithm(
11963 old_content,
11964 new_content,
11965 old_path,
11966 new_path,
11967 context_lines,
11968 0,
11969 "a/",
11970 "b/",
11971 None,
11972 algorithm,
11973 false,
11974 use_git_histogram,
11975 indent_heuristic,
11976 quote_path_fully,
11977 );
11978 }
11979
11980 anchor_pairs.sort_by_key(|&(old_idx, _)| old_idx);
11982
11983 let mut filtered: Vec<(usize, usize)> = Vec::new();
11986 for &pair in &anchor_pairs {
11987 if filtered.is_empty() || filtered.last().is_some_and(|last| pair.1 > last.1) {
11988 filtered.push(pair);
11989 }
11990 }
11991 let anchor_pairs = filtered;
11992
11993 struct LineDiffOp {
12002 tag: char, line: String,
12004 }
12005
12006 let append_segment_diff =
12007 |ops: &mut Vec<LineDiffOp>, old_seg_input: &str, new_seg_input: &str| {
12008 use similar::ChangeTag;
12009 let old_ls: Vec<&str> = old_seg_input.lines().collect();
12010 let new_ls: Vec<&str> = new_seg_input.lines().collect();
12011 if old_ls.is_empty() && new_ls.is_empty() {
12012 return;
12013 }
12014 let seg_diff = TextDiff::configure()
12015 .algorithm(algorithm)
12016 .diff_slices(&old_ls, &new_ls);
12017 let raw = seg_diff.ops().to_vec();
12018 let compacted = diff_indent_heuristic::apply_change_compact_to_ops(
12019 &raw,
12020 &old_ls,
12021 &new_ls,
12022 indent_heuristic,
12023 );
12024 for op in &compacted {
12025 for ch in op.iter_changes(&old_ls, &new_ls) {
12026 let t = match ch.tag() {
12027 ChangeTag::Equal => ' ',
12028 ChangeTag::Delete => '-',
12029 ChangeTag::Insert => '+',
12030 };
12031 ops.push(LineDiffOp {
12032 tag: t,
12033 line: ch.value().to_string(),
12034 });
12035 }
12036 }
12037 };
12038
12039 let mut ops: Vec<LineDiffOp> = Vec::new();
12040 let mut old_pos = 0usize;
12041 let mut new_pos = 0usize;
12042
12043 for &(old_anchor, new_anchor) in &anchor_pairs {
12044 let old_segment: Vec<&str> = old_lines[old_pos..old_anchor].to_vec();
12046 let new_segment: Vec<&str> = new_lines[new_pos..new_anchor].to_vec();
12047
12048 let old_seg_text = old_segment.join("\n");
12049 let new_seg_text = new_segment.join("\n");
12050
12051 if !old_seg_text.is_empty() || !new_seg_text.is_empty() {
12052 let old_seg_input = if old_seg_text.is_empty() {
12053 String::new()
12054 } else {
12055 format!("{}\n", old_seg_text)
12056 };
12057 let new_seg_input = if new_seg_text.is_empty() {
12058 String::new()
12059 } else {
12060 format!("{}\n", new_seg_text)
12061 };
12062 append_segment_diff(&mut ops, &old_seg_input, &new_seg_input);
12063 }
12064
12065 ops.push(LineDiffOp {
12067 tag: ' ',
12068 line: old_lines[old_anchor].to_string(),
12069 });
12070
12071 old_pos = old_anchor + 1;
12072 new_pos = new_anchor + 1;
12073 }
12074
12075 let old_segment: Vec<&str> = old_lines[old_pos..].to_vec();
12077 let new_segment: Vec<&str> = new_lines[new_pos..].to_vec();
12078 let old_seg_text = old_segment.join("\n");
12079 let new_seg_text = new_segment.join("\n");
12080
12081 if !old_seg_text.is_empty() || !new_seg_text.is_empty() {
12082 let old_seg_input = if old_seg_text.is_empty() {
12083 String::new()
12084 } else {
12085 format!("{}\n", old_seg_text)
12086 };
12087 let new_seg_input = if new_seg_text.is_empty() {
12088 String::new()
12089 } else {
12090 format!("{}\n", new_seg_text)
12091 };
12092 append_segment_diff(&mut ops, &old_seg_input, &new_seg_input);
12093 }
12094
12095 let mut output = String::new();
12097 if old_path == "/dev/null" {
12098 output.push_str("--- /dev/null\n");
12099 } else {
12100 output.push_str("--- ");
12101 output.push_str(&format_diff_path_with_prefix(
12102 "a/",
12103 old_path,
12104 quote_path_fully,
12105 ));
12106 output.push('\n');
12107 }
12108 if new_path == "/dev/null" {
12109 output.push_str("+++ /dev/null\n");
12110 } else {
12111 output.push_str("+++ ");
12112 output.push_str(&format_diff_path_with_prefix(
12113 "b/",
12114 new_path,
12115 quote_path_fully,
12116 ));
12117 output.push('\n');
12118 }
12119
12120 let total_ops = ops.len();
12122 if total_ops == 0 {
12123 return output;
12124 }
12125
12126 let mut hunks: Vec<(usize, usize)> = Vec::new(); let mut i = 0;
12129 while i < total_ops {
12130 if ops[i].tag != ' ' {
12131 let start = i.saturating_sub(context_lines);
12132 let mut end = i;
12133 while end < total_ops {
12135 if ops[end].tag != ' ' {
12136 end += 1;
12137 continue;
12138 }
12139 let mut next_change = end;
12141 while next_change < total_ops && ops[next_change].tag == ' ' {
12142 next_change += 1;
12143 }
12144 if next_change < total_ops && next_change - end <= context_lines * 2 {
12145 end = next_change + 1;
12146 } else {
12147 end = (end + context_lines).min(total_ops);
12148 break;
12149 }
12150 }
12151 if let Some(last) = hunks.last_mut() {
12153 if start <= last.1 {
12154 last.1 = end;
12155 } else {
12156 hunks.push((start, end));
12157 }
12158 } else {
12159 hunks.push((start, end));
12160 }
12161 i = end;
12162 } else {
12163 i += 1;
12164 }
12165 }
12166
12167 for (start, end) in hunks {
12169 let mut old_start = 1usize;
12171 let mut new_start = 1usize;
12172 for op in &ops[..start] {
12174 match op.tag {
12175 ' ' => {
12176 old_start += 1;
12177 new_start += 1;
12178 }
12179 '-' => {
12180 old_start += 1;
12181 }
12182 '+' => {
12183 new_start += 1;
12184 }
12185 _ => {}
12186 }
12187 }
12188 let mut old_count = 0usize;
12189 let mut new_count = 0usize;
12190 for op in &ops[start..end] {
12191 match op.tag {
12192 ' ' => {
12193 old_count += 1;
12194 new_count += 1;
12195 }
12196 '-' => {
12197 old_count += 1;
12198 }
12199 '+' => {
12200 new_count += 1;
12201 }
12202 _ => {}
12203 }
12204 }
12205
12206 output.push_str(&format!(
12207 "@@ -{},{} +{},{} @@\n",
12208 old_start, old_count, new_start, new_count
12209 ));
12210 for op in &ops[start..end] {
12211 output.push(op.tag);
12212 output.push_str(&op.line);
12213 output.push('\n');
12214 }
12215 }
12216
12217 output
12218}
12219
12220fn func_context_line_matches(line: &str, funcname_matcher: Option<&FuncnameMatcher>) -> bool {
12226 if let Some(m) = funcname_matcher {
12227 return m.match_line(line).is_some();
12228 }
12229 is_func_line(line, None)
12230}
12231
12232fn extract_function_context(
12233 header: &str,
12234 old_lines: &[&str],
12235 funcname_matcher: Option<&FuncnameMatcher>,
12236 _source_path: Option<&str>,
12237) -> Option<String> {
12238 let at_pos = header.find("-")?;
12240 let rest = &header[at_pos + 1..];
12241 let comma_or_space = rest.find([',', ' '])?;
12242 let start_str = &rest[..comma_or_space];
12243 let start_line: usize = start_str.parse().ok()?;
12244
12245 let old_token_end = rest.find([' ', '\t']).unwrap_or(rest.len());
12249 let old_token = &rest[..old_token_end];
12250 let old_count: usize = if let Some(comma) = old_token.find(',') {
12251 old_token[comma + 1..].parse().unwrap_or(1)
12252 } else {
12253 1
12254 };
12255
12256 if start_line == 0 {
12257 return None;
12258 }
12259
12260 if start_line == 0 {
12267 return None;
12268 }
12269 let search_end = if old_count == 0 {
12270 start_line.min(old_lines.len())
12271 } else {
12272 if start_line <= 1 {
12273 return None;
12274 }
12275 (start_line - 1).min(old_lines.len())
12276 };
12277 let truncate = |line: &str| {
12278 let mut end = line.len().min(80);
12279 while end > 0 && !line.is_char_boundary(end) {
12280 end -= 1;
12281 }
12282 let mut out = line[..end].to_owned();
12283 let tail = line[end..].trim_start();
12284 if tail.is_empty()
12289 || (!tail.starts_with('"')
12290 && !tail.starts_with('\'')
12291 && !line.starts_with("msgstr")
12292 && !line.starts_with("msgid"))
12293 {
12294 out = out.trim_end().to_owned();
12295 }
12296 out
12297 };
12298
12299 for i in (0..search_end).rev() {
12300 let line = old_lines[i];
12301 if line.is_empty() {
12302 continue;
12303 }
12304 if func_context_line_matches(line, funcname_matcher) {
12305 if let Some(matcher) = funcname_matcher {
12306 if let Some(matched) = matcher.match_line(line) {
12307 return Some(truncate(&matched));
12308 }
12309 continue;
12310 }
12311 return Some(truncate(line));
12312 }
12313 }
12314 None
12315}
12316
12317fn last_line_index_without_trailing_newline(text: &str) -> Option<usize> {
12318 LineSide::from_text(text).incomplete_last_line_index()
12319}
12320
12321fn parse_unified_hunk_starts(header: &str) -> Option<(usize, usize)> {
12322 let rest = header.strip_prefix("@@ ")?;
12323 let (old_part, new_part) = rest.split_once(" +")?;
12324 let old_start = parse_unified_range_start(old_part.strip_prefix('-')?)?;
12325 let new_start = parse_unified_range_start(new_part.split(" @@").next()?)?;
12326 Some((old_start, new_start))
12327}
12328
12329fn parse_unified_range_start(spec: &str) -> Option<usize> {
12330 let spec = spec.trim();
12331 if let Some((start, _)) = spec.split_once(',') {
12332 start.parse().ok()
12333 } else {
12334 spec.parse().ok()
12335 }
12336}
12337
12338struct UnifiedDiffNoNewlineAnnotator {
12340 old_last: Option<usize>,
12341 new_last: Option<usize>,
12342 old_line: usize,
12343 new_line: usize,
12344 passthrough: bool,
12345}
12346
12347impl UnifiedDiffNoNewlineAnnotator {
12348 fn new(old: &str, new: &str) -> Self {
12349 let old_last = last_line_index_without_trailing_newline(old);
12350 let new_last = last_line_index_without_trailing_newline(new);
12351 Self {
12352 old_last,
12353 new_last,
12354 old_line: 1,
12355 new_line: 1,
12356 passthrough: old_last.is_none() && new_last.is_none(),
12357 }
12358 }
12359
12360 fn write_hunk<W: std::io::Write>(&mut self, out: &mut W, patch: &str) -> Result<()> {
12361 if self.passthrough {
12362 out.write_all(patch.as_bytes())?;
12363 return Ok(());
12364 }
12365 let mut lines = patch.lines().peekable();
12366 while let Some(line) = lines.next() {
12367 self.write_annotated_line(out, line, &mut lines)?;
12368 }
12369 Ok(())
12370 }
12371
12372 fn write_annotated_line<W: std::io::Write>(
12373 &mut self,
12374 out: &mut W,
12375 line: &str,
12376 lines: &mut std::iter::Peekable<std::str::Lines<'_>>,
12377 ) -> Result<()> {
12378 if let Some((o, n)) = line
12379 .strip_prefix("@@ ")
12380 .and_then(|_| parse_unified_hunk_starts(line))
12381 {
12382 self.old_line = o;
12383 self.new_line = n;
12384 writeln!(out, "{line}")?;
12385 return Ok(());
12386 }
12387 if line.starts_with("--- ")
12388 || line.starts_with("+++ ")
12389 || line.starts_with("diff --git ")
12390 || line.starts_with("diff --cc ")
12391 || line.starts_with("diff --combined ")
12392 {
12393 writeln!(out, "{line}")?;
12394 return Ok(());
12395 }
12396 if line.starts_with('-') {
12397 writeln!(out, "{line}")?;
12398 let already_marked = lines
12399 .peek()
12400 .is_some_and(|next| *next == "\\ No newline at end of file");
12401 if !already_marked && self.old_last == Some(self.old_line.saturating_sub(1)) {
12402 writeln!(out, "\\ No newline at end of file")?;
12403 }
12404 self.old_line += 1;
12405 return Ok(());
12406 }
12407 if line.starts_with('+') {
12408 writeln!(out, "{line}")?;
12409 let already_marked = lines
12410 .peek()
12411 .is_some_and(|next| *next == "\\ No newline at end of file");
12412 if !already_marked && self.new_last == Some(self.new_line.saturating_sub(1)) {
12413 writeln!(out, "\\ No newline at end of file")?;
12414 }
12415 self.new_line += 1;
12416 return Ok(());
12417 }
12418 if line.starts_with(' ') {
12419 writeln!(out, "{line}")?;
12420 let already_marked = lines
12421 .peek()
12422 .is_some_and(|next| *next == "\\ No newline at end of file");
12423 if !already_marked {
12424 let old_idx = self.old_line.saturating_sub(1);
12425 let new_idx = self.new_line.saturating_sub(1);
12426 if self.old_last == Some(old_idx) || self.new_last == Some(new_idx) {
12427 writeln!(out, "\\ No newline at end of file")?;
12428 }
12429 }
12430 self.old_line += 1;
12431 self.new_line += 1;
12432 return Ok(());
12433 }
12434 writeln!(out, "{line}")?;
12435 Ok(())
12436 }
12437}
12438
12439#[must_use]
12442pub fn annotate_unified_diff_no_newline_at_eof(patch: &str, old: &str, new: &str) -> String {
12443 let mut annotator = UnifiedDiffNoNewlineAnnotator::new(old, new);
12444 if annotator.passthrough {
12445 return patch.to_string();
12446 }
12447 let mut out = Vec::with_capacity(patch.len() + 64);
12448 let mut lines = patch.lines().peekable();
12449 while let Some(line) = lines.next() {
12450 if annotator
12451 .write_annotated_line(&mut out, line, &mut lines)
12452 .is_err()
12453 {
12454 return patch.to_string();
12455 }
12456 }
12457 String::from_utf8(out).unwrap_or_else(|_| patch.to_string())
12458}
12459
12460#[must_use]
12462pub fn normalize_ignore_space_change_line(line: &str) -> String {
12463 let mut result = String::with_capacity(line.len());
12464 let mut in_space = false;
12465 for c in line.chars() {
12466 if c.is_whitespace() {
12467 if !in_space {
12468 result.push(' ');
12469 in_space = true;
12470 }
12471 } else {
12472 result.push(c);
12473 in_space = false;
12474 }
12475 }
12476 while result.ends_with(' ') {
12477 result.pop();
12478 }
12479 result
12480}
12481
12482#[must_use]
12488pub fn normalize_ignore_space_change(content: &str) -> String {
12489 content
12490 .lines()
12491 .map(normalize_ignore_space_change_line)
12492 .collect::<Vec<_>>()
12493 .join("\n")
12494}
12495
12496#[must_use]
12500pub fn count_changes_with_algorithm(
12501 old_content: &str,
12502 new_content: &str,
12503 algorithm: similar::Algorithm,
12504 use_git_histogram: bool,
12505) -> (usize, usize) {
12506 use crate::diff::options::{DiffAlgorithm, DiffOptions};
12507 let diff_algorithm = if use_git_histogram {
12508 DiffAlgorithm::Histogram
12509 } else {
12510 match algorithm {
12511 similar::Algorithm::Myers => DiffAlgorithm::Myers,
12512 similar::Algorithm::Lcs => DiffAlgorithm::Minimal,
12513 similar::Algorithm::Patience => DiffAlgorithm::Patience,
12514 _ => DiffAlgorithm::Myers,
12515 }
12516 };
12517 count_line_changes_with_options(
12518 old_content,
12519 new_content,
12520 &DiffOptions {
12521 algorithm: diff_algorithm,
12522 ..DiffOptions::default()
12523 },
12524 )
12525}
12526
12527#[must_use]
12529pub fn count_changes_git_histogram(old_content: &str, new_content: &str) -> (usize, usize) {
12530 count_changes_with_algorithm(old_content, new_content, similar::Algorithm::Myers, true)
12531}
12532
12533#[must_use]
12538pub fn count_git_lines(data: &[u8]) -> usize {
12539 if data.is_empty() {
12540 return 0;
12541 }
12542 let mut count = 0usize;
12543 let mut nl_just_seen = false;
12544 for &ch in data {
12545 if ch == b'\n' {
12546 count += 1;
12547 nl_just_seen = true;
12548 } else {
12549 nl_just_seen = false;
12550 }
12551 }
12552 if !nl_just_seen {
12553 count += 1;
12554 }
12555 count
12556}
12557
12558pub const GIT_DIFF_MAX_SCORE: u64 = 60_000;
12560const DIFF_MAX_SCORE: u64 = GIT_DIFF_MAX_SCORE;
12561const DIFF_MINIMUM_BREAK_SIZE: usize = 400;
12562const DIFF_DEFAULT_BREAK_SCORE: u64 = 30_000;
12563pub const GIT_DIFF_DEFAULT_BREAK_SCORE: u64 = DIFF_DEFAULT_BREAK_SCORE;
12565const DIFF_HASHBASE: u32 = 107_927;
12566
12567#[derive(Clone, Copy, Default)]
12568struct SpanSlot {
12569 hashval: u32,
12570 cnt: u32,
12571}
12572
12573struct SpanHashTop {
12574 alloc_log2: u8,
12575 free_slots: i32,
12576 data: Vec<SpanSlot>,
12577}
12578
12579impl SpanHashTop {
12580 fn new(initial_log2: u8) -> Self {
12581 let cap = 1usize << initial_log2;
12582 Self {
12583 alloc_log2: initial_log2,
12584 free_slots: initial_free(initial_log2),
12585 data: vec![SpanSlot::default(); cap],
12586 }
12587 }
12588
12589 fn len(&self) -> usize {
12590 1usize << self.alloc_log2
12591 }
12592
12593 fn add_span(&mut self, hashval: u32, cnt: u32) {
12594 loop {
12595 let lim = self.len();
12596 let mut bucket = (hashval as usize) & (lim - 1);
12597 loop {
12598 let h = &mut self.data[bucket];
12599 if h.cnt == 0 {
12600 h.hashval = hashval;
12601 h.cnt = cnt;
12602 self.free_slots -= 1;
12603 if self.free_slots < 0 {
12604 self.rehash();
12605 break;
12606 }
12607 return;
12608 }
12609 if h.hashval == hashval {
12610 h.cnt = h.cnt.saturating_add(cnt);
12611 return;
12612 }
12613 bucket += 1;
12614 if bucket >= lim {
12615 bucket = 0;
12616 }
12617 }
12618 }
12619 }
12620
12621 fn rehash(&mut self) {
12622 let old = std::mem::take(&mut self.data);
12623 let old_log = self.alloc_log2;
12624 self.alloc_log2 = old_log.saturating_add(1);
12625 let new_len = 1usize << self.alloc_log2;
12626 self.free_slots = initial_free(self.alloc_log2);
12627 self.data = vec![SpanSlot::default(); new_len];
12628 let old_sz = 1usize << old_log;
12629 for o in old.iter().take(old_sz) {
12630 let o = *o;
12631 if o.cnt == 0 {
12632 continue;
12633 }
12634 self.add_span_after_rehash(o.hashval, o.cnt);
12635 }
12636 }
12637
12638 fn add_span_after_rehash(&mut self, hashval: u32, cnt: u32) {
12639 loop {
12640 let lim = self.len();
12641 let mut bucket = (hashval as usize) & (lim - 1);
12642 loop {
12643 let h = &mut self.data[bucket];
12644 if h.cnt == 0 {
12645 h.hashval = hashval;
12646 h.cnt = cnt;
12647 self.free_slots -= 1;
12648 if self.free_slots < 0 {
12649 self.rehash();
12650 break;
12651 }
12652 return;
12653 }
12654 if h.hashval == hashval {
12655 h.cnt = h.cnt.saturating_add(cnt);
12656 return;
12657 }
12658 bucket += 1;
12659 if bucket >= lim {
12660 bucket = 0;
12661 }
12662 }
12663 }
12664 }
12665
12666 fn sort_by_hashval(&mut self) {
12667 let sz = self.len();
12668 self.data[..sz].sort_by(|a, b| {
12669 if a.cnt == 0 {
12670 return std::cmp::Ordering::Greater;
12671 }
12672 if b.cnt == 0 {
12673 return std::cmp::Ordering::Less;
12674 }
12675 a.hashval.cmp(&b.hashval)
12676 });
12677 }
12678}
12679
12680fn initial_free(sz_log2: u8) -> i32 {
12681 let sz = sz_log2 as i32;
12682 ((1i32 << sz_log2) * (sz - 3) / sz).max(0)
12683}
12684
12685fn hash_blob_spans(buf: &[u8], is_text: bool) -> SpanHashTop {
12686 let mut hash = SpanHashTop::new(9);
12687 let mut n = 0u32;
12688 let mut accum1: u32 = 0;
12689 let mut accum2: u32 = 0;
12690 let mut i = 0usize;
12691 while i < buf.len() {
12692 let c = buf[i] as u32;
12693 let old_1 = accum1;
12694 i += 1;
12695
12696 if is_text && c == b'\r' as u32 && i < buf.len() && buf[i] == b'\n' {
12697 continue;
12698 }
12699
12700 accum1 = accum1.wrapping_shl(7) ^ accum2.wrapping_shr(25);
12701 accum2 = accum2.wrapping_shl(7) ^ old_1.wrapping_shr(25);
12702 accum1 = accum1.wrapping_add(c);
12703 n += 1;
12704 if n < 64 && c != b'\n' as u32 {
12705 continue;
12706 }
12707 let hashval = (accum1.wrapping_add(accum2.wrapping_mul(0x61))) % DIFF_HASHBASE;
12708 hash.add_span(hashval, n);
12709 n = 0;
12710 accum1 = 0;
12711 accum2 = 0;
12712 }
12713 if n > 0 {
12714 let hashval = (accum1.wrapping_add(accum2.wrapping_mul(0x61))) % DIFF_HASHBASE;
12715 hash.add_span(hashval, n);
12716 }
12717 hash.sort_by_hashval();
12718 hash
12719}
12720
12721#[must_use]
12726pub fn diffcore_count_changes(old: &[u8], new: &[u8]) -> (u64, u64) {
12727 crate::trace::trace_span!("grit.diff.blob");
12728 let src_is_text = !crate::merge_file::is_binary(old);
12729 let dst_is_text = !crate::merge_file::is_binary(new);
12730 let src_count = hash_blob_spans(old, src_is_text);
12731 let dst_count = hash_blob_spans(new, dst_is_text);
12732 let mut sc: u64 = 0;
12733 let mut la: u64 = 0;
12734 let mut si = 0usize;
12735 let mut di = 0usize;
12736 let src_len = src_count.len();
12737 let dst_len = dst_count.len();
12738 loop {
12739 if si >= src_len || src_count.data[si].cnt == 0 {
12740 break;
12741 }
12742 let s_hash = src_count.data[si].hashval;
12743 let s_cnt = u64::from(src_count.data[si].cnt);
12744 while di < dst_len && dst_count.data[di].cnt != 0 && dst_count.data[di].hashval < s_hash {
12745 la += u64::from(dst_count.data[di].cnt);
12746 di += 1;
12747 }
12748 let mut dst_cnt = 0u64;
12749 if di < dst_len && dst_count.data[di].cnt != 0 && dst_count.data[di].hashval == s_hash {
12750 dst_cnt = u64::from(dst_count.data[di].cnt);
12751 di += 1;
12752 }
12753 if s_cnt < dst_cnt {
12754 la += dst_cnt - s_cnt;
12755 sc += s_cnt;
12756 } else {
12757 sc += dst_cnt;
12758 }
12759 si += 1;
12760 }
12761 while di < dst_len && dst_count.data[di].cnt != 0 {
12762 la += u64::from(dst_count.data[di].cnt);
12763 di += 1;
12764 }
12765 (sc, la)
12766}
12767
12768#[must_use]
12772pub fn should_break_rewrite_pair(old: &[u8], new: &[u8], break_score: u64) -> bool {
12773 should_break_rewrite_inner(old, new, break_score)
12774}
12775
12776#[must_use]
12778pub fn apply_diff_filter(
12779 entries: Vec<DiffEntry>,
12780 filter: Option<super::filter::DiffFilter>,
12781) -> Vec<DiffEntry> {
12782 match filter {
12783 Some(f) => f.filter_entries(entries),
12784 None => entries,
12785 }
12786}
12787
12788#[must_use]
12790pub fn parse_diff_rename_threshold_percent(arg: &str) -> Option<u8> {
12791 let token = if arg.is_empty() { "50" } else { arg };
12792 let internal = parse_diff_rename_score_token(token)?;
12793 Some((internal.saturating_mul(100) / GIT_DIFF_MAX_SCORE).min(100) as u8)
12794}
12795
12796pub fn parse_diff_rename_score_token(arg: &str) -> Option<u64> {
12799 let mut num: u64 = 0;
12800 let mut scale: u64 = 1;
12801 let mut dot = false;
12802 let mut saw_digit = false;
12803 for ch in arg.chars() {
12804 if !dot && ch == '.' {
12805 scale = 1;
12806 dot = true;
12807 continue;
12808 }
12809 if ch == '%' {
12810 scale = if dot { scale.saturating_mul(100) } else { 100 };
12811 break;
12812 }
12813 if ch.is_ascii_digit() {
12814 saw_digit = true;
12815 if scale < 100_000 {
12816 scale = scale.saturating_mul(10);
12817 num = num.saturating_mul(10) + u64::from(ch as u8 - b'0');
12818 }
12819 } else {
12820 break;
12821 }
12822 }
12823 if !saw_digit {
12824 return None;
12825 }
12826 Some(if num >= scale {
12827 GIT_DIFF_MAX_SCORE
12828 } else {
12829 GIT_DIFF_MAX_SCORE * num / scale
12830 })
12831}
12832
12833#[must_use]
12836pub fn rewrite_merge_score(old: &[u8], new: &[u8]) -> Option<u64> {
12837 if old.is_empty() {
12838 return None;
12839 }
12840 let max_size = old.len().max(new.len());
12841 if max_size < DIFF_MINIMUM_BREAK_SIZE {
12842 return None;
12843 }
12844 let (src_copied, _) = diffcore_count_changes(old, new);
12845 let src_copied = src_copied.min(old.len() as u64);
12846 let src_removed = (old.len() as u64).saturating_sub(src_copied);
12847 Some(src_removed * DIFF_MAX_SCORE / old.len() as u64)
12848}
12849
12850#[must_use]
12852pub fn rewrite_dissimilarity_index_percent(old: &[u8], new: &[u8]) -> Option<u32> {
12853 let score = rewrite_merge_score(old, new)?;
12854 Some((score * 100 / DIFF_MAX_SCORE).min(100) as u32)
12855}
12856
12857fn should_break_rewrite_inner(src: &[u8], dst: &[u8], break_score: u64) -> bool {
12858 if src.is_empty() {
12859 return false;
12860 }
12861 let max_size = src.len().max(dst.len());
12862 if max_size < DIFF_MINIMUM_BREAK_SIZE {
12863 return false;
12864 }
12865 let (src_copied, literal_added) = diffcore_count_changes(src, dst);
12866 let src_copied = src_copied.min(src.len() as u64);
12867 let mut literal_added = literal_added;
12868 let dst_len = dst.len() as u64;
12869 if src_copied < dst_len && literal_added + src_copied > dst_len {
12870 literal_added = dst_len.saturating_sub(src_copied);
12871 }
12872 let src_removed = (src.len() as u64).saturating_sub(src_copied);
12873 let merge_score = src_removed * DIFF_MAX_SCORE / src.len() as u64;
12874 if merge_score > break_score {
12875 return true;
12876 }
12877 let delta_size = src_removed.saturating_add(literal_added);
12878 if delta_size * DIFF_MAX_SCORE / (max_size as u64) < break_score {
12879 return false;
12880 }
12881 let s = src.len() as u64;
12882 if (s * break_score < src_removed * DIFF_MAX_SCORE)
12883 && (literal_added * 20 < src_removed)
12884 && (literal_added * 20 < src_copied)
12885 {
12886 return false;
12887 }
12888 true
12889}
12890
12891struct FlatEntry {
12895 path: String,
12896 mode: u32,
12897 oid: ObjectId,
12898}
12899
12900fn flatten_tree(odb: &Odb, tree_oid: &ObjectId, prefix: &str) -> Result<Vec<FlatEntry>> {
12901 flatten_tree_with_index(odb, tree_oid, prefix, None)
12902}
12903
12904fn sparse_index_skips_tree_descent(index: &Index, dir_path: &str, tree_oid: &ObjectId) -> bool {
12905 let mut storage = crate::repo_relpath::storage_bytes(dir_path).into_owned();
12906 if !storage.ends_with(b"/") {
12907 storage.push(b'/');
12908 }
12909 index.find_entry_pos(&storage, 0).is_some_and(|pos| {
12910 let entry = &index.entries[pos];
12911 entry.is_sparse_directory_placeholder() && entry.oid == *tree_oid
12912 })
12913}
12914
12915fn subtree_oid_at_path(odb: &Odb, root: &ObjectId, path: &str) -> Result<Option<ObjectId>> {
12917 let mut oid = *root;
12918 for component in path.split('/').filter(|c| !c.is_empty()) {
12919 let entries = read_tree(odb, &oid)?;
12920 let Some(entry) = entries
12921 .iter()
12922 .find(|e| e.name.as_slice() == component.as_bytes())
12923 else {
12924 return Ok(None);
12925 };
12926 if !is_tree_mode(entry.mode) {
12927 return Ok(None);
12928 }
12929 oid = entry.oid;
12930 }
12931 Ok(Some(oid))
12932}
12933
12934fn emit_flattened_subtree_as_added(
12936 odb: &Odb,
12937 subtree: &ObjectId,
12938 prefix: &str,
12939 ignore_submodules: bool,
12940 result: &mut Vec<DiffEntry>,
12941) -> Result<()> {
12942 for entry in flatten_tree(odb, subtree, prefix)? {
12943 if ignore_submodules && entry.mode == 0o160000 {
12944 continue;
12945 }
12946 result.push(DiffEntry {
12947 status: DiffStatus::Added,
12948 old_path: None,
12949 new_path: Some(entry.path),
12950 old_mode: FileMode::ABSENT,
12951 new_mode: FileMode::new(entry.mode),
12952 old_oid: zero_oid(),
12953 new_oid: entry.oid,
12954 score: None,
12955 });
12956 }
12957 Ok(())
12958}
12959
12960fn diff_flattened_subtrees(
12962 head: &[FlatEntry],
12963 index: &[FlatEntry],
12964 ignore_submodules: bool,
12965 result: &mut Vec<DiffEntry>,
12966 stage0_paths: &mut std::collections::BTreeSet<String>,
12967 tree_map: &mut std::collections::BTreeMap<&str, &FlatEntry>,
12968) {
12969 let mut head_map: std::collections::BTreeMap<&str, &FlatEntry> = head
12970 .iter()
12971 .map(|entry| (entry.path.as_str(), entry))
12972 .collect();
12973 for entry in index {
12974 stage0_paths.insert(entry.path.clone());
12975 tree_map.remove(entry.path.as_str());
12976 match head_map.remove(entry.path.as_str()) {
12977 Some(te) => {
12978 if te.oid != entry.oid || te.mode != entry.mode {
12979 result.push(DiffEntry {
12980 status: DiffStatus::for_content_change(te.mode, entry.mode),
12981 old_path: Some(entry.path.clone()),
12982 new_path: Some(entry.path.clone()),
12983 old_mode: FileMode::new(te.mode),
12984 new_mode: FileMode::new(entry.mode),
12985 old_oid: te.oid,
12986 new_oid: entry.oid,
12987 score: None,
12988 });
12989 }
12990 }
12991 None => {
12992 result.push(DiffEntry {
12993 status: DiffStatus::Added,
12994 old_path: None,
12995 new_path: Some(entry.path.clone()),
12996 old_mode: FileMode::ABSENT,
12997 new_mode: FileMode::new(entry.mode),
12998 old_oid: zero_oid(),
12999 new_oid: entry.oid,
13000 score: None,
13001 });
13002 }
13003 }
13004 }
13005 for (path, te) in head_map {
13006 if ignore_submodules && te.mode == 0o160000 {
13007 continue;
13008 }
13009 tree_map.remove(path);
13010 result.push(DiffEntry {
13011 status: DiffStatus::Deleted,
13012 old_path: Some(path.to_owned()),
13013 new_path: None,
13014 old_mode: FileMode::new(te.mode),
13015 new_mode: FileMode::ABSENT,
13016 old_oid: te.oid,
13017 new_oid: zero_oid(),
13018 score: None,
13019 });
13020 }
13021}
13022
13023fn flatten_tree_with_index(
13024 odb: &Odb,
13025 tree_oid: &ObjectId,
13026 prefix: &str,
13027 index: Option<&Index>,
13028) -> Result<Vec<FlatEntry>> {
13029 let entries = read_tree(odb, tree_oid)?;
13030 let mut result = Vec::new();
13031
13032 for entry in entries {
13033 let name_str = String::from_utf8_lossy(&entry.name);
13034 let path = format_path(prefix, &name_str);
13035 if is_tree_mode(entry.mode) {
13036 if index.is_some_and(|idx| sparse_index_skips_tree_descent(idx, &path, &entry.oid)) {
13037 continue;
13038 }
13039 let nested = flatten_tree_with_index(odb, &entry.oid, &path, index)?;
13040 result.extend(nested);
13041 } else {
13042 result.push(FlatEntry {
13043 path,
13044 mode: entry.mode,
13045 oid: entry.oid,
13046 });
13047 }
13048 }
13049
13050 Ok(result)
13051}
13052
13053pub fn head_path_states(
13055 odb: &Odb,
13056 head_tree: Option<&ObjectId>,
13057) -> Result<std::collections::BTreeMap<String, (u32, ObjectId)>> {
13058 let mut m = std::collections::BTreeMap::new();
13059 let Some(t) = head_tree else {
13060 return Ok(m);
13061 };
13062 for fe in flatten_tree(odb, t, "")? {
13063 m.insert(fe.path, (fe.mode, fe.oid));
13064 }
13065 Ok(m)
13066}
13067
13068fn is_tree_mode(mode: u32) -> bool {
13070 crate::index::is_tree_mode(mode)
13071}
13072
13073fn path_order(a: &DiffEntry, b: &DiffEntry) -> std::cmp::Ordering {
13076 use crate::repo_relpath::storage_bytes;
13077 storage_bytes(a.path()).cmp(&storage_bytes(b.path()))
13078}
13079
13080fn entries_sorted_by_path(entries: &[DiffEntry]) -> bool {
13081 entries.is_sorted_by(|a, b| path_order(a, b).is_le())
13082}
13083
13084fn entries_strictly_sorted_by_path(entries: &[DiffEntry]) -> bool {
13085 entries.is_sorted_by(|a, b| path_order(a, b).is_lt())
13086}
13087
13088pub fn sort_entries_by_path(entries: &mut [DiffEntry]) {
13099 if entries_sorted_by_path(entries) {
13100 return;
13101 }
13102 entries.sort_by(path_order);
13103}
13104
13105fn format_path(prefix: &str, name: &str) -> String {
13107 if prefix.is_empty() {
13108 name.to_owned()
13109 } else {
13110 let mut path = String::with_capacity(prefix.len() + 1 + name.len());
13111 path.push_str(prefix);
13112 path.push('/');
13113 path.push_str(name);
13114 path
13115 }
13116}
13117
13118pub fn format_mode(mode: u32) -> String {
13120 format!("{mode:06o}")
13121}
13122
13123#[must_use]
13127pub fn read_submodule_head_for_checkout(sub_dir: &Path) -> Option<ObjectId> {
13128 read_submodule_head(sub_dir)
13129}
13130
13131#[must_use]
13136pub fn submodule_commit_subject_line(c: &CommitData) -> String {
13137 let enc = c.encoding.as_deref().unwrap_or("UTF-8");
13138 let is_latin1 = enc.eq_ignore_ascii_case("ISO8859-1")
13139 || enc.eq_ignore_ascii_case("ISO-8859-1")
13140 || enc.eq_ignore_ascii_case("LATIN1")
13141 || enc.eq_ignore_ascii_case("ISO-8859-15");
13142 if let Some(raw) = c.raw_message.as_deref() {
13143 let line = raw.split(|b| *b == b'\n').next().unwrap_or(raw);
13144 if is_latin1 {
13145 return line
13146 .iter()
13147 .map(|&b| b as char)
13148 .collect::<String>()
13149 .trim()
13150 .to_owned();
13151 }
13152 return String::from_utf8_lossy(line).trim().to_string();
13153 }
13154 c.message.lines().next().unwrap_or("").trim().to_owned()
13155}
13156
13157fn uninitialized_gitlink_worktree_matches_index(sub_dir: &Path) -> bool {
13161 submodule_embedded_git_dir(sub_dir).is_none()
13162}
13163
13164fn read_submodule_head(sub_dir: &Path) -> Option<ObjectId> {
13165 read_submodule_head_oid(sub_dir)
13166}
13167
13168#[must_use]
13170pub fn submodule_embedded_git_dir(sub_dir: &Path) -> Option<PathBuf> {
13171 let gitfile = sub_dir.join(".git");
13172 let meta = fs::metadata(&gitfile).ok()?;
13173 if meta.is_file() {
13174 let content = fs::read_to_string(&gitfile).ok()?;
13175 let gitdir = content
13176 .lines()
13177 .find_map(|l| l.strip_prefix("gitdir: "))?
13178 .trim();
13179 Some(if Path::new(gitdir).is_absolute() {
13180 PathBuf::from(gitdir)
13181 } else {
13182 sub_dir.join(gitdir)
13183 })
13184 } else if meta.is_dir() {
13185 Some(gitfile)
13186 } else {
13187 None
13188 }
13189}
13190
13191#[must_use]
13197pub(crate) fn is_unregistered_embedded_git_repo(index: &Index, rel: &str, abs: &Path) -> bool {
13198 if rel.is_empty() || index.has_gitlink_at(rel.as_bytes()) {
13199 return false;
13200 }
13201 let Some(git_dir) = submodule_embedded_git_dir(abs) else {
13202 return false;
13203 };
13204 crate::submodule_gitdir::is_valid_git_dir(&git_dir)
13205}
13206
13207pub(crate) enum EmbeddedGitRepoStage {
13209 NotEmbedded,
13211 Gitlink(crate::objects::ObjectId),
13213}
13214
13215pub(crate) fn classify_embedded_git_repo_for_stage(
13224 _index: &Index,
13225 rel: &str,
13226 abs: &Path,
13227) -> crate::error::Result<EmbeddedGitRepoStage> {
13228 if rel.is_empty() {
13229 return Ok(EmbeddedGitRepoStage::NotEmbedded);
13230 }
13231 let Some(git_dir) = submodule_embedded_git_dir(abs) else {
13232 return Ok(EmbeddedGitRepoStage::NotEmbedded);
13233 };
13234 if !crate::submodule_gitdir::is_valid_git_dir(&git_dir) {
13235 return Ok(EmbeddedGitRepoStage::NotEmbedded);
13236 }
13237 let path = rel.trim_end_matches('/').to_owned();
13238 match read_submodule_head_oid_for_stage(abs) {
13239 Some(oid) => Ok(EmbeddedGitRepoStage::Gitlink(oid)),
13240 None => Err(crate::error::Error::EmbeddedRepoWithoutCommit { path }),
13241 }
13242}
13243
13244fn find_superproject_git(sub_dir: &Path) -> Option<(PathBuf, PathBuf)> {
13246 let mut cur = sub_dir.parent()?;
13247 loop {
13248 let git_path = cur.join(".git");
13249 if git_path.exists() {
13250 let gd = if git_path.is_file() {
13251 let content = fs::read_to_string(&git_path).ok()?;
13252 let line = content
13253 .lines()
13254 .find_map(|l| l.strip_prefix("gitdir: "))?
13255 .trim();
13256 if Path::new(line).is_absolute() {
13257 PathBuf::from(line)
13258 } else {
13259 cur.join(line)
13260 }
13261 } else {
13262 git_path
13263 };
13264 return Some((cur.to_path_buf(), gd));
13265 }
13266 cur = cur.parent()?;
13267 }
13268}
13269
13270fn submodule_git_dir_for_head_read(sub_dir: &Path) -> Option<PathBuf> {
13271 let mut git_dir = submodule_embedded_git_dir(sub_dir)?;
13272 if let Some((super_wt, super_git_dir)) = find_superproject_git(sub_dir) {
13273 let rel = sub_dir.strip_prefix(&super_wt).ok()?;
13274 let rel_str = rel.to_string_lossy().replace('\\', "/");
13275 let local_mod = super_git_dir
13276 .join("modules")
13277 .join(rel_str.trim_start_matches('/'));
13278 if local_mod.join("HEAD").exists() {
13279 let sg = super_git_dir
13280 .canonicalize_simplified()
13281 .unwrap_or(super_git_dir);
13282 let cur = git_dir
13283 .canonicalize_simplified()
13284 .unwrap_or_else(|_| git_dir.clone());
13285 if !cur.starts_with(&sg) {
13286 git_dir = local_mod;
13287 }
13288 }
13289 }
13290 Some(git_dir)
13291}
13292
13293fn read_submodule_head_oid_from_git_dir(
13294 git_dir: &Path,
13295 allow_branch_fallback: bool,
13296) -> Option<ObjectId> {
13297 let head_content = fs::read_to_string(git_dir.join("HEAD")).ok()?;
13298 let head_trimmed = head_content.trim();
13299 if head_trimmed.starts_with("ref: ") {
13300 match crate::refs::resolve_ref(git_dir, "HEAD") {
13301 Ok(oid) => Some(oid),
13302 Err(_) if allow_branch_fallback => {
13303 for branch in ["main", "master"] {
13304 let p = git_dir.join("refs/heads").join(branch);
13305 if let Ok(s) = fs::read_to_string(&p) {
13306 if let Ok(o) = ObjectId::from_hex(s.trim()) {
13307 return Some(o);
13308 }
13309 }
13310 }
13311 None
13312 }
13313 Err(_) => None,
13314 }
13315 } else {
13316 ObjectId::from_hex(head_trimmed).ok()
13317 }
13318}
13319
13320pub fn read_submodule_head_oid(sub_dir: &Path) -> Option<ObjectId> {
13326 let git_dir = submodule_git_dir_for_head_read(sub_dir)?;
13327 read_submodule_head_oid_from_git_dir(&git_dir, true)
13328}
13329
13330pub(crate) fn read_submodule_head_oid_for_stage(sub_dir: &Path) -> Option<ObjectId> {
13333 let git_dir = submodule_git_dir_for_head_read(sub_dir)?;
13334 read_submodule_head_oid_from_git_dir(&git_dir, false)
13335}
13336
13337#[must_use]
13349pub fn submodule_head_object_broken(sub_dir: &Path) -> bool {
13350 let Some(sub_git_dir) = submodule_embedded_git_dir(sub_dir) else {
13351 return false;
13352 };
13353 let Some(head_oid) = read_submodule_head_oid(sub_dir) else {
13354 return false;
13355 };
13356 let odb = Odb::new(&sub_git_dir.join("objects"));
13357 match odb.read(&head_oid) {
13358 Ok(obj) => parse_commit(&obj.data).is_err(),
13361 Err(_) => true,
13362 }
13363}
13364
13365#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
13367pub struct SubmodulePorcelainFlags {
13368 pub new_commits: bool,
13370 pub modified: bool,
13372 pub untracked: bool,
13374}
13375
13376pub fn submodule_porcelain_flags(
13382 super_worktree: &Path,
13383 rel_path: &str,
13384 recorded_oid: ObjectId,
13385) -> SubmodulePorcelainFlags {
13386 let sub_dir = super_worktree.join(rel_path);
13387 let Some(sub_git_dir) = submodule_embedded_git_dir(&sub_dir) else {
13388 return SubmodulePorcelainFlags::default();
13389 };
13390 let Some(sub_head) = read_submodule_head_oid(&sub_dir) else {
13391 return SubmodulePorcelainFlags::default();
13392 };
13393
13394 let new_commits = sub_head != recorded_oid;
13395
13396 let index_path = sub_git_dir.join("index");
13397 let mut sub_index = match crate::index::Index::load(&index_path) {
13398 Ok(ix) => ix,
13399 Err(_) => {
13400 return SubmodulePorcelainFlags {
13401 new_commits,
13402 ..Default::default()
13403 }
13404 }
13405 };
13406
13407 let tracked: std::collections::BTreeSet<String> = sub_index
13408 .entries
13409 .iter()
13410 .filter(|e| e.stage() == 0)
13411 .map(|e| String::from_utf8_lossy(&e.path).into_owned())
13412 .collect();
13413 let untracked = submodule_dir_has_untracked_inner(&sub_dir, &sub_dir, &tracked, &sub_index);
13414
13415 let objects_dir = sub_git_dir.join("objects");
13416 let odb = Odb::new(&objects_dir);
13417
13418 let sub_head_tree = (|| -> Option<ObjectId> {
13419 let h = fs::read_to_string(sub_git_dir.join("HEAD")).ok()?;
13420 let h_str = h.trim();
13421 let commit_oid = if let Some(r) = h_str.strip_prefix("ref: ") {
13422 let oid_hex = fs::read_to_string(sub_git_dir.join(r)).ok()?;
13423 ObjectId::from_hex(oid_hex.trim()).ok()?
13424 } else {
13425 ObjectId::from_hex(h_str).ok()?
13426 };
13427 let obj = odb.read(&commit_oid).ok()?;
13428 let commit = parse_commit(&obj.data).ok()?;
13429 Some(commit.tree)
13430 })();
13431
13432 let staged_dirty = sub_head_tree
13433 .as_ref()
13434 .map(|t| diff_index_to_tree(&odb, &sub_index, Some(t), false).map(|v| !v.is_empty()))
13435 .unwrap_or(Ok(false));
13436 let staged_dirty = staged_dirty.unwrap_or(false);
13437
13438 let unstaged_dirty = diff_index_to_worktree(&odb, &mut sub_index, &sub_dir, false, true)
13439 .map(|v| !v.is_empty())
13440 .unwrap_or(false);
13441
13442 let mut modified = staged_dirty || unstaged_dirty;
13443
13444 for e in &sub_index.entries {
13449 if e.stage() != 0 || e.mode != 0o160000 {
13450 continue;
13451 }
13452 let child = String::from_utf8_lossy(&e.path).into_owned();
13453 let full_rel = if rel_path.is_empty() {
13454 child
13455 } else {
13456 format!("{rel_path}/{child}")
13457 };
13458 let nested = submodule_porcelain_flags(super_worktree, &full_rel, e.oid);
13459 modified |= nested.modified;
13460 }
13461
13462 SubmodulePorcelainFlags {
13463 new_commits,
13464 modified,
13465 untracked,
13466 }
13467}
13468
13469fn submodule_dir_has_untracked_inner(
13470 dir: &Path,
13471 root: &Path,
13472 tracked: &std::collections::BTreeSet<String>,
13473 owning_index: &Index,
13474) -> bool {
13475 let entries = match fs::read_dir(dir) {
13476 Ok(e) => e,
13477 Err(_) => return false,
13478 };
13479 let mut sorted: Vec<_> = entries.filter_map(|e| e.ok()).collect();
13480 sorted.sort_by_key(|e| e.file_name());
13481
13482 for entry in sorted {
13483 let name = entry.file_name().to_string_lossy().to_string();
13484 if name == ".git" {
13485 continue;
13486 }
13487 let path = entry.path();
13488 let rel = path
13489 .strip_prefix(root)
13490 .map(|p| p.to_string_lossy().to_string())
13491 .unwrap_or_else(|_| name.clone());
13492
13493 let is_dir = entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false);
13494 if is_dir {
13495 let is_gitlink = owning_index
13496 .get(rel.as_bytes(), 0)
13497 .is_some_and(|e| e.mode == 0o160000);
13498 if is_gitlink {
13499 let Some(nested_git) = submodule_embedded_git_dir(&path) else {
13500 continue;
13501 };
13502 let nested_index_path = nested_git.join("index");
13503 let Ok(nested_ix) = crate::index::Index::load(&nested_index_path) else {
13504 continue;
13505 };
13506 let nested_tracked: std::collections::BTreeSet<String> = nested_ix
13507 .entries
13508 .iter()
13509 .filter(|e| e.stage() == 0)
13510 .map(|e| String::from_utf8_lossy(&e.path).into_owned())
13511 .collect();
13512 if submodule_dir_has_untracked_inner(&path, &path, &nested_tracked, &nested_ix) {
13513 return true;
13514 }
13515 } else if submodule_dir_has_untracked_inner(&path, root, tracked, owning_index) {
13516 return true;
13517 }
13518 } else if !tracked.contains(&rel) {
13519 return true;
13520 }
13521 }
13522 false
13523}
13524
13525#[cfg(test)]
13526mod line_compare_key_tests {
13527 use super::*;
13528 use crate::diff::unified_emit::line_diff_token;
13529 use crate::diff::WhitespaceMode;
13530
13531 #[test]
13532 fn borrowed_keys_match_owned_line_tokens() {
13533 let texts = [
13534 "",
13535 "\n",
13536 "a",
13537 "a\n",
13538 "a\nb",
13539 "a\r\nb\r\n",
13540 "a\r\nb\r",
13541 "\n\n\nlast",
13542 "x\n\r\n\ry\n",
13543 ];
13544 for text in texts {
13545 let side = LineSide::from_text(text);
13546 let mut normalized = Vec::new();
13547 let keys = line_compare_keys(&side, WhitespaceMode::default(), &mut normalized);
13548 let expected: Vec<String> = side.lines().map(|l| line_diff_token(&l)).collect();
13549 assert_eq!(keys, expected, "text {text:?}");
13550 let source = text.as_bytes().as_ptr_range();
13551 assert!(
13552 keys.iter()
13553 .all(|key| source.contains(&key.as_ptr()) || key.is_empty()),
13554 "text {text:?}"
13555 );
13556 }
13557 }
13558
13559 #[test]
13560 fn whitespace_modes_compare_normalized_bodies() {
13561 let text = "a b\nab \n";
13562 let side = LineSide::from_text(text);
13563 let ws = WhitespaceMode {
13564 ignore_all_space: true,
13565 ..WhitespaceMode::default()
13566 };
13567 let mut normalized = Vec::new();
13568 let keys = line_compare_keys(&side, ws, &mut normalized);
13569 assert_eq!(keys, vec!["ab", "ab"]);
13570 }
13571
13572 #[test]
13573 fn xdiff_marks_only_edited_lines_in_large_file() {
13574 let old: String = (0..5_000).map(|i| format!("line{i}\n")).collect();
13575 let new: String = (0..5_000)
13576 .map(|i| {
13577 if i % 100 == 0 {
13578 format!("LINE{i}\n")
13579 } else {
13580 format!("line{i}\n")
13581 }
13582 })
13583 .collect();
13584 let body =
13585 git_xdiff_unified_body_raw(&old, &new, 3, 0, true, false, WhitespaceMode::default());
13586 assert_eq!(body.matches("\n@@ ").count() + 1, 50);
13587 assert_eq!(body.lines().filter(|l| l.starts_with('-')).count(), 50);
13588 assert_eq!(body.lines().filter(|l| l.starts_with('+')).count(), 50);
13589 assert!(body.starts_with("@@ -1,4 +1,4 @@\n-line0\n+LINE0\n line1\n"));
13590 }
13591}
13592
13593#[cfg(test)]
13594mod entry_order_tests {
13595 use super::*;
13596
13597 fn entry(status: DiffStatus, path: &str, score: Option<u32>) -> DiffEntry {
13598 let mode = |present: bool| {
13599 if present {
13600 FileMode::REGULAR
13601 } else {
13602 FileMode::ABSENT
13603 }
13604 };
13605 DiffEntry {
13606 status,
13607 old_path: (status != DiffStatus::Added).then(|| path.to_owned()),
13608 new_path: (status != DiffStatus::Deleted).then(|| path.to_owned()),
13609 old_mode: mode(status != DiffStatus::Added),
13610 new_mode: mode(status != DiffStatus::Deleted),
13611 old_oid: ObjectId::zero(),
13612 new_oid: ObjectId::zero(),
13613 score,
13614 }
13615 }
13616
13617 #[test]
13618 fn sort_entries_by_path_orders_by_path_and_keeps_equal_paths_in_place() {
13619 let mut entries = vec![
13620 entry(DiffStatus::Modified, "b", None),
13621 entry(DiffStatus::Deleted, "a/x", Some(1)),
13622 entry(DiffStatus::Added, "a/x", Some(2)),
13623 entry(DiffStatus::Added, "a", None),
13624 ];
13625 sort_entries_by_path(&mut entries);
13626 let order: Vec<_> = entries.iter().map(|e| (e.path(), e.score)).collect();
13627 assert_eq!(
13628 order,
13629 [("a", None), ("a/x", Some(1)), ("a/x", Some(2)), ("b", None)]
13630 );
13631 let sorted = entries.clone();
13632 sort_entries_by_path(&mut entries);
13633 assert_eq!(entries, sorted);
13634 sort_entries_by_path(&mut []);
13635 }
13636
13637 #[test]
13638 fn sort_entries_by_path_compares_non_utf8_paths_by_raw_bytes() {
13639 let raw = crate::repo_relpath::format_path_stored("", b"raw-\xff");
13640 assert!(raw.starts_with("grit+pathhex:"));
13641 let mut entries = vec![
13642 entry(DiffStatus::Modified, &raw, None),
13643 entry(DiffStatus::Modified, "node/leaf", None),
13644 entry(DiffStatus::Modified, "zeta", None),
13645 ];
13646 sort_entries_by_path(&mut entries);
13647 let paths: Vec<_> = entries.iter().map(DiffEntry::path).collect();
13648 assert_eq!(paths, ["node/leaf", raw.as_str(), "zeta"]);
13649 }
13650
13651 #[test]
13652 fn rename_pass_returns_unpairable_lists_in_path_order() {
13653 let config = ConfigSet::new();
13654 let policy = ThreadPolicy::default();
13655 let odb = Odb::new(Path::new("/nonexistent-objects"));
13656 let run = |entries: Vec<DiffEntry>| {
13657 detect_renames_with_policy(
13658 &odb,
13659 None,
13660 entries,
13661 50,
13662 &config,
13663 &policy,
13664 None,
13665 DetectRenamesOptions::default(),
13666 )
13667 .expect("no blob is read without both adds and deletes")
13668 .entries
13669 };
13670 let sorted = vec![
13671 entry(DiffStatus::Deleted, "a", None),
13672 entry(DiffStatus::Modified, "b", None),
13673 entry(DiffStatus::Deleted, "c", None),
13674 ];
13675 assert_eq!(run(sorted.clone()), sorted);
13676 let unsorted = vec![
13677 entry(DiffStatus::Added, "z", None),
13678 entry(DiffStatus::Modified, "m", None),
13679 entry(DiffStatus::Added, "a", None),
13680 ];
13681 let paths: Vec<_> = run(unsorted).iter().map(|e| e.path().to_owned()).collect();
13682 assert_eq!(paths, ["a", "m", "z"]);
13683 let duplicate = vec![
13685 entry(DiffStatus::Added, "p", None),
13686 entry(DiffStatus::Modified, "p", None),
13687 ];
13688 let statuses: Vec<_> = run(duplicate).iter().map(|e| e.status).collect();
13689 assert_eq!(statuses, [DiffStatus::Modified, DiffStatus::Added]);
13690 }
13691}
13692
13693#[cfg(test)]
13694mod diff_index_to_tree_cache_tree_tests {
13695 use std::rc::Rc;
13696 use std::sync::atomic::{AtomicUsize, Ordering};
13697
13698 use super::*;
13699 use crate::index::{Index, IndexEntry, MODE_EXECUTABLE, MODE_GITLINK, MODE_REGULAR};
13700 use crate::objects::ObjectKind;
13701 use crate::write_tree::{build_cache_tree_from_index, write_tree_from_index};
13702 use tempfile::TempDir;
13703
13704 struct TreeReadCounter {
13705 counter: Rc<AtomicUsize>,
13706 }
13707
13708 impl TreeReadCounter {
13709 fn start() -> Self {
13710 let counter = Rc::new(AtomicUsize::new(0));
13711 set_diff_tree_read_hook_for_tests(Some(counter.clone()));
13712 Self { counter }
13713 }
13714
13715 fn get(&self) -> usize {
13716 self.counter.load(Ordering::Relaxed)
13717 }
13718 }
13719
13720 impl Drop for TreeReadCounter {
13721 fn drop(&mut self) {
13722 set_diff_tree_read_hook_for_tests(None);
13723 }
13724 }
13725
13726 fn diff_index_to_tree_flatten_reference(
13727 odb: &Odb,
13728 index: &Index,
13729 head_tree: &ObjectId,
13730 ignore_submodules: bool,
13731 ) -> Result<Vec<DiffEntry>> {
13732 let mut flat_index = index.clone();
13733 flat_index.clear_cache_tree();
13734 diff_index_to_tree(odb, &flat_index, Some(head_tree), ignore_submodules)
13735 }
13736
13737 fn index_entry(path: &str, mode: u32, oid: ObjectId) -> IndexEntry {
13738 IndexEntry {
13739 ctime_sec: 0,
13740 ctime_nsec: 0,
13741 mtime_sec: 0,
13742 mtime_nsec: 0,
13743 dev: 0,
13744 ino: 0,
13745 mode,
13746 uid: 0,
13747 gid: 0,
13748 size: 0,
13749 oid,
13750 flags: path.len().min(0xfff) as u16,
13751 flags_extended: None,
13752 path: path.as_bytes().to_vec(),
13753 base_index_pos: 0,
13754 }
13755 }
13756
13757 fn repo_with_tree(depth: usize, width: usize) -> (TempDir, Odb, ObjectId, Index) {
13758 let temp = TempDir::new().expect("tempdir");
13759 let odb = Odb::new(temp.path());
13760 let mut index = Index::new();
13761 for d in 0..depth {
13762 for w in 0..width {
13763 let path = format!("unchanged/d{d}/f{w}.txt");
13764 let oid = odb
13765 .write(ObjectKind::Blob, format!("body-{d}-{w}").as_bytes())
13766 .expect("blob");
13767 index.add_or_replace(index_entry(&path, MODE_REGULAR, oid));
13768 }
13769 }
13770 let head_tree = write_tree_from_index(&odb, &index, "").expect("write tree");
13771 let cache = build_cache_tree_from_index(&odb, &index).expect("cache-tree");
13772 index.set_cache_tree(cache);
13773 index.cache_tree_root = Some(head_tree);
13774 (temp, odb, head_tree, index)
13775 }
13776
13777 #[test]
13778 fn diff_index_to_tree_cache_tree_fast_path_matches_slow() {
13779 let (_temp, odb, head_tree, mut index) = repo_with_tree(4, 5);
13780 assert!(index.cache_tree.as_ref().is_some_and(|c| c.is_valid()));
13781
13782 let mut state: u64 = 0xDEAD_BEEF;
13783 for _ in 0..24 {
13784 state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
13785 let d = (state % 4) as usize;
13786 state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
13787 let w = (state % 5) as usize;
13788 let path = format!("unchanged/d{d}/f{w}.txt");
13789 index.invalidate_cache_tree_for_path(path.as_bytes());
13790 assert!(
13791 index.cache_tree_root.is_none(),
13792 "partial invalidation clears cache_tree_root like Index::add"
13793 );
13794 assert!(
13795 index.cache_tree.as_ref().is_some_and(|r| !r.is_valid()),
13796 "root cache node must be invalid after partial invalidation"
13797 );
13798 if let Some(e) = index.get_mut(path.as_bytes(), 0) {
13799 let new_oid = odb
13800 .write(ObjectKind::Blob, format!("new-{d}-{w}").as_bytes())
13801 .expect("blob");
13802 e.oid = new_oid;
13803 }
13804 let fast = diff_index_to_tree(&odb, &index, Some(&head_tree), false).expect("fast");
13805 let flat = diff_index_to_tree_flatten_reference(&odb, &index, &head_tree, false)
13806 .expect("flat");
13807 assert_eq!(fast, flat, "mismatch after mutating {path}");
13808 }
13809 }
13810
13811 #[test]
13812 fn diff_index_to_tree_skips_unchanged_subtrees() {
13813 let (_temp, odb, head_tree, index) = repo_with_tree(5, 6);
13814 let reads = TreeReadCounter::start();
13815 diff_index_to_tree(&odb, &index, Some(&head_tree), false).expect("cached diff");
13816 let cached_reads = reads.get();
13817
13818 let mut no_cache = index.clone();
13819 no_cache.clear_cache_tree();
13820 let reads = TreeReadCounter::start();
13821 diff_index_to_tree(&odb, &no_cache, Some(&head_tree), false).expect("flat diff");
13822 let flat_reads = reads.get();
13823
13824 assert!(
13825 cached_reads < flat_reads,
13826 "expected fewer tree reads with cache-tree (cached={cached_reads}, flat={flat_reads})"
13827 );
13828 assert_eq!(
13829 cached_reads, 1,
13830 "unchanged repo should only read the root tree"
13831 );
13832 }
13833
13834 #[test]
13835 fn diff_index_to_tree_partial_invalidation_skips_sibling_subtree() {
13836 let (_temp, odb, head_tree, mut index) = repo_with_tree(5, 6);
13837 let path = "unchanged/d1/f2.txt";
13838 index.invalidate_cache_tree_for_path(path.as_bytes());
13839 let new_oid = odb.write(ObjectKind::Blob, b"changed").expect("blob");
13840 index.get_mut(path.as_bytes(), 0).expect("entry").oid = new_oid;
13841
13842 let flat =
13843 diff_index_to_tree_flatten_reference(&odb, &index, &head_tree, false).expect("flat");
13844 let reads = TreeReadCounter::start();
13845 let fast = diff_index_to_tree(&odb, &index, Some(&head_tree), false).expect("fast");
13846 let cached_reads = reads.get();
13847 assert_eq!(fast, flat);
13848
13849 let reads = TreeReadCounter::start();
13850 diff_index_to_tree_flatten_reference(&odb, &index, &head_tree, false).expect("flat");
13851 let flat_reads = reads.get();
13852
13853 assert!(
13854 cached_reads < flat_reads,
13855 "partial cache-tree should skip sibling dirs (cached={cached_reads}, flat={flat_reads})"
13856 );
13857 }
13858
13859 fn cache_leaf(name: &str) -> CacheTreeNode {
13860 CacheTreeNode::valid(name.as_bytes().to_vec(), 1, ObjectId::zero(), Vec::new())
13861 }
13862
13863 #[test]
13864 fn cache_children_lookup_matches_linear_scan_on_wide_unsorted_nodes() {
13865 let mut children: Vec<CacheTreeNode> = (0..CACHE_CHILD_LINEAR_SCAN_MAX * 3)
13866 .rev()
13867 .map(|i| cache_leaf(&format!("d{i:03}")))
13868 .collect();
13869 let mut first_dup = cache_leaf("d007");
13870 first_dup.entry_count = 7;
13871 children.insert(3, first_dup);
13872 let root = CacheTreeNode::valid(Vec::new(), 0, ObjectId::zero(), children);
13873
13874 let lookup = CacheChildren::new(Some(&root));
13875 assert!(
13876 !lookup.sorted.is_empty(),
13877 "wide nodes must use the sorted lookup"
13878 );
13879 for name in ["d000", "d007", "d020", "d047", "zzz", "d", ""] {
13880 let linear = root.children.iter().find(|c| c.name == name.as_bytes());
13881 let fast = lookup.get(name.as_bytes());
13882 assert_eq!(
13883 fast.map(|c| (&c.name, c.entry_count)),
13884 linear.map(|c| (&c.name, c.entry_count)),
13885 "lookup of {name:?}"
13886 );
13887 }
13888 assert_eq!(lookup.get(b"d007").map(|c| c.entry_count), Some(7));
13889
13890 let narrow = CacheTreeNode::valid(
13891 Vec::new(),
13892 0,
13893 ObjectId::zero(),
13894 vec![cache_leaf("b"), cache_leaf("a")],
13895 );
13896 let lookup = CacheChildren::new(Some(&narrow));
13897 assert!(lookup.sorted.is_empty());
13898 assert_eq!(lookup.get(b"a").map(|c| c.name.as_slice()), Some(&b"a"[..]));
13899 assert!(lookup.get(b"c").is_none());
13900 assert!(CacheChildren::new(None).get(b"a").is_none());
13901 }
13902
13903 #[test]
13904 fn index_relative_in_dir_requires_a_component_boundary() {
13905 assert_eq!(index_relative_in_dir(b"a/b", b""), Some(&b"a/b"[..]));
13906 assert_eq!(index_relative_in_dir(b"a/b", b"a"), Some(&b"b"[..]));
13907 assert_eq!(index_relative_in_dir(b"a", b"a"), Some(&b""[..]));
13908 assert_eq!(index_relative_in_dir(b"ab/c", b"a"), None);
13909 assert_eq!(index_relative_in_dir(b"a.c", b"a"), None);
13910 assert_eq!(index_relative_in_dir(b"b", b"a"), None);
13911 assert_eq!(join_index_path(b"", b"x"), b"x".to_vec());
13912 assert_eq!(join_index_path(b"a/b", b"x"), b"a/b/x".to_vec());
13913 }
13914
13915 #[test]
13918 fn diff_index_to_tree_wide_unsorted_cache_tree_matches_flatten() {
13919 let temp = TempDir::new().expect("tempdir");
13920 let odb = Odb::new(temp.path());
13921 let mut index = Index::new();
13922 let width = CACHE_CHILD_LINEAR_SCAN_MAX * 4;
13923 for d in 0..width {
13924 for f in 0..2 {
13925 let path = format!("dir{d:03}/sub/f{f}.txt");
13926 let oid = odb.write(ObjectKind::Blob, path.as_bytes()).expect("blob");
13927 index.add_or_replace(index_entry(&path, MODE_REGULAR, oid));
13928 }
13929 }
13930 let head_tree = write_tree_from_index(&odb, &index, "").expect("write tree");
13931 let mut cache = build_cache_tree_from_index(&odb, &index).expect("cache-tree");
13932 cache.children.reverse();
13933 index.set_cache_tree(cache);
13934 index.cache_tree_root = Some(head_tree);
13935
13936 let changed = [
13937 "dir003/sub/f1.txt",
13938 "dir031/sub/f0.txt",
13939 "dir063/sub/f1.txt",
13940 ];
13941 for path in changed {
13942 index.invalidate_cache_tree_for_path(path.as_bytes());
13943 let oid = odb.write(ObjectKind::Blob, b"changed").expect("blob");
13944 index.get_mut(path.as_bytes(), 0).expect("entry").oid = oid;
13945 }
13946 for path in ["dir010/sub/new.txt", "dir999/new.txt", "top.txt"] {
13947 index.invalidate_cache_tree_for_path(path.as_bytes());
13948 let oid = odb.write(ObjectKind::Blob, path.as_bytes()).expect("blob");
13949 index.add_or_replace(index_entry(path, MODE_REGULAR, oid));
13950 }
13951 for path in [
13952 "dir020/sub/f0.txt",
13953 "dir020/sub/f1.txt",
13954 "dir021/sub/f0.txt",
13955 ] {
13956 index.invalidate_cache_tree_for_path(path.as_bytes());
13957 index.remove(path.as_bytes());
13958 }
13959
13960 let flat =
13961 diff_index_to_tree_flatten_reference(&odb, &index, &head_tree, false).expect("flat");
13962 let reads = TreeReadCounter::start();
13963 let fast = diff_index_to_tree(&odb, &index, Some(&head_tree), false).expect("fast");
13964 let cached_reads = reads.get();
13965 assert_eq!(fast, flat);
13966 assert_eq!(fast.len(), changed.len() + 3 + 3);
13967 assert!(
13968 cached_reads <= 1 + 2 * 6,
13969 "unchanged siblings must be skipped through the cache-tree (reads={cached_reads})"
13970 );
13971 }
13972
13973 #[test]
13974 fn diff_index_to_tree_cache_tree_handles_non_utf8_directory_names() {
13975 let temp = TempDir::new().expect("tempdir");
13976 let odb = Odb::new(temp.path());
13977 let mut index = Index::new();
13978 let raw_dirs: [&[u8]; 3] = [b"caf\xe9", b"caf\xef\xbf\xbd", b"plain"];
13979 for dir in raw_dirs {
13980 for f in [&b"a.txt"[..], b"b.txt"] {
13981 let path = [dir, b"/", f].concat();
13982 let oid = odb.write(ObjectKind::Blob, &path).expect("blob");
13983 let mut entry = index_entry("x", MODE_REGULAR, oid);
13984 entry.flags = path.len().min(0xfff) as u16;
13985 entry.path = path;
13986 index.add_or_replace(entry);
13987 }
13988 }
13989 let head_tree = write_tree_from_index(&odb, &index, "").expect("write tree");
13990 let cache = build_cache_tree_from_index(&odb, &index).expect("cache-tree");
13991 index.set_cache_tree(cache);
13992 index.cache_tree_root = Some(head_tree);
13993
13994 let changed: &[u8] = b"caf\xe9/b.txt";
13995 index.invalidate_cache_tree_for_path(changed);
13996 let oid = odb.write(ObjectKind::Blob, b"changed").expect("blob");
13997 index.get_mut(changed, 0).expect("entry").oid = oid;
13998
13999 let fast = diff_index_to_tree(&odb, &index, Some(&head_tree), false).expect("fast");
14002 assert_eq!(fast.len(), 1, "{fast:?}");
14003 assert_eq!(fast[0].status, DiffStatus::Modified);
14004 assert_eq!(
14005 fast[0].old_oid,
14006 odb.write(ObjectKind::Blob, changed).expect("blob")
14007 );
14008 assert_eq!(fast[0].new_oid, oid);
14009 }
14010
14011 #[test]
14012 fn diff_index_to_tree_reports_mode_gitlink_and_intent_to_add() {
14013 let temp = TempDir::new().expect("tempdir");
14014 let odb = Odb::new(temp.path());
14015 let blob = odb.write(ObjectKind::Blob, b"x").expect("blob");
14016 let mut index = Index::new();
14017 index.add_or_replace(index_entry("tracked.txt", MODE_REGULAR, blob));
14018 let head_tree = write_tree_from_index(&odb, &index, "").expect("tree");
14019 let cache = build_cache_tree_from_index(&odb, &index).expect("cache");
14020 index.set_cache_tree(cache);
14021
14022 let mode_entry = index_entry("tracked.txt", MODE_EXECUTABLE, blob);
14023 index.add_or_replace(mode_entry);
14024 index.invalidate_cache_tree_for_path(b"tracked.txt");
14025
14026 let diffs = diff_index_to_tree(&odb, &index, Some(&head_tree), false).expect("diff");
14027 assert_eq!(diffs.len(), 1);
14028 assert_eq!(diffs[0].status, DiffStatus::Modified);
14029 assert_eq!(diffs[0].old_mode, FileMode::new(MODE_REGULAR));
14030 assert_eq!(diffs[0].new_mode, FileMode::new(MODE_EXECUTABLE));
14031
14032 let mut ita = index_entry("new.txt", MODE_REGULAR, blob);
14034 ita.set_intent_to_add(true);
14035 index.add_or_replace(ita);
14036 let diffs = diff_index_to_tree(&odb, &index, Some(&head_tree), false).expect("diff");
14037 assert!(
14038 diffs.iter().all(|d| d.path() != "new.txt"),
14039 "intent-to-add must not appear in staged diff"
14040 );
14041
14042 let gitlink: ObjectId = "855827c583bc30645ba427885caa40c5b81764d2"
14044 .parse()
14045 .expect("oid");
14046 let mut sub_index = Index::new();
14047 sub_index.add_or_replace(index_entry("sub", MODE_GITLINK, gitlink));
14048 let sub_tree = write_tree_from_index(&odb, &sub_index, "").expect("tree");
14049 let sub_cache = build_cache_tree_from_index(&odb, &sub_index).expect("cache");
14050 sub_index.set_cache_tree(sub_cache);
14051 let diffs =
14052 diff_index_to_tree(&odb, &sub_index, Some(&sub_tree), true).expect("gitlink diff");
14053 assert!(
14054 diffs.is_empty(),
14055 "ignored gitlink should not diff: {diffs:?}"
14056 );
14057 }
14058
14059 #[test]
14060 fn ignore_submodules_skips_gitlink_in_added_directory_cache_walk() {
14061 let temp = TempDir::new().expect("tempdir");
14062 let odb = Odb::new(temp.path());
14063 let gitlink: ObjectId = "855827c583bc30645ba427885caa40c5b81764d2"
14064 .parse()
14065 .expect("oid");
14066 let head_tree = write_tree_from_index(&odb, &Index::new(), "").expect("empty tree");
14067 let mut index = Index::new();
14068 index.add_or_replace(index_entry("newdir/sub", MODE_GITLINK, gitlink));
14069 let cache = build_cache_tree_from_index(&odb, &index).expect("cache");
14070 index.set_cache_tree(cache);
14071 index.invalidate_cache_tree_for_path(b"newdir/sub");
14072
14073 let fast = diff_index_to_tree(&odb, &index, Some(&head_tree), true).expect("fast");
14074 let flat =
14075 diff_index_to_tree_flatten_reference(&odb, &index, &head_tree, true).expect("flat");
14076 assert_eq!(fast, flat);
14077 assert!(
14078 fast.is_empty(),
14079 "nested gitlinks in added dirs must be ignored: {fast:?}"
14080 );
14081 }
14082
14083 #[test]
14084 fn ignore_submodules_skips_gitlink_in_deleted_directory_cache_walk() {
14085 let temp = TempDir::new().expect("tempdir");
14086 let odb = Odb::new(temp.path());
14087 let blob = odb.write(ObjectKind::Blob, b"keep").expect("blob");
14088 let gitlink: ObjectId = "855827c583bc30645ba427885caa40c5b81764d2"
14089 .parse()
14090 .expect("oid");
14091 let mut index = Index::new();
14092 index.add_or_replace(index_entry("keep.txt", MODE_REGULAR, blob));
14093 index.add_or_replace(index_entry("olddir/sub", MODE_GITLINK, gitlink));
14094 let head_tree = write_tree_from_index(&odb, &index, "").expect("head");
14095 let cache = build_cache_tree_from_index(&odb, &index).expect("cache");
14096 index.set_cache_tree(cache);
14097 index
14098 .entries
14099 .retain(|e| !String::from_utf8_lossy(&e.path).starts_with("olddir/"));
14100 index.invalidate_cache_tree_for_path(b"olddir/sub");
14101
14102 let fast = diff_index_to_tree(&odb, &index, Some(&head_tree), true).expect("fast");
14103 let flat =
14104 diff_index_to_tree_flatten_reference(&odb, &index, &head_tree, true).expect("flat");
14105 assert_eq!(fast, flat);
14106 assert!(
14107 fast.is_empty(),
14108 "nested gitlinks in deleted dirs must be ignored: {fast:?}"
14109 );
14110 }
14111
14112 #[test]
14113 fn diff_trees_skips_identical_subtree_oids() {
14114 let temp = TempDir::new().expect("tempdir");
14115 let odb = Odb::new(temp.path());
14116 let leaf = odb.write(ObjectKind::Blob, b"leaf").expect("blob");
14117 let mut deep = Index::new();
14118 for i in 0..20 {
14119 deep.add_or_replace(index_entry(&format!("deep/n{i}.txt"), MODE_REGULAR, leaf));
14120 }
14121 let tree = write_tree_from_index(&odb, &deep, "").expect("tree");
14122 let reads = TreeReadCounter::start();
14123 let entries = diff_trees(&odb, Some(&tree), Some(&tree), "").expect("diff");
14124 assert!(entries.is_empty());
14125 assert_eq!(
14126 reads.get(),
14127 0,
14128 "identical tree oids should short-circuit without reading tree objects"
14129 );
14130 }
14131
14132 #[test]
14133 fn diff_tree_path_change_count_matches_diff_trees() {
14134 let temp = TempDir::new().expect("tempdir");
14135 let odb = Odb::new(temp.path());
14136 let a = odb.write(ObjectKind::Blob, b"a").expect("blob");
14137 let b = odb.write(ObjectKind::Blob, b"b").expect("blob");
14138 let mut left = Index::new();
14139 left.add_or_replace(index_entry("f.txt", MODE_REGULAR, a));
14140 let mut right = Index::new();
14141 right.add_or_replace(index_entry("f.txt", MODE_REGULAR, b));
14142 right.add_or_replace(index_entry("g.txt", MODE_REGULAR, a));
14143 let old_tree = write_tree_from_index(&odb, &left, "").expect("old");
14144 let new_tree = write_tree_from_index(&odb, &right, "").expect("new");
14145 let entries = diff_trees(&odb, Some(&old_tree), Some(&new_tree), "").expect("diff");
14146 let count =
14147 diff_tree_path_change_count(&odb, Some(&old_tree), Some(&new_tree), "").expect("count");
14148 assert_eq!(count, entries.len());
14149 assert_eq!(count, 2);
14150 }
14151
14152 #[test]
14153 fn diff_trees_matches_whether_trees_were_cached_parsed_or_raw() {
14154 let temp = TempDir::new().expect("tempdir");
14155 let a = Odb::new(temp.path())
14156 .write(ObjectKind::Blob, b"a")
14157 .expect("blob");
14158 let b = Odb::new(temp.path())
14159 .write(ObjectKind::Blob, b"b")
14160 .expect("blob");
14161 let mut left = Index::new();
14162 let mut right = Index::new();
14163 for i in 0..50 {
14164 left.add_or_replace(index_entry(&format!("same{i:02}.txt"), MODE_REGULAR, a));
14165 right.add_or_replace(index_entry(&format!("same{i:02}.txt"), MODE_REGULAR, a));
14166 }
14167 left.add_or_replace(index_entry("dir/changed.txt", MODE_REGULAR, a));
14168 left.add_or_replace(index_entry("dir/kept.txt", MODE_REGULAR, a));
14169 left.add_or_replace(index_entry("gone/x.txt", MODE_REGULAR, a));
14170 left.add_or_replace(index_entry("mode.sh", MODE_REGULAR, a));
14171 left.add_or_replace(index_entry("swap", MODE_REGULAR, a));
14172 right.add_or_replace(index_entry("dir/changed.txt", MODE_REGULAR, b));
14173 right.add_or_replace(index_entry("dir/kept.txt", MODE_REGULAR, a));
14174 right.add_or_replace(index_entry("mode.sh", MODE_EXECUTABLE, a));
14175 right.add_or_replace(index_entry("new/y.txt", MODE_REGULAR, b));
14176 right.add_or_replace(index_entry("swap/inner.txt", MODE_REGULAR, b));
14177 let setup = Odb::new(temp.path());
14178 let old_tree = write_tree_from_index(&setup, &left, "").expect("old");
14179 let new_tree = write_tree_from_index(&setup, &right, "").expect("new");
14180
14181 let raw = diff_trees(&Odb::new(temp.path()), Some(&old_tree), Some(&new_tree), "")
14182 .expect("raw diff");
14183 let changed: Vec<(DiffStatus, &str)> = raw.iter().map(|e| (e.status, e.path())).collect();
14184 assert_eq!(
14185 changed,
14186 vec![
14187 (DiffStatus::Modified, "dir/changed.txt"),
14188 (DiffStatus::Deleted, "gone/x.txt"),
14189 (DiffStatus::Modified, "mode.sh"),
14190 (DiffStatus::Added, "new/y.txt"),
14191 (DiffStatus::Deleted, "swap"),
14192 (DiffStatus::Added, "swap/inner.txt"),
14193 ]
14194 );
14195
14196 let parsed = Odb::new(temp.path());
14197 parsed.cached_tree_entries(&old_tree).expect("parse old");
14198 parsed.cached_tree_entries(&new_tree).expect("parse new");
14199 assert_eq!(
14200 diff_trees(&parsed, Some(&old_tree), Some(&new_tree), "").expect("parsed diff"),
14201 raw
14202 );
14203
14204 let mixed = Odb::new(temp.path());
14205 mixed.cached_tree_entries(&new_tree).expect("parse new");
14206 assert_eq!(
14207 diff_trees(&mixed, Some(&old_tree), Some(&new_tree), "").expect("mixed diff"),
14208 raw
14209 );
14210 let reparsed = mixed
14211 .cached_tree_entries(&old_tree)
14212 .expect("parse cached raw");
14213 assert_eq!(
14214 reparsed.as_slice(),
14215 crate::objects::parse_tree(&mixed.read(&old_tree).expect("read").data)
14216 .expect("parse")
14217 .as_slice(),
14218 "a tree cached raw by a diff parses to the same entries"
14219 );
14220 }
14221
14222 #[test]
14223 fn diff_trees_reports_a_corrupt_tree() {
14224 let temp = TempDir::new().expect("tempdir");
14225 let odb = Odb::new(temp.path());
14226 let good = write_tree_from_index(&odb, &Index::new(), "").expect("empty tree");
14227 let corrupt = odb
14228 .write(ObjectKind::Tree, b"100644 name-without-oid")
14229 .expect("tree");
14230 let err = diff_trees(&odb, Some(&good), Some(&corrupt), "").expect_err("corrupt");
14231 assert!(matches!(err, Error::CorruptObject(_)), "{err:?}");
14232 }
14233}
14234
14235#[cfg(test)]
14236mod changed_flags_shared_tests {
14237 use super::git_xdiff;
14238 use crate::line_interner::LineInterner;
14239
14240 #[test]
14243 fn shared_counts_stand_in_for_records_left_out() {
14244 let mut old: Vec<String> = Vec::new();
14247 let mut new: Vec<String> = Vec::new();
14248 for i in 0..60 {
14249 for side in [&mut old, &mut new] {
14250 side.push(format!("fn f{i}() {{\n"));
14251 side.push("}\n".to_owned());
14252 }
14253 }
14254 old.extend(
14255 [
14256 "u1\n", "u2\n", "u3\n", "u4\n", "}\n", "u5\n", "u6\n", "u7\n",
14257 ]
14258 .map(str::to_owned),
14259 );
14260 new.extend(
14261 [
14262 "v1\n", "v2\n", "v3\n", "v4\n", "}\n", "v5\n", "v6\n", "v7\n",
14263 ]
14264 .map(str::to_owned),
14265 );
14266 for i in 0..40 {
14267 for side in [&mut old, &mut new] {
14268 side.push(format!("fn g{i}() {{\n"));
14269 side.push("}\n".to_owned());
14270 }
14271 }
14272 let old: Vec<&str> = old.iter().map(String::as_str).collect();
14273 let new: Vec<&str> = new.iter().map(String::as_str).collect();
14274 let (head, tail) = (120, 80);
14275
14276 let mut interner = LineInterner::with_capacity(old.len() + new.len());
14277 let ids1 = interner.intern_all(&old);
14278 let ids2 = interner.intern_all(&new);
14279 let (full1, full2) = git_xdiff::changed_flags(&ids1, &ids2, false);
14280
14281 let mut cut = LineInterner::default();
14282 let cut1 = cut.intern_all(&old[head..old.len() - tail]);
14283 let cut2 = cut.intern_all(&new[head..new.len() - tail]);
14284 let mut shared = vec![0u32; cut.len()];
14285 for line in old[..head].iter().chain(&old[old.len() - tail..]) {
14286 if let Some(id) = cut.get(line) {
14287 shared[id as usize] += 1;
14288 }
14289 }
14290 let (part1, part2) =
14291 git_xdiff::changed_flags_with_shared(&cut1, &cut2, &shared, head + tail, false);
14292 assert_eq!(part1, full1[head..old.len() - tail]);
14293 assert_eq!(part2, full2[head..new.len() - tail]);
14294 assert!(full1[..head]
14295 .iter()
14296 .chain(&full1[old.len() - tail..])
14297 .all(|c| !c));
14298 assert!(full1[head..old.len() - tail].iter().all(|&c| c));
14301 let (alone1, _) = git_xdiff::changed_flags(&cut1, &cut2, false);
14302 assert!(
14303 !alone1[4],
14304 "counted alone, the cut keeps the `}}` as a match"
14305 );
14306 }
14307}
14308
14309#[cfg(test)]
14310mod gitlink_tree_worktree_tests {
14311 use std::fs;
14312
14313 use crate::index::{Index, IndexEntry, MODE_GITLINK};
14314 use crate::objects::{ObjectId, ObjectKind};
14315 use crate::repo::init_repository;
14316
14317 use super::diff_tree_to_worktree;
14318
14319 fn encode_tree_gitlink(name: &str, oid: &ObjectId) -> Vec<u8> {
14320 let mut data = format!("160000 {name}\0").into_bytes();
14321 data.extend_from_slice(oid.as_bytes());
14322 data
14323 }
14324
14325 #[test]
14326 fn unpopulated_gitlink_matching_index_produces_no_diff_entry() {
14327 let dir = tempfile::tempdir().expect("tempdir");
14328 let work_tree = dir.path();
14329 fs::create_dir(work_tree.join("sub")).expect("sub placeholder");
14330
14331 let repo = init_repository(
14332 work_tree,
14333 false,
14334 "main",
14335 None,
14336 crate::RefStorageFormat::Files,
14337 )
14338 .expect("init");
14339 let gitlink: ObjectId = "855827c583bc30645ba427885caa40c5b81764d2"
14340 .parse()
14341 .expect("gitlink oid");
14342 let tree_oid = repo
14343 .odb()
14344 .write(ObjectKind::Tree, &encode_tree_gitlink("sub", &gitlink))
14345 .expect("write tree");
14346
14347 let path = b"sub".to_vec();
14348 let mut index = Index::new();
14349 index.add_or_replace(IndexEntry {
14350 ctime_sec: 0,
14351 ctime_nsec: 0,
14352 mtime_sec: 0,
14353 mtime_nsec: 0,
14354 dev: 0,
14355 ino: 0,
14356 mode: MODE_GITLINK,
14357 uid: 0,
14358 gid: 0,
14359 size: 0,
14360 oid: gitlink,
14361 flags: (path.len().min(0xfff)) as u16,
14362 flags_extended: None,
14363 path,
14364 base_index_pos: 0,
14365 });
14366
14367 let entries = diff_tree_to_worktree(&repo.odb(), Some(&tree_oid), work_tree, &index)
14368 .expect("diff_tree_to_worktree");
14369 assert!(
14370 entries.is_empty(),
14371 "clean uninitialized gitlink should not appear in tree→worktree diff: {entries:?}"
14372 );
14373 }
14374
14375 #[test]
14376 fn unpopulated_gitlink_with_stray_file_produces_no_diff_entry() {
14377 let dir = tempfile::tempdir().expect("tempdir");
14378 let work_tree = dir.path();
14379 let sub = work_tree.join("sub");
14380 fs::create_dir(&sub).expect("sub placeholder");
14381 fs::write(sub.join("x.txt"), b"stray\n").expect("stray file");
14382
14383 let repo = init_repository(
14384 work_tree,
14385 false,
14386 "main",
14387 None,
14388 crate::RefStorageFormat::Files,
14389 )
14390 .expect("init");
14391 let gitlink: ObjectId = "855827c583bc30645ba427885caa40c5b81764d2"
14392 .parse()
14393 .expect("gitlink oid");
14394 let tree_oid = repo
14395 .odb()
14396 .write(ObjectKind::Tree, &encode_tree_gitlink("sub", &gitlink))
14397 .expect("write tree");
14398
14399 let path = b"sub".to_vec();
14400 let mut index = Index::new();
14401 index.add_or_replace(IndexEntry {
14402 ctime_sec: 0,
14403 ctime_nsec: 0,
14404 mtime_sec: 0,
14405 mtime_nsec: 0,
14406 dev: 0,
14407 ino: 0,
14408 mode: MODE_GITLINK,
14409 uid: 0,
14410 gid: 0,
14411 size: 0,
14412 oid: gitlink,
14413 flags: (path.len().min(0xfff)) as u16,
14414 flags_extended: None,
14415 path,
14416 base_index_pos: 0,
14417 });
14418
14419 let tree_entries = diff_tree_to_worktree(&repo.odb(), Some(&tree_oid), work_tree, &index)
14420 .expect("diff_tree_to_worktree");
14421 assert!(
14422 tree_entries.is_empty(),
14423 "uninitialized gitlink with stray files should not diff: {tree_entries:?}"
14424 );
14425
14426 let wt_entries =
14427 super::diff_index_to_worktree(&repo.odb(), &mut index.clone(), work_tree, false, true)
14428 .expect("diff_index_to_worktree");
14429 assert!(
14430 wt_entries.is_empty(),
14431 "uninitialized gitlink with stray files should not diff: {wt_entries:?}"
14432 );
14433
14434 let ie = index.entries()[0].clone();
14435 assert!(
14436 !super::worktree_differs_from_index_entry(
14437 repo.odb(),
14438 repo.git_dir(),
14439 work_tree,
14440 &ie,
14441 false,
14442 None,
14443 None,
14444 )
14445 .expect("differs"),
14446 "stray files in uninitialized submodule should not differ from index"
14447 );
14448 }
14449
14450 #[cfg_attr(
14451 not(feature = "full-tests"),
14452 ignore = "starts system git or is heavy; run with --features full-tests"
14453 )]
14454 #[test]
14455 fn untracked_only_in_checked_out_submodule_produces_no_diff_entry() {
14456 use std::process::Command;
14457
14458 let dir = tempfile::tempdir().expect("tempdir");
14459 let work_tree = dir.path();
14460 fs::create_dir(work_tree.join("sub")).expect("sub dir");
14461
14462 let repo = init_repository(
14463 work_tree,
14464 false,
14465 "main",
14466 None,
14467 crate::RefStorageFormat::Files,
14468 )
14469 .expect("init");
14470 let sub = work_tree.join("sub");
14471 for args in [
14472 &["init", "-q"][..],
14473 &["config", "user.name", "T"][..],
14474 &["config", "user.email", "t@e.com"][..],
14475 ] {
14476 let status = Command::new("git")
14477 .current_dir(&sub)
14478 .args(args)
14479 .status()
14480 .expect("git");
14481 assert!(status.success(), "git {args:?} failed");
14482 }
14483 fs::write(sub.join("tracked.txt"), b"v1\n").expect("write");
14484 for args in [&["add", "tracked.txt"][..], &["commit", "-qm", "sub"][..]] {
14485 let status = Command::new("git")
14486 .current_dir(&sub)
14487 .args(args)
14488 .status()
14489 .expect("git");
14490 assert!(status.success());
14491 }
14492 let head_out = Command::new("git")
14493 .current_dir(&sub)
14494 .args(["rev-parse", "HEAD"])
14495 .output()
14496 .expect("rev-parse");
14497 let gitlink = std::str::from_utf8(&head_out.stdout)
14498 .expect("utf8")
14499 .trim()
14500 .parse::<ObjectId>()
14501 .expect("oid");
14502
14503 let tree_oid = repo
14504 .odb()
14505 .write(ObjectKind::Tree, &encode_tree_gitlink("sub", &gitlink))
14506 .expect("tree");
14507 let path = b"sub".to_vec();
14508 let mut index = Index::new();
14509 index.add_or_replace(IndexEntry {
14510 ctime_sec: 0,
14511 ctime_nsec: 0,
14512 mtime_sec: 0,
14513 mtime_nsec: 0,
14514 dev: 0,
14515 ino: 0,
14516 mode: MODE_GITLINK,
14517 uid: 0,
14518 gid: 0,
14519 size: 0,
14520 oid: gitlink,
14521 flags: (path.len().min(0xfff)) as u16,
14522 flags_extended: None,
14523 path,
14524 base_index_pos: 0,
14525 });
14526
14527 fs::write(sub.join("untracked.txt"), b"extra\n").expect("untracked");
14528
14529 let entries = diff_tree_to_worktree(&repo.odb(), Some(&tree_oid), work_tree, &index)
14530 .expect("diff_tree_to_worktree");
14531 assert!(
14532 entries.is_empty(),
14533 "untracked-only submodule dirt must not appear in default tree→worktree diff: {entries:?}"
14534 );
14535 }
14536}
14537
14538#[cfg(test)]
14539mod racy_index_mtime_diff_tests {
14540 use super::*;
14541 use crate::index::{entry_from_metadata, Index, MODE_REGULAR};
14542 use crate::objects::{HashAlgo, ObjectKind};
14543 use filetime::FileTime;
14544 use std::fs;
14545 use std::path::PathBuf;
14546
14547 const INDEX_MTIME: (u32, u32) = (1_700_000_000, 123_456_789);
14548
14549 fn pin_mtime(path: &Path, sec: u32, nsec: u32) {
14550 filetime::set_file_mtime(path, FileTime::from_unix_time(i64::from(sec), nsec))
14551 .expect("set mtime");
14552 }
14553
14554 fn minimal_sha256_repo(work_tree: &Path) -> (PathBuf, Odb) {
14555 let git_dir = work_tree.join(".git");
14556 fs::create_dir_all(git_dir.join("objects")).expect("objects dir");
14557 fs::write(
14558 git_dir.join("config"),
14559 "[extensions]\n\tobjectformat = sha256\n",
14560 )
14561 .expect("config");
14562 let odb = Odb::new(&git_dir.join("objects")).with_config_git_dir(git_dir.clone());
14563 (git_dir, odb)
14564 }
14565
14566 #[test]
14567 fn detects_same_size_change_when_entry_mtime_matches_index_mtime() {
14568 let td = tempfile::tempdir().expect("tempdir");
14569 let wt = td.path();
14570 let content_a = b"same length!!";
14571 let content_b = b"same length??";
14572 assert_eq!(content_a.len(), content_b.len());
14573
14574 fs::write(wt.join("f.txt"), content_a).expect("write");
14575 pin_mtime(&wt.join("f.txt"), INDEX_MTIME.0, INDEX_MTIME.1);
14576 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
14577 let git_dir = wt.join(".git");
14578 fs::create_dir_all(git_dir.join("objects")).expect("objects dir");
14579 let odb = Odb::new(&git_dir.join("objects"));
14580 let oid_a = odb.hash(ObjectKind::Blob, content_a).expect("hash");
14581 let entry = entry_from_metadata(&meta, b"f.txt", oid_a, MODE_REGULAR);
14582
14583 let mut index = Index::new();
14584 index.source_mtime = Some(INDEX_MTIME);
14585 index.entries.push(entry);
14586
14587 fs::write(wt.join("f.txt"), content_b).expect("rewrite");
14588 pin_mtime(&wt.join("f.txt"), INDEX_MTIME.0, INDEX_MTIME.1);
14589
14590 let diff = diff_index_to_worktree(&odb, &mut index, wt, false, false).expect("diff");
14591 assert_eq!(
14592 diff.len(),
14593 1,
14594 "racy entry must be re-hashed, not stat-trusted"
14595 );
14596 assert_eq!(diff[0].path(), "f.txt");
14597 }
14598
14599 #[test]
14600 #[cfg(unix)]
14601 fn non_utf8_symlink_clean_racy_equal_mtime_reports_no_diff() {
14602 use crate::index::MODE_SYMLINK;
14603 use std::os::unix::ffi::OsStrExt as _;
14604 use std::os::unix::fs::symlink;
14605
14606 let td = tempfile::tempdir().expect("tempdir");
14607 let wt = td.path();
14608 let target = std::ffi::OsStr::from_bytes(b"bad-\xff-target");
14609 let link_path = wt.join("link");
14610 symlink(target, &link_path).expect("symlink");
14611
14612 let ft = FileTime::from_unix_time(i64::from(INDEX_MTIME.0), INDEX_MTIME.1);
14613 filetime::set_symlink_file_times(&link_path, ft, ft).expect("set symlink mtime");
14614 let meta = fs::symlink_metadata(&link_path).expect("stat");
14615 let git_dir = wt.join(".git");
14616 fs::create_dir_all(git_dir.join("objects")).expect("objects dir");
14617 let odb = Odb::new(&git_dir.join("objects"));
14618 let oid = odb.hash(ObjectKind::Blob, target.as_bytes()).expect("hash");
14619 let entry = entry_from_metadata(&meta, b"link", oid, MODE_SYMLINK);
14620
14621 let mut index = Index::new();
14622 index.source_mtime = Some(INDEX_MTIME);
14623 index.entries.push(entry);
14624
14625 let diff = diff_index_to_worktree(&odb, &mut index, wt, false, false).expect("diff");
14626 assert!(
14627 diff.is_empty(),
14628 "clean non-UTF-8 symlink must not be reported modified: {diff:?}"
14629 );
14630 }
14631
14632 #[test]
14633 fn sha256_clean_racy_equal_mtime_reports_no_diff() {
14634 let td = tempfile::tempdir().expect("tempdir");
14635 let wt = td.path();
14636 let (_git_dir, odb) = minimal_sha256_repo(wt);
14637 assert_eq!(odb.hash_algo(), HashAlgo::Sha256);
14638
14639 let bytes = b"sha256 width!!";
14640 fs::write(wt.join("f.txt"), bytes).expect("write");
14641 pin_mtime(&wt.join("f.txt"), INDEX_MTIME.0, INDEX_MTIME.1);
14642 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
14643 let oid = odb.hash(ObjectKind::Blob, bytes).expect("hash");
14644 let entry = entry_from_metadata(&meta, b"f.txt", oid, MODE_REGULAR);
14645
14646 let mut index = Index::new();
14647 index.hash_algo = HashAlgo::Sha256;
14648 index.source_mtime = Some(INDEX_MTIME);
14649 index.entries.push(entry);
14650
14651 let diff = diff_index_to_worktree(&odb, &mut index, wt, false, false).expect("diff");
14652 assert!(
14653 diff.is_empty(),
14654 "clean SHA-256 racy entry must not be reported modified: {diff:?}"
14655 );
14656 }
14657
14658 #[test]
14661 fn racy_entries_hash_with_their_own_attributes() {
14662 let td = tempfile::tempdir().expect("tempdir");
14663 let wt = td.path();
14664 let git_dir = wt.join(".git");
14665 fs::create_dir_all(git_dir.join("objects")).expect("objects dir");
14666 fs::write(git_dir.join("config"), "[core]\n").expect("config");
14667 fs::write(wt.join(".gitattributes"), "*.crlf eol=crlf\n*.bin -text\n").expect("attrs");
14668 let odb = Odb::new(&git_dir.join("objects")).with_config_git_dir(git_dir.clone());
14669
14670 let files: [(&str, &[u8], &[u8]); 5] = [
14671 ("a.crlf", b"one\r\ntwo\r\n", b"one\ntwo\n"),
14672 ("b.bin", b"raw\r\nbytes\r\n", b"raw\r\nbytes\r\n"),
14673 ("c.crlf", b"three\r\n", b"three\n"),
14674 ("d.txt", b"plain\n", b"plain\n"),
14675 ("e.bin", b"more\r\n", b"more\r\n"),
14676 ];
14677 let mut index = Index::new();
14678 index.source_mtime = Some(INDEX_MTIME);
14679 for (path, on_disk, blob) in files {
14680 let file = wt.join(path);
14681 fs::write(&file, on_disk).expect("write");
14682 pin_mtime(&file, INDEX_MTIME.0, INDEX_MTIME.1);
14683 let meta = fs::symlink_metadata(&file).expect("stat");
14684 let oid = odb.hash(ObjectKind::Blob, blob).expect("hash");
14685 index.entries.push(entry_from_metadata(
14686 &meta,
14687 path.as_bytes(),
14688 oid,
14689 MODE_REGULAR,
14690 ));
14691 }
14692
14693 let status_diff = |index: &mut Index| {
14694 diff_index_to_worktree_with_options(
14695 &odb,
14696 index,
14697 wt,
14698 DiffIndexToWorktreeOptions {
14699 index_mtime: Some(INDEX_MTIME),
14700 repository_git_dir: Some(git_dir.clone()),
14701 refresh_index_stat_in_pass: true,
14702 for_status: true,
14703 ..Default::default()
14704 },
14705 )
14706 .expect("diff")
14707 .0
14708 };
14709 let diff = status_diff(&mut index.clone());
14710 assert!(diff.is_empty(), "clean racy entries reported: {diff:?}");
14711
14712 fs::write(wt.join("c.crlf"), b"thrEe\r\n").expect("rewrite");
14713 pin_mtime(&wt.join("c.crlf"), INDEX_MTIME.0, INDEX_MTIME.1);
14714 let diff = status_diff(&mut index);
14715 let paths: Vec<_> = diff.iter().map(DiffEntry::path).collect();
14716 assert_eq!(paths, ["c.crlf"]);
14717 }
14718}
14719
14720#[cfg(test)]
14721mod smudge_racily_clean_tests {
14722 use super::*;
14723 use crate::config::ConfigSet;
14724 use crate::crlf::{self, FileAttrs};
14725 use crate::index::{entry_from_metadata, Index, MODE_REGULAR};
14726 use crate::objects::{HashAlgo, ObjectKind};
14727 use filetime::FileTime;
14728 use std::fs;
14729 use std::path::PathBuf;
14730
14731 const INDEX_MTIME_PIN: (u32, u32) = (1_700_000_000, 123_456_789);
14732
14733 fn mtime_from_metadata(meta: &fs::Metadata) -> (u32, u32) {
14734 #[cfg(unix)]
14735 {
14736 use std::os::unix::fs::MetadataExt;
14737 (meta.mtime() as u32, meta.mtime_nsec() as u32)
14738 }
14739 #[cfg(not(unix))]
14740 {
14741 use std::time::UNIX_EPOCH;
14742 let mtime = meta
14743 .modified()
14744 .ok()
14745 .and_then(|t| t.duration_since(UNIX_EPOCH).ok())
14746 .unwrap_or_default();
14747 (mtime.as_secs() as u32, mtime.subsec_nanos())
14748 }
14749 }
14750
14751 fn pin_mtime_and_sample(path: &Path, sec: u32, nsec: u32) -> (u32, u32) {
14753 filetime::set_file_mtime(path, FileTime::from_unix_time(i64::from(sec), nsec))
14754 .expect("set mtime");
14755 let meta = fs::symlink_metadata(path).expect("stat file");
14756 mtime_from_metadata(&meta)
14757 }
14758
14759 fn setup_index_mtime(work_tree: &Path) -> (u32, u32) {
14761 let probe = work_tree.join(".grit-index-mtime-probe");
14762 fs::write(&probe, b"").expect("probe file");
14763 pin_mtime_and_sample(&probe, INDEX_MTIME_PIN.0, INDEX_MTIME_PIN.1)
14764 }
14765
14766 fn minimal_repo(work_tree: &Path, config: &str) -> (PathBuf, Odb) {
14767 let git_dir = work_tree.join(".git");
14768 fs::create_dir_all(git_dir.join("objects")).expect("objects dir");
14769 fs::write(git_dir.join("config"), config).expect("config");
14770 let odb = Odb::new(&git_dir.join("objects")).with_config_git_dir(git_dir.clone());
14771 (git_dir, odb)
14772 }
14773
14774 fn staged_entry_for_file(
14775 odb: &Odb,
14776 work_tree: &Path,
14777 rel: &str,
14778 bytes: &[u8],
14779 mtime: (u32, u32),
14780 ) -> IndexEntry {
14781 let path = work_tree.join(rel);
14782 fs::write(&path, bytes).expect("write file");
14783 pin_mtime_and_sample(&path, mtime.0, mtime.1);
14784 let meta = fs::symlink_metadata(&path).expect("stat file");
14785 let oid = odb.hash(ObjectKind::Blob, bytes).expect("hash");
14786 entry_from_metadata(&meta, rel.as_bytes(), oid, MODE_REGULAR)
14787 }
14788
14789 fn staged_entry_with_oid(
14790 work_tree: &Path,
14791 rel: &str,
14792 worktree_bytes: &[u8],
14793 oid: crate::objects::ObjectId,
14794 mtime: (u32, u32),
14795 ) -> IndexEntry {
14796 let path = work_tree.join(rel);
14797 fs::write(&path, worktree_bytes).expect("write file");
14798 pin_mtime_and_sample(&path, mtime.0, mtime.1);
14799 let meta = fs::symlink_metadata(&path).expect("stat file");
14800 entry_from_metadata(&meta, rel.as_bytes(), oid, MODE_REGULAR)
14801 }
14802
14803 #[test]
14804 fn index_stat_cache_trustworthy_rejects_unknown_mtime() {
14805 assert!(!super::index_stat_cache_trustworthy(None));
14806 assert!(!super::index_stat_cache_trustworthy(Some((0, 0))));
14807 assert!(super::index_stat_cache_trustworthy(Some((1, 0))));
14808 }
14809
14810 #[test]
14811 fn index_entry_skips_content_hash_after_stat_match_when_trusted_and_not_racy() {
14812 use crate::index::StatMatchPolicy;
14813 let td = tempfile::tempdir().expect("tempdir");
14814 let wt = td.path();
14815 let (git_dir, odb) = minimal_repo(wt, "[core]\n\tfilemode = true\n");
14816 let rel = "trusted.txt";
14817 let file_mtime = (1_700_000_000, 0);
14818 let mut entry = staged_entry_for_file(&odb, wt, rel, b"payload\n", file_mtime);
14819 let index_mtime = (file_mtime.0 + 1, 0);
14820 let policy = StatMatchPolicy::from_config(None);
14821 assert!(
14822 super::index_entry_skips_worktree_content_hash_after_stat_match(
14823 &entry,
14824 Some(index_mtime),
14825 policy,
14826 )
14827 );
14828 entry.size = 0;
14829 entry.mtime_sec = 0;
14830 entry.mtime_nsec = 0;
14831 assert!(
14832 !super::index_entry_skips_worktree_content_hash_after_stat_match(
14833 &entry,
14834 Some(index_mtime),
14835 policy,
14836 )
14837 );
14838 let _ = git_dir;
14839 }
14840
14841 #[cfg(unix)]
14842 #[test]
14843 fn symlink_dir_cache_detects_symlinked_ancestors_across_threads() {
14844 let td = tempfile::tempdir().expect("tempdir");
14845 let wt = td.path();
14846 fs::create_dir_all(wt.join("real/deep")).expect("mkdir");
14847 fs::create_dir_all(wt.join("target/deep")).expect("mkdir");
14848 std::os::unix::fs::symlink(wt.join("target"), wt.join("linked")).expect("symlink");
14849 std::thread::scope(|scope| {
14850 for _ in 0..4 {
14851 scope.spawn(|| {
14852 let mut cache = super::SymlinkDirCache::default();
14853 assert!(!cache.has_symlink_in_path_bytes(wt, b"top.txt", false, false));
14854 assert!(!cache.has_symlink_in_path_bytes(wt, b"linked", false, false));
14855 assert!(!cache.has_symlink_in_path_bytes(wt, b"real/deep/f", false, false));
14856 assert!(cache.has_symlink_in_path_bytes(wt, b"linked/f", false, false));
14857 assert!(cache.has_symlink_in_path_bytes(wt, b"linked/deep/f", false, false));
14858 assert!(!cache.has_symlink_in_path_bytes(wt, b"\xff\xfe", false, false));
14859 });
14860 }
14861 });
14862 }
14863
14864 #[cfg(unix)]
14865 #[test]
14866 fn symlink_dir_cache_accepts_a_work_tree_reached_through_a_symlink() {
14867 let td = tempfile::tempdir().expect("tempdir");
14868 let real = td.path().join("real");
14869 fs::create_dir_all(real.join("a/b/c")).expect("mkdir");
14870 fs::create_dir_all(real.join("t/b/c")).expect("mkdir");
14871 std::os::unix::fs::symlink(real.join("t"), real.join("l")).expect("symlink");
14872 let wt = td.path().join("alias");
14873 std::os::unix::fs::symlink(&real, &wt).expect("symlink");
14874 let mut cache = super::SymlinkDirCache::default();
14875 assert!(!cache.has_symlink_in_path_bytes(&wt, b"a/b/c/f", false, false));
14876 assert!(cache.has_symlink_in_path_bytes(&wt, b"l/b/c/f", false, false));
14877 assert!(!cache.has_symlink_in_path_bytes(&real, b"a/b/c/f", false, false));
14878 assert!(cache.has_symlink_in_path_bytes(&real, b"l/b/c/f", false, false));
14879 }
14880
14881 #[cfg(all(target_os = "linux", target_pointer_width = "64"))]
14882 #[test]
14883 fn real_dir_chain_confirms_only_symlink_free_directories() {
14884 let td = tempfile::tempdir().expect("tempdir");
14885 let wt = td.path();
14886 fs::create_dir_all(wt.join("a/b/c")).expect("mkdir");
14887 fs::create_dir_all(wt.join("t/c")).expect("mkdir");
14888 fs::write(wt.join("a/file"), b"f").expect("write");
14889 std::os::unix::fs::symlink(wt.join("t"), wt.join("a/l")).expect("symlink");
14890 let mut chain = crate::worktree_scan::RealDirChain::open(wt);
14891 assert_eq!(chain.work_tree(), wt);
14892 assert!(chain.confirms(b"a"));
14893 assert!(chain.confirms(b"a/b/c"));
14894 assert!(!chain.confirms(b"a/l"));
14895 assert!(!chain.confirms(b"a/l/c"));
14896 assert!(!chain.confirms(b"a/file"));
14897 assert!(!chain.confirms(b"a/missing/c"));
14898 assert!(!chain.confirms(b""));
14899 assert!(!chain.confirms(b"a/\0b"));
14900 assert!(chain.confirms(b"a/b"));
14901 }
14902
14903 #[test]
14904 fn symlink_dir_cache_probes_each_new_directory_once_in_index_order() {
14905 let td = tempfile::tempdir().expect("tempdir");
14906 let wt = td.path();
14907 let depth = 12;
14908 let chains = 20;
14909 let mut paths = Vec::new();
14910 for chain in 0..chains {
14911 let dir: Vec<String> = (0..depth)
14912 .map(|level| format!("c{chain:02}l{level:02}"))
14913 .collect();
14914 let dir = format!("shared/{}", dir.join("/"));
14915 fs::create_dir_all(wt.join(&dir)).expect("mkdir");
14916 paths.extend((0..3).map(|file| format!("{dir}/f{file}")));
14917 }
14918 paths.sort();
14919 let mut cache = super::SymlinkDirCache::default();
14920 for rel in &paths {
14921 assert!(!cache.has_symlink_in_path_bytes(wt, rel.as_bytes(), false, false));
14922 }
14923 let unique_dirs = 1 + chains * depth;
14925 assert!(
14926 cache.fs_queries <= unique_dirs,
14927 "{} filesystem queries for {unique_dirs} directories",
14928 cache.fs_queries
14929 );
14930 #[cfg(all(target_os = "linux", target_pointer_width = "64"))]
14931 assert!(
14932 cache.fs_queries <= chains + 1,
14933 "one chain walk per new directory: {} queries for {chains} chains",
14934 cache.fs_queries
14935 );
14936 }
14937
14938 #[test]
14939 fn common_dir_prefix_len_stops_at_whole_components() {
14940 use super::{common_dir_prefix_len, path_within_dir};
14941 assert_eq!(common_dir_prefix_len(b"a/b/c", b"a/b/c"), 5);
14942 assert_eq!(common_dir_prefix_len(b"a/b/c", b"a/b"), 3);
14943 assert_eq!(common_dir_prefix_len(b"a/b", b"a/b/c"), 3);
14944 assert_eq!(common_dir_prefix_len(b"a/bc/d", b"a/b"), 1);
14945 assert_eq!(common_dir_prefix_len(b"a/b", b"a/bc/d"), 1);
14946 assert_eq!(common_dir_prefix_len(b"ab", b"a"), 0);
14947 assert_eq!(common_dir_prefix_len(b"a/b", b""), 0);
14948 assert!(path_within_dir(b"a/b", b"a/b"));
14949 assert!(path_within_dir(b"a/b/c", b"a/b"));
14950 assert!(!path_within_dir(b"a/bc", b"a/b"));
14951 assert!(!path_within_dir(b"a", b"a/b"));
14952 }
14953
14954 #[cfg(unix)]
14956 fn naive_has_symlink_ancestor(wt: &Path, rel: &str) -> bool {
14957 let Some((dir, _)) = rel.rsplit_once('/') else {
14958 return false;
14959 };
14960 let mut prefix = wt.to_path_buf();
14961 dir.split('/').any(|component| {
14962 prefix.push(component);
14963 fs::symlink_metadata(&prefix).is_ok_and(|m| m.file_type().is_symlink())
14964 })
14965 }
14966
14967 #[cfg(unix)]
14968 #[test]
14969 fn symlink_dir_cache_matches_uncached_probe_in_any_query_order() {
14970 let td = tempfile::tempdir().expect("tempdir");
14971 let wt = td.path();
14972 fs::create_dir_all(wt.join("a/b/c/d")).expect("mkdir");
14973 fs::create_dir_all(wt.join("a/bb/c")).expect("mkdir");
14974 fs::create_dir_all(wt.join("a/b2")).expect("mkdir");
14975 fs::create_dir_all(wt.join("t/c/d")).expect("mkdir");
14976 fs::create_dir_all(wt.join("x/y")).expect("mkdir");
14977 fs::write(wt.join("a/file"), b"f").expect("write");
14978 fs::write(wt.join("x/y/plain"), b"f").expect("write");
14979 std::os::unix::fs::symlink(wt.join("t"), wt.join("a/b/link")).expect("symlink");
14980 std::os::unix::fs::symlink(wt.join("t"), wt.join("x/l")).expect("symlink");
14981 std::os::unix::fs::symlink(wt.join("t"), wt.join("top")).expect("symlink");
14982
14983 let paths = [
14984 "a/b/c/d/f",
14985 "a/b/c/f",
14986 "a/b/f",
14987 "a/b/link/f",
14988 "a/b/link/c/d/f",
14989 "a/b/linker/f",
14990 "a/b2/f",
14991 "a/bb/c/f",
14992 "a/file/under/f",
14993 "a/missing/deeper/f",
14994 "a/missing/f",
14995 "a/f",
14996 "f",
14997 "top/c/f",
14998 "top/f",
14999 "topper/f",
15000 "x/l/c/d/f",
15001 "x/y/f",
15002 "x/y/plain/f",
15003 ];
15004 let mut orders: Vec<Vec<&str>> = vec![paths.to_vec()];
15005 orders.push(paths.iter().rev().copied().collect());
15006 orders.push(
15007 paths
15008 .iter()
15009 .step_by(2)
15010 .chain(paths.iter().skip(1).step_by(2))
15011 .copied()
15012 .collect(),
15013 );
15014 orders.push(paths.iter().flat_map(|p| [*p, *p]).collect());
15015 for order in orders {
15016 let mut cache = super::SymlinkDirCache::default();
15017 for rel in order {
15018 assert_eq!(
15019 cache.has_symlink_in_path_bytes(wt, rel.as_bytes(), false, false),
15020 naive_has_symlink_ancestor(wt, rel),
15021 "ancestor symlink answer for {rel}"
15022 );
15023 }
15024 }
15025 }
15026
15027 #[test]
15028 fn content_hash_skip_trusts_empty_blob_but_not_smudged_size() {
15029 use crate::index::StatMatchPolicy;
15030 let td = tempfile::tempdir().expect("tempdir");
15031 let wt = td.path();
15032 let (_git_dir, odb) = minimal_repo(wt, "[core]\n\tfilemode = false\n");
15033 let file_mtime = (1_700_000_000, 0);
15034 let index_mtime = Some((file_mtime.0 + 1, 0));
15035 let policy = StatMatchPolicy::from_config(None);
15036
15037 let empty = staged_entry_for_file(&odb, wt, "empty.txt", b"", file_mtime);
15038 assert_eq!(empty.size, 0);
15039 assert!(
15040 super::index_entry_skips_worktree_content_hash_after_stat_match(
15041 &empty,
15042 index_mtime,
15043 policy,
15044 )
15045 );
15046
15047 let smudged_oid = odb.hash(ObjectKind::Blob, b"abc\n").expect("hash");
15048 let smudged = staged_entry_with_oid(wt, "smudged.txt", b"", smudged_oid, file_mtime);
15049 assert_eq!(smudged.size, 0);
15050 assert!(
15051 !super::index_entry_skips_worktree_content_hash_after_stat_match(
15052 &smudged,
15053 index_mtime,
15054 policy,
15055 )
15056 );
15057 }
15058
15059 #[test]
15060 fn status_diff_reports_smudged_entry_whose_file_became_empty() {
15061 let td = tempfile::tempdir().expect("tempdir");
15062 let wt = td.path();
15063 let (git_dir, odb) = minimal_repo(wt, "[core]\n\tfilemode = false\n");
15064 let oid = odb.write(ObjectKind::Blob, b"abc\n").expect("write blob");
15065 let file_mtime = (1_700_000_000, 0);
15066 let mut index = Index::new();
15067 index.entries.push(staged_entry_with_oid(
15068 wt,
15069 "smudged.txt",
15070 b"",
15071 oid,
15072 file_mtime,
15073 ));
15074 let (entries, _) = super::diff_index_to_worktree_with_options(
15075 &odb,
15076 &mut index,
15077 wt,
15078 super::DiffIndexToWorktreeOptions {
15079 repository_git_dir: Some(git_dir),
15080 index_mtime: Some((file_mtime.0 + 1, 0)),
15081 for_status: true,
15082 stat_parallel_threads: Some(1),
15083 ..Default::default()
15084 },
15085 )
15086 .expect("diff");
15087 assert_eq!(entries.len(), 1, "{entries:?}");
15088 assert_eq!(entries[0].status, DiffStatus::Modified);
15089 }
15090
15091 #[test]
15092 fn status_diff_verifies_content_when_index_stat_cache_unset() {
15093 let td = tempfile::tempdir().expect("tempdir");
15094 let wt = td.path();
15095 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
15096 let rel = "unset-stat.txt";
15097 let bytes = b"same bytes\n";
15098 let oid = odb.hash(ObjectKind::Blob, bytes).expect("hash");
15099 fs::write(wt.join(rel), bytes).expect("write");
15100 let mut index = Index::new();
15101 let mut entry = staged_entry_for_file(&odb, wt, rel, bytes, (100, 0));
15102 entry.size = 0;
15103 entry.mtime_sec = 0;
15104 entry.mtime_nsec = 0;
15105 entry.oid = oid;
15106 index.entries.push(entry);
15107 let (entries, _) = super::diff_index_to_worktree_with_options(
15108 &odb,
15109 &mut index,
15110 wt,
15111 super::DiffIndexToWorktreeOptions {
15112 repository_git_dir: Some(git_dir),
15113 for_status: true,
15114 ..Default::default()
15115 },
15116 )
15117 .expect("diff");
15118 assert!(
15119 entries.is_empty(),
15120 "unset stat must not size-mismatch a matching worktree: {entries:?}"
15121 );
15122 }
15123
15124 #[cfg_attr(
15125 not(feature = "full-tests"),
15126 ignore = "starts system git or is heavy; run with --features full-tests"
15127 )]
15128 #[test]
15129 fn status_reports_modified_when_ctime_differs_but_mtime_sec_and_size_match_non_racy() {
15130 let td = tempfile::tempdir().expect("tempdir");
15131 let wt = td.path();
15132 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
15133 let rel = "tracked.txt";
15134 let original = b"original\n";
15135 let modified = b"modified\n";
15136 assert_eq!(original.len(), modified.len());
15137
15138 let entry_mtime = (1_700_000_000u32, 0u32);
15139 let index_file_mtime = (1_700_000_100u32, 0u32);
15140
15141 std::process::Command::new("git")
15142 .args(["-C", &wt.to_string_lossy(), "init", "-q"])
15143 .status()
15144 .expect("git init");
15145 std::process::Command::new("git")
15146 .args([
15147 "-C",
15148 &wt.to_string_lossy(),
15149 "config",
15150 "user.email",
15151 "t@example.com",
15152 ])
15153 .status()
15154 .expect("git config email");
15155 std::process::Command::new("git")
15156 .args(["-C", &wt.to_string_lossy(), "config", "user.name", "T"])
15157 .status()
15158 .expect("git config name");
15159
15160 fs::write(wt.join(rel), original).expect("write original");
15161 pin_mtime_and_sample(&wt.join(rel), entry_mtime.0, entry_mtime.1);
15162 std::process::Command::new("git")
15163 .args(["-C", &wt.to_string_lossy(), "add", rel])
15164 .status()
15165 .expect("git add");
15166 std::process::Command::new("git")
15167 .args([
15168 "-C",
15169 &wt.to_string_lossy(),
15170 "commit",
15171 "-qm",
15172 "seed",
15173 "--no-gpg-sign",
15174 ])
15175 .status()
15176 .expect("git commit");
15177
15178 let mut index = Index::new();
15179 index.source_mtime = Some(index_file_mtime);
15180 let staged = staged_entry_for_file(&odb, wt, rel, original, entry_mtime);
15181 let oid_original = staged.oid;
15182 index.entries.push(staged);
15183
15184 fs::write(wt.join(rel), modified).expect("modify worktree");
15185 let git_dirty = std::process::Command::new("git")
15186 .args([
15187 "-C",
15188 &wt.to_string_lossy(),
15189 "--no-optional-locks",
15190 "status",
15191 "--short",
15192 ])
15193 .output()
15194 .expect("git status");
15195 assert!(
15196 git_dirty.status.success(),
15197 "git status failed: {}",
15198 String::from_utf8_lossy(&git_dirty.stderr)
15199 );
15200 assert!(
15201 String::from_utf8_lossy(&git_dirty.stdout).contains("tracked.txt"),
15202 "system git must see modification before mtime pin: {}",
15203 String::from_utf8_lossy(&git_dirty.stdout)
15204 );
15205
15206 pin_mtime_and_sample(&wt.join(rel), entry_mtime.0, entry_mtime.1);
15207
15208 let meta = fs::symlink_metadata(wt.join(rel)).expect("stat");
15209 let ie = &mut index.entries[0];
15210 assert_eq!(ie.oid, oid_original);
15211 if stat_matches(ie, &meta) {
15212 ie.ctime_sec = ie.ctime_sec.wrapping_sub(3600);
15213 }
15214 assert!(
15215 !stat_matches(ie, &meta),
15216 "ctime/inode change must fail full stat match"
15217 );
15218 let stat_policy = crate::index::StatMatchPolicy::default();
15219 assert!(
15220 !entry_is_racy_with_policy(ie, Some(index_file_mtime), stat_policy),
15221 "fixture must be non-racy"
15222 );
15223
15224 let (entries, index_changed) = super::diff_index_to_worktree_with_options(
15225 &odb,
15226 &mut index,
15227 wt,
15228 super::DiffIndexToWorktreeOptions {
15229 index_mtime: Some(index_file_mtime),
15230 repository_git_dir: Some(git_dir),
15231 refresh_index_stat_in_pass: true,
15232 for_status: true,
15233 ..Default::default()
15234 },
15235 )
15236 .expect("diff");
15237 assert_eq!(
15238 entries.len(),
15239 1,
15240 "must report modified, not refresh stat clean: {entries:?}, index_changed={index_changed}"
15241 );
15242 assert_eq!(entries[0].path(), rel);
15243 }
15244
15245 #[test]
15246 fn diff_name_status_skips_big_worktree_read_on_size_mismatch() {
15247 let td = tempfile::tempdir().expect("tempdir");
15248 let wt = td.path();
15249 let (git_dir, odb) = minimal_repo(wt, "[core]\n\tbigFileThreshold = 4k\n");
15250 let rel = "big.bin";
15251 let indexed = vec![b'x'; 100];
15252 let mut index = Index::new();
15253 index
15254 .entries
15255 .push(staged_entry_for_file(&odb, wt, rel, &indexed, (100, 0)));
15256 fs::write(wt.join(rel), vec![b'y'; 5000]).expect("rewrite bigger");
15257 let metrics = odb.hot_path_metrics();
15258 metrics.reset_blob_content_reads();
15259 metrics.set_blob_counting(true);
15260 let (entries, _) = super::diff_index_to_worktree_with_options(
15261 &odb,
15262 &mut index,
15263 wt,
15264 super::DiffIndexToWorktreeOptions {
15265 repository_git_dir: Some(git_dir),
15266 for_status: false,
15267 ..Default::default()
15268 },
15269 )
15270 .expect("diff");
15271 metrics.set_blob_counting(false);
15272 assert_eq!(entries.len(), 1);
15273 assert_eq!(entries[0].status, super::DiffStatus::Modified);
15274 assert_eq!(entries[0].new_oid, super::zero_oid());
15275 assert_eq!(
15276 metrics.blob_content_reads(),
15277 0,
15278 "large size-only drift must not hash or read the worktree"
15279 );
15280 }
15281
15282 #[test]
15283 fn status_diff_skips_hash_when_worktree_size_differs() {
15284 let td = tempfile::tempdir().expect("tempdir");
15285 let wt = td.path();
15286 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
15287 let rel = "size-change.txt";
15288 let original = b"short\n";
15289 let mut index = Index::new();
15290 index
15291 .entries
15292 .push(staged_entry_for_file(&odb, wt, rel, original, (100, 0)));
15293 fs::write(wt.join(rel), b"longer content\n").expect("rewrite");
15294 let metrics = odb.hot_path_metrics();
15295 metrics.reset_blob_content_reads();
15296 metrics.set_blob_counting(true);
15297 let (entries, _) = super::diff_index_to_worktree_with_options(
15298 &odb,
15299 &mut index,
15300 wt,
15301 super::DiffIndexToWorktreeOptions {
15302 repository_git_dir: Some(git_dir),
15303 for_status: true,
15304 ..Default::default()
15305 },
15306 )
15307 .expect("diff");
15308 metrics.set_blob_counting(false);
15309 assert_eq!(entries.len(), 1);
15310 assert_eq!(entries[0].status, super::DiffStatus::Modified);
15311 assert_eq!(entries[0].path(), rel);
15312 assert_eq!(entries[0].old_oid, index.entries[0].oid);
15313 assert_eq!(entries[0].new_oid, super::zero_oid());
15314 assert_eq!(
15315 metrics.blob_content_reads(),
15316 0,
15317 "identity conversion: a size change alone proves the file modified"
15318 );
15319 }
15320
15321 #[test]
15322 fn status_diff_hashes_size_change_when_eol_conversion_restores_blob() {
15323 for refresh_index_stat_in_pass in [false, true] {
15324 let td = tempfile::tempdir().expect("tempdir");
15325 let wt = td.path();
15326 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
15327 fs::write(wt.join(".gitattributes"), "*.txt text eol=crlf\n").expect("attrs");
15328 let rel = "n.txt";
15329 let blob = odb
15330 .write(ObjectKind::Blob, b"one\ntwo\n")
15331 .expect("write blob");
15332 let mut index = Index::new();
15333 index.entries.push(staged_entry_with_oid(
15334 wt,
15335 rel,
15336 b"one\r\ntwo\r\n",
15337 blob,
15338 (100, 0),
15339 ));
15340 fs::write(wt.join(rel), b"one\ntwo\n").expect("rewrite as LF");
15341 let (entries, index_changed) = super::diff_index_to_worktree_with_options(
15342 &odb,
15343 &mut index,
15344 wt,
15345 super::DiffIndexToWorktreeOptions {
15346 index_mtime: Some((200, 0)),
15347 repository_git_dir: Some(git_dir),
15348 refresh_index_stat_in_pass,
15349 for_status: true,
15350 ..Default::default()
15351 },
15352 )
15353 .expect("diff");
15354 assert!(
15355 entries.is_empty(),
15356 "refresh={refresh_index_stat_in_pass}: LF worktree cleans to the indexed blob: \
15357 {entries:?}"
15358 );
15359 if refresh_index_stat_in_pass {
15360 assert!(index_changed, "verified entry adopts the new stat");
15361 assert_eq!(index.entries[0].size, 8);
15362 }
15363 }
15364 }
15365
15366 #[test]
15367 fn status_diff_reads_size_changed_converted_file_once() {
15368 for refresh_index_stat_in_pass in [false, true] {
15369 let td = tempfile::tempdir().expect("tempdir");
15370 let wt = td.path();
15371 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
15372 fs::write(wt.join(".gitattributes"), "*.txt text eol=crlf\n").expect("attrs");
15373 let rel = "n.txt";
15374 let blob = odb
15375 .write(ObjectKind::Blob, b"one\ntwo\n")
15376 .expect("write blob");
15377 let mut index = Index::new();
15378 index.entries.push(staged_entry_with_oid(
15379 wt,
15380 rel,
15381 b"one\r\ntwo\r\n",
15382 blob,
15383 (100, 0),
15384 ));
15385 fs::write(wt.join(rel), b"one\ntwo\nthree\n").expect("real edit");
15386 let metrics = odb.hot_path_metrics();
15387 metrics.reset_blob_content_reads();
15388 metrics.set_blob_counting(true);
15389 let (entries, _) = super::diff_index_to_worktree_with_options(
15390 &odb,
15391 &mut index,
15392 wt,
15393 super::DiffIndexToWorktreeOptions {
15394 index_mtime: Some((200, 0)),
15395 repository_git_dir: Some(git_dir),
15396 refresh_index_stat_in_pass,
15397 for_status: true,
15398 ..Default::default()
15399 },
15400 )
15401 .expect("diff");
15402 metrics.set_blob_counting(false);
15403 assert_eq!(entries.len(), 1, "refresh={refresh_index_stat_in_pass}");
15404 assert_eq!(entries[0].status, super::DiffStatus::Modified);
15405 assert_eq!(entries[0].new_oid, zero_oid());
15406 assert_eq!(
15407 index.entries[0].size, 10,
15408 "a modified file keeps its stale stat"
15409 );
15410 assert_eq!(
15411 metrics.blob_content_reads(),
15412 1,
15413 "refresh={refresh_index_stat_in_pass}: read once, not again after the refresh"
15414 );
15415 }
15416 }
15417
15418 #[test]
15419 fn stat_matches_ignores_subsecond_nsec_by_default() {
15420 let td = tempfile::tempdir().expect("tempdir");
15421 let wt = td.path();
15422 fs::write(wt.join("f.txt"), b"x\n").expect("write");
15423 pin_mtime_and_sample(&wt.join("f.txt"), 100, 0);
15424 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
15425 let mut entry = entry_from_metadata(&meta, b"f.txt", zero_oid(), MODE_REGULAR);
15426 entry.mtime_nsec = entry.mtime_nsec.wrapping_add(1_000_000);
15427 assert!(stat_matches(&entry, &meta));
15428 }
15429
15430 #[test]
15431 fn classify_add_up_to_date_when_stat_matches() {
15432 let td = tempfile::tempdir().expect("tempdir");
15433 let wt = td.path();
15434 let (_git_dir, odb) = minimal_repo(wt, "[core]\n");
15435 let bytes = b"hello world\n";
15436 let entry = staged_entry_for_file(&odb, wt, "f.txt", bytes, (100, 0));
15437 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
15438 let conv = crlf::ConversionConfig::from_config(&ConfigSet::new());
15439 let attrs = FileAttrs::default();
15440 let params = WorktreeAddRefreshParams {
15441 odb: &odb,
15442 ie: &entry,
15443 meta: &meta,
15444 abs_path: &wt.join("f.txt"),
15445 rel_path: "f.txt",
15446 conv: &conv,
15447 file_attrs: &attrs,
15448 index_mtime: Some((200, 0)),
15449 staged_mode: MODE_REGULAR,
15450 filter_process: None,
15451 };
15452 let refresh = classify_worktree_entry_for_add(¶ms).expect("classify");
15453 assert_eq!(refresh, WorktreeAddRefresh::UpToDate);
15454 }
15455
15456 #[test]
15457 fn classify_add_rehashes_when_index_mtime_unknown_and_stat_matches() {
15458 let td = tempfile::tempdir().expect("tempdir");
15459 let wt = td.path();
15460 let (_git_dir, odb) = minimal_repo(wt, "[core]\n");
15461 let entry = staged_entry_for_file(&odb, wt, "f.txt", b"old content\n", (100, 0));
15462 fs::write(wt.join("f.txt"), b"new content\n").expect("rewrite");
15463 pin_mtime_and_sample(&wt.join("f.txt"), 100, 0);
15464 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
15465 let conv = crlf::ConversionConfig::from_config(&ConfigSet::new());
15466 let attrs = FileAttrs::default();
15467 let params = WorktreeAddRefreshParams {
15468 odb: &odb,
15469 ie: &entry,
15470 meta: &meta,
15471 abs_path: &wt.join("f.txt"),
15472 rel_path: "f.txt",
15473 conv: &conv,
15474 file_attrs: &attrs,
15475 index_mtime: None,
15476 staged_mode: MODE_REGULAR,
15477 filter_process: None,
15478 };
15479 let refresh = classify_worktree_entry_for_add(¶ms).expect("classify");
15480 assert_eq!(refresh, WorktreeAddRefresh::NeedsRestage);
15481 }
15482
15483 #[test]
15484 fn classify_add_needs_restaging_when_content_differs() {
15485 let td = tempfile::tempdir().expect("tempdir");
15486 let wt = td.path();
15487 let (_git_dir, odb) = minimal_repo(wt, "[core]\n");
15488 let entry = staged_entry_for_file(&odb, wt, "f.txt", b"old content\n", (100, 0));
15489 fs::write(wt.join("f.txt"), b"new content\n").expect("rewrite");
15490 pin_mtime_and_sample(&wt.join("f.txt"), 101, 0);
15491 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
15492 let conv = crlf::ConversionConfig::from_config(&ConfigSet::new());
15493 let attrs = FileAttrs::default();
15494 let params = WorktreeAddRefreshParams {
15495 odb: &odb,
15496 ie: &entry,
15497 meta: &meta,
15498 abs_path: &wt.join("f.txt"),
15499 rel_path: "f.txt",
15500 conv: &conv,
15501 file_attrs: &attrs,
15502 index_mtime: Some((200, 0)),
15503 staged_mode: MODE_REGULAR,
15504 filter_process: None,
15505 };
15506 let refresh = classify_worktree_entry_for_add(¶ms).expect("classify");
15507 assert_eq!(refresh, WorktreeAddRefresh::NeedsRestage);
15508 }
15509
15510 #[test]
15511 fn smudges_racily_clean_modified_entry() {
15512 let td = tempfile::tempdir().expect("tempdir");
15513 let wt = td.path();
15514 let index_mtime = setup_index_mtime(wt);
15515 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
15516 let content_a = b"same length!!";
15517 let content_b = b"same length??";
15518 assert_eq!(content_a.len(), content_b.len());
15519
15520 let mut index = Index::new();
15521 let entry = staged_entry_for_file(&odb, wt, "f.txt", content_a, index_mtime);
15522 let stale_oid = entry.oid;
15523
15524 fs::write(wt.join("f.txt"), content_b).expect("rewrite");
15525 pin_mtime_and_sample(&wt.join("f.txt"), index_mtime.0, index_mtime.1);
15526 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
15527 let entry = entry_from_metadata(&meta, b"f.txt", stale_oid, MODE_REGULAR);
15529 index.entries.push(entry.clone());
15530
15531 assert!(stat_matches(&entry, &meta));
15532 assert!(entry_is_racy(&entry, Some(index_mtime)));
15533
15534 let smudged = smudge_racily_clean_entries(
15535 &odb,
15536 &git_dir,
15537 &mut index,
15538 wt,
15539 Some(index_mtime),
15540 None,
15541 None,
15542 );
15543 assert!(smudged);
15544 assert_eq!(index.entries[0].size, 0);
15545
15546 let diff = diff_index_to_worktree(&odb, &mut index, wt, false, false).expect("diff");
15547 assert_eq!(diff.len(), 1);
15548 assert_eq!(diff[0].path(), "f.txt");
15549 }
15550
15551 fn racy_stale_entry(wt: &Path) -> (PathBuf, Odb, Index, (u32, u32), ObjectId) {
15553 let index_mtime = setup_index_mtime(wt);
15554 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
15555 let entry = staged_entry_for_file(&odb, wt, "f.txt", b"same length!!", index_mtime);
15556 let stale_oid = entry.oid;
15557 fs::write(wt.join("f.txt"), b"same length??").expect("rewrite");
15558 pin_mtime_and_sample(&wt.join("f.txt"), index_mtime.0, index_mtime.1);
15559 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
15560 let mut index = Index::new();
15561 index.entries.push(entry_from_metadata(
15562 &meta,
15563 b"f.txt",
15564 stale_oid,
15565 MODE_REGULAR,
15566 ));
15567 (git_dir, odb, index, index_mtime, stale_oid)
15568 }
15569
15570 #[test]
15571 fn content_verified_entry_is_not_reread_and_the_mark_is_consumed() {
15572 let td = tempfile::tempdir().expect("tempdir");
15573 let wt = td.path();
15574 let (git_dir, odb, mut index, index_mtime, stale_oid) = racy_stale_entry(wt);
15575 index.mark_content_verified(b"f.txt", stale_oid);
15576
15577 let smudge = |index: &mut Index| {
15578 smudge_racily_clean_entries(&odb, &git_dir, index, wt, Some(index_mtime), None, None)
15579 };
15580 assert!(!smudge(&mut index), "a verified entry is trusted");
15581 assert_ne!(index.entries[0].size, 0);
15582 assert!(index.content_verified.is_empty());
15583 assert!(smudge(&mut index), "without the mark the entry is checked");
15584 assert_eq!(index.entries[0].size, 0);
15585 }
15586
15587 #[test]
15588 fn content_verified_entry_bit_is_trusted_and_consumed() {
15589 let td = tempfile::tempdir().expect("tempdir");
15590 let wt = td.path();
15591 let (git_dir, odb, mut index, index_mtime, _) = racy_stale_entry(wt);
15592 index.entries[0].mark_content_verified();
15593
15594 let smudge = |index: &mut Index| {
15595 smudge_racily_clean_entries(&odb, &git_dir, index, wt, Some(index_mtime), None, None)
15596 };
15597 assert!(!smudge(&mut index), "a marked entry is trusted");
15598 assert_ne!(index.entries[0].size, 0);
15599 assert_eq!(
15600 index.entries[0].flags_extended, None,
15601 "the mark is consumed"
15602 );
15603 assert!(smudge(&mut index), "without the mark the entry is checked");
15604 assert_eq!(index.entries[0].size, 0);
15605 }
15606
15607 #[test]
15608 fn content_verified_mark_for_another_oid_does_not_skip_the_check() {
15609 let td = tempfile::tempdir().expect("tempdir");
15610 let wt = td.path();
15611 let (git_dir, odb, mut index, index_mtime, _) = racy_stale_entry(wt);
15612 let other = odb.hash(ObjectKind::Blob, b"other").expect("hash");
15613 index.mark_content_verified(b"f.txt", other);
15614 assert!(smudge_racily_clean_entries(
15615 &odb,
15616 &git_dir,
15617 &mut index,
15618 wt,
15619 Some(index_mtime),
15620 None,
15621 None,
15622 ));
15623 assert_eq!(index.entries[0].size, 0);
15624 }
15625
15626 #[test]
15627 fn leaves_non_racy_entry_alone() {
15628 let td = tempfile::tempdir().expect("tempdir");
15629 let wt = td.path();
15630 let index_mtime = setup_index_mtime(wt);
15631 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
15632 let content_a = b"same length!!";
15633 let content_b = b"same length??";
15634 let entry_mtime = (index_mtime.0 - 10, 0);
15635
15636 let mut index = Index::new();
15637 let entry = staged_entry_for_file(&odb, wt, "f.txt", content_a, entry_mtime);
15638 index.entries.push(entry.clone());
15639
15640 fs::write(wt.join("f.txt"), content_b).expect("rewrite");
15641 pin_mtime_and_sample(&wt.join("f.txt"), entry_mtime.0, entry_mtime.1);
15642
15643 assert!(!entry_is_racy(&entry, Some(index_mtime)));
15644 let smudged = smudge_racily_clean_entries(
15645 &odb,
15646 &git_dir,
15647 &mut index,
15648 wt,
15649 Some(index_mtime),
15650 None,
15651 None,
15652 );
15653 assert!(!smudged);
15654 assert_eq!(index.entries[0].size, entry.size);
15655 }
15656
15657 #[test]
15658 fn leaves_matching_content_alone() {
15659 let td = tempfile::tempdir().expect("tempdir");
15660 let wt = td.path();
15661 let index_mtime = setup_index_mtime(wt);
15662 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
15663 let bytes = b"unchanged!!!!";
15664
15665 let mut index = Index::new();
15666 let entry = staged_entry_for_file(&odb, wt, "f.txt", bytes, index_mtime);
15667 index.entries.push(entry.clone());
15668
15669 assert!(entry_is_racy(&entry, Some(index_mtime)));
15670 let smudged = smudge_racily_clean_entries(
15671 &odb,
15672 &git_dir,
15673 &mut index,
15674 wt,
15675 Some(index_mtime),
15676 None,
15677 None,
15678 );
15679 assert!(!smudged);
15680 assert_eq!(index.entries[0].size, entry.size);
15681 }
15682
15683 #[test]
15684 fn status_diff_skips_refresh_rewrite_for_racy_clean_matching_stat() {
15685 let td = tempfile::tempdir().expect("tempdir");
15686 let wt = td.path();
15687 let index_mtime = setup_index_mtime(wt);
15688 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
15689 let bytes = b"racy clean payload\n";
15690
15691 let mut index = Index::new();
15692 index.source_mtime = Some(index_mtime);
15693 let entry = staged_entry_for_file(&odb, wt, "f.txt", bytes, index_mtime);
15694 index.entries.push(entry.clone());
15695 assert!(entry_is_racy(&entry, Some(index_mtime)));
15696
15697 let (entries, index_changed) = super::diff_index_to_worktree_with_options(
15698 &odb,
15699 &mut index,
15700 wt,
15701 super::DiffIndexToWorktreeOptions {
15702 repository_git_dir: Some(git_dir),
15703 index_mtime: Some(index_mtime),
15704 refresh_index_stat_in_pass: true,
15705 for_status: true,
15706 ..Default::default()
15707 },
15708 )
15709 .expect("status diff");
15710 assert!(entries.is_empty(), "unexpected unstaged: {entries:?}");
15711 assert!(
15712 !index_changed,
15713 "matching stat on a racy entry must skip refresh without rewriting the index"
15714 );
15715 }
15716
15717 #[test]
15718 fn leaves_matching_sha256_racy_content_alone() {
15719 let td = tempfile::tempdir().expect("tempdir");
15720 let wt = td.path();
15721 let index_mtime = setup_index_mtime(wt);
15722 let (git_dir, odb) = minimal_repo(wt, "[extensions]\n\tobjectformat = sha256\n");
15723 assert_eq!(odb.hash_algo(), HashAlgo::Sha256);
15724
15725 let bytes = b"sha256 width!!";
15726 let oid = odb.hash(ObjectKind::Blob, bytes).expect("hash");
15727 let mut index = Index::new();
15728 let entry = staged_entry_with_oid(wt, "f.txt", bytes, oid, index_mtime);
15729 index.entries.push(entry.clone());
15730
15731 assert!(entry_is_racy(&entry, Some(index_mtime)));
15732 let smudged = smudge_racily_clean_entries(
15733 &odb,
15734 &git_dir,
15735 &mut index,
15736 wt,
15737 Some(index_mtime),
15738 None,
15739 None,
15740 );
15741 assert!(!smudged, "matching SHA-256 blob must not be smudged");
15742 assert_eq!(index.entries[0].size, entry.size);
15743 }
15744
15745 #[test]
15746 fn materialize_big_binary_under_autocrlf_streams_into_pack() {
15747 use std::io::Write;
15748
15749 let td = tempfile::tempdir().expect("tempdir");
15750 let wt = td.path();
15751 let (git_dir, odb) =
15752 minimal_repo(wt, "[core]\n\tautocrlf = true\n\tbigFileThreshold = 64k\n");
15753 let path = wt.join("big.bin");
15754 {
15755 let mut f = fs::File::create(&path).expect("create");
15756 f.write_all(&[0u8; 9000]).expect("nul prefix");
15757 let chunk = vec![1u8; 256 * 1024];
15758 for _ in 0..20 {
15759 f.write_all(&chunk).expect("chunk");
15760 }
15761 }
15762 let meta = fs::metadata(&path).expect("stat");
15763 let mut conv = crlf::ConversionConfig::from_config(&ConfigSet::new());
15764 conv.autocrlf = crlf::AutoCrlf::True;
15765 let attrs = FileAttrs::default();
15766 let oid =
15767 materialize_worktree_blob(&odb, &path, &meta, &conv, &attrs, "big.bin", None, None)
15768 .expect("materialize");
15769 assert!(
15770 !odb.test_loose_object_path(&oid).exists(),
15771 "big binary under autocrlf should pack, not write loose"
15772 );
15773 let pack_dir = git_dir.join("objects/pack");
15774 let names: Vec<_> = fs::read_dir(&pack_dir)
15775 .expect("pack dir")
15776 .map(|e| e.expect("entry").file_name())
15777 .collect();
15778 assert!(
15779 names.iter().any(|n| n.to_string_lossy().ends_with(".pack")),
15780 "{names:?}"
15781 );
15782 assert!(odb.read(&oid).expect("read blob").data.len() > 9000);
15783 }
15784
15785 #[test]
15786 fn indexed_binary_under_autocrlf_rehash_streams() {
15787 use std::io::Write;
15788
15789 let td = tempfile::tempdir().expect("tempdir");
15790 let wt = td.path();
15791 let (_git_dir, odb) =
15792 minimal_repo(wt, "[core]\n\tautocrlf = true\n\tbigFileThreshold = 1m\n");
15793 let path = wt.join("big.bin");
15794 {
15795 let mut f = fs::File::create(&path).expect("create");
15796 f.write_all(&[0u8; 9000]).expect("nul prefix");
15797 let chunk = vec![1u8; 512 * 1024];
15798 for _ in 0..3 {
15799 f.write_all(&chunk).expect("chunk");
15800 }
15801 }
15802 let meta = fs::symlink_metadata(&path).expect("stat");
15803 let oid = hash_worktree_file(
15804 &odb,
15805 &path,
15806 &meta,
15807 &crlf::ConversionConfig::from_config(&ConfigSet::new()),
15808 &FileAttrs::default(),
15809 "big.bin",
15810 None,
15811 None,
15812 )
15813 .expect("hash");
15814 let entry = entry_from_metadata(&meta, b"big.bin", oid, MODE_REGULAR);
15815 let mut conv = crlf::ConversionConfig::from_config(&ConfigSet::new());
15816 conv.autocrlf = crlf::AutoCrlf::True;
15817 let attrs = FileAttrs::default();
15818 let content = read_regular_worktree_blob(
15819 &odb,
15820 &path,
15821 &conv,
15822 &attrs,
15823 "big.bin",
15824 Some(&entry),
15825 None,
15826 1024 * 1024,
15827 )
15828 .expect("read blob");
15829 assert!(
15830 matches!(content, WorktreeBlobContent::Stream { .. }),
15831 "indexed binary re-hash must stream, not buffer"
15832 );
15833 assert_eq!(content.hash(&odb).expect("hash"), entry.oid);
15834 }
15835
15836 #[test]
15837 fn leaves_matching_crlf_worktree_content_alone() {
15838 let td = tempfile::tempdir().expect("tempdir");
15839 let wt = td.path();
15840 let index_mtime = setup_index_mtime(wt);
15841 let (git_dir, odb) = minimal_repo(wt, "[core]\n\tautocrlf = true\n");
15842 let git_form = b"line one\nline two\n";
15843 let worktree_form = b"line one\r\nline two\r\n";
15844 let oid = odb.hash(ObjectKind::Blob, git_form).expect("hash");
15845
15846 let mut index = Index::new();
15847 let entry = staged_entry_with_oid(wt, "f.txt", worktree_form, oid, index_mtime);
15848 index.entries.push(entry.clone());
15849
15850 assert!(entry_is_racy(&entry, Some(index_mtime)));
15851 let smudged = smudge_racily_clean_entries(
15852 &odb,
15853 &git_dir,
15854 &mut index,
15855 wt,
15856 Some(index_mtime),
15857 None,
15858 None,
15859 );
15860 assert!(
15861 !smudged,
15862 "CRLF worktree matching clean index OID must not be smudged"
15863 );
15864 assert_eq!(index.entries[0].size, entry.size);
15865 }
15866
15867 #[test]
15868 fn skips_skip_worktree_and_intent_to_add() {
15869 let td = tempfile::tempdir().expect("tempdir");
15870 let wt = td.path();
15871 let index_mtime = setup_index_mtime(wt);
15872 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
15873 let content_a = b"same length!!";
15874 let content_b = b"same length??";
15875
15876 let mut index = Index::new();
15877 let mut entry = staged_entry_for_file(&odb, wt, "f.txt", content_a, index_mtime);
15878 entry.set_skip_worktree(true);
15879 index.entries.push(entry);
15880 fs::write(wt.join("f.txt"), content_b).expect("rewrite");
15881 pin_mtime_and_sample(&wt.join("f.txt"), index_mtime.0, index_mtime.1);
15882 assert!(!smudge_racily_clean_entries(
15883 &odb,
15884 &git_dir,
15885 &mut index,
15886 wt,
15887 Some(index_mtime),
15888 None,
15889 None,
15890 ));
15891 assert_ne!(index.entries[0].size, 0);
15892
15893 let mut index = Index::new();
15894 let mut entry = staged_entry_for_file(&odb, wt, "f.txt", content_a, index_mtime);
15895 entry.set_intent_to_add(true);
15896 index.entries.push(entry);
15897 fs::write(wt.join("f.txt"), content_b).expect("rewrite");
15898 pin_mtime_and_sample(&wt.join("f.txt"), index_mtime.0, index_mtime.1);
15899 assert!(!smudge_racily_clean_entries(
15900 &odb,
15901 &git_dir,
15902 &mut index,
15903 wt,
15904 Some(index_mtime),
15905 None,
15906 None,
15907 ));
15908 assert_ne!(index.entries[0].size, 0);
15909 }
15910
15911 #[test]
15912 fn smudges_nothing_without_prior_index_mtime() {
15913 let td = tempfile::tempdir().expect("tempdir");
15914 let wt = td.path();
15915 let index_mtime = setup_index_mtime(wt);
15916 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
15917 let mut index = Index::new();
15918 let entry = staged_entry_for_file(&odb, wt, "f.txt", b"same length!!", index_mtime);
15919 index.entries.push(entry);
15920
15921 fs::write(wt.join("f.txt"), b"same length??").expect("rewrite");
15922 pin_mtime_and_sample(&wt.join("f.txt"), index_mtime.0, index_mtime.1);
15923
15924 let smudged = smudge_racily_clean_entries(&odb, &git_dir, &mut index, wt, None, None, None);
15925 assert!(!smudged);
15926 assert_ne!(index.entries[0].size, 0);
15927 }
15928}
15929
15930#[cfg(all(test, unix))]
15931mod read_worktree_bytes_for_mode_tests {
15932 use super::*;
15933 use crate::index::MODE_SYMLINK;
15934 use std::fs;
15935 use std::os::unix::fs::symlink;
15936
15937 #[test]
15938 fn symlink_mode_reads_link_target_not_followed_file() {
15939 let td = tempfile::tempdir().expect("tempdir");
15940 let wt = td.path();
15941 fs::write(wt.join("payload.txt"), b"secret\n").expect("write");
15942 symlink("payload.txt", wt.join("link")).expect("symlink");
15943 let bytes =
15944 read_worktree_bytes_for_mode(&wt.join("link"), MODE_SYMLINK).expect("read link");
15945 assert_eq!(bytes, b"payload.txt");
15946 }
15947
15948 #[test]
15949 fn symlink_mode_reads_plain_file_holding_the_target() {
15950 let td = tempfile::tempdir().expect("tempdir");
15951 let path = td.path().join("link");
15952 fs::write(&path, b"payload.txt").expect("write");
15953 let bytes = read_worktree_bytes_for_mode(&path, MODE_SYMLINK).expect("read plain link");
15954 assert_eq!(bytes, b"payload.txt");
15955 }
15956
15957 #[test]
15958 fn regular_mode_reads_file_contents() {
15959 let td = tempfile::tempdir().expect("tempdir");
15960 let path = td.path().join("f.txt");
15961 fs::write(&path, b"hello\n").expect("write");
15962 let bytes = read_worktree_bytes_for_mode(&path, crate::index::MODE_REGULAR).expect("read");
15963 assert_eq!(bytes, b"hello\n");
15964 }
15965
15966 #[test]
15967 fn annotate_marks_context_line_when_eof_line_is_unchanged() {
15968 let old = "a\nb\nc";
15969 let new = "a\nx\nc";
15970 let raw = super::unified_diff_histogram_hunks_only(old, new, 3, 0, false);
15971 let out = annotate_unified_diff_no_newline_at_eof(&raw, old, new);
15972 assert!(
15973 out.contains(" c\n\\ No newline at end of file"),
15974 "histogram hunk should mark trailing context line:\n{out}"
15975 );
15976 }
15977
15978 #[test]
15979 fn annotate_does_not_treat_removed_dash_lines_as_file_headers() {
15980 use super::unified_diff_with_prefix_and_funcname_and_algorithm;
15981 use similar::Algorithm;
15982
15983 let old = "--old";
15984 let new = "--new";
15985 let raw = unified_diff_with_prefix_and_funcname_and_algorithm(
15986 old,
15987 new,
15988 "a/x",
15989 "b/x",
15990 3,
15991 0,
15992 "",
15993 "",
15994 None,
15995 Algorithm::Myers,
15996 false,
15997 false,
15998 false,
15999 false,
16000 );
16001 assert_eq!(
16002 raw.matches("\\ No newline at end of file").count(),
16003 2,
16004 "expected two markers like git, got:\n{raw}"
16005 );
16006 }
16007}
16008
16009#[cfg(test)]
16010mod annotate_no_newline_tests {
16011 use super::annotate_unified_diff_no_newline_at_eof;
16012
16013 #[test]
16014 fn annotate_marks_both_sides_without_trailing_newline() {
16015 let raw = "@@ -1 +1 @@\n-foo\n+f o o\n";
16016 let out = annotate_unified_diff_no_newline_at_eof(raw, "foo", "f o o");
16017 assert!(out.contains("\\ No newline at end of file"));
16018 assert_eq!(out.matches("\\ No newline at end of file").count(), 2);
16019 }
16020}
16021
16022#[cfg(test)]
16023mod blob_diff_algorithm_smoke {
16024 use super::{
16025 anchored_unified_diff, count_changes_with_algorithm, unified_diff_histogram_hunks_only,
16026 unified_diff_with_prefix_and_funcname_and_algorithm, unified_hunk_body_for_diff_options,
16027 };
16028 use similar::Algorithm;
16029
16030 const OLD: &str = "alpha\nbeta\ngamma\n";
16031 const NEW: &str = "alpha\nBETA\ngamma\ndelta\n";
16032
16033 #[test]
16034 fn seed15_histogram_u0_body_matches_git() {
16035 use crate::diff::options::{DiffAlgorithm, DiffOptions};
16036 use std::collections::hash_map::DefaultHasher;
16037 use std::hash::{Hash, Hasher};
16038 let seed = 15u64;
16039 let mut hasher = DefaultHasher::new();
16040 seed.hash(&mut hasher);
16041 let h = hasher.finish();
16042 let line_count = 4 + (h as usize % 12);
16043 let mut old = String::new();
16044 let mut new = String::new();
16045 for i in 0..line_count {
16046 let token = (h.wrapping_add(i as u64 * 131) % 7) as u8 + b'a';
16047 let ch = token as char;
16048 old.push(ch);
16049 if (h >> (i % 8)) & 1 == 1 {
16050 new.push(((token as u8).wrapping_add(1)) as char);
16051 } else {
16052 new.push(ch);
16053 }
16054 old.push('\n');
16055 new.push('\n');
16056 }
16057 let opts = DiffOptions {
16058 context_lines: 0,
16059 algorithm: DiffAlgorithm::Histogram,
16060 indent_heuristic: false,
16061 ..DiffOptions::default()
16062 };
16063 let body = unified_hunk_body_for_diff_options(&old, &new, &opts);
16064 assert_eq!(
16065 body.matches("@@ ").count(),
16066 2,
16067 "git 2.55 splits u0 histogram hunks at seed 15: {body}"
16068 );
16069 }
16070
16071 #[test]
16072 fn t4071_histogram_changed_flags_git_myers_completes() {
16073 let old: Vec<&str> = "x\nx\nx\nx\n".lines().collect();
16074 let new: Vec<&str> = "x\nx\nx\nA\nB\nC\nD\nx\nE\nF\nG\n".lines().collect();
16075 let (c1, c2) = super::git_xdiff::histogram_changed_flags(&old, &new);
16076 assert_eq!(c1.len(), 4);
16077 assert_eq!(c2.len(), 11);
16078 let ops = super::changed_flags_to_ops(&c1, &c2, old.len(), new.len());
16079 assert!(!ops.is_empty());
16080 }
16081
16082 #[test]
16083 fn t4071_histogram_compacted_ops_completes() {
16084 use crate::diff::WhitespaceMode;
16085 let old = "x\nx\nx\nx\n";
16086 let new = "x\nx\nx\nA\nB\nC\nD\nx\nE\nF\nG\n";
16087 let ops = super::histogram_compacted_line_ops(old, new, false, WhitespaceMode::default());
16088 assert!(!ops.is_empty(), "expected diff ops");
16089 }
16090
16091 #[test]
16092 fn t4071_histogram_u0_body_completes() {
16093 use crate::diff::options::{DiffAlgorithm, DiffOptions};
16094 let old = "x\nx\nx\nx\n";
16095 let new = "x\nx\nx\nA\nB\nC\nD\nx\nE\nF\nG\n";
16096 let opts = DiffOptions {
16097 context_lines: 0,
16098 algorithm: DiffAlgorithm::Histogram,
16099 indent_heuristic: false,
16100 ..DiffOptions::default()
16101 };
16102 let body = unified_hunk_body_for_diff_options(old, new, &opts);
16103 assert!(body.contains("@@"), "expected hunk header: {body}");
16104 }
16105
16106 #[test]
16107 fn t4071_minimal_u0_matches_git_hunk_headers() {
16108 use crate::diff::options::{DiffAlgorithm, DiffOptions};
16109 let old = "x\nx\nx\nx\n";
16110 let new = "x\nx\nx\nA\nB\nC\nD\nx\nE\nF\nG\n";
16111 let opts = DiffOptions {
16112 context_lines: 0,
16113 algorithm: DiffAlgorithm::Minimal,
16114 indent_heuristic: false,
16115 ..DiffOptions::default()
16116 };
16117 let body = unified_hunk_body_for_diff_options(old, new, &opts);
16118 assert_eq!(
16119 body.matches("@@ ").count(),
16120 2,
16121 "git 2.55+ splits u0 minimal hunks at repeated anchors: {body}"
16122 );
16123 }
16124
16125 #[test]
16126 fn hunks_do_not_slide_onto_final_line_without_newline() {
16127 use crate::diff::options::{DiffAlgorithm, DiffOptions};
16128
16129 let old = "{\n \"a\": 1\n}\n";
16130 let new = "{\n \"a\": 2\n}\n{\n \"b\": 2\n}";
16131 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";
16132 for algorithm in [
16133 DiffAlgorithm::Myers,
16134 DiffAlgorithm::Minimal,
16135 DiffAlgorithm::Patience,
16136 DiffAlgorithm::Histogram,
16137 ] {
16138 for indent_heuristic in [true, false] {
16139 let options = DiffOptions {
16140 algorithm,
16141 indent_heuristic,
16142 ..DiffOptions::default()
16143 };
16144 assert_eq!(
16145 unified_hunk_body_for_diff_options(old, new, &options),
16146 expected,
16147 "{algorithm:?}, indent heuristic {indent_heuristic}"
16148 );
16149 }
16150 }
16151 }
16152
16153 #[test]
16154 fn myers_patience_histogram_minimal_emit_unified_hunks() {
16155 for algo in [Algorithm::Myers, Algorithm::Patience, Algorithm::Histogram] {
16156 let patch = unified_diff_with_prefix_and_funcname_and_algorithm(
16157 OLD,
16158 NEW,
16159 "a/old.txt",
16160 "b/new.txt",
16161 3,
16162 0,
16163 "a/",
16164 "b/",
16165 None,
16166 algo,
16167 false,
16168 false,
16169 true,
16170 false,
16171 );
16172 assert!(patch.contains("@@"), "algo {algo:?} should emit hunks");
16173 assert!(patch.contains("-beta") || patch.contains("-BETA"));
16174 }
16175 }
16176
16177 #[test]
16178 fn git_histogram_printer_and_anchored_diff_smoke() {
16179 let hist = unified_diff_histogram_hunks_only(OLD, NEW, 3, 0, false);
16180 assert!(!hist.is_empty());
16181 let anchors = vec!["beta".to_string()];
16182 let anchored = anchored_unified_diff(
16183 OLD,
16184 NEW,
16185 "old",
16186 "new",
16187 3,
16188 &anchors,
16189 Algorithm::Myers,
16190 false,
16191 true,
16192 false,
16193 );
16194 assert!(anchored.contains("@@"));
16195 }
16196
16197 #[test]
16198 fn count_changes_git_histogram_vs_similar() {
16199 let hist = count_changes_with_algorithm(OLD, NEW, Algorithm::Myers, true);
16200 let myers = count_changes_with_algorithm(OLD, NEW, Algorithm::Myers, false);
16201 assert!(hist.0 + hist.1 > 0);
16202 assert!(myers.0 + myers.1 > 0);
16203 }
16204}
16205
16206#[cfg(test)]
16207mod line_coverage_tests {
16208 use super::*;
16209 use crate::config::ConfigSet;
16210 use crate::index::StatMatchPolicy;
16211 use crate::index::{Index, IndexEntry, MODE_REGULAR};
16212 use crate::objects::ObjectKind;
16213 use crate::parallel::ThreadPolicy;
16214 use crate::write_tree::write_tree_from_index;
16215 use similar::Algorithm;
16216 use tempfile::TempDir;
16217
16218 fn index_entry(path: &str, mode: u32, oid: ObjectId) -> IndexEntry {
16219 IndexEntry {
16220 ctime_sec: 0,
16221 ctime_nsec: 0,
16222 mtime_sec: 0,
16223 mtime_nsec: 0,
16224 dev: 0,
16225 ino: 0,
16226 mode,
16227 uid: 0,
16228 gid: 0,
16229 size: 0,
16230 oid,
16231 flags: path.len().min(0xfff) as u16,
16232 flags_extended: None,
16233 path: path.as_bytes().to_vec(),
16234 base_index_pos: 0,
16235 }
16236 }
16237
16238 fn write_tree(odb: &Odb, entries: &[(&str, ObjectId)]) -> ObjectId {
16239 let mut body = Vec::new();
16240 for (name, oid) in entries {
16241 body.extend_from_slice(format!("100644 {name}\0").as_bytes());
16242 body.extend_from_slice(oid.as_bytes());
16243 }
16244 odb.write(ObjectKind::Tree, &body).expect("tree")
16245 }
16246
16247 fn pair_of_trees_with_rename() -> (TempDir, Odb, ObjectId, ObjectId) {
16248 let temp = TempDir::new().expect("tempdir");
16249 let odb = Odb::new(temp.path());
16250 let old_blob = odb
16251 .write(ObjectKind::Blob, b"line one\nline two\n")
16252 .expect("old");
16253 let new_blob = odb
16254 .write(ObjectKind::Blob, b"line one\nline TWO\nline three\n")
16255 .expect("new");
16256 let old_tree = write_tree(&odb, &[("old.txt", old_blob)]);
16257 let new_tree = write_tree(&odb, &[("new.txt", new_blob), ("kept.txt", old_blob)]);
16258 (temp, odb, old_tree, new_tree)
16259 }
16260
16261 #[test]
16262 fn detect_copies_reports_source_modes_without_tree_entries() {
16263 let temp = TempDir::new().expect("tempdir");
16264 let odb = Odb::new(temp.path());
16265 let content = odb
16266 .write(ObjectKind::Blob, b"shared content\n")
16267 .expect("blob");
16268 let changed = odb.write(ObjectKind::Blob, b"changed\n").expect("blob");
16269 let old_tree = write_tree(&odb, &[("gone.txt", content), ("mod.txt", content)]);
16270 let new_tree = write_tree(
16271 &odb,
16272 &[
16273 ("copy.txt", content),
16274 ("mod.txt", changed),
16275 ("moved.txt", content),
16276 ],
16277 );
16278 let entries = diff_trees(&odb, Some(&old_tree), Some(&new_tree), "").expect("diff");
16279 let result = detect_copies(&odb, None, entries, 50, false, &[]).expect("copies");
16280 let pairs: Vec<_> = result
16281 .iter()
16282 .filter(|e| matches!(e.status, DiffStatus::Renamed | DiffStatus::Copied))
16283 .map(|e| (e.old_mode, e.new_mode))
16284 .collect();
16285 assert_eq!(pairs.len(), 2, "{result:?}");
16286 assert!(
16287 pairs
16288 .iter()
16289 .all(|p| *p == (FileMode::REGULAR, FileMode::REGULAR)),
16290 "{pairs:?}"
16291 );
16292 }
16293
16294 #[test]
16295 fn exact_rename_detection_pairs_only_equal_blobs_among_many() -> Result<()> {
16296 let temp = TempDir::new().expect("tempdir");
16297 let odb = Odb::new(temp.path());
16298 const N: usize = 200;
16299 let shared = odb.write(ObjectKind::Blob, b"shared\n").expect("blob");
16300 let mut old_names = Vec::with_capacity(N + 1);
16301 let mut old_oids = Vec::with_capacity(N + 1);
16302 for i in 0..N {
16303 old_names.push(format!("old/{i:03}.txt"));
16304 old_oids.push(
16305 odb.write(ObjectKind::Blob, format!("old {i}\n").as_bytes())
16306 .expect("blob"),
16307 );
16308 }
16309 old_names.push("shared-old.txt".to_owned());
16310 old_oids.push(shared);
16311 let old_pairs: Vec<(&str, ObjectId)> = old_names
16312 .iter()
16313 .zip(old_oids.iter().copied())
16314 .map(|(name, oid)| (name.as_str(), oid))
16315 .collect();
16316
16317 let mut new_names = Vec::with_capacity(N + 1);
16318 let mut new_oids = Vec::with_capacity(N + 1);
16319 for i in 0..N {
16320 new_names.push(format!("new/{i:03}.txt"));
16321 new_oids.push(
16322 odb.write(ObjectKind::Blob, format!("new {i}\n").as_bytes())
16323 .expect("blob"),
16324 );
16325 }
16326 new_names.push("shared-new.txt".to_owned());
16327 new_oids.push(shared);
16328 let new_pairs: Vec<(&str, ObjectId)> = new_names
16329 .iter()
16330 .zip(new_oids.iter().copied())
16331 .map(|(name, oid)| (name.as_str(), oid))
16332 .collect();
16333
16334 let old_tree = write_tree(&odb, &old_pairs);
16335 let new_tree = write_tree(&odb, &new_pairs);
16336 let raw = diff_trees(&odb, Some(&old_tree), Some(&new_tree), "")?;
16337 assert_eq!(raw.len(), (N + 1) * 2);
16338 let config = ConfigSet::new();
16339 let policy = ThreadPolicy::default();
16340 let renamed = detect_renames_with_policy(
16341 &odb,
16342 None,
16343 raw,
16344 50,
16345 &config,
16346 &policy,
16347 None,
16348 DetectRenamesOptions::default(),
16349 )?;
16350 assert_eq!(
16351 renamed
16352 .entries
16353 .iter()
16354 .filter(|e| e.status == DiffStatus::Renamed)
16355 .count(),
16356 1
16357 );
16358 Ok(())
16359 }
16360
16361 #[test]
16362 fn pathspec_limited_diff_does_not_pair_outside_pathspec() -> Result<()> {
16363 let temp = TempDir::new().expect("tempdir");
16364 let odb = Odb::new(temp.path());
16365 let shared = odb.write(ObjectKind::Blob, b"payload\n").expect("blob");
16366 let mut old_names = Vec::new();
16367 let mut old_oids = Vec::new();
16368 for i in 0..50 {
16369 old_names.push(format!("bulk/old/{i:02}.txt"));
16370 old_oids.push(
16371 odb.write(ObjectKind::Blob, format!("bulk old {i}\n").as_bytes())
16372 .expect("blob"),
16373 );
16374 }
16375 old_names.push("target.txt".to_owned());
16376 old_oids.push(shared);
16377 let old_pairs: Vec<(&str, ObjectId)> = old_names
16378 .iter()
16379 .zip(old_oids.iter().copied())
16380 .map(|(name, oid)| (name.as_str(), oid))
16381 .collect();
16382
16383 let mut new_names = Vec::new();
16384 let mut new_oids = Vec::new();
16385 for i in 0..50 {
16386 new_names.push(format!("bulk/new/{i:02}.txt"));
16387 new_oids.push(
16388 odb.write(ObjectKind::Blob, format!("bulk new {i}\n").as_bytes())
16389 .expect("blob"),
16390 );
16391 }
16392 new_names.push("elsewhere.txt".to_owned());
16393 new_oids.push(shared);
16394 let new_pairs: Vec<(&str, ObjectId)> = new_names
16395 .iter()
16396 .zip(new_oids.iter().copied())
16397 .map(|(name, oid)| (name.as_str(), oid))
16398 .collect();
16399
16400 let old_tree = write_tree(&odb, &old_pairs);
16401 let new_tree = write_tree(&odb, &new_pairs);
16402 let config = ConfigSet::new();
16403 let policy = ThreadPolicy::default();
16404 let pathspecs = vec!["target.txt".to_owned()];
16405 let entries = diff_trees_with_renames_for_pathspecs_and_policy(
16406 &odb,
16407 Some(&old_tree),
16408 Some(&new_tree),
16409 "",
16410 &pathspecs,
16411 None,
16412 50,
16413 &config,
16414 &policy,
16415 )?;
16416 assert_eq!(entries.len(), 1);
16417 assert_eq!(entries[0].status, DiffStatus::Deleted);
16418 assert_eq!(entries[0].path(), "target.txt");
16419 Ok(())
16420 }
16421
16422 #[test]
16423 fn detect_copies_keeps_modifications_as_modifications() {
16424 let temp = TempDir::new().expect("tempdir");
16427 let odb = Odb::new(temp.path());
16428 let one = odb.write(ObjectKind::Blob, b"1\n").expect("blob");
16429 let two = odb.write(ObjectKind::Blob, b"2\n").expect("blob");
16430 let old_tree = write_tree(&odb, &[("a", one), ("b", two)]);
16431 let new_tree = write_tree(&odb, &[("a", two), ("b", one)]);
16432 let entries = diff_trees(&odb, Some(&old_tree), Some(&new_tree), "").expect("diff");
16433 let result = detect_copies(&odb, None, entries, 50, false, &[]).expect("copies");
16434 let statuses: Vec<_> = result.iter().map(|e| (e.path(), e.status)).collect();
16435 assert_eq!(
16436 statuses,
16437 [("a", DiffStatus::Modified), ("b", DiffStatus::Modified)]
16438 );
16439 }
16440
16441 #[test]
16442 fn diff_helper_and_format_sweep() {
16443 let cfg = ConfigSet::new();
16444 assert!(resolve_indent_heuristic(&cfg, false, false));
16445 assert!(!resolve_indent_heuristic(&cfg, false, true));
16446
16447 let old = "alpha beta\ngamma\n";
16448 let new = "alpha BETA\ngamma delta\n";
16449 let _ = normalize_ignore_space_change(old);
16450 let _ = normalize_ignore_space_change_line("- alpha beta ");
16451 let _ = count_git_lines(old.as_bytes());
16452 let _ = diffcore_count_changes(old.as_bytes(), new.as_bytes());
16453 assert!(parse_diff_rename_score_token("50%").is_some());
16454 let _ = rewrite_merge_score(b"a", b"b");
16455 let _ = rewrite_dissimilarity_index_percent(b"abc", b"abd");
16456 assert_eq!(format_mode(0o100644), "100644");
16457
16458 let old_lines = ["a", "b", "c"];
16459 let new_lines = ["a", "b", "d"];
16460 let compact = diff_slice_ops_compacted(&old_lines, &new_lines, Algorithm::Myers, false);
16461 assert!(!compact.is_empty());
16462 }
16463
16464 #[test]
16465 fn tree_diff_rename_copy_postprocess_matrix() -> Result<()> {
16466 let (_temp, odb, old_tree, new_tree) = pair_of_trees_with_rename();
16467 let raw = diff_trees(&odb, Some(&old_tree), Some(&new_tree), "")?;
16468 assert!(!raw.is_empty());
16469 let with_renames = diff_trees_with_renames(&odb, Some(&old_tree), Some(&new_tree), "", 50)?;
16470 assert!(!with_renames.is_empty());
16471 let config = ConfigSet::new();
16472 let policy = ThreadPolicy::default();
16473 let detected = detect_renames_with_policy(
16474 &odb,
16475 None,
16476 raw.clone(),
16477 50,
16478 &config,
16479 &policy,
16480 None,
16481 DetectRenamesOptions::default(),
16482 )?
16483 .entries;
16484 let source_flat = flatten_tree_entries(&odb, &old_tree)?;
16485 let _ = detect_copies_with_policy(
16486 &odb,
16487 None,
16488 detected.clone(),
16489 50,
16490 true,
16491 &source_flat,
16492 &config,
16493 &policy,
16494 )?;
16495 let opts = DiffPostprocessOptions {
16496 break_score: Some(50),
16497 rename_threshold: 50,
16498 copy_threshold: Some(50),
16499 find_copies_harder: true,
16500 materialize_break_worktree_new_oid: false,
16501 };
16502 let processed = postprocess_diff_entries(
16503 &odb,
16504 raw,
16505 Some(&old_tree),
16506 None,
16507 None,
16508 &config,
16509 &policy,
16510 &opts,
16511 None,
16512 )?;
16513 assert!(!processed.is_empty());
16514 let _ = status_apply_rename_copy_detection(
16515 &odb,
16516 with_renames.clone(),
16517 50,
16518 true,
16519 Some(&old_tree),
16520 &config,
16521 &policy,
16522 None,
16523 )?;
16524 let flat = flatten_tree_entries(&odb, &new_tree)?;
16525 assert!(!flat.is_empty());
16526 let _ = diff_trees_show_tree_entries(&odb, Some(&old_tree), Some(&new_tree), "")?;
16527 Ok(())
16528 }
16529
16530 #[test]
16531 fn unified_diff_flag_matrix() {
16532 let old = "fn main() {\n println!(\"hi\");\n}\n";
16533 let new = "fn main() {\n println!(\"bye\");\n}\n";
16534 for algo in [Algorithm::Myers, Algorithm::Patience, Algorithm::Histogram] {
16535 for (ws, ws_all, fc, ig) in [(true, false, false, false), (false, true, true, true)] {
16536 let patch = unified_diff_with_prefix_and_funcname_and_algorithm(
16537 old,
16538 new,
16539 "a/main.rs",
16540 "b/main.rs",
16541 3,
16542 0,
16543 "a/",
16544 "b/",
16545 None,
16546 algo,
16547 ws,
16548 ws_all,
16549 fc,
16550 ig,
16551 );
16552 assert!(patch.contains("@@"), "algo {algo:?}");
16553 }
16554 }
16555 let hist = unified_diff_histogram_ignore_with_prefix_and_funcname(
16556 old,
16557 new,
16558 "a/x",
16559 "b/x",
16560 3,
16561 0,
16562 "",
16563 "",
16564 None,
16565 false,
16566 false,
16567 false,
16568 |_old, _new| false,
16569 );
16570 assert!(!hist.is_empty());
16571 let raw = format_raw_abbrev(
16572 &DiffEntry {
16573 status: DiffStatus::Modified,
16574 old_path: Some("x".into()),
16575 new_path: Some("x".into()),
16576 old_mode: FileMode::REGULAR,
16577 new_mode: FileMode::REGULAR,
16578 old_oid: empty_blob_oid(),
16579 new_oid: empty_blob_oid(),
16580 score: Some(80),
16581 },
16582 7,
16583 );
16584 assert!(raw.contains('M') || raw.contains('x'));
16585 }
16586
16587 #[test]
16588 fn index_and_commit_diff_paths() -> Result<()> {
16589 let temp = TempDir::new().expect("tempdir");
16590 let odb = Odb::new(temp.path());
16591 let b1 = odb.write(ObjectKind::Blob, b"v1\n").expect("b1");
16592 let b2 = odb.write(ObjectKind::Blob, b"v2\n").expect("b2");
16593 let b3 = odb.write(ObjectKind::Blob, b"v3\n").expect("b3");
16594 let mut index = Index::new();
16595 index.add_or_replace(index_entry("file.txt", MODE_REGULAR, b2));
16596 let head = write_tree_from_index(&odb, &index, "").expect("tree");
16597 index.entries_mut()[0].oid = b1;
16598 let idx_diff = diff_index_to_tree(&odb, &index, Some(&head), false)?;
16599 assert!(!idx_diff.is_empty());
16600 let commit_body = format!(
16601 "tree {head}\nauthor T <t@e.com> 1 +0000\ncommitter T <t@e.com> 1 +0000\n\nm\n"
16602 );
16603 let c1 = odb
16604 .write(ObjectKind::Commit, commit_body.as_bytes())
16605 .expect("c1");
16606 let mut index2 = Index::new();
16607 index2.add_or_replace(index_entry("file.txt", MODE_REGULAR, b3));
16608 let head2 = write_tree_from_index(&odb, &index2, "").expect("head2");
16609 let c2_body = format!(
16610 "tree {head2}\nparent {c1}\nauthor T <t@e.com> 2 +0000\ncommitter T <t@e.com> 2 +0000\n\nm2\n"
16611 );
16612 let c2 = odb
16613 .write(ObjectKind::Commit, c2_body.as_bytes())
16614 .expect("c2");
16615 let commit_diff = diff_commits_with_renames(&odb, &c1, &c2, 50)?;
16616 assert!(!commit_diff.is_empty());
16617 let _ = tree_oid_of_commit(&odb, &c2)?;
16618 let mtime = index_mtime_for_diff(&index, Some((1, 0)));
16619 let _ = entry_is_racy(&index.entries()[0], mtime);
16620 let _ = entry_is_racy_with_policy(&index.entries()[0], mtime, StatMatchPolicy::default());
16621 Ok(())
16622 }
16623
16624 #[test]
16625 fn tree_oid_of_commit_reports_invalid_ref_for_non_commits() {
16626 let temp = TempDir::new().expect("tempdir");
16627 let odb = Odb::new(temp.path());
16628 let tree = odb.write(ObjectKind::Tree, &[]).expect("tree");
16629 let body = format!(
16630 "tree {tree}\nauthor T <t@e.com> 1 +0000\ncommitter T <t@e.com> 1 +0000\n\nm\n"
16631 );
16632 let commit = odb
16633 .write(ObjectKind::Commit, body.as_bytes())
16634 .expect("commit");
16635 let blob = odb.write(ObjectKind::Blob, b"x\n").expect("blob");
16636
16637 assert_eq!(tree_oid_of_commit(&odb, &commit).expect("tree"), tree);
16638 assert!(matches!(
16639 tree_oid_of_commit(&odb, &blob),
16640 Err(Error::InvalidRef(_))
16641 ));
16642 assert!(matches!(
16643 tree_oid_of_commit(&odb, &tree),
16644 Err(Error::InvalidRef(_))
16645 ));
16646 }
16647
16648 #[test]
16649 fn worktree_index_diff_and_rename_score() -> Result<()> {
16650 let temp = TempDir::new().expect("tempdir");
16651 let wt = temp.path();
16652 let git_dir = wt.join(".git");
16653 std::fs::create_dir_all(git_dir.join("objects")).expect("objects");
16654 let objects_dir = git_dir.join("objects");
16655 let odb = Odb::new(&objects_dir);
16656 let content = b"hello world\n";
16657 std::fs::write(wt.join("tracked.txt"), content).expect("write");
16658 let oid = odb.write(ObjectKind::Blob, content).expect("blob");
16659 let mut index = Index::new();
16660 index.add_or_replace(index_entry("tracked.txt", MODE_REGULAR, oid));
16661 let head = write_tree_from_index(&odb, &index, "").expect("tree");
16662 std::fs::write(wt.join("tracked.txt"), b"hello WORLD\n").expect("mutate");
16663 let mut index_mut = index.clone();
16664 let diff = diff_index_to_worktree(&odb, &mut index_mut, wt, false, false)?;
16665 assert!(!diff.is_empty());
16666 let _ = diff_tree_to_worktree(&odb, Some(&head), wt, &index)?;
16667 assert_eq!(rename_similarity_score(b"a", b"a\n"), 0);
16668 assert_eq!(rename_similarity_score(b"a\n", b"a"), 0);
16669 let score = rename_similarity_score(b"line one\nline two\n", b"line one\nline TWO\n");
16670 assert!(score >= 50);
16671 let _ = detect_renames(&odb, Some(wt), diff, 50);
16672 Ok(())
16673 }
16674
16675 #[test]
16676 fn extended_diff_helpers_sweep() -> Result<()> {
16677 use crate::crlf::{ConversionConfig, FileAttrs};
16678 let temp = TempDir::new().expect("tempdir");
16679 let wt = temp.path();
16680 let git_dir = wt.join(".git");
16681 std::fs::create_dir_all(git_dir.join("objects")).expect("objects");
16682 let objects_dir = git_dir.join("objects");
16683 let odb = Odb::new(&objects_dir);
16684 let b_old = odb.write(ObjectKind::Blob, b"alpha\nbeta\n").expect("old");
16685 let b_new = odb
16686 .write(ObjectKind::Blob, b"alpha\nBETA\ngamma\n")
16687 .expect("new");
16688 let old_tree = write_tree(&odb, &[("a.txt", b_old), ("b.txt", b_old)]);
16689 let new_tree = write_tree(
16690 &odb,
16691 &[("a.txt", b_new), ("b.txt", b_new), ("c.txt", b_new)],
16692 );
16693 let entries = diff_trees(&odb, Some(&old_tree), Some(&new_tree), "")?;
16694 assert!(entries.len() >= 2);
16695
16696 let cfg = ConfigSet::new();
16697 assert!(indent_heuristic_from_config(&cfg));
16698 let _ = unified_diff_histogram_hunks_only("a\nb\n", "a\nB\n", 3, 0, false);
16699 let _ = unified_diff_histogram_with_prefix_and_funcname_indent(
16700 "x\n", "y\n", "a/f", "b/f", 3, 0, "", "", None, false, false,
16701 );
16702 let _ = count_changes_with_algorithm("a\n", "b\n", Algorithm::Myers, false);
16703 let _ = should_break_rewrite_pair(b"short", b"totally different body", 50);
16704 let _ = head_path_states(&odb, Some(&new_tree))?;
16705 std::fs::write(wt.join("tracked.txt"), b"hello\n").expect("wt");
16706 let abs = wt.join("tracked.txt");
16707 let meta = std::fs::metadata(&abs).expect("meta");
16708 let mut index = Index::new();
16709 index.add_or_replace(index_entry("tracked.txt", MODE_REGULAR, b_old));
16710 let _ = stat_matches_with_policy(&index.entries()[0], &meta, StatMatchPolicy::default());
16711 let _ = worktree_differs_from_index_entry(
16712 &odb,
16713 &git_dir,
16714 wt,
16715 &index.entries()[0],
16716 false,
16717 None,
16718 None,
16719 )?;
16720 let conv = ConversionConfig::from_config(&cfg);
16721 let attrs = FileAttrs::default();
16722 let params = WorktreeAddRefreshParams {
16723 odb: &odb,
16724 ie: &index.entries()[0],
16725 meta: &meta,
16726 abs_path: &abs,
16727 rel_path: "tracked.txt",
16728 conv: &conv,
16729 file_attrs: &attrs,
16730 index_mtime: None,
16731 staged_mode: MODE_REGULAR,
16732 filter_process: None,
16733 };
16734 let _ = classify_worktree_entry_for_add(¶ms)?;
16735 let _ = hash_worktree_file(
16736 &odb,
16737 &abs,
16738 &meta,
16739 &conv,
16740 &attrs,
16741 "tracked.txt",
16742 Some(&index.entries()[0]),
16743 None,
16744 )?;
16745 let _ = materialize_worktree_blob(
16746 &odb,
16747 &abs,
16748 &meta,
16749 &conv,
16750 &attrs,
16751 "tracked.txt",
16752 Some(&index.entries()[0]),
16753 None,
16754 )?;
16755 let _ = read_worktree_bytes_for_mode(&abs, MODE_REGULAR);
16756 let _ = mode_from_metadata(&meta);
16757 let _ = smudge_racily_clean_entries(&odb, &git_dir, &mut index, wt, None, Some(&cfg), None);
16758 let _ = refresh_index_stat_content_verified(
16759 &odb,
16760 &git_dir,
16761 &mut index,
16762 wt,
16763 None,
16764 Some(&cfg),
16765 None,
16766 )?;
16767 let _ = refresh_index_stat_content_verified_with_rules(
16768 &odb,
16769 &git_dir,
16770 &mut index,
16771 wt,
16772 None,
16773 Some(&cfg),
16774 None,
16775 None,
16776 )?;
16777 let _ = path_checkout_skip_blob_write_when_up_to_date_with_config(
16778 &odb,
16779 &git_dir,
16780 wt,
16781 &index,
16782 &index.entries()[0],
16783 &b_old,
16784 MODE_REGULAR,
16785 "tracked.txt",
16786 None,
16787 None,
16788 )?;
16789 let patch = anchored_unified_diff(
16790 "a\nb\n",
16791 "a\nB\n",
16792 "f",
16793 "f",
16794 3,
16795 &[String::from("b")],
16796 Algorithm::Myers,
16797 false,
16798 true,
16799 false,
16800 );
16801 assert!(patch.contains("@@"));
16802 let _ = annotate_unified_diff_no_newline_at_eof("@@\n+a\n\\ No newline\n", "a", "a\n");
16803 Ok(())
16804 }
16805
16806 #[test]
16807 fn unmerged_only_tree_worktree_diff_and_func_heuristics() -> Result<()> {
16808 use similar::DiffOp;
16809 assert!(inserted_block_starts_with_c_like_function_definition(
16810 "static int foo(int bar) {"
16811 ));
16812 assert!(!inserted_block_starts_with_c_like_function_definition(
16813 "printf(hi);"
16814 ));
16815 let ops = vec![
16816 DiffOp::Equal {
16817 old_index: 0,
16818 new_index: 0,
16819 len: 4,
16820 },
16821 DiffOp::Insert {
16822 old_index: 4,
16823 new_index: 4,
16824 new_len: 2,
16825 },
16826 ];
16827 let new_lines = ["a\n", "b\n", "c\n", "d\n", "void added() {\n", "}\n"];
16828 assert!(append_with_whole_function_added(
16829 &ops, 4, 6, &new_lines, None
16830 ));
16831 let _ = hunk_old_change_end_exclusive(&ops);
16832
16833 let temp = TempDir::new().expect("tempdir");
16834 let wt = temp.path();
16835 let git_dir = wt.join(".git");
16836 let objects_dir = git_dir.join("objects");
16837 std::fs::create_dir_all(&objects_dir).expect("objects");
16838 let odb = Odb::new(&objects_dir);
16839 let tree_blob = odb.write(ObjectKind::Blob, b"base\n").expect("blob");
16840 std::fs::write(wt.join("conflict.txt"), b"worktree\n").expect("wt");
16841 let tree = write_tree(&odb, &[("conflict.txt", tree_blob)]);
16842 let mut index = Index::new();
16843 let mut stage1 = index_entry("conflict.txt", MODE_REGULAR, tree_blob);
16844 stage1.flags = (stage1.flags & 0x0FFF) | (1 << 12);
16845 index.entries_mut().push(stage1);
16846 let cfg = ConfigSet::new();
16847 let diff = diff_tree_to_worktree_with_git_dir(
16848 &odb,
16849 Some(&tree),
16850 wt,
16851 &git_dir,
16852 &index,
16853 Some(&cfg),
16854 )?;
16855 assert!(!diff.is_empty());
16856 Ok(())
16857 }
16858
16859 #[test]
16860 fn high_volume_unified_diff_and_postprocess() -> Result<()> {
16861 let (_temp, odb, old_tree, new_tree) = pair_of_trees_with_rename();
16862 let config = ConfigSet::new();
16863 let policy = ThreadPolicy::default();
16864 for i in 0..600u32 {
16865 let old = format!("seed {i}\nalpha\n");
16866 let new = format!("seed {i}\nalpha\nvoid fn_{i}(int arg) {{\n return arg;\n}}\n");
16867 let _ = unified_diff_with_prefix_and_funcname_and_algorithm(
16868 &old,
16869 &new,
16870 "a/f.c",
16871 "b/f.c",
16872 3,
16873 0,
16874 "a/",
16875 "b/",
16876 None,
16877 Algorithm::Myers,
16878 i % 2 == 0,
16879 i % 3 == 0,
16880 i % 5 == 0,
16881 i % 7 == 0,
16882 );
16883 let threshold = 30 + (i % 40);
16884 let raw = diff_trees(&odb, Some(&old_tree), Some(&new_tree), "")?;
16885 let opts = DiffPostprocessOptions {
16886 rename_threshold: threshold,
16887 copy_threshold: Some(threshold),
16888 find_copies_harder: i % 2 == 0,
16889 break_score: Some(threshold as u64),
16890 materialize_break_worktree_new_oid: i % 4 == 0,
16891 };
16892 let _ = postprocess_diff_entries(
16893 &odb,
16894 raw,
16895 Some(&old_tree),
16896 None,
16897 None,
16898 &config,
16899 &policy,
16900 &opts,
16901 None,
16902 )?;
16903 }
16904 Ok(())
16905 }
16906}
16907
16908#[cfg(test)]
16909mod refresh_checks_tests {
16910 use super::{DistinctFileAttrs, RefreshChecks, RefreshContent};
16911 use crate::crlf::{EolAttr, FileAttrs, TextAttr};
16912 use crate::objects::ObjectId;
16913
16914 fn attrs_with_eol(eol: EolAttr) -> FileAttrs {
16915 FileAttrs {
16916 eol,
16917 ..FileAttrs::default()
16918 }
16919 }
16920
16921 #[test]
16922 fn equal_attributes_share_one_slot() {
16923 let mut attrs = DistinctFileAttrs::default();
16924 assert_eq!(attrs.intern(FileAttrs::default()), 0);
16925 assert_eq!(attrs.intern(attrs_with_eol(EolAttr::Crlf)), 1);
16926 assert_eq!(attrs.intern(FileAttrs::default()), 0);
16927 assert_eq!(attrs.intern(attrs_with_eol(EolAttr::Crlf)), 1);
16928 let binary = FileAttrs {
16929 text: TextAttr::Unset,
16930 ..FileAttrs::default()
16931 };
16932 assert_eq!(attrs.intern(binary.clone()), 2);
16933 assert_eq!(attrs.intern(binary), 2);
16934 assert_eq!(attrs.0.len(), 3);
16935 assert_eq!(attrs.0[1], attrs_with_eol(EolAttr::Crlf));
16936 }
16937
16938 #[test]
16939 fn sets_past_the_search_limit_are_stored_again() {
16940 let mut attrs = DistinctFileAttrs::default();
16941 let first = attrs.intern(FileAttrs::default());
16942 for n in 0..DistinctFileAttrs::SEARCH_LIMIT {
16943 let distinct = FileAttrs {
16944 whitespace: Some(format!("tabwidth={}", n + 1)),
16945 ..FileAttrs::default()
16946 };
16947 assert_eq!(attrs.intern(distinct), n + 1);
16948 }
16949 let again = attrs.intern(FileAttrs::default());
16950 assert_ne!(again, first, "only the most recent sets are searched");
16951 assert_eq!(attrs.0[again], attrs.0[first]);
16952 assert_eq!(attrs.intern(FileAttrs::default()), again);
16953 }
16954
16955 #[test]
16956 fn empty_checks_answer_none_for_every_entry() {
16957 let checks = RefreshChecks::default();
16958 assert!(checks.0.is_empty());
16959 assert!(checks.get(0).is_none());
16960 assert!(checks.get(9_999).is_none());
16961 }
16962
16963 #[test]
16964 fn filled_checks_answer_by_entry_position() {
16965 let oid = ObjectId::from_bytes(&[7u8; 20]).unwrap();
16966 let other = ObjectId::from_bytes(&[8u8; 20]).unwrap();
16967 let checks = RefreshChecks(vec![None, Some(RefreshContent::Hashed(oid)), None]);
16968 assert!(checks.get(0).is_none());
16969 assert!(RefreshContent::matches(checks.get(1), &oid));
16970 assert!(RefreshContent::differs(checks.get(1), &other));
16971 assert!(
16972 checks.get(3).is_none(),
16973 "positions past the end have no check"
16974 );
16975 }
16976}