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};
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::userdiff::FuncnameMatcher;
46
47use super::rename_lines::RenameLineTable;
48use super::unified_emit::{
49 emit_unified_body_from_edits, line_ending_only_edits, EditSpan, LineEnding, LineSide,
50 UnifiedEmitOptions,
51};
52
53fn line_compare_keys<'a>(
59 text: &'a str,
60 side: &LineSide<'a>,
61 ws: super::WhitespaceMode,
62 normalized: &'a mut Vec<Cow<'a, str>>,
63) -> Vec<&'a str> {
64 if ws.normalizes_line_compare() {
65 normalized.extend(
67 side.lines
68 .iter()
69 .map(|line| ws.normalize_line_for_compare(line.body)),
70 );
71 return normalized.iter().map(Cow::as_ref).collect();
72 }
73 let base = text.as_ptr() as usize;
76 side.lines
77 .iter()
78 .map(|line| {
79 let start = line.body.as_ptr() as usize - base;
80 let end = start + line.body.len() + usize::from(line.ending != LineEnding::Missing);
81 &text[start..end]
82 })
83 .collect()
84}
85
86#[path = "../structured_diff.rs"]
87pub mod structured;
88
89pub(crate) fn imara_unified_hunk_slices(body: &str) -> Vec<&str> {
90 let mut starts: Vec<usize> = Vec::new();
91 if body.starts_with("@@") {
92 starts.push(0);
93 }
94 for (idx, _) in body.match_indices("\n@@ ") {
95 starts.push(idx + 1);
96 }
97 starts.push(body.len());
98 starts.windows(2).map(|w| &body[w[0]..w[1]]).collect()
99}
100
101pub(crate) fn histogram_compacted_line_ops(
103 old_content: &str,
104 new_content: &str,
105 indent_heuristic: bool,
106 ws: super::WhitespaceMode,
107) -> Vec<similar::DiffOp> {
108 let before = LineSide::from_text(old_content);
109 let after = LineSide::from_text(new_content);
110 let old_lines: Vec<&str> = before.lines.iter().map(|l| l.body).collect();
111 let new_lines: Vec<&str> = after.lines.iter().map(|l| l.body).collect();
112 let old_len = old_lines.len();
113 let new_len = new_lines.len();
114 let (mut old_normalized, mut new_normalized) = (Vec::new(), Vec::new());
115 let old_compare = line_compare_keys(old_content, &before, ws, &mut old_normalized);
116 let new_compare = line_compare_keys(new_content, &after, ws, &mut new_normalized);
117
118 let (changed1, changed2) = git_xdiff::histogram_changed_flags(&old_compare, &new_compare);
119
120 if !changed1.iter().any(|c| *c) && !changed2.iter().any(|c| *c) {
121 return Vec::new();
122 }
123 let raw = changed_flags_to_ops(&changed1, &changed2, old_len, new_len);
124 diff_indent_heuristic::apply_change_compact_to_ops_with_ws_histogram(
125 &raw,
126 diff_indent_heuristic::CompactSide {
127 lines: &old_lines,
128 keys: &old_compare,
129 compare_ids: None,
130 },
131 diff_indent_heuristic::CompactSide {
132 lines: &new_lines,
133 keys: &new_compare,
134 compare_ids: None,
135 },
136 indent_heuristic,
137 ws,
138 true,
139 )
140}
141
142pub(crate) fn histogram_unified_body_raw(
143 old_content: &str,
144 new_content: &str,
145 context_lines: usize,
146 inter_hunk_context: usize,
147 indent_heuristic: bool,
148 ws: super::WhitespaceMode,
149) -> String {
150 let before_side = LineSide::from_text(old_content);
151 let after_side = LineSide::from_text(new_content);
152 let ops = histogram_compacted_line_ops(old_content, new_content, indent_heuristic, ws);
153 let mut edits = diff_ops_to_edit_spans(&ops);
154 if edits.is_empty() {
155 if old_content != new_content && !ws.normalizes_line_compare() {
156 edits = line_ending_only_edits(&before_side, &after_side);
157 }
158 if edits.is_empty() {
159 return String::new();
160 }
161 }
162 emit_unified_body_from_edits(
163 &before_side,
164 &after_side,
165 &edits,
166 UnifiedEmitOptions {
167 context_lines,
168 inter_hunk_context,
169 function_context: false,
170 },
171 None,
172 )
173}
174
175#[allow(clippy::too_many_arguments)]
191fn diff_ops_to_ignorable_edit_spans<F>(
192 ops: &[similar::DiffOp],
193 old_lines: &[&str],
194 new_lines: &[&str],
195 is_ignorable_change: F,
196) -> Vec<EditSpan>
197where
198 F: Fn(&[&str], &[&str]) -> bool,
199{
200 use similar::DiffOp;
201 let mut out = Vec::new();
202 for op in ops {
203 match *op {
204 DiffOp::Equal { .. } => {}
205 DiffOp::Delete {
206 old_index,
207 old_len,
208 new_index,
209 } => {
210 let removed = &old_lines[old_index..old_index + old_len];
211 out.push(EditSpan {
212 before_start: old_index as i64,
213 before_len: old_len as i64,
214 after_start: new_index as i64,
215 after_len: 0,
216 skip_in_output: is_ignorable_change(removed, &[]),
217 });
218 }
219 DiffOp::Insert {
220 old_index,
221 new_index,
222 new_len,
223 } => {
224 let added = &new_lines[new_index..new_index + new_len];
225 out.push(EditSpan {
226 before_start: old_index as i64,
227 before_len: 0,
228 after_start: new_index as i64,
229 after_len: new_len as i64,
230 skip_in_output: is_ignorable_change(&[], added),
231 });
232 }
233 DiffOp::Replace {
234 old_index,
235 old_len,
236 new_index,
237 new_len,
238 } => {
239 let removed = &old_lines[old_index..old_index + old_len];
240 let added = &new_lines[new_index..new_index + new_len];
241 out.push(EditSpan {
242 before_start: old_index as i64,
243 before_len: old_len as i64,
244 after_start: new_index as i64,
245 after_len: new_len as i64,
246 skip_in_output: is_ignorable_change(removed, added),
247 });
248 }
249 }
250 }
251 out
252}
253
254#[allow(clippy::too_many_arguments)]
255pub(crate) fn histogram_unified_body_ignore_fc<F>(
256 old_content: &str,
257 new_content: &str,
258 context_lines: usize,
259 inter_hunk_context: usize,
260 function_context: bool,
261 funcname_matcher: Option<&FuncnameMatcher>,
262 indent_heuristic: bool,
263 ws: super::WhitespaceMode,
264 is_ignorable_change: F,
265) -> String
266where
267 F: Fn(&[&str], &[&str]) -> bool,
268{
269 let before = LineSide::from_text(old_content);
270 let after = LineSide::from_text(new_content);
271 let old_lines: Vec<&str> = before.lines.iter().map(|l| l.body).collect();
272 let new_lines: Vec<&str> = after.lines.iter().map(|l| l.body).collect();
273 let ops = histogram_compacted_line_ops(old_content, new_content, indent_heuristic, ws);
274 if ops.is_empty() {
275 return String::new();
276 }
277 let edits = diff_ops_to_ignorable_edit_spans(&ops, &old_lines, &new_lines, is_ignorable_change);
278 emit_unified_body_from_edits(
279 &before,
280 &after,
281 &edits,
282 UnifiedEmitOptions {
283 context_lines,
284 inter_hunk_context,
285 function_context,
286 },
287 funcname_matcher,
288 )
289}
290
291fn line_bodies<'a>(side: &LineSide<'a>) -> Vec<&'a str> {
293 side.lines.iter().map(|l| l.body).collect()
294}
295
296#[must_use]
298fn count_diff_ops(ops: &[similar::DiffOp]) -> (usize, usize) {
299 use similar::DiffOp;
300 let mut insertions = 0usize;
301 let mut deletions = 0usize;
302 for op in ops {
303 match *op {
304 DiffOp::Equal { .. } => {}
305 DiffOp::Delete { old_len, .. } => deletions += old_len,
306 DiffOp::Insert { new_len, .. } => insertions += new_len,
307 DiffOp::Replace {
308 old_len, new_len, ..
309 } => {
310 deletions += old_len;
311 insertions += new_len;
312 }
313 }
314 }
315 (insertions, deletions)
316}
317
318pub(crate) fn compacted_line_ops_for_options(
320 old_content: &str,
321 new_content: &str,
322 options: &crate::diff::options::DiffOptions,
323) -> Vec<similar::DiffOp> {
324 use crate::diff::options::DiffAlgorithm;
325
326 let ws = options.whitespace;
327 match options.algorithm {
328 DiffAlgorithm::Histogram => {
329 histogram_compacted_line_ops(old_content, new_content, options.indent_heuristic, ws)
330 }
331 DiffAlgorithm::Patience => {
332 let before = LineSide::from_text(old_content);
333 let after = LineSide::from_text(new_content);
334 if before.is_empty() && after.is_empty() {
335 return Vec::new();
336 }
337 let old_refs: Vec<&str> = before.lines.iter().map(|l| l.body).collect();
338 let new_refs: Vec<&str> = after.lines.iter().map(|l| l.body).collect();
339 let (mut old_normalized, mut new_normalized) = (Vec::new(), Vec::new());
340 let old_compare = line_compare_keys(old_content, &before, ws, &mut old_normalized);
341 let new_compare = line_compare_keys(new_content, &after, ws, &mut new_normalized);
342 let (changed1, changed2) =
343 git_xdiff::patience_changed_flags(&old_compare, &new_compare);
344 let raw = changed_flags_to_ops(&changed1, &changed2, old_refs.len(), new_refs.len());
345 diff_indent_heuristic::apply_change_compact_to_ops_with_ws_histogram(
346 &raw,
347 diff_indent_heuristic::CompactSide {
348 lines: &old_refs,
349 keys: &old_compare,
350 compare_ids: None,
351 },
352 diff_indent_heuristic::CompactSide {
353 lines: &new_refs,
354 keys: &new_compare,
355 compare_ids: None,
356 },
357 options.indent_heuristic,
358 ws,
359 false,
360 )
361 }
362 DiffAlgorithm::Myers | DiffAlgorithm::Minimal => {
363 let before = LineSide::from_text(old_content);
364 let after = LineSide::from_text(new_content);
365 if before.is_empty() && after.is_empty() {
366 return Vec::new();
367 }
368 let (mut old_normalized, mut new_normalized) = (Vec::new(), Vec::new());
369 let old_compare = line_compare_keys(old_content, &before, ws, &mut old_normalized);
370 let new_compare = line_compare_keys(new_content, &after, ws, &mut new_normalized);
371 git_xdiff_compacted_line_ops(
372 &old_compare,
373 &new_compare,
374 &before,
375 &after,
376 options.indent_heuristic,
377 options.algorithm == DiffAlgorithm::Minimal,
378 )
379 }
380 }
381}
382
383#[must_use]
385pub(crate) fn count_line_changes_with_options(
386 old_content: &str,
387 new_content: &str,
388 options: &crate::diff::options::DiffOptions,
389) -> (usize, usize) {
390 let ws = options.whitespace;
391 let ops = compacted_line_ops_for_options(old_content, new_content, options);
392 let (mut insertions, mut deletions) = count_diff_ops(&ops);
393 if insertions == 0
394 && deletions == 0
395 && old_content != new_content
396 && !ws.normalizes_line_compare()
397 {
398 let before = LineSide::from_text(old_content);
399 let after = LineSide::from_text(new_content);
400 for edit in line_ending_only_edits(&before, &after) {
401 insertions += edit.after_len as usize;
402 deletions += edit.before_len as usize;
403 }
404 }
405 (insertions, deletions)
406}
407
408type XdiffTrimmedIds = ((Vec<bool>, Vec<bool>), Vec<u32>, Vec<u32>);
410
411fn xdiff_changed_flags_trimmed_ids(
412 old_compare: &[&str],
413 new_compare: &[&str],
414 need_min: bool,
415) -> XdiffTrimmedIds {
416 use crate::line_interner::LineInterner;
417
418 let nrec1 = old_compare.len();
419 let nrec2 = new_compare.len();
420 if nrec1 == 0 && nrec2 == 0 {
421 return ((Vec::new(), Vec::new()), Vec::new(), Vec::new());
422 }
423 let mut interner = LineInterner::with_capacity(nrec1 + nrec2);
424 let ids1_full = interner.intern_all(old_compare);
425 let ids2_full = interner.intern_all(new_compare);
426
427 let lim = nrec1.min(nrec2);
428 let mut head = 0usize;
429 while head < lim && ids1_full[head] == ids2_full[head] {
430 head += 1;
431 }
432 let mut tail = 0usize;
433 let rem = lim - head;
434 while tail < rem && ids1_full[nrec1 - 1 - tail] == ids2_full[nrec2 - 1 - tail] {
435 tail += 1;
436 }
437 let shared_records = head + tail;
438 let (full1, full2) = if shared_records == 0 {
439 git_xdiff::changed_flags(&ids1_full, &ids2_full, need_min)
440 } else {
441 let cut_ids1 = &ids1_full[head..nrec1 - tail];
442 let cut_ids2 = &ids2_full[head..nrec2 - tail];
443 let nclasses = ids1_full
444 .iter()
445 .chain(&ids2_full)
446 .max()
447 .map_or(0, |&max| max as usize + 1);
448 let mut shared_counts = vec![0u32; nclasses];
449 for &id in &ids1_full[..head] {
450 shared_counts[id as usize] += 1;
451 }
452 for &id in &ids1_full[nrec1 - tail..] {
453 shared_counts[id as usize] += 1;
454 }
455 let (mid1, mid2) = git_xdiff::changed_flags_with_shared(
456 cut_ids1,
457 cut_ids2,
458 &shared_counts,
459 shared_records,
460 need_min,
461 );
462 let mut full1 = vec![false; nrec1];
463 let mut full2 = vec![false; nrec2];
464 if !mid1.is_empty() {
465 full1[head..nrec1 - tail].copy_from_slice(&mid1);
466 }
467 if !mid2.is_empty() {
468 full2[head..nrec2 - tail].copy_from_slice(&mid2);
469 }
470 (full1, full2)
471 };
472 ((full1, full2), ids1_full, ids2_full)
473}
474
475pub(crate) fn git_xdiff_compacted_line_ops(
476 old_compare: &[&str],
477 new_compare: &[&str],
478 before: &LineSide<'_>,
479 after: &LineSide<'_>,
480 indent_heuristic: bool,
481 need_min: bool,
482) -> Vec<similar::DiffOp> {
483 let ((changed1, changed2), old_key_ids, new_key_ids) =
484 xdiff_changed_flags_trimmed_ids(old_compare, new_compare, need_min);
485 let old_lines = line_bodies(before);
486 let new_lines = line_bodies(after);
487 diff_indent_heuristic::apply_change_compact_to_masks(
488 changed1,
489 changed2,
490 diff_indent_heuristic::CompactSide {
491 lines: &old_lines,
492 keys: old_compare,
493 compare_ids: Some(&old_key_ids),
494 },
495 diff_indent_heuristic::CompactSide {
496 lines: &new_lines,
497 keys: new_compare,
498 compare_ids: Some(&new_key_ids),
499 },
500 indent_heuristic,
501 )
502}
503
504fn diff_ops_to_edit_spans(ops: &[similar::DiffOp]) -> Vec<EditSpan> {
505 use similar::DiffOp;
506 let mut out = Vec::new();
507 for op in ops {
508 match *op {
509 DiffOp::Equal { .. } => {}
510 DiffOp::Delete {
511 old_index,
512 old_len,
513 new_index,
514 } => out.push(EditSpan {
515 before_start: old_index as i64,
516 before_len: old_len as i64,
517 after_start: new_index as i64,
518 after_len: 0,
519 skip_in_output: false,
520 }),
521 DiffOp::Insert {
522 old_index,
523 new_index,
524 new_len,
525 } => out.push(EditSpan {
526 before_start: old_index as i64,
527 before_len: 0,
528 after_start: new_index as i64,
529 after_len: new_len as i64,
530 skip_in_output: false,
531 }),
532 DiffOp::Replace {
533 old_index,
534 old_len,
535 new_index,
536 new_len,
537 } => out.push(EditSpan {
538 before_start: old_index as i64,
539 before_len: old_len as i64,
540 after_start: new_index as i64,
541 after_len: new_len as i64,
542 skip_in_output: false,
543 }),
544 }
545 }
546 out
547}
548
549fn git_xdiff_unified_body_raw(
551 old_content: &str,
552 new_content: &str,
553 context_lines: usize,
554 inter_hunk_context: usize,
555 indent_heuristic: bool,
556 need_min: bool,
557 ws: super::WhitespaceMode,
558) -> String {
559 git_xdiff_unified_body_raw_with_ignorable(
560 old_content,
561 new_content,
562 context_lines,
563 inter_hunk_context,
564 indent_heuristic,
565 need_min,
566 ws,
567 None::<fn(&[&str], &[&str]) -> bool>,
568 )
569}
570
571#[allow(clippy::too_many_arguments)]
572fn git_xdiff_unified_body_raw_with_ignorable<F>(
573 old_content: &str,
574 new_content: &str,
575 context_lines: usize,
576 inter_hunk_context: usize,
577 indent_heuristic: bool,
578 need_min: bool,
579 ws: super::WhitespaceMode,
580 is_ignorable_change: Option<F>,
581) -> String
582where
583 F: Fn(&[&str], &[&str]) -> bool,
584{
585 let before = LineSide::from_text(old_content);
586 let after = LineSide::from_text(new_content);
587 if before.is_empty() && after.is_empty() {
588 return String::new();
589 }
590 let (mut old_normalized, mut new_normalized) = (Vec::new(), Vec::new());
591 let old_compare = line_compare_keys(old_content, &before, ws, &mut old_normalized);
592 let new_compare = line_compare_keys(new_content, &after, ws, &mut new_normalized);
593 let ops = git_xdiff_compacted_line_ops(
594 &old_compare,
595 &new_compare,
596 &before,
597 &after,
598 indent_heuristic,
599 need_min,
600 );
601 let mut edits = if let Some(is_ignorable) = is_ignorable_change {
602 diff_ops_to_ignorable_edit_spans(
603 &ops,
604 &line_bodies(&before),
605 &line_bodies(&after),
606 is_ignorable,
607 )
608 } else {
609 diff_ops_to_edit_spans(&ops)
610 };
611 if edits.is_empty() {
612 if old_content != new_content && !ws.normalizes_line_compare() {
613 edits = line_ending_only_edits(&before, &after);
614 }
615 if edits.is_empty() {
616 return String::new();
617 }
618 }
619 emit_unified_body_from_edits(
620 &before,
621 &after,
622 &edits,
623 UnifiedEmitOptions {
624 context_lines,
625 inter_hunk_context,
626 function_context: false,
627 },
628 None,
629 )
630}
631
632#[allow(clippy::too_many_arguments)]
633fn git_patience_unified_body_raw_with_ignorable<F>(
634 old_content: &str,
635 new_content: &str,
636 context_lines: usize,
637 inter_hunk_context: usize,
638 indent_heuristic: bool,
639 ws: super::WhitespaceMode,
640 is_ignorable_change: Option<F>,
641) -> String
642where
643 F: Fn(&[&str], &[&str]) -> bool,
644{
645 let before = LineSide::from_text(old_content);
646 let after = LineSide::from_text(new_content);
647 if before.is_empty() && after.is_empty() {
648 return String::new();
649 }
650 let old_refs: Vec<&str> = before.lines.iter().map(|l| l.body).collect();
651 let new_refs: Vec<&str> = after.lines.iter().map(|l| l.body).collect();
652 let (mut old_normalized, mut new_normalized) = (Vec::new(), Vec::new());
653 let old_compare = line_compare_keys(old_content, &before, ws, &mut old_normalized);
654 let new_compare = line_compare_keys(new_content, &after, ws, &mut new_normalized);
655 let (changed1, changed2) = git_xdiff::patience_changed_flags(&old_compare, &new_compare);
656 let raw = changed_flags_to_ops(&changed1, &changed2, old_refs.len(), new_refs.len());
657 let ops = diff_indent_heuristic::apply_change_compact_to_ops_with_ws_histogram(
658 &raw,
659 diff_indent_heuristic::CompactSide {
660 lines: &old_refs,
661 keys: &old_compare,
662 compare_ids: None,
663 },
664 diff_indent_heuristic::CompactSide {
665 lines: &new_refs,
666 keys: &new_compare,
667 compare_ids: None,
668 },
669 indent_heuristic,
670 ws,
671 false,
672 );
673 let mut edits = if let Some(is_ignorable) = is_ignorable_change {
674 diff_ops_to_ignorable_edit_spans(&ops, &old_refs, &new_refs, is_ignorable)
675 } else {
676 diff_ops_to_edit_spans(&ops)
677 };
678 if edits.is_empty() {
679 if old_content != new_content && !ws.normalizes_line_compare() {
680 edits = line_ending_only_edits(&before, &after);
681 }
682 if edits.is_empty() {
683 return String::new();
684 }
685 }
686 emit_unified_body_from_edits(
687 &before,
688 &after,
689 &edits,
690 UnifiedEmitOptions {
691 context_lines,
692 inter_hunk_context,
693 function_context: false,
694 },
695 None,
696 )
697}
698
699fn blank_line_ignorable(ws: super::WhitespaceMode) -> impl Fn(&[&str], &[&str]) -> bool + Copy {
700 move |removed: &[&str], added: &[&str]| {
701 ws.ignore_blank_lines
702 && removed.iter().all(|l| crate::ws::ws_blank_line(l))
703 && added.iter().all(|l| crate::ws::ws_blank_line(l))
704 }
705}
706
707pub(crate) fn unified_hunk_body_for_diff_options(
709 old_content: &str,
710 new_content: &str,
711 options: &crate::diff::options::DiffOptions,
712) -> String {
713 use crate::diff::options::DiffAlgorithm;
714
715 let ws = options.whitespace;
716 if ws.is_active() {
717 let blank_only = blank_line_ignorable(ws);
718 if ws.normalizes_line_compare() {
719 let ignorable = ws.ignore_blank_lines.then_some(blank_only);
720 return match options.algorithm {
721 DiffAlgorithm::Myers => git_xdiff_unified_body_raw_with_ignorable(
722 old_content,
723 new_content,
724 options.context_lines,
725 options.inter_hunk_context,
726 options.indent_heuristic,
727 false,
728 ws,
729 ignorable,
730 ),
731 DiffAlgorithm::Minimal => git_xdiff_unified_body_raw_with_ignorable(
732 old_content,
733 new_content,
734 options.context_lines,
735 options.inter_hunk_context,
736 options.indent_heuristic,
737 true,
738 ws,
739 ignorable,
740 ),
741 DiffAlgorithm::Patience => git_patience_unified_body_raw_with_ignorable(
742 old_content,
743 new_content,
744 options.context_lines,
745 options.inter_hunk_context,
746 options.indent_heuristic,
747 ws,
748 ignorable,
749 ),
750 DiffAlgorithm::Histogram => {
751 if ws.ignore_blank_lines {
752 histogram_unified_body_ignore_fc(
753 old_content,
754 new_content,
755 options.context_lines,
756 options.inter_hunk_context,
757 false,
758 None,
759 options.indent_heuristic,
760 ws,
761 blank_only,
762 )
763 } else {
764 histogram_unified_body_raw(
765 old_content,
766 new_content,
767 options.context_lines,
768 options.inter_hunk_context,
769 options.indent_heuristic,
770 ws,
771 )
772 }
773 }
774 };
775 }
776 if ws.ignore_blank_lines {
777 return match options.algorithm {
778 DiffAlgorithm::Myers => git_xdiff_unified_body_raw_with_ignorable(
779 old_content,
780 new_content,
781 options.context_lines,
782 options.inter_hunk_context,
783 options.indent_heuristic,
784 false,
785 ws,
786 Some(blank_only),
787 ),
788 DiffAlgorithm::Minimal => git_xdiff_unified_body_raw_with_ignorable(
789 old_content,
790 new_content,
791 options.context_lines,
792 options.inter_hunk_context,
793 options.indent_heuristic,
794 true,
795 ws,
796 Some(blank_only),
797 ),
798 DiffAlgorithm::Patience => git_patience_unified_body_raw_with_ignorable(
799 old_content,
800 new_content,
801 options.context_lines,
802 options.inter_hunk_context,
803 options.indent_heuristic,
804 ws,
805 Some(blank_only),
806 ),
807 DiffAlgorithm::Histogram => histogram_unified_body_ignore_fc(
808 old_content,
809 new_content,
810 options.context_lines,
811 options.inter_hunk_context,
812 false,
813 None,
814 options.indent_heuristic,
815 ws,
816 blank_only,
817 ),
818 };
819 }
820 }
821 match options.algorithm {
822 DiffAlgorithm::Myers => git_xdiff_unified_body_raw(
823 old_content,
824 new_content,
825 options.context_lines,
826 options.inter_hunk_context,
827 options.indent_heuristic,
828 false,
829 ws,
830 ),
831 DiffAlgorithm::Minimal => git_xdiff_unified_body_raw(
832 old_content,
833 new_content,
834 options.context_lines,
835 options.inter_hunk_context,
836 options.indent_heuristic,
837 true,
838 ws,
839 ),
840 DiffAlgorithm::Patience => git_patience_unified_body_raw(
841 old_content,
842 new_content,
843 options.context_lines,
844 options.inter_hunk_context,
845 options.indent_heuristic,
846 ),
847 DiffAlgorithm::Histogram => histogram_unified_body_raw(
848 old_content,
849 new_content,
850 options.context_lines,
851 options.inter_hunk_context,
852 options.indent_heuristic,
853 ws,
854 ),
855 }
856}
857
858fn git_patience_unified_body_raw(
860 old_content: &str,
861 new_content: &str,
862 context_lines: usize,
863 inter_hunk_context: usize,
864 indent_heuristic: bool,
865) -> String {
866 let before = LineSide::from_text(old_content);
867 let after = LineSide::from_text(new_content);
868 if before.is_empty() && after.is_empty() {
869 return String::new();
870 }
871 let old_refs: Vec<&str> = before.lines.iter().map(|l| l.body).collect();
872 let new_refs: Vec<&str> = after.lines.iter().map(|l| l.body).collect();
873 let ws = super::WhitespaceMode::default();
874 let (mut old_normalized, mut new_normalized) = (Vec::new(), Vec::new());
875 let old_compare = line_compare_keys(old_content, &before, ws, &mut old_normalized);
876 let new_compare = line_compare_keys(new_content, &after, ws, &mut new_normalized);
877 let (changed1, changed2) = git_xdiff::patience_changed_flags(&old_compare, &new_compare);
878 let raw = changed_flags_to_ops(&changed1, &changed2, old_refs.len(), new_refs.len());
879 let ops = diff_indent_heuristic::apply_change_compact_to_ops_with_ws_histogram(
880 &raw,
881 diff_indent_heuristic::CompactSide {
882 lines: &old_refs,
883 keys: &old_compare,
884 compare_ids: None,
885 },
886 diff_indent_heuristic::CompactSide {
887 lines: &new_refs,
888 keys: &new_compare,
889 compare_ids: None,
890 },
891 indent_heuristic,
892 ws,
893 false,
894 );
895 let mut edits = diff_ops_to_edit_spans(&ops);
896 if edits.is_empty() {
897 if old_content != new_content {
898 edits = line_ending_only_edits(&before, &after);
899 }
900 if edits.is_empty() {
901 return String::new();
902 }
903 }
904 emit_unified_body_from_edits(
905 &before,
906 &after,
907 &edits,
908 UnifiedEmitOptions {
909 context_lines,
910 inter_hunk_context,
911 function_context: false,
912 },
913 None,
914 )
915}
916
917pub(crate) fn unified_hunk_header_counts(
919 header_line: &str,
920) -> Option<(usize, usize, usize, usize)> {
921 let rest = header_line.strip_prefix("@@ ")?;
922 let (old_part, rest2) = rest.split_once(" +")?;
923 let old_tok = old_part.strip_prefix('-')?.split(' ').next()?;
924 let new_tok = rest2.split(" @@").next()?.split(' ').next()?;
925
926 fn parse_side(spec: &str) -> (usize, usize) {
927 if let Some((start, count)) = spec.split_once(',') {
928 (start.parse().unwrap_or(0), count.parse().unwrap_or(0))
929 } else {
930 (spec.parse().unwrap_or(0), 1)
931 }
932 }
933
934 let (os, ol) = parse_side(old_tok);
935 let (ns, nl) = parse_side(new_tok);
936 Some((os, ol, ns, nl))
937}
938
939#[allow(clippy::too_many_arguments)]
941pub(crate) fn write_unified_diff_from_diff_options<W: std::io::Write>(
942 out: &mut W,
943 old_content: &str,
944 new_content: &str,
945 old_path: &str,
946 new_path: &str,
947 src_prefix: &str,
948 dst_prefix: &str,
949 funcname_matcher: Option<&FuncnameMatcher>,
950 options: &crate::diff::options::DiffOptions,
951 quote_path_fully: bool,
952) -> Result<()> {
953 use crate::quote_path::format_diff_path_with_prefix;
954
955 if old_path == "/dev/null" {
956 out.write_all(b"--- /dev/null\n")?;
957 } else if src_prefix.is_empty() {
958 writeln!(out, "--- {old_path}")?;
959 } else {
960 out.write_all(b"--- ")?;
961 out.write_all(
962 format_diff_path_with_prefix(src_prefix, old_path, quote_path_fully).as_bytes(),
963 )?;
964 out.write_all(b"\n")?;
965 }
966 if new_path == "/dev/null" {
967 out.write_all(b"+++ /dev/null\n")?;
968 } else if dst_prefix.is_empty() {
969 writeln!(out, "+++ {new_path}")?;
970 } else {
971 out.write_all(b"+++ ")?;
972 out.write_all(
973 format_diff_path_with_prefix(dst_prefix, new_path, quote_path_fully).as_bytes(),
974 )?;
975 out.write_all(b"\n")?;
976 }
977
978 let body = unified_hunk_body_for_diff_options(old_content, new_content, options);
979 if body.is_empty() {
980 return Ok(());
981 }
982 let old_lines: Vec<&str> = old_content.lines().collect();
983 for hunk_str in imara_unified_hunk_slices(&body) {
984 if hunk_str.is_empty() {
985 continue;
986 }
987 if let Some(first_newline) = hunk_str.find('\n') {
988 let header_line = &hunk_str[..first_newline];
989 let rest = &hunk_str[first_newline..];
990 if let Some(func_ctx) =
991 extract_function_context(header_line, &old_lines, funcname_matcher, Some(old_path))
992 {
993 out.write_all(header_line.as_bytes())?;
994 out.write_all(b" ")?;
995 out.write_all(func_ctx.as_bytes())?;
996 out.write_all(rest.as_bytes())?;
997 } else {
998 out.write_all(hunk_str.as_bytes())?;
999 }
1000 } else {
1001 out.write_all(hunk_str.as_bytes())?;
1002 }
1003 }
1004 Ok(())
1005}
1006
1007#[expect(clippy::too_many_arguments)]
1008fn unified_diff_git_myers_with_prefix_and_funcname(
1009 old_content: &str,
1010 new_content: &str,
1011 old_path: &str,
1012 new_path: &str,
1013 context_lines: usize,
1014 inter_hunk_context: usize,
1015 src_prefix: &str,
1016 dst_prefix: &str,
1017 funcname_matcher: Option<&FuncnameMatcher>,
1018 indent_heuristic: bool,
1019 quote_path_fully: bool,
1020) -> String {
1021 use crate::quote_path::format_diff_path_with_prefix;
1022
1023 let body = git_xdiff_unified_body_raw(
1024 old_content,
1025 new_content,
1026 context_lines,
1027 inter_hunk_context,
1028 indent_heuristic,
1029 false,
1030 super::WhitespaceMode::default(),
1031 );
1032
1033 let mut output = String::new();
1034 if old_path == "/dev/null" {
1035 output.push_str("--- /dev/null\n");
1036 } else if src_prefix.is_empty() {
1037 output.push_str(&format!("--- {old_path}\n"));
1038 } else {
1039 output.push_str("--- ");
1040 output.push_str(&format_diff_path_with_prefix(
1041 src_prefix,
1042 old_path,
1043 quote_path_fully,
1044 ));
1045 output.push('\n');
1046 }
1047 if new_path == "/dev/null" {
1048 output.push_str("+++ /dev/null\n");
1049 } else if dst_prefix.is_empty() {
1050 output.push_str(&format!("+++ {new_path}\n"));
1051 } else {
1052 output.push_str("+++ ");
1053 output.push_str(&format_diff_path_with_prefix(
1054 dst_prefix,
1055 new_path,
1056 quote_path_fully,
1057 ));
1058 output.push('\n');
1059 }
1060
1061 let old_lines: Vec<&str> = old_content.lines().collect();
1062 for hunk_str in imara_unified_hunk_slices(&body) {
1063 if hunk_str.is_empty() {
1064 continue;
1065 }
1066 if let Some(first_newline) = hunk_str.find('\n') {
1067 let header_line = &hunk_str[..first_newline];
1068 let rest = &hunk_str[first_newline..];
1069 if let Some(func_ctx) =
1070 extract_function_context(header_line, &old_lines, funcname_matcher, Some(old_path))
1071 {
1072 output.push_str(header_line);
1073 output.push(' ');
1074 output.push_str(&func_ctx);
1075 output.push_str(rest);
1076 } else {
1077 output.push_str(hunk_str);
1078 }
1079 } else {
1080 output.push_str(hunk_str);
1081 }
1082 }
1083
1084 annotate_unified_diff_no_newline_at_eof(&output, old_content, new_content)
1085}
1086
1087#[expect(clippy::too_many_arguments)]
1088fn unified_diff_git_minimal_with_prefix_and_funcname(
1089 old_content: &str,
1090 new_content: &str,
1091 old_path: &str,
1092 new_path: &str,
1093 context_lines: usize,
1094 inter_hunk_context: usize,
1095 src_prefix: &str,
1096 dst_prefix: &str,
1097 funcname_matcher: Option<&FuncnameMatcher>,
1098 indent_heuristic: bool,
1099 quote_path_fully: bool,
1100) -> String {
1101 use crate::quote_path::format_diff_path_with_prefix;
1102
1103 let body = git_xdiff_unified_body_raw(
1104 old_content,
1105 new_content,
1106 context_lines,
1107 inter_hunk_context,
1108 indent_heuristic,
1109 true,
1110 super::WhitespaceMode::default(),
1111 );
1112
1113 let mut output = String::new();
1114 if old_path == "/dev/null" {
1115 output.push_str("--- /dev/null\n");
1116 } else if src_prefix.is_empty() {
1117 output.push_str(&format!("--- {old_path}\n"));
1118 } else {
1119 output.push_str("--- ");
1120 output.push_str(&format_diff_path_with_prefix(
1121 src_prefix,
1122 old_path,
1123 quote_path_fully,
1124 ));
1125 output.push('\n');
1126 }
1127 if new_path == "/dev/null" {
1128 output.push_str("+++ /dev/null\n");
1129 } else if dst_prefix.is_empty() {
1130 output.push_str(&format!("+++ {new_path}\n"));
1131 } else {
1132 output.push_str("+++ ");
1133 output.push_str(&format_diff_path_with_prefix(
1134 dst_prefix,
1135 new_path,
1136 quote_path_fully,
1137 ));
1138 output.push('\n');
1139 }
1140
1141 let old_lines: Vec<&str> = old_content.lines().collect();
1142 for hunk_str in imara_unified_hunk_slices(&body) {
1143 if hunk_str.is_empty() {
1144 continue;
1145 }
1146 if let Some(first_newline) = hunk_str.find('\n') {
1147 let header_line = &hunk_str[..first_newline];
1148 let rest = &hunk_str[first_newline..];
1149 if let Some(func_ctx) =
1150 extract_function_context(header_line, &old_lines, funcname_matcher, Some(old_path))
1151 {
1152 output.push_str(header_line);
1153 output.push(' ');
1154 output.push_str(&func_ctx);
1155 output.push_str(rest);
1156 } else {
1157 output.push_str(hunk_str);
1158 }
1159 } else {
1160 output.push_str(hunk_str);
1161 }
1162 }
1163
1164 annotate_unified_diff_no_newline_at_eof(&output, old_content, new_content)
1165}
1166
1167#[expect(clippy::too_many_arguments)]
1168fn unified_diff_git_patience_with_prefix_and_funcname(
1169 old_content: &str,
1170 new_content: &str,
1171 old_path: &str,
1172 new_path: &str,
1173 context_lines: usize,
1174 inter_hunk_context: usize,
1175 src_prefix: &str,
1176 dst_prefix: &str,
1177 funcname_matcher: Option<&FuncnameMatcher>,
1178 indent_heuristic: bool,
1179 quote_path_fully: bool,
1180) -> String {
1181 use crate::quote_path::format_diff_path_with_prefix;
1182
1183 let body = git_patience_unified_body_raw(
1184 old_content,
1185 new_content,
1186 context_lines,
1187 inter_hunk_context,
1188 indent_heuristic,
1189 );
1190
1191 let mut output = String::new();
1192 if old_path == "/dev/null" {
1193 output.push_str("--- /dev/null\n");
1194 } else if src_prefix.is_empty() {
1195 output.push_str(&format!("--- {old_path}\n"));
1196 } else {
1197 output.push_str("--- ");
1198 output.push_str(&format_diff_path_with_prefix(
1199 src_prefix,
1200 old_path,
1201 quote_path_fully,
1202 ));
1203 output.push('\n');
1204 }
1205 if new_path == "/dev/null" {
1206 output.push_str("+++ /dev/null\n");
1207 } else if dst_prefix.is_empty() {
1208 output.push_str(&format!("+++ {new_path}\n"));
1209 } else {
1210 output.push_str("+++ ");
1211 output.push_str(&format_diff_path_with_prefix(
1212 dst_prefix,
1213 new_path,
1214 quote_path_fully,
1215 ));
1216 output.push('\n');
1217 }
1218
1219 let old_lines: Vec<&str> = old_content.lines().collect();
1220 for hunk_str in imara_unified_hunk_slices(&body) {
1221 if hunk_str.is_empty() {
1222 continue;
1223 }
1224 if let Some(first_newline) = hunk_str.find('\n') {
1225 let header_line = &hunk_str[..first_newline];
1226 let rest = &hunk_str[first_newline..];
1227 if let Some(func_ctx) =
1228 extract_function_context(header_line, &old_lines, funcname_matcher, Some(old_path))
1229 {
1230 output.push_str(header_line);
1231 output.push(' ');
1232 output.push_str(&func_ctx);
1233 output.push_str(rest);
1234 } else {
1235 output.push_str(hunk_str);
1236 }
1237 } else {
1238 output.push_str(hunk_str);
1239 }
1240 }
1241
1242 annotate_unified_diff_no_newline_at_eof(&output, old_content, new_content)
1243}
1244
1245#[must_use]
1249pub fn unified_diff_histogram_hunks_only(
1250 old_content: &str,
1251 new_content: &str,
1252 context_lines: usize,
1253 inter_hunk_context: usize,
1254 indent_heuristic: bool,
1255) -> String {
1256 histogram_unified_body_raw(
1257 old_content,
1258 new_content,
1259 context_lines,
1260 inter_hunk_context,
1261 indent_heuristic,
1262 super::WhitespaceMode::default(),
1263 )
1264}
1265
1266#[expect(clippy::too_many_arguments)]
1267fn unified_diff_histogram_with_prefix_and_funcname_indent(
1268 old_content: &str,
1269 new_content: &str,
1270 old_path: &str,
1271 new_path: &str,
1272 context_lines: usize,
1273 inter_hunk_context: usize,
1274 src_prefix: &str,
1275 dst_prefix: &str,
1276 funcname_matcher: Option<&FuncnameMatcher>,
1277 indent_heuristic: bool,
1278 quote_path_fully: bool,
1279) -> String {
1280 use crate::quote_path::format_diff_path_with_prefix;
1281
1282 let body = histogram_unified_body_raw(
1283 old_content,
1284 new_content,
1285 context_lines,
1286 inter_hunk_context,
1287 indent_heuristic,
1288 super::WhitespaceMode::default(),
1289 );
1290
1291 let mut output = String::new();
1292 if old_path == "/dev/null" {
1293 output.push_str("--- /dev/null\n");
1294 } else if src_prefix.is_empty() {
1295 output.push_str(&format!("--- {old_path}\n"));
1296 } else {
1297 output.push_str("--- ");
1298 output.push_str(&format_diff_path_with_prefix(
1299 src_prefix,
1300 old_path,
1301 quote_path_fully,
1302 ));
1303 output.push('\n');
1304 }
1305 if new_path == "/dev/null" {
1306 output.push_str("+++ /dev/null\n");
1307 } else if dst_prefix.is_empty() {
1308 output.push_str(&format!("+++ {new_path}\n"));
1309 } else {
1310 output.push_str("+++ ");
1311 output.push_str(&format_diff_path_with_prefix(
1312 dst_prefix,
1313 new_path,
1314 quote_path_fully,
1315 ));
1316 output.push('\n');
1317 }
1318
1319 let old_lines: Vec<&str> = old_content.lines().collect();
1320 for hunk_str in imara_unified_hunk_slices(&body) {
1321 if hunk_str.is_empty() {
1322 continue;
1323 }
1324 if let Some(first_newline) = hunk_str.find('\n') {
1325 let header_line = &hunk_str[..first_newline];
1326 let rest = &hunk_str[first_newline..];
1327 if let Some(func_ctx) =
1328 extract_function_context(header_line, &old_lines, funcname_matcher, Some(old_path))
1329 {
1330 output.push_str(header_line);
1331 output.push(' ');
1332 output.push_str(&func_ctx);
1333 output.push_str(rest);
1334 } else {
1335 output.push_str(hunk_str);
1336 }
1337 } else {
1338 output.push_str(hunk_str);
1339 }
1340 }
1341
1342 annotate_unified_diff_no_newline_at_eof(&output, old_content, new_content)
1343}
1344
1345#[allow(clippy::too_many_arguments)]
1355pub fn unified_diff_histogram_ignore_with_prefix_and_funcname<F>(
1356 old_content: &str,
1357 new_content: &str,
1358 old_path: &str,
1359 new_path: &str,
1360 context_lines: usize,
1361 inter_hunk_context: usize,
1362 src_prefix: &str,
1363 dst_prefix: &str,
1364 funcname_matcher: Option<&FuncnameMatcher>,
1365 quote_path_fully: bool,
1366 function_context: bool,
1367 indent_heuristic: bool,
1368 is_ignorable_change: F,
1369) -> String
1370where
1371 F: Fn(&[&str], &[&str]) -> bool,
1372{
1373 use crate::quote_path::format_diff_path_with_prefix;
1374
1375 let body = histogram_unified_body_ignore_fc(
1376 old_content,
1377 new_content,
1378 context_lines,
1379 inter_hunk_context,
1380 function_context,
1381 funcname_matcher,
1382 indent_heuristic,
1383 super::WhitespaceMode::default(),
1384 is_ignorable_change,
1385 );
1386 if body.is_empty() {
1387 return String::new();
1388 }
1389
1390 let mut output = String::new();
1391 if old_path == "/dev/null" {
1392 output.push_str("--- /dev/null\n");
1393 } else if src_prefix.is_empty() {
1394 output.push_str(&format!("--- {old_path}\n"));
1395 } else {
1396 output.push_str("--- ");
1397 output.push_str(&format_diff_path_with_prefix(
1398 src_prefix,
1399 old_path,
1400 quote_path_fully,
1401 ));
1402 output.push('\n');
1403 }
1404 if new_path == "/dev/null" {
1405 output.push_str("+++ /dev/null\n");
1406 } else if dst_prefix.is_empty() {
1407 output.push_str(&format!("+++ {new_path}\n"));
1408 } else {
1409 output.push_str("+++ ");
1410 output.push_str(&format_diff_path_with_prefix(
1411 dst_prefix,
1412 new_path,
1413 quote_path_fully,
1414 ));
1415 output.push('\n');
1416 }
1417
1418 let old_lines: Vec<&str> = old_content.lines().collect();
1419 for hunk_str in imara_unified_hunk_slices(&body) {
1420 if hunk_str.is_empty() {
1421 continue;
1422 }
1423 if let Some(first_newline) = hunk_str.find('\n') {
1424 let header_line = &hunk_str[..first_newline];
1425 let rest = &hunk_str[first_newline..];
1426 if let Some(func_ctx) =
1427 extract_function_context(header_line, &old_lines, funcname_matcher, Some(old_path))
1428 {
1429 output.push_str(header_line);
1430 output.push(' ');
1431 output.push_str(&func_ctx);
1432 output.push_str(rest);
1433 } else {
1434 output.push_str(hunk_str);
1435 }
1436 } else {
1437 output.push_str(hunk_str);
1438 }
1439 }
1440
1441 output
1442}
1443
1444#[must_use]
1446pub fn indent_heuristic_from_config(config: &ConfigSet) -> bool {
1447 match config.get_bool("diff.indentHeuristic") {
1448 Some(Ok(b)) => b,
1449 Some(Err(_)) | None => true,
1450 }
1451}
1452
1453#[must_use]
1455pub fn resolve_indent_heuristic(
1456 config: &ConfigSet,
1457 cli_indent_heuristic: bool,
1458 cli_no_indent_heuristic: bool,
1459) -> bool {
1460 if cli_no_indent_heuristic {
1461 false
1462 } else if cli_indent_heuristic {
1463 true
1464 } else {
1465 indent_heuristic_from_config(config)
1466 }
1467}
1468
1469mod git_xdiff {
1479 use std::cell::RefCell;
1480
1481 const XDL_MAX_COST_MIN: i64 = 256;
1482 const XDL_HEUR_MIN_COST: i64 = 256;
1483 const XDL_SNAKE_CNT: i64 = 20;
1484 const XDL_K_HEUR: i64 = 4;
1485 const XDL_LINE_MAX: i64 = i64::MAX;
1486 const XDL_KPDIS_RUN: i64 = 4;
1487 const XDL_MAX_EQLIMIT: i64 = 1024;
1488 const XDL_SIMSCAN_WINDOW: i64 = 100;
1489
1490 const DISCARD: u8 = 0;
1492 const KEEP: u8 = 1;
1493 const INVESTIGATE: u8 = 2;
1494
1495 fn bogosqrt(mut n: i64) -> i64 {
1497 let mut i: i64 = 1;
1498 while n > 0 {
1499 i <<= 1;
1500 n >>= 2;
1501 }
1502 i
1503 }
1504
1505 struct XdAlgoEnv {
1506 mxcost: i64,
1507 snake_cnt: i64,
1508 heur_min: i64,
1509 }
1510
1511 struct XdpSplit {
1512 i1: i64,
1513 i2: i64,
1514 min_lo: bool,
1515 min_hi: bool,
1516 }
1517
1518 struct XdFile<'a> {
1521 #[allow(dead_code)]
1522 ids: &'a [u32],
1523 changed: Vec<u8>,
1524 reference_index: Vec<usize>,
1525 effective_ids: Vec<u32>,
1527 dstart: i64,
1528 dend: i64,
1529 nreff: i64,
1530 }
1531
1532 #[inline]
1533 fn effective_id(xdf: &XdFile<'_>, idx: i64) -> u32 {
1534 xdf.effective_ids[idx as usize]
1535 }
1536
1537 struct XdiffScratch {
1538 count1: Vec<i64>,
1539 count2: Vec<i64>,
1540 action1: Vec<u8>,
1541 action2: Vec<u8>,
1542 kvd: Vec<i64>,
1543 }
1544
1545 impl XdiffScratch {
1546 fn prepare_counts(&mut self, nclasses: usize) {
1547 if self.count1.len() < nclasses {
1548 self.count1.resize(nclasses, 0);
1549 self.count2.resize(nclasses, 0);
1550 }
1551 self.count1[..nclasses].fill(0);
1552 self.count2[..nclasses].fill(0);
1553 }
1554
1555 fn prepare_actions(&mut self, nrec1: usize, nrec2: usize) {
1556 let a1 = nrec1 + 1;
1557 let a2 = nrec2 + 1;
1558 if self.action1.len() < a1 {
1559 self.action1.resize(a1, 0);
1560 } else {
1561 self.action1[..a1].fill(0);
1562 }
1563 if self.action2.len() < a2 {
1564 self.action2.resize(a2, 0);
1565 } else {
1566 self.action2[..a2].fill(0);
1567 }
1568 }
1569
1570 fn prepare_kvd(&mut self, kvd_len: usize) {
1571 if self.kvd.len() < kvd_len {
1572 self.kvd.resize(kvd_len, 0);
1573 }
1574 }
1575 }
1576
1577 thread_local! { static XDIFF_SCRATCH: RefCell<XdiffScratch> = const {
1579 RefCell::new(XdiffScratch {
1580 count1: Vec::new(),
1581 count2: Vec::new(),
1582 action1: Vec::new(),
1583 action2: Vec::new(),
1584 kvd: Vec::new(),
1585 })
1586 };
1587 }
1588
1589 fn bool_changed(changed: Vec<u8>) -> Vec<bool> {
1590 changed.into_iter().map(|b| b != 0).collect()
1591 }
1592
1593 #[allow(clippy::too_many_arguments)]
1596 fn xdl_split(
1597 xdf1: &XdFile<'_>,
1598 off1: i64,
1599 lim1: i64,
1600 xdf2: &XdFile<'_>,
1601 off2: i64,
1602 lim2: i64,
1603 kvdf: &mut [i64],
1604 kvdb: &mut [i64],
1605 koff: i64,
1606 need_min: bool,
1607 spl: &mut XdpSplit,
1608 xenv: &XdAlgoEnv,
1609 ) -> i64 {
1610 let dmin = off1 - lim2;
1611 let dmax = lim1 - off2;
1612 let fmid = off1 - off2;
1613 let bmid = lim1 - lim2;
1614 let odd = ((fmid - bmid) & 1) != 0;
1615 let mut fmin = fmid;
1616 let mut fmax = fmid;
1617 let mut bmin = bmid;
1618 let mut bmax = bmid;
1619
1620 let kf = |k: i64| -> usize { (k + koff) as usize };
1621
1622 kvdf[kf(fmid)] = off1;
1623 kvdb[kf(bmid)] = lim1;
1624
1625 let mut ec: i64 = 1;
1626 loop {
1627 let mut got_snake = false;
1628
1629 if fmin > dmin {
1630 fmin -= 1;
1631 kvdf[kf(fmin - 1)] = -1;
1632 } else {
1633 fmin += 1;
1634 }
1635 if fmax < dmax {
1636 fmax += 1;
1637 kvdf[kf(fmax + 1)] = -1;
1638 } else {
1639 fmax -= 1;
1640 }
1641
1642 let mut d = fmax;
1643 while d >= fmin {
1644 let mut i1 = if kvdf[kf(d - 1)] >= kvdf[kf(d + 1)] {
1645 kvdf[kf(d - 1)] + 1
1646 } else {
1647 kvdf[kf(d + 1)]
1648 };
1649 let prev1 = i1;
1650 let mut i2 = i1 - d;
1651 while i1 < lim1 && i2 < lim2 && effective_id(xdf1, i1) == effective_id(xdf2, i2) {
1652 i1 += 1;
1653 i2 += 1;
1654 }
1655 if i1 - prev1 > xenv.snake_cnt {
1656 got_snake = true;
1657 }
1658 kvdf[kf(d)] = i1;
1659 if odd && bmin <= d && d <= bmax && kvdb[kf(d)] <= i1 {
1660 spl.i1 = i1;
1661 spl.i2 = i2;
1662 spl.min_lo = true;
1663 spl.min_hi = true;
1664 return ec;
1665 }
1666 d -= 2;
1667 }
1668
1669 if bmin > dmin {
1670 bmin -= 1;
1671 kvdb[kf(bmin - 1)] = XDL_LINE_MAX;
1672 } else {
1673 bmin += 1;
1674 }
1675 if bmax < dmax {
1676 bmax += 1;
1677 kvdb[kf(bmax + 1)] = XDL_LINE_MAX;
1678 } else {
1679 bmax -= 1;
1680 }
1681
1682 let mut d = bmax;
1683 while d >= bmin {
1684 let mut i1 = if kvdb[kf(d - 1)] < kvdb[kf(d + 1)] {
1685 kvdb[kf(d - 1)]
1686 } else {
1687 kvdb[kf(d + 1)] - 1
1688 };
1689 let prev1 = i1;
1690 let mut i2 = i1 - d;
1691 while i1 > off1
1692 && i2 > off2
1693 && effective_id(xdf1, i1 - 1) == effective_id(xdf2, i2 - 1)
1694 {
1695 i1 -= 1;
1696 i2 -= 1;
1697 }
1698 if prev1 - i1 > xenv.snake_cnt {
1699 got_snake = true;
1700 }
1701 kvdb[kf(d)] = i1;
1702 if !odd && fmin <= d && d <= fmax && i1 <= kvdf[kf(d)] {
1703 spl.i1 = i1;
1704 spl.i2 = i2;
1705 spl.min_lo = true;
1706 spl.min_hi = true;
1707 return ec;
1708 }
1709 d -= 2;
1710 }
1711
1712 if need_min {
1713 ec += 1;
1714 continue;
1715 }
1716
1717 if got_snake && ec > xenv.heur_min {
1718 let mut best = 0i64;
1719 let mut d = fmax;
1720 while d >= fmin {
1721 let dd = if d > fmid { d - fmid } else { fmid - d };
1722 let i1 = kvdf[kf(d)];
1723 let i2 = i1 - d;
1724 let v = (i1 - off1) + (i2 - off2) - dd;
1725 if v > XDL_K_HEUR * ec
1726 && v > best
1727 && off1 + xenv.snake_cnt <= i1
1728 && i1 < lim1
1729 && off2 + xenv.snake_cnt <= i2
1730 && i2 < lim2
1731 {
1732 let mut k = 1i64;
1733 while effective_id(xdf1, i1 - k) == effective_id(xdf2, i2 - k) {
1734 if k == xenv.snake_cnt {
1735 best = v;
1736 spl.i1 = i1;
1737 spl.i2 = i2;
1738 break;
1739 }
1740 k += 1;
1741 }
1742 }
1743 d -= 2;
1744 }
1745 if best > 0 {
1746 spl.min_lo = true;
1747 spl.min_hi = false;
1748 return ec;
1749 }
1750
1751 let mut best = 0i64;
1752 let mut d = bmax;
1753 while d >= bmin {
1754 let dd = if d > bmid { d - bmid } else { bmid - d };
1755 let i1 = kvdb[kf(d)];
1756 let i2 = i1 - d;
1757 let v = (lim1 - i1) + (lim2 - i2) - dd;
1758 if v > XDL_K_HEUR * ec
1759 && v > best
1760 && off1 < i1
1761 && i1 <= lim1 - xenv.snake_cnt
1762 && off2 < i2
1763 && i2 <= lim2 - xenv.snake_cnt
1764 {
1765 let mut k = 0i64;
1766 while effective_id(xdf1, i1 + k) == effective_id(xdf2, i2 + k) {
1767 if k == xenv.snake_cnt - 1 {
1768 best = v;
1769 spl.i1 = i1;
1770 spl.i2 = i2;
1771 break;
1772 }
1773 k += 1;
1774 }
1775 }
1776 d -= 2;
1777 }
1778 if best > 0 {
1779 spl.min_lo = false;
1780 spl.min_hi = true;
1781 return ec;
1782 }
1783 }
1784
1785 if ec >= xenv.mxcost {
1786 let mut fbest = -1i64;
1787 let mut fbest1 = -1i64;
1788 let mut d = fmax;
1789 while d >= fmin {
1790 let mut i1 = kvdf[kf(d)].min(lim1);
1791 let mut i2 = i1 - d;
1792 if lim2 < i2 {
1793 i1 = lim2 + d;
1794 i2 = lim2;
1795 }
1796 if fbest < i1 + i2 {
1797 fbest = i1 + i2;
1798 fbest1 = i1;
1799 }
1800 d -= 2;
1801 }
1802
1803 let mut bbest = XDL_LINE_MAX;
1804 let mut bbest1 = XDL_LINE_MAX;
1805 let mut d = bmax;
1806 while d >= bmin {
1807 let mut i1 = off1.max(kvdb[kf(d)]);
1808 let mut i2 = i1 - d;
1809 if i2 < off2 {
1810 i1 = off2 + d;
1811 i2 = off2;
1812 }
1813 if i1 + i2 < bbest {
1814 bbest = i1 + i2;
1815 bbest1 = i1;
1816 }
1817 d -= 2;
1818 }
1819
1820 if (lim1 + lim2) - bbest < fbest - (off1 + off2) {
1821 spl.i1 = fbest1;
1822 spl.i2 = fbest - fbest1;
1823 spl.min_lo = true;
1824 spl.min_hi = false;
1825 } else {
1826 spl.i1 = bbest1;
1827 spl.i2 = bbest - bbest1;
1828 spl.min_lo = false;
1829 spl.min_hi = true;
1830 }
1831 return ec;
1832 }
1833
1834 ec += 1;
1835 }
1836 }
1837
1838 #[allow(clippy::too_many_arguments)]
1840 fn xdl_recs_cmp(
1841 xdf1: &mut XdFile<'_>,
1842 mut off1: i64,
1843 mut lim1: i64,
1844 xdf2: &mut XdFile<'_>,
1845 mut off2: i64,
1846 mut lim2: i64,
1847 kvdf: &mut [i64],
1848 kvdb: &mut [i64],
1849 koff: i64,
1850 need_min: bool,
1851 xenv: &XdAlgoEnv,
1852 ) {
1853 while off1 < lim1 && off2 < lim2 && effective_id(xdf1, off1) == effective_id(xdf2, off2) {
1854 off1 += 1;
1855 off2 += 1;
1856 }
1857 while off1 < lim1
1858 && off2 < lim2
1859 && effective_id(xdf1, lim1 - 1) == effective_id(xdf2, lim2 - 1)
1860 {
1861 lim1 -= 1;
1862 lim2 -= 1;
1863 }
1864
1865 if off1 == lim1 {
1866 while off2 < lim2 {
1867 let r = xdf2.reference_index[off2 as usize];
1868 xdf2.changed[r] = 1;
1869 off2 += 1;
1870 }
1871 } else if off2 == lim2 {
1872 while off1 < lim1 {
1873 let r = xdf1.reference_index[off1 as usize];
1874 xdf1.changed[r] = 1;
1875 off1 += 1;
1876 }
1877 } else {
1878 let mut spl = XdpSplit {
1879 i1: 0,
1880 i2: 0,
1881 min_lo: false,
1882 min_hi: false,
1883 };
1884 xdl_split(
1885 xdf1, off1, lim1, xdf2, off2, lim2, kvdf, kvdb, koff, need_min, &mut spl, xenv,
1886 );
1887 xdl_recs_cmp(
1888 xdf1, off1, spl.i1, xdf2, off2, spl.i2, kvdf, kvdb, koff, spl.min_lo, xenv,
1889 );
1890 xdl_recs_cmp(
1891 xdf1, spl.i1, lim1, xdf2, spl.i2, lim2, kvdf, kvdb, koff, spl.min_hi, xenv,
1892 );
1893 }
1894 }
1895
1896 fn clean_mmatch(action: &[u8], i: i64, mut s: i64, mut e: i64) -> bool {
1898 if i - s > XDL_SIMSCAN_WINDOW {
1899 s = i - XDL_SIMSCAN_WINDOW;
1900 }
1901 if e - i > XDL_SIMSCAN_WINDOW {
1902 e = i + XDL_SIMSCAN_WINDOW;
1903 }
1904
1905 let mut rdis0 = 0i64;
1906 let mut rpdis0 = 1i64;
1907 let mut r = 1i64;
1908 while i - r >= s {
1909 match action[(i - r) as usize] {
1910 DISCARD => rdis0 += 1,
1911 INVESTIGATE => rpdis0 += 1,
1912 _ => break, }
1914 r += 1;
1915 }
1916 if rdis0 == 0 {
1917 return false;
1918 }
1919 let mut rdis1 = 0i64;
1920 let mut rpdis1 = 1i64;
1921 let mut r = 1i64;
1922 while i + r <= e {
1923 match action[(i + r) as usize] {
1924 DISCARD => rdis1 += 1,
1925 INVESTIGATE => rpdis1 += 1,
1926 _ => break, }
1928 r += 1;
1929 }
1930 if rdis1 == 0 {
1931 return false;
1932 }
1933 rdis1 += rdis0;
1934 rpdis1 += rpdis0;
1935 rpdis1 * XDL_KPDIS_RUN < (rpdis1 + rdis1)
1936 }
1937
1938 pub fn changed_flags(ids1: &[u32], ids2: &[u32], need_min: bool) -> (Vec<bool>, Vec<bool>) {
1944 changed_flags_with_shared(ids1, ids2, &[], 0, need_min)
1945 }
1946
1947 pub fn changed_flags_with_shared(
1954 ids1: &[u32],
1955 ids2: &[u32],
1956 shared_counts: &[u32],
1957 shared_records: usize,
1958 need_min: bool,
1959 ) -> (Vec<bool>, Vec<bool>) {
1960 XDIFF_SCRATCH.with(|cell| {
1961 changed_flags_with_shared_inner(
1962 cell,
1963 ids1,
1964 ids2,
1965 shared_counts,
1966 shared_records,
1967 need_min,
1968 )
1969 })
1970 }
1971
1972 fn changed_flags_with_shared_inner(
1973 scratch: &RefCell<XdiffScratch>,
1974 ids1: &[u32],
1975 ids2: &[u32],
1976 shared_counts: &[u32],
1977 shared_records: usize,
1978 need_min: bool,
1979 ) -> (Vec<bool>, Vec<bool>) {
1980 let nrec1 = ids1.len();
1981 let nrec2 = ids2.len();
1982
1983 let nclasses = ids1
1985 .iter()
1986 .chain(ids2)
1987 .max()
1988 .map_or(0, |&max| max as usize + 1);
1989 let mut work = scratch.borrow_mut();
1990 work.prepare_counts(nclasses);
1991 for &id in ids1 {
1992 work.count1[id as usize] += 1;
1993 }
1994 for &id in ids2 {
1995 work.count2[id as usize] += 1;
1996 }
1997 for (id, &shared) in shared_counts.iter().enumerate().take(nclasses) {
1998 work.count1[id] += i64::from(shared);
1999 work.count2[id] += i64::from(shared);
2000 }
2001
2002 let mut xdf1 = XdFile {
2003 ids: ids1,
2004 changed: vec![0u8; nrec1],
2005 reference_index: Vec::new(),
2006 effective_ids: Vec::new(),
2007 dstart: 0,
2008 dend: nrec1 as i64 - 1,
2009 nreff: 0,
2010 };
2011 let mut xdf2 = XdFile {
2012 ids: ids2,
2013 changed: vec![0u8; nrec2],
2014 reference_index: Vec::new(),
2015 effective_ids: Vec::new(),
2016 dstart: 0,
2017 dend: nrec2 as i64 - 1,
2018 nreff: 0,
2019 };
2020
2021 let lim = nrec1.min(nrec2) as i64;
2023 let mut i = 0i64;
2024 while i < lim && ids1[i as usize] == ids2[i as usize] {
2025 i += 1;
2026 }
2027 xdf1.dstart = i;
2028 xdf2.dstart = i;
2029 let mut j = 0i64;
2030 let rem = lim - i;
2031 while j < rem && ids1[nrec1 - 1 - j as usize] == ids2[nrec2 - 1 - j as usize] {
2032 j += 1;
2033 }
2034 xdf1.dend = nrec1 as i64 - j - 1;
2035 xdf2.dend = nrec2 as i64 - j - 1;
2036
2037 work.prepare_actions(nrec1, nrec2);
2039
2040 let mlim1 = if need_min {
2041 i64::MAX
2042 } else {
2043 let mut mlim = bogosqrt((nrec1 + shared_records) as i64);
2044 if mlim > XDL_MAX_EQLIMIT {
2045 mlim = XDL_MAX_EQLIMIT;
2046 }
2047 mlim
2048 };
2049 let mut idx = xdf1.dstart;
2050 while idx <= xdf1.dend {
2051 let id = ids1[idx as usize];
2052 let nm = work.count2[id as usize];
2053 work.action1[idx as usize] = if nm == 0 {
2054 DISCARD
2055 } else if nm >= mlim1 {
2056 INVESTIGATE
2057 } else {
2058 KEEP
2059 };
2060 idx += 1;
2061 }
2062
2063 let mlim2 = if need_min {
2064 i64::MAX
2065 } else {
2066 let mut mlim = bogosqrt((nrec2 + shared_records) as i64);
2067 if mlim > XDL_MAX_EQLIMIT {
2068 mlim = XDL_MAX_EQLIMIT;
2069 }
2070 mlim
2071 };
2072 let mut idx = xdf2.dstart;
2073 while idx <= xdf2.dend {
2074 let id = ids2[idx as usize];
2075 let nm = work.count1[id as usize];
2076 work.action2[idx as usize] = if nm == 0 {
2077 DISCARD
2078 } else if nm >= mlim2 {
2079 INVESTIGATE
2080 } else {
2081 KEEP
2082 };
2083 idx += 1;
2084 }
2085
2086 let mut idx = xdf1.dstart;
2087 while idx <= xdf1.dend {
2088 let id = ids1[idx as usize];
2089 let a = work.action1[idx as usize];
2090 if a == KEEP
2091 || (a == INVESTIGATE
2092 && !clean_mmatch(&work.action1[..nrec1 + 1], idx, xdf1.dstart, xdf1.dend))
2093 {
2094 xdf1.reference_index.push(idx as usize);
2095 xdf1.effective_ids.push(id);
2096 } else {
2097 xdf1.changed[idx as usize] = 1;
2098 }
2099 idx += 1;
2100 }
2101 xdf1.nreff = xdf1.reference_index.len() as i64;
2102
2103 let mut idx = xdf2.dstart;
2104 while idx <= xdf2.dend {
2105 let id = ids2[idx as usize];
2106 let a = work.action2[idx as usize];
2107 if a == KEEP
2108 || (a == INVESTIGATE
2109 && !clean_mmatch(&work.action2[..nrec2 + 1], idx, xdf2.dstart, xdf2.dend))
2110 {
2111 xdf2.reference_index.push(idx as usize);
2112 xdf2.effective_ids.push(id);
2113 } else {
2114 xdf2.changed[idx as usize] = 1;
2115 }
2116 idx += 1;
2117 }
2118 xdf2.nreff = xdf2.reference_index.len() as i64;
2119
2120 let ndiags = xdf1.nreff + xdf2.nreff + 3;
2122 let kvd_len = (2 * ndiags + 2) as usize;
2123 work.prepare_kvd(kvd_len);
2124 let kvd = &mut work.kvd[..kvd_len];
2125 let koff = xdf2.nreff + 1;
2127 let (kvdf_slice, kvdb_slice) = kvd.split_at_mut(ndiags as usize);
2128 let xenv = XdAlgoEnv {
2131 mxcost: bogosqrt(ndiags).max(XDL_MAX_COST_MIN),
2132 snake_cnt: XDL_SNAKE_CNT,
2133 heur_min: XDL_HEUR_MIN_COST,
2134 };
2135
2136 let nreff1 = xdf1.nreff;
2137 let nreff2 = xdf2.nreff;
2138 xdl_recs_cmp(
2139 &mut xdf1, 0, nreff1, &mut xdf2, 0, nreff2, kvdf_slice, kvdb_slice, koff, need_min,
2140 &xenv,
2141 );
2142
2143 (bool_changed(xdf1.changed), bool_changed(xdf2.changed))
2144 }
2145
2146 pub fn patience_changed_flags(lines1: &[&str], lines2: &[&str]) -> (Vec<bool>, Vec<bool>) {
2148 let mut changed1 = vec![false; lines1.len()];
2149 let mut changed2 = vec![false; lines2.len()];
2150 patience_recurse(
2151 lines1,
2152 lines2,
2153 &mut changed1,
2154 &mut changed2,
2155 0,
2156 lines1.len(),
2157 0,
2158 lines2.len(),
2159 );
2160 (changed1, changed2)
2161 }
2162
2163 #[allow(clippy::too_many_arguments)]
2164 fn patience_recurse(
2165 lines1: &[&str],
2166 lines2: &[&str],
2167 changed1: &mut [bool],
2168 changed2: &mut [bool],
2169 o_start: usize,
2170 o_end: usize,
2171 n_start: usize,
2172 n_end: usize,
2173 ) {
2174 if o_start >= o_end && n_start >= n_end {
2175 return;
2176 }
2177 if o_start >= o_end {
2178 changed2[n_start..n_end].fill(true);
2179 return;
2180 }
2181 if n_start >= n_end {
2182 changed1[o_start..o_end].fill(true);
2183 return;
2184 }
2185
2186 let anchors = patience_anchors(lines1, lines2, o_start, o_end, n_start, n_end);
2187 if anchors.is_empty() {
2188 merge_myers_region(
2189 lines1, lines2, changed1, changed2, o_start, o_end, n_start, n_end,
2190 );
2191 return;
2192 }
2193
2194 let mut prev_o = o_start;
2195 let mut prev_n = n_start;
2196 for (ao, an) in anchors {
2197 patience_recurse(lines1, lines2, changed1, changed2, prev_o, ao, prev_n, an);
2198 prev_o = ao + 1;
2199 prev_n = an + 1;
2200 }
2201 patience_recurse(
2202 lines1, lines2, changed1, changed2, prev_o, o_end, prev_n, n_end,
2203 );
2204 }
2205
2206 #[allow(clippy::too_many_arguments)]
2207 fn patience_anchors(
2208 lines1: &[&str],
2209 lines2: &[&str],
2210 o_start: usize,
2211 o_end: usize,
2212 n_start: usize,
2213 n_end: usize,
2214 ) -> Vec<(usize, usize)> {
2215 use std::collections::HashMap;
2216
2217 let old_slice = &lines1[o_start..o_end];
2218 let new_slice = &lines2[n_start..n_end];
2219 let mut old_pos: HashMap<&str, Vec<usize>> = HashMap::new();
2220 let mut new_pos: HashMap<&str, Vec<usize>> = HashMap::new();
2221 for (i, line) in old_slice.iter().enumerate() {
2222 old_pos.entry(*line).or_default().push(o_start + i);
2223 }
2224 for (i, line) in new_slice.iter().enumerate() {
2225 new_pos.entry(*line).or_default().push(n_start + i);
2226 }
2227
2228 let mut pairs: Vec<(usize, usize)> = Vec::new();
2229 for (line, o_indices) in &old_pos {
2230 if o_indices.len() != 1 {
2231 continue;
2232 }
2233 if let Some(n_indices) = new_pos.get(line) {
2234 if n_indices.len() == 1 {
2235 pairs.push((o_indices[0], n_indices[0]));
2236 }
2237 }
2238 }
2239 pairs.sort_by_key(|p| p.0);
2240 longest_increasing_by_second(&pairs)
2241 }
2242
2243 fn longest_increasing_by_second(pairs: &[(usize, usize)]) -> Vec<(usize, usize)> {
2244 if pairs.is_empty() {
2245 return Vec::new();
2246 }
2247 let n = pairs.len();
2248 let mut tails = vec![0usize; n];
2249 let mut prev = vec![usize::MAX; n];
2250 let mut len = 0usize;
2251 for i in 0..n {
2252 let v = pairs[i].1;
2253 let mut lo = 0usize;
2254 let mut hi = len;
2255 while lo < hi {
2256 let mid = (lo + hi) / 2;
2257 if pairs[tails[mid]].1 < v {
2258 lo = mid + 1;
2259 } else {
2260 hi = mid;
2261 }
2262 }
2263 if lo > 0 {
2264 prev[i] = tails[lo - 1];
2265 }
2266 tails[lo] = i;
2267 if lo == len {
2268 len += 1;
2269 }
2270 }
2271 let mut out = Vec::with_capacity(len);
2272 let mut k = tails[len.saturating_sub(1)];
2273 while k != usize::MAX {
2274 out.push(pairs[k]);
2275 k = prev[k];
2276 }
2277 out.reverse();
2278 out
2279 }
2280
2281 pub(crate) fn histogram_changed_flags(
2282 lines1: &[&str],
2283 lines2: &[&str],
2284 ) -> (Vec<bool>, Vec<bool>) {
2285 crate::diff::git_histogram::histogram_changed_flags(
2286 lines1,
2287 lines2,
2288 |changed1, changed2, o_start, o_end, n_start, n_end| {
2289 merge_myers_region(
2290 lines1, lines2, changed1, changed2, o_start, o_end, n_start, n_end,
2291 );
2292 },
2293 )
2294 }
2295
2296 #[allow(clippy::too_many_arguments)]
2297 fn merge_myers_region(
2298 lines1: &[&str],
2299 lines2: &[&str],
2300 changed1: &mut [bool],
2301 changed2: &mut [bool],
2302 o_start: usize,
2303 o_end: usize,
2304 n_start: usize,
2305 n_end: usize,
2306 ) {
2307 use crate::line_interner::LineInterner;
2308
2309 let old_slice = &lines1[o_start..o_end];
2310 let new_slice = &lines2[n_start..n_end];
2311 if old_slice.is_empty() && new_slice.is_empty() {
2312 return;
2313 }
2314 let mut interner = LineInterner::with_capacity(old_slice.len() + new_slice.len());
2315 let ids1 = interner.intern_all(old_slice);
2316 let ids2 = interner.intern_all(new_slice);
2317 let (sub1, sub2) = changed_flags(&ids1, &ids2, false);
2318 for (i, ch) in sub1.iter().enumerate() {
2319 if *ch {
2320 changed1[o_start + i] = true;
2321 }
2322 }
2323 for (j, ch) in sub2.iter().enumerate() {
2324 if *ch {
2325 changed2[n_start + j] = true;
2326 }
2327 }
2328 }
2329}
2330
2331pub(crate) fn myers_rediff_changed_masks_for_region<'a>(
2333 old_lines: &[&'a str],
2334 new_lines: &[&'a str],
2335 ws: super::WhitespaceMode,
2336) -> (Vec<bool>, Vec<bool>) {
2337 use crate::line_interner::LineInterner;
2338
2339 if old_lines.is_empty() && new_lines.is_empty() {
2340 return (Vec::new(), Vec::new());
2341 }
2342 let normalize = |lines: &[&'a str]| -> Vec<Cow<'a, str>> {
2343 lines
2344 .iter()
2345 .map(|line| ws.normalize_line_for_compare(line))
2346 .collect()
2347 };
2348 let (old_normalized, new_normalized) = (normalize(old_lines), normalize(new_lines));
2349 let old_keys: Vec<&str> = old_normalized.iter().map(Cow::as_ref).collect();
2350 let new_keys: Vec<&str> = new_normalized.iter().map(Cow::as_ref).collect();
2351 let mut interner = LineInterner::with_capacity(old_keys.len() + new_keys.len());
2352 let ids1 = interner.intern_all(&old_keys);
2353 let ids2 = interner.intern_all(&new_keys);
2354 git_xdiff::changed_flags(&ids1, &ids2, false)
2355}
2356
2357fn changed_flags_to_ops(
2359 changed1: &[bool],
2360 changed2: &[bool],
2361 old_len: usize,
2362 new_len: usize,
2363) -> Vec<similar::DiffOp> {
2364 use similar::DiffOp;
2365 let mut ops: Vec<DiffOp> = Vec::new();
2366 let mut i = 0usize;
2367 let mut j = 0usize;
2368 while i < old_len || j < new_len {
2369 let del = i < old_len && changed1[i];
2370 let ins = j < new_len && changed2[j];
2371 if !del && !ins {
2372 if i < old_len && j < new_len {
2373 let start_i = i;
2375 let start_j = j;
2376 while i < old_len && j < new_len && !changed1[i] && !changed2[j] {
2377 i += 1;
2378 j += 1;
2379 }
2380 ops.push(DiffOp::Equal {
2381 old_index: start_i,
2382 new_index: start_j,
2383 len: i - start_i,
2384 });
2385 } else if i < old_len {
2386 let start_i = i;
2387 while i < old_len {
2388 i += 1;
2389 }
2390 ops.push(DiffOp::Delete {
2391 old_index: start_i,
2392 old_len: i - start_i,
2393 new_index: j,
2394 });
2395 } else if j < new_len {
2396 let start_j = j;
2397 while j < new_len {
2398 j += 1;
2399 }
2400 ops.push(DiffOp::Insert {
2401 old_index: i,
2402 new_index: start_j,
2403 new_len: j - start_j,
2404 });
2405 }
2406 } else {
2407 let start_i = i;
2409 let start_j = j;
2410 while i < old_len && changed1[i] {
2411 i += 1;
2412 }
2413 while j < new_len && changed2[j] {
2414 j += 1;
2415 }
2416 let dlen = i - start_i;
2417 let ilen = j - start_j;
2418 if dlen > 0 && ilen > 0 {
2419 ops.push(DiffOp::Replace {
2420 old_index: start_i,
2421 old_len: dlen,
2422 new_index: start_j,
2423 new_len: ilen,
2424 });
2425 } else if dlen > 0 {
2426 ops.push(DiffOp::Delete {
2427 old_index: start_i,
2428 old_len: dlen,
2429 new_index: start_j,
2430 });
2431 } else if ilen > 0 {
2432 ops.push(DiffOp::Insert {
2433 old_index: start_i,
2434 new_index: start_j,
2435 new_len: ilen,
2436 });
2437 }
2438 }
2439 }
2440 ops
2441}
2442
2443#[must_use]
2445pub fn diff_slice_ops_compacted(
2446 old_lines: &[&str],
2447 new_lines: &[&str],
2448 algorithm: similar::Algorithm,
2449 indent_heuristic: bool,
2450) -> Vec<similar::DiffOp> {
2451 diff_indent_heuristic::diff_slice_ops_compacted(
2452 old_lines,
2453 new_lines,
2454 algorithm,
2455 indent_heuristic,
2456 )
2457}
2458
2459#[must_use]
2462#[allow(clippy::too_many_arguments)]
2466pub(crate) fn line_diff_ops_git_xdiff_compacted_ids(
2467 old_ids: &[u32],
2468 new_ids: &[u32],
2469 shared_counts: &[u32],
2470 shared_records: usize,
2471 old_lines: &[&str],
2472 new_lines: &[&str],
2473 indent_heuristic: bool,
2474 ws: super::WhitespaceMode,
2475) -> Vec<similar::DiffOp> {
2476 let (changed1, changed2) = git_xdiff::changed_flags_with_shared(
2477 old_ids,
2478 new_ids,
2479 shared_counts,
2480 shared_records,
2481 false,
2482 );
2483 let ops = changed_flags_to_ops(&changed1, &changed2, old_lines.len(), new_lines.len());
2484 diff_indent_heuristic::apply_change_compact_to_ops_with_ws(
2485 &ops,
2486 old_lines,
2487 new_lines,
2488 indent_heuristic,
2489 ws,
2490 )
2491}
2492
2493#[cfg(test)]
2495#[must_use]
2496pub(crate) fn xdiff_new_line_changed_flags(
2497 old_lines: &[&str],
2498 new_lines: &[&str],
2499 indent_heuristic: bool,
2500) -> Vec<bool> {
2501 let mut interner =
2502 crate::line_interner::LineInterner::with_capacity(old_lines.len() + new_lines.len());
2503 let ids1 = interner.intern_all(old_lines);
2504 let ids2 = interner.intern_all(new_lines);
2505 let (changed1, changed2) = git_xdiff::changed_flags(&ids1, &ids2, false);
2506 let ops = changed_flags_to_ops(&changed1, &changed2, old_lines.len(), new_lines.len());
2507 let compacted = diff_indent_heuristic::apply_change_compact_to_ops(
2508 &ops,
2509 old_lines,
2510 new_lines,
2511 indent_heuristic,
2512 );
2513 let mut flags = vec![false; new_lines.len()];
2514 for op in compacted {
2515 match op.tag() {
2516 similar::DiffTag::Insert | similar::DiffTag::Replace => {
2517 for idx in op.new_range() {
2518 if idx < flags.len() {
2519 flags[idx] = true;
2520 }
2521 }
2522 }
2523 similar::DiffTag::Equal | similar::DiffTag::Delete => {}
2524 }
2525 }
2526 flags
2527}
2528
2529#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2531#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
2532#[cfg_attr(feature = "serde-models", serde(rename_all = "snake_case"))]
2533pub enum DiffStatus {
2534 Added,
2536 Deleted,
2538 Modified,
2540 Renamed,
2542 Copied,
2544 TypeChanged,
2546 Unmerged,
2548}
2549
2550impl DiffStatus {
2551 #[must_use]
2553 pub fn letter(&self) -> char {
2554 match self {
2555 Self::Added => 'A',
2556 Self::Deleted => 'D',
2557 Self::Modified => 'M',
2558 Self::Renamed => 'R',
2559 Self::Copied => 'C',
2560 Self::TypeChanged => 'T',
2561 Self::Unmerged => 'U',
2562 }
2563 }
2564
2565 #[must_use]
2572 pub(crate) fn for_content_change(old_mode: u32, new_mode: u32) -> Self {
2573 if old_mode & 0o170000 != new_mode & 0o170000 {
2574 Self::TypeChanged
2575 } else {
2576 Self::Modified
2577 }
2578 }
2579}
2580
2581#[derive(Debug, Clone, PartialEq, Eq)]
2583#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
2584pub struct DiffEntry {
2585 pub status: DiffStatus,
2587 pub old_path: Option<String>,
2589 pub new_path: Option<String>,
2591 pub old_mode: FileMode,
2593 pub new_mode: FileMode,
2595 pub old_oid: ObjectId,
2597 pub new_oid: ObjectId,
2600 pub score: Option<u32>,
2602}
2603
2604impl DiffEntry {
2605 #[must_use]
2607 pub fn path(&self) -> &str {
2608 self.new_path
2609 .as_deref()
2610 .or(self.old_path.as_deref())
2611 .unwrap_or("")
2612 }
2613
2614 #[must_use]
2619 pub fn display_path(&self) -> String {
2620 match self.status {
2621 DiffStatus::Renamed | DiffStatus::Copied => {
2622 let old = self.old_path.as_deref().unwrap_or("");
2623 let new = self.new_path.as_deref().unwrap_or("");
2624 if old.is_empty() || new.is_empty() {
2625 self.path().to_owned()
2626 } else {
2627 format!("{old} -> {new}")
2628 }
2629 }
2630 _ => self.path().to_owned(),
2631 }
2632 }
2633}
2634
2635#[must_use]
2637pub fn zero_oid() -> ObjectId {
2638 ObjectId::from_bytes(&[0u8; 20]).unwrap_or_else(|_| {
2639 panic!("internal error: failed to create zero OID");
2641 })
2642}
2643
2644#[must_use]
2646pub fn empty_blob_oid() -> ObjectId {
2647 ObjectId::from_hex("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391").unwrap_or_else(|_| {
2648 panic!("internal error: failed to create empty blob OID");
2650 })
2651}
2652
2653pub fn diff_trees(
2668 odb: &Odb,
2669 old_tree_oid: Option<&ObjectId>,
2670 new_tree_oid: Option<&ObjectId>,
2671 prefix: &str,
2672) -> Result<Vec<DiffEntry>> {
2673 crate::trace::trace_span!("grit.diff.tree");
2674 diff_trees_opts(odb, old_tree_oid, new_tree_oid, prefix, false, None)
2675}
2676
2677pub(crate) fn diff_trees_with_index(
2680 odb: &Odb,
2681 index: &Index,
2682 old_tree_oid: Option<&ObjectId>,
2683 new_tree_oid: Option<&ObjectId>,
2684 prefix: &str,
2685 trust_index_for_old_tree: bool,
2686) -> Result<Vec<DiffEntry>> {
2687 let hint = (trust_index_for_old_tree
2688 && old_tree_oid.is_some()
2689 && index.entries_sorted
2690 && !index.entries.iter().any(|e| e.stage() != 0))
2691 .then_some(DiffTreesWalkHint {
2692 index: Some(index),
2693 old_tree_matches: true,
2694 ..DiffTreesWalkHint::default()
2695 });
2696 diff_trees_opts(
2697 odb,
2698 old_tree_oid,
2699 new_tree_oid,
2700 prefix,
2701 false,
2702 hint.as_ref(),
2703 )
2704}
2705
2706pub(crate) fn diff_trees_outside_sparse_directories(
2710 odb: &Odb,
2711 index: &Index,
2712 old_tree_oid: Option<&ObjectId>,
2713 new_tree_oid: Option<&ObjectId>,
2714) -> Result<Vec<DiffEntry>> {
2715 crate::trace::trace_span!("grit.diff.tree");
2716 let hint = index
2717 .has_sparse_directory_placeholders()
2718 .then_some(DiffTreesWalkHint {
2719 index: Some(index),
2720 skip_sparse_directories: true,
2721 ..DiffTreesWalkHint::default()
2722 });
2723 diff_trees_opts(odb, old_tree_oid, new_tree_oid, "", false, hint.as_ref())
2724}
2725
2726pub(crate) fn diff_trees_for_pathspecs(
2730 odb: &Odb,
2731 old_tree_oid: Option<&ObjectId>,
2732 new_tree_oid: Option<&ObjectId>,
2733 pathspecs: &[String],
2734) -> Result<Vec<DiffEntry>> {
2735 if pathspecs.is_empty() {
2736 return diff_trees(odb, old_tree_oid, new_tree_oid, "");
2737 }
2738 crate::trace::trace_span!("grit.diff.tree");
2739 let hint = DiffTreesWalkHint {
2740 pathspecs,
2741 ..DiffTreesWalkHint::default()
2742 };
2743 let mut entries = diff_trees_opts(odb, old_tree_oid, new_tree_oid, "", false, Some(&hint))?;
2744 entries.retain(|entry| {
2745 let path = entry.path();
2746 !path.is_empty() && crate::pathspec::matches_pathspec_list(path, pathspecs)
2747 });
2748 Ok(entries)
2749}
2750
2751#[derive(Clone, Copy, Default)]
2753struct DiffTreesWalkHint<'a> {
2754 index: Option<&'a Index>,
2755 old_tree_matches: bool,
2757 skip_sparse_directories: bool,
2759 pathspecs: &'a [String],
2761}
2762
2763impl DiffTreesWalkHint<'_> {
2764 fn skips(&self, dir: &str) -> bool {
2766 if dir.is_empty() {
2767 return false;
2768 }
2769 if !self.pathspecs.is_empty()
2770 && !crate::pathspec::pathspec_entry_is_relevant(dir, true, self.pathspecs)
2771 {
2772 return true;
2773 }
2774 let Some(index) = self.index.filter(|_| self.skip_sparse_directories) else {
2775 return false;
2776 };
2777 let mut row = crate::repo_relpath::storage_bytes(dir).into_owned();
2778 row.push(b'/');
2779 index
2780 .find_entry_pos(&row, 0)
2781 .is_some_and(|pos| index.entries[pos].is_sparse_directory_placeholder())
2782 }
2783}
2784
2785pub fn diff_tree_path_change_count(
2793 odb: &Odb,
2794 old_tree_oid: Option<&ObjectId>,
2795 new_tree_oid: Option<&ObjectId>,
2796 prefix: &str,
2797) -> Result<usize> {
2798 if old_tree_oid == new_tree_oid {
2799 return Ok(0);
2800 }
2801 let old_entries = match old_tree_oid {
2802 Some(oid) => read_tree(odb, oid)?,
2803 None => Vec::new(),
2804 };
2805 let new_entries = match new_tree_oid {
2806 Some(oid) => read_tree(odb, oid)?,
2807 None => Vec::new(),
2808 };
2809 let mut count = 0usize;
2810 diff_tree_entries_count(odb, &old_entries, &new_entries, prefix, false, &mut count)?;
2811 Ok(count)
2812}
2813
2814pub fn diff_trees_with_renames(
2823 odb: &Odb,
2824 old_tree_oid: Option<&ObjectId>,
2825 new_tree_oid: Option<&ObjectId>,
2826 prefix: &str,
2827 rename_threshold: u32,
2828) -> Result<Vec<DiffEntry>> {
2829 let config = ConfigSet::new();
2830 let policy = ThreadPolicy::default();
2831 diff_trees_with_renames_and_policy(
2832 odb,
2833 old_tree_oid,
2834 new_tree_oid,
2835 prefix,
2836 rename_threshold,
2837 &config,
2838 &policy,
2839 )
2840}
2841
2842pub fn diff_trees_with_renames_and_policy(
2848 odb: &Odb,
2849 old_tree_oid: Option<&ObjectId>,
2850 new_tree_oid: Option<&ObjectId>,
2851 prefix: &str,
2852 rename_threshold: u32,
2853 config: &ConfigSet,
2854 thread_policy: &ThreadPolicy,
2855) -> Result<Vec<DiffEntry>> {
2856 let mut entries = diff_trees(odb, old_tree_oid, new_tree_oid, prefix)?;
2857 if rename_threshold <= 100 {
2858 entries = detect_renames_with_policy(
2859 odb,
2860 None,
2861 entries,
2862 rename_threshold,
2863 config,
2864 thread_policy,
2865 None,
2866 )?;
2867 }
2868 Ok(entries)
2869}
2870
2871pub fn tree_oid_of_commit(odb: &Odb, commit_oid: &ObjectId) -> Result<ObjectId> {
2878 odb.commit_tree(commit_oid).map_err(|err| match err {
2879 Error::UnexpectedObjectKind { .. } => {
2880 Error::InvalidRef(format!("invalid ref: {commit_oid} is not a commit"))
2881 }
2882 other => other,
2883 })
2884}
2885
2886pub(crate) fn tree_oid_for_diff_side(odb: &Odb, oid: &ObjectId) -> Result<ObjectId> {
2893 use crate::objects::{CommitRef, ObjectKind};
2894 let object = odb.read(oid)?;
2895 match object.kind {
2896 ObjectKind::Commit => Ok(CommitRef::parse(&object.data)?.tree()),
2897 ObjectKind::Tree => Ok(*oid),
2898 actual => Err(Error::InvalidRef(format!(
2899 "invalid ref: {oid} is not a commit or tree ({actual:?})"
2900 ))),
2901 }
2902}
2903
2904pub fn tree_oid_of_commit_in_repo(
2910 repo: &crate::repo::Repository,
2911 commit_oid: &ObjectId,
2912) -> Result<ObjectId> {
2913 if let Some(chain) = repo.odb().commit_graph_chain() {
2914 if let Some(tree) = chain.graph_commit_tree(commit_oid) {
2915 return Ok(tree);
2916 }
2917 }
2918 tree_oid_of_commit(repo.odb(), commit_oid)
2919}
2920
2921pub fn diff_commits(odb: &Odb, base: &ObjectId, tip: &ObjectId) -> Result<Vec<DiffEntry>> {
2927 let base_tree = tree_oid_of_commit(odb, base)?;
2928 let tip_tree = tree_oid_of_commit(odb, tip)?;
2929 diff_trees(odb, Some(&base_tree), Some(&tip_tree), "")
2930}
2931
2932pub fn diff_commits_with_renames(
2941 odb: &Odb,
2942 base: &ObjectId,
2943 tip: &ObjectId,
2944 rename_threshold: u32,
2945) -> Result<Vec<DiffEntry>> {
2946 let config = ConfigSet::new();
2947 let policy = ThreadPolicy::default();
2948 diff_commits_with_renames_and_policy(odb, base, tip, rename_threshold, &config, &policy)
2949}
2950
2951pub fn diff_commits_with_renames_and_policy(
2957 odb: &Odb,
2958 base: &ObjectId,
2959 tip: &ObjectId,
2960 rename_threshold: u32,
2961 config: &ConfigSet,
2962 thread_policy: &ThreadPolicy,
2963) -> Result<Vec<DiffEntry>> {
2964 let base_tree = tree_oid_of_commit(odb, base)?;
2965 let tip_tree = tree_oid_of_commit(odb, tip)?;
2966 diff_trees_with_renames_and_policy(
2967 odb,
2968 Some(&base_tree),
2969 Some(&tip_tree),
2970 "",
2971 rename_threshold,
2972 config,
2973 thread_policy,
2974 )
2975}
2976
2977pub fn diff_trees_show_tree_entries(
2984 odb: &Odb,
2985 old_tree_oid: Option<&ObjectId>,
2986 new_tree_oid: Option<&ObjectId>,
2987 prefix: &str,
2988) -> Result<Vec<DiffEntry>> {
2989 diff_trees_opts(odb, old_tree_oid, new_tree_oid, prefix, true, None)
2990}
2991
2992fn diff_subtrees_in_place(
2993 odb: &Odb,
2994 old_tree_oid: Option<&ObjectId>,
2995 new_tree_oid: Option<&ObjectId>,
2996 prefix: &str,
2997 show_trees: bool,
2998 index_hint: Option<&DiffTreesWalkHint<'_>>,
2999 result: &mut Vec<DiffEntry>,
3000) -> Result<()> {
3001 if index_hint.is_some_and(|hint| hint.skips(prefix)) {
3002 return Ok(());
3003 }
3004 if !show_trees {
3005 if let (Some(a), Some(b)) = (old_tree_oid, new_tree_oid) {
3006 if a == b {
3007 return Ok(());
3008 }
3009 }
3010 }
3011 let old_tree = old_tree_oid
3014 .map(|oid| odb.cached_tree_for_walk(oid))
3015 .transpose()?;
3016 let new_tree = new_tree_oid
3017 .map(|oid| odb.cached_tree_for_walk(oid))
3018 .transpose()?;
3019 let oid_len = odb.hash_algo().len();
3020 diff_tree_entries_opts(
3021 odb,
3022 WalkEntries::new(old_tree.as_ref(), oid_len),
3023 WalkEntries::new(new_tree.as_ref(), oid_len),
3024 prefix,
3025 show_trees,
3026 index_hint,
3027 result,
3028 )
3029}
3030
3031enum WalkEntries<'a> {
3034 Parsed(std::slice::Iter<'a, TreeEntry>),
3035 Raw(TreeRefIter<'a>),
3036}
3037
3038impl<'a> WalkEntries<'a> {
3039 fn new(tree: Option<&'a SessionTree>, oid_len: usize) -> Self {
3041 match tree {
3042 Some(SessionTree::Parsed(entries)) => Self::Parsed(entries.iter()),
3043 Some(SessionTree::Raw(data)) => Self::Raw(TreeRefIter::with_oid_len(data, oid_len)),
3044 None => Self::Parsed([].iter()),
3045 }
3046 }
3047
3048 fn next_entry(&mut self) -> Result<Option<TreeEntryRef<'a>>> {
3049 match self {
3050 Self::Parsed(entries) => Ok(entries.next().map(|entry| TreeEntryRef {
3051 mode: entry.mode,
3052 name: &entry.name,
3053 oid: entry.oid,
3054 })),
3055 Self::Raw(entries) => entries.next().transpose(),
3056 }
3057 }
3058}
3059
3060fn diff_trees_opts(
3061 odb: &Odb,
3062 old_tree_oid: Option<&ObjectId>,
3063 new_tree_oid: Option<&ObjectId>,
3064 prefix: &str,
3065 show_trees: bool,
3066 index_hint: Option<&DiffTreesWalkHint<'_>>,
3067) -> Result<Vec<DiffEntry>> {
3068 let mut result = Vec::new();
3069 diff_subtrees_in_place(
3070 odb,
3071 old_tree_oid,
3072 new_tree_oid,
3073 prefix,
3074 show_trees,
3075 index_hint,
3076 &mut result,
3077 )?;
3078 Ok(result)
3079}
3080
3081#[cfg(test)]
3082use std::cell::RefCell;
3083#[cfg(test)]
3084use std::rc::Rc;
3085#[cfg(test)]
3086use std::sync::atomic::{AtomicUsize, Ordering};
3087
3088#[cfg(test)]
3089thread_local! {
3090 static DIFF_TREE_READ_HOOK: RefCell<Option<Rc<AtomicUsize>>> = const { RefCell::new(None) };
3091}
3092
3093#[cfg(test)]
3095pub(crate) fn set_diff_tree_read_hook_for_tests(hook: Option<Rc<AtomicUsize>>) {
3096 DIFF_TREE_READ_HOOK.with(|cell| *cell.borrow_mut() = hook);
3097}
3098
3099fn read_tree_from_odb(odb: &Odb, oid: &ObjectId) -> Result<Vec<TreeEntry>> {
3101 #[cfg(test)]
3102 if let Some(counter) = DIFF_TREE_READ_HOOK.with(|cell| cell.borrow().clone()) {
3103 counter.fetch_add(1, Ordering::Relaxed);
3104 }
3105 Ok((*odb.cached_tree_entries(oid)?).clone())
3106}
3107
3108fn read_tree(odb: &Odb, oid: &ObjectId) -> Result<Vec<TreeEntry>> {
3110 read_tree_from_odb(odb, oid)
3111}
3112
3113fn diff_tree_entries_count(
3114 odb: &Odb,
3115 old: &[TreeEntry],
3116 new: &[TreeEntry],
3117 prefix: &str,
3118 show_trees: bool,
3119 count: &mut usize,
3120) -> Result<()> {
3121 let mut oi = 0;
3122 let mut ni = 0;
3123
3124 while oi < old.len() || ni < new.len() {
3125 match (old.get(oi), new.get(ni)) {
3126 (Some(o), Some(n)) => {
3127 let cmp = crate::objects::tree_entry_cmp(
3128 &o.name,
3129 is_tree_mode(o.mode),
3130 &n.name,
3131 is_tree_mode(n.mode),
3132 );
3133 match cmp {
3134 std::cmp::Ordering::Less => {
3135 emit_deleted_count(odb, o, prefix, show_trees, count)?;
3136 oi += 1;
3137 }
3138 std::cmp::Ordering::Greater => {
3139 emit_added_count(odb, n, prefix, show_trees, count)?;
3140 ni += 1;
3141 }
3142 std::cmp::Ordering::Equal => {
3143 if o.oid != n.oid
3144 || crate::index::canon_mode(o.mode) != crate::index::canon_mode(n.mode)
3145 {
3146 let path = crate::repo_relpath::format_path_stored(prefix, &o.name);
3147 if is_tree_mode(o.mode) && is_tree_mode(n.mode) {
3148 if show_trees {
3149 *count += 1;
3150 }
3151 *count += diff_tree_path_change_count(
3152 odb,
3153 Some(&o.oid),
3154 Some(&n.oid),
3155 &path,
3156 )?;
3157 } else if is_tree_mode(o.mode) != is_tree_mode(n.mode) {
3158 emit_deleted_count(odb, o, prefix, show_trees, count)?;
3159 emit_added_count(odb, n, prefix, show_trees, count)?;
3160 } else {
3161 *count += 1;
3162 }
3163 }
3164 oi += 1;
3165 ni += 1;
3166 }
3167 }
3168 }
3169 (Some(o), None) => {
3170 emit_deleted_count(odb, o, prefix, show_trees, count)?;
3171 oi += 1;
3172 }
3173 (None, Some(n)) => {
3174 emit_added_count(odb, n, prefix, show_trees, count)?;
3175 ni += 1;
3176 }
3177 (None, None) => break,
3178 }
3179 }
3180
3181 Ok(())
3182}
3183
3184fn emit_deleted_count(
3185 odb: &Odb,
3186 entry: &TreeEntry,
3187 prefix: &str,
3188 show_trees: bool,
3189 count: &mut usize,
3190) -> Result<()> {
3191 let path = crate::repo_relpath::format_path_stored(prefix, &entry.name);
3192 if is_tree_mode(entry.mode) {
3193 if show_trees {
3194 *count += 1;
3195 }
3196 *count += diff_tree_path_change_count(odb, Some(&entry.oid), None, &path)?;
3197 } else {
3198 *count += 1;
3199 }
3200 Ok(())
3201}
3202
3203fn emit_added_count(
3204 odb: &Odb,
3205 entry: &TreeEntry,
3206 prefix: &str,
3207 show_trees: bool,
3208 count: &mut usize,
3209) -> Result<()> {
3210 let path = crate::repo_relpath::format_path_stored(prefix, &entry.name);
3211 if is_tree_mode(entry.mode) {
3212 if show_trees {
3213 *count += 1;
3214 }
3215 *count += diff_tree_path_change_count(odb, None, Some(&entry.oid), &path)?;
3216 } else {
3217 *count += 1;
3218 }
3219 Ok(())
3220}
3221
3222fn diff_tree_entries_opts(
3224 odb: &Odb,
3225 mut old: WalkEntries<'_>,
3226 mut new: WalkEntries<'_>,
3227 prefix: &str,
3228 show_trees: bool,
3229 index_hint: Option<&DiffTreesWalkHint<'_>>,
3230 result: &mut Vec<DiffEntry>,
3231) -> Result<()> {
3232 let mut old_entry = old.next_entry()?;
3233 let mut new_entry = new.next_entry()?;
3234
3235 loop {
3236 match (&old_entry, &new_entry) {
3237 (Some(o), Some(n)) => {
3238 let cmp = crate::objects::tree_entry_cmp(
3239 o.name,
3240 is_tree_mode(o.mode),
3241 n.name,
3242 is_tree_mode(n.mode),
3243 );
3244 match cmp {
3245 std::cmp::Ordering::Less => {
3246 emit_deleted_opts(odb, o, prefix, show_trees, index_hint, result)?;
3248 old_entry = old.next_entry()?;
3249 }
3250 std::cmp::Ordering::Greater => {
3251 emit_added_opts(odb, n, prefix, show_trees, index_hint, result)?;
3253 new_entry = new.next_entry()?;
3254 }
3255 std::cmp::Ordering::Equal => {
3256 if o.oid != n.oid
3258 || crate::index::canon_mode(o.mode) != crate::index::canon_mode(n.mode)
3259 {
3260 let path = crate::repo_relpath::format_path_stored(prefix, o.name);
3261 if is_tree_mode(o.mode) && is_tree_mode(n.mode) {
3262 if show_trees {
3264 result.push(DiffEntry {
3265 status: DiffStatus::Modified,
3266 old_path: Some(path.clone()),
3267 new_path: Some(path.clone()),
3268 old_mode: FileMode::new(crate::index::canon_mode(o.mode)),
3269 new_mode: FileMode::new(crate::index::canon_mode(n.mode)),
3270 old_oid: o.oid,
3271 new_oid: n.oid,
3272 score: None,
3273 });
3274 }
3275 diff_subtrees_in_place(
3276 odb,
3277 Some(&o.oid),
3278 Some(&n.oid),
3279 &path,
3280 show_trees,
3281 index_hint,
3282 result,
3283 )?;
3284 } else if is_tree_mode(o.mode) && !is_tree_mode(n.mode) {
3285 emit_deleted_opts(odb, o, prefix, show_trees, index_hint, result)?;
3287 emit_added_opts(odb, n, prefix, show_trees, index_hint, result)?;
3288 } else if !is_tree_mode(o.mode) && is_tree_mode(n.mode) {
3289 emit_deleted_opts(odb, o, prefix, show_trees, index_hint, result)?;
3291 emit_added_opts(odb, n, prefix, show_trees, index_hint, result)?;
3292 } else {
3293 result.push(DiffEntry {
3294 status: DiffStatus::for_content_change(o.mode, n.mode),
3295 old_path: Some(path.clone()),
3296 new_path: Some(path),
3297 old_mode: FileMode::new(crate::index::canon_mode(o.mode)),
3298 new_mode: FileMode::new(crate::index::canon_mode(n.mode)),
3299 old_oid: o.oid,
3300 new_oid: n.oid,
3301 score: None,
3302 });
3303 }
3304 }
3305 old_entry = old.next_entry()?;
3306 new_entry = new.next_entry()?;
3307 }
3308 }
3309 }
3310 (Some(o), None) => {
3311 emit_deleted_opts(odb, o, prefix, show_trees, index_hint, result)?;
3312 old_entry = old.next_entry()?;
3313 }
3314 (None, Some(n)) => {
3315 emit_added_opts(odb, n, prefix, show_trees, index_hint, result)?;
3316 new_entry = new.next_entry()?;
3317 }
3318 (None, None) => break,
3319 }
3320 }
3321
3322 Ok(())
3323}
3324
3325fn emit_deleted_tree_from_index(index: &Index, dir_prefix: &str, result: &mut Vec<DiffEntry>) {
3326 let scan_prefix: Vec<u8> = if dir_prefix.is_empty() {
3327 Vec::new()
3328 } else {
3329 format!("{dir_prefix}/").into_bytes()
3330 };
3331 if index.entries_sorted {
3332 let start = if scan_prefix.is_empty() {
3333 0
3334 } else {
3335 index
3336 .entries
3337 .partition_point(|e| e.path.as_slice() < scan_prefix.as_slice())
3338 };
3339 for ie in index.entries[start..].iter() {
3340 if ie.stage() != 0 {
3341 continue;
3342 }
3343 if !scan_prefix.is_empty() && !ie.path.starts_with(&scan_prefix) {
3344 break;
3345 }
3346 if scan_prefix.is_empty() && ie.path.contains(&b'/') {
3347 continue;
3348 }
3349 let path = String::from_utf8_lossy(&ie.path).into_owned();
3350 result.push(DiffEntry {
3351 status: DiffStatus::Deleted,
3352 old_path: Some(path),
3353 new_path: None,
3354 old_mode: FileMode::new(ie.mode),
3355 new_mode: FileMode::ABSENT,
3356 old_oid: ie.oid,
3357 new_oid: zero_oid(),
3358 score: None,
3359 });
3360 }
3361 return;
3362 }
3363 for ie in &index.entries {
3364 if ie.stage() != 0 {
3365 continue;
3366 }
3367 let Ok(path) = std::str::from_utf8(&ie.path) else {
3368 continue;
3369 };
3370 let under = if dir_prefix.is_empty() {
3371 !path.contains('/')
3372 } else if path == dir_prefix {
3373 false
3374 } else {
3375 path.strip_prefix(dir_prefix)
3376 .is_some_and(|rest| rest.starts_with('/'))
3377 };
3378 if !under {
3379 continue;
3380 }
3381 result.push(DiffEntry {
3382 status: DiffStatus::Deleted,
3383 old_path: Some(path.to_owned()),
3384 new_path: None,
3385 old_mode: FileMode::new(ie.mode),
3386 new_mode: FileMode::ABSENT,
3387 old_oid: ie.oid,
3388 new_oid: zero_oid(),
3389 score: None,
3390 });
3391 }
3392}
3393
3394fn emit_deleted_opts(
3395 odb: &Odb,
3396 entry: &TreeEntryRef<'_>,
3397 prefix: &str,
3398 show_trees: bool,
3399 index_hint: Option<&DiffTreesWalkHint<'_>>,
3400 result: &mut Vec<DiffEntry>,
3401) -> Result<()> {
3402 let path = crate::repo_relpath::format_path_stored(prefix, entry.name);
3403 if is_tree_mode(entry.mode) {
3404 if show_trees {
3405 result.push(DiffEntry {
3406 status: DiffStatus::Deleted,
3407 old_path: Some(path.clone()),
3408 new_path: None,
3409 old_mode: FileMode::new(entry.mode),
3410 new_mode: FileMode::ABSENT,
3411 old_oid: entry.oid,
3412 new_oid: zero_oid(),
3413 score: None,
3414 });
3415 }
3416 if let Some(index) = index_hint
3417 .filter(|hint| hint.old_tree_matches)
3418 .and_then(|hint| hint.index)
3419 .filter(|index| !index.has_sparse_directory_placeholders())
3420 {
3421 emit_deleted_tree_from_index(index, &path, result);
3422 return Ok(());
3423 }
3424 diff_subtrees_in_place(
3425 odb,
3426 Some(&entry.oid),
3427 None,
3428 &path,
3429 show_trees,
3430 index_hint,
3431 result,
3432 )?;
3433 } else {
3434 result.push(DiffEntry {
3435 status: DiffStatus::Deleted,
3436 old_path: Some(path.clone()),
3437 new_path: None,
3438 old_mode: FileMode::new(crate::index::canon_mode(entry.mode)),
3439 new_mode: FileMode::ABSENT,
3440 old_oid: entry.oid,
3441 new_oid: zero_oid(),
3442 score: None,
3443 });
3444 }
3445 Ok(())
3446}
3447
3448fn emit_added_opts(
3449 odb: &Odb,
3450 entry: &TreeEntryRef<'_>,
3451 prefix: &str,
3452 show_trees: bool,
3453 index_hint: Option<&DiffTreesWalkHint<'_>>,
3454 result: &mut Vec<DiffEntry>,
3455) -> Result<()> {
3456 let path = crate::repo_relpath::format_path_stored(prefix, entry.name);
3457 if is_tree_mode(entry.mode) {
3458 if show_trees {
3459 result.push(DiffEntry {
3460 status: DiffStatus::Added,
3461 old_path: None,
3462 new_path: Some(path.clone()),
3463 old_mode: FileMode::ABSENT,
3464 new_mode: FileMode::new(entry.mode),
3465 old_oid: zero_oid(),
3466 new_oid: entry.oid,
3467 score: None,
3468 });
3469 }
3470 diff_subtrees_in_place(
3471 odb,
3472 None,
3473 Some(&entry.oid),
3474 &path,
3475 show_trees,
3476 index_hint,
3477 result,
3478 )?;
3479 } else {
3480 result.push(DiffEntry {
3481 status: DiffStatus::Added,
3482 old_path: None,
3483 new_path: Some(path),
3484 old_mode: FileMode::ABSENT,
3485 new_mode: FileMode::new(crate::index::canon_mode(entry.mode)),
3486 old_oid: zero_oid(),
3487 new_oid: entry.oid,
3488 score: None,
3489 });
3490 }
3491 Ok(())
3492}
3493
3494pub fn diff_index_to_tree(
3514 odb: &Odb,
3515 index: &Index,
3516 tree_oid: Option<&ObjectId>,
3517 ignore_submodules: bool,
3518) -> Result<Vec<DiffEntry>> {
3519 match index_cache_tree_walk(index, tree_oid) {
3520 CacheTreeWalk::Fast {
3521 head_tree,
3522 cache_root,
3523 } => {
3524 diff_index_to_tree_with_cache_tree(odb, index, head_tree, cache_root, ignore_submodules)
3525 }
3526 CacheTreeWalk::Flatten => {
3527 diff_index_to_tree_flatten(odb, index, tree_oid, ignore_submodules)
3528 }
3529 }
3530}
3531
3532pub fn diff_index_to_tree_with_renames(
3541 odb: &Odb,
3542 index: &Index,
3543 tree_oid: Option<&ObjectId>,
3544 ignore_submodules: bool,
3545 rename_threshold: u32,
3546 config: &ConfigSet,
3547 thread_policy: &ThreadPolicy,
3548) -> Result<Vec<DiffEntry>> {
3549 let entries = diff_index_to_tree(odb, index, tree_oid, ignore_submodules)?;
3550 apply_rename_detection_to_diff_entries(
3551 odb,
3552 entries,
3553 None,
3554 rename_threshold,
3555 config,
3556 thread_policy,
3557 )
3558}
3559
3560pub fn apply_rename_detection_to_diff_entries(
3569 odb: &Odb,
3570 entries: Vec<DiffEntry>,
3571 work_root: Option<&Path>,
3572 rename_threshold: u32,
3573 config: &ConfigSet,
3574 thread_policy: &ThreadPolicy,
3575) -> Result<Vec<DiffEntry>> {
3576 if entries.is_empty() || rename_threshold >= 100 {
3577 return Ok(entries);
3578 }
3579 if work_root.is_some() {
3580 let opts = DiffPostprocessOptions {
3581 rename_threshold,
3582 ..DiffPostprocessOptions::default()
3583 };
3584 postprocess_diff_entries(
3585 odb,
3586 entries,
3587 None,
3588 work_root,
3589 None,
3590 config,
3591 thread_policy,
3592 &opts,
3593 )
3594 } else {
3595 detect_renames_with_policy(
3596 odb,
3597 None,
3598 entries,
3599 rename_threshold,
3600 config,
3601 thread_policy,
3602 None,
3603 )
3604 }
3605}
3606
3607pub fn diff_index_to_worktree_with_renames(
3617 repo: &Repository,
3618 index: &mut Index,
3619 work_tree: &Path,
3620 options: DiffIndexToWorktreeOptions,
3621 rename_threshold: u32,
3622) -> Result<(Vec<DiffEntry>, bool)> {
3623 let config = repo.config()?.as_ref().clone();
3624 let thread_policy = repo.thread_policy();
3625 let odb = repo.odb();
3626 let (entries, changed) = diff_index_to_worktree_with_options(odb, index, work_tree, options)?;
3627 let entries = apply_rename_detection_to_diff_entries(
3628 odb,
3629 entries,
3630 Some(work_tree),
3631 rename_threshold,
3632 &config,
3633 &thread_policy,
3634 )?;
3635 Ok((entries, changed))
3636}
3637
3638enum CacheTreeWalk<'a> {
3639 Fast {
3640 head_tree: &'a ObjectId,
3641 cache_root: &'a CacheTreeNode,
3642 },
3643 Flatten,
3644}
3645
3646fn index_cache_tree_walk<'a>(
3649 index: &'a Index,
3650 tree_oid: Option<&'a ObjectId>,
3651) -> CacheTreeWalk<'a> {
3652 let Some(head_tree) = tree_oid else {
3653 return CacheTreeWalk::Flatten;
3654 };
3655 let Some(cache_root) = index.cache_tree.as_ref() else {
3656 return CacheTreeWalk::Flatten;
3657 };
3658 if index.entries.iter().any(|e| e.stage() != 0) {
3659 return CacheTreeWalk::Flatten;
3660 }
3661 CacheTreeWalk::Fast {
3662 head_tree,
3663 cache_root,
3664 }
3665}
3666
3667fn diff_index_to_tree_flatten(
3669 odb: &Odb,
3670 index: &Index,
3671 tree_oid: Option<&ObjectId>,
3672 ignore_submodules: bool,
3673) -> Result<Vec<DiffEntry>> {
3674 let sparse_index = index.has_sparse_directory_placeholders();
3675 let tree_entries = match tree_oid {
3677 Some(oid) if sparse_index => flatten_tree_with_index(odb, oid, "", Some(index))?,
3678 Some(oid) => flatten_tree(odb, oid, "")?,
3679 None => Vec::new(),
3680 };
3681
3682 let mut tree_map: std::collections::BTreeMap<&str, &FlatEntry> =
3684 std::collections::BTreeMap::new();
3685 for entry in &tree_entries {
3686 tree_map.insert(&entry.path, entry);
3687 }
3688
3689 let mut result = Vec::new();
3690 let mut stage0_paths = std::collections::BTreeSet::new();
3691 let mut unmerged_modes: std::collections::BTreeMap<String, (u8, u32)> =
3692 std::collections::BTreeMap::new();
3693
3694 for ie in &index.entries {
3696 let path = String::from_utf8_lossy(&ie.path).to_string();
3697 if ie.stage() == 0 && ie.intent_to_add() {
3698 continue;
3701 }
3702 if ie.stage() == 0 && ie.is_sparse_directory_placeholder() {
3703 let dir = path.strip_suffix('/').unwrap_or(path.as_str());
3704 let index_flat = flatten_tree(odb, &ie.oid, dir)?;
3705 let head_flat = match tree_oid {
3706 None => Vec::new(),
3707 Some(head_root) => match subtree_oid_at_path(odb, head_root, dir)? {
3708 None => Vec::new(),
3709 Some(head_sub) if head_sub == ie.oid => {
3710 continue;
3711 }
3712 Some(head_sub) => flatten_tree(odb, &head_sub, dir)?,
3713 },
3714 };
3715 diff_flattened_subtrees(
3716 &head_flat,
3717 &index_flat,
3718 ignore_submodules,
3719 &mut result,
3720 &mut stage0_paths,
3721 &mut tree_map,
3722 );
3723 continue;
3724 }
3725 if ie.stage() != 0 {
3726 let rank = match ie.stage() {
3727 2 => 0u8,
3728 3 => 1u8,
3729 1 => 2u8,
3730 _ => 3u8,
3731 };
3732 match unmerged_modes.get(&path) {
3733 Some((existing_rank, _)) if *existing_rank <= rank => {}
3734 _ => {
3735 unmerged_modes.insert(path, (rank, ie.mode));
3736 }
3737 }
3738 continue;
3739 }
3740 if ignore_submodules && ie.mode == 0o160000 {
3741 let _ = tree_map.remove(path.as_str());
3742 stage0_paths.insert(path.clone());
3743 continue;
3744 }
3745 stage0_paths.insert(path.clone());
3746 match tree_map.remove(path.as_str()) {
3747 Some(te) => {
3748 if te.oid != ie.oid
3750 || crate::index::canon_mode(te.mode) != crate::index::canon_mode(ie.mode)
3751 {
3752 let tree_mode = crate::index::canon_mode(te.mode);
3753 let index_mode = crate::index::canon_mode(ie.mode);
3754 result.push(DiffEntry {
3755 status: DiffStatus::for_content_change(tree_mode, index_mode),
3756 old_path: Some(path.clone()),
3757 new_path: Some(path),
3758 old_mode: FileMode::new(tree_mode),
3759 new_mode: FileMode::new(index_mode),
3760 old_oid: te.oid,
3761 new_oid: ie.oid,
3762 score: None,
3763 });
3764 }
3765 }
3766 None => {
3767 result.push(DiffEntry {
3769 status: DiffStatus::Added,
3770 old_path: None,
3771 new_path: Some(path),
3772 old_mode: FileMode::ABSENT,
3773 new_mode: FileMode::new(ie.mode),
3774 old_oid: zero_oid(),
3775 new_oid: ie.oid,
3776 score: None,
3777 });
3778 }
3779 }
3780 }
3781
3782 diff_index_to_tree_finish_unmerged_and_deleted(
3783 ignore_submodules,
3784 &mut tree_map,
3785 &unmerged_modes,
3786 &stage0_paths,
3787 &mut result,
3788 );
3789
3790 sort_entries_by_path(&mut result);
3791 Ok(result)
3792}
3793
3794fn diff_index_to_tree_finish_unmerged_and_deleted(
3795 ignore_submodules: bool,
3796 tree_map: &mut std::collections::BTreeMap<&str, &FlatEntry>,
3797 unmerged_modes: &std::collections::BTreeMap<String, (u8, u32)>,
3798 stage0_paths: &std::collections::BTreeSet<String>,
3799 result: &mut Vec<DiffEntry>,
3800) {
3801 for (path, (_, mode)) in unmerged_modes {
3802 if stage0_paths.contains(path) {
3803 continue;
3804 }
3805 tree_map.remove(path.as_str());
3806 result.push(DiffEntry {
3807 status: DiffStatus::Unmerged,
3808 old_path: Some(path.clone()),
3809 new_path: Some(path.clone()),
3810 old_mode: FileMode::ABSENT,
3811 new_mode: FileMode::new(*mode),
3812 old_oid: zero_oid(),
3813 new_oid: zero_oid(),
3814 score: None,
3815 });
3816 }
3817
3818 for (path, te) in std::mem::take(tree_map) {
3820 if ignore_submodules && te.mode == 0o160000 {
3821 continue;
3822 }
3823 result.push(DiffEntry {
3824 status: DiffStatus::Deleted,
3825 old_path: Some(path.to_owned()),
3826 new_path: None,
3827 old_mode: FileMode::new(te.mode),
3828 new_mode: FileMode::ABSENT,
3829 old_oid: te.oid,
3830 new_oid: zero_oid(),
3831 score: None,
3832 });
3833 }
3834}
3835
3836fn diff_index_to_tree_with_cache_tree(
3839 odb: &Odb,
3840 index: &Index,
3841 head_tree: &ObjectId,
3842 cache_root: &CacheTreeNode,
3843 ignore_submodules: bool,
3844) -> Result<Vec<DiffEntry>> {
3845 let mut result = Vec::new();
3846 let mut idx_cursor = 0usize;
3847
3848 let mut walk = DiffIndexTreeWalk {
3849 odb,
3850 index,
3851 idx_cursor: &mut idx_cursor,
3852 ignore_submodules,
3853 result: &mut result,
3854 abort_to_flatten: false,
3855 };
3856 diff_index_tree_at(&mut walk, head_tree, Some(cache_root), b"")?;
3857 if walk.abort_to_flatten {
3858 return diff_index_to_tree_flatten(odb, index, Some(head_tree), ignore_submodules);
3859 }
3860
3861 while idx_cursor < index.entries.len() {
3863 let ie = &index.entries[idx_cursor];
3864 idx_cursor += 1;
3865 let path = String::from_utf8_lossy(&ie.path).to_string();
3866 if ie.stage() == 0 && ie.intent_to_add() {
3867 continue;
3868 }
3869 if ie.stage() != 0 {
3870 continue;
3871 }
3872 if ignore_submodules && ie.mode == 0o160000 {
3873 continue;
3874 }
3875 result.push(DiffEntry {
3876 status: DiffStatus::Added,
3877 old_path: None,
3878 new_path: Some(path),
3879 old_mode: FileMode::ABSENT,
3880 new_mode: FileMode::new(ie.mode),
3881 old_oid: zero_oid(),
3882 new_oid: ie.oid,
3883 score: None,
3884 });
3885 }
3886
3887 sort_entries_by_path(&mut result);
3888 Ok(result)
3889}
3890
3891enum IndexLevelKind<'a> {
3892 Blob {
3893 entry: &'a IndexEntry,
3894 name: &'a [u8],
3895 },
3896 Tree {
3897 name: &'a [u8],
3898 },
3899}
3900
3901fn index_relative_in_dir<'a>(path: &'a [u8], dir_prefix: &[u8]) -> Option<&'a [u8]> {
3904 if dir_prefix.is_empty() {
3905 return Some(path);
3906 }
3907 let rest = path.strip_prefix(dir_prefix)?;
3908 match rest.split_first() {
3909 None => Some(rest),
3910 Some((b'/', tail)) => Some(tail),
3911 Some(_) => None,
3912 }
3913}
3914
3915fn join_index_path(dir_prefix: &[u8], name: &[u8]) -> Vec<u8> {
3916 if dir_prefix.is_empty() {
3917 return name.to_vec();
3918 }
3919 let mut path = Vec::with_capacity(dir_prefix.len() + 1 + name.len());
3920 path.extend_from_slice(dir_prefix);
3921 path.push(b'/');
3922 path.extend_from_slice(name);
3923 path
3924}
3925
3926fn peek_index_level_entry<'a>(
3927 index: &'a Index,
3928 cursor: &mut usize,
3929 dir_prefix: &[u8],
3930 ignore_submodules: bool,
3931) -> Option<IndexLevelKind<'a>> {
3932 while let Some(ie) = index.entries.get(*cursor) {
3933 let rel = index_relative_in_dir(&ie.path, dir_prefix)?;
3934 if ie.stage() != 0 || ie.intent_to_add() || rel.is_empty() {
3935 *cursor += 1;
3936 continue;
3937 }
3938 if let Some(slash) = rel.iter().position(|&b| b == b'/') {
3939 return Some(IndexLevelKind::Tree {
3940 name: &rel[..slash],
3941 });
3942 }
3943 if ignore_submodules && ie.mode == 0o160000 {
3944 *cursor += 1;
3945 continue;
3946 }
3947 return Some(IndexLevelKind::Blob {
3948 entry: ie,
3949 name: rel,
3950 });
3951 }
3952 None
3953}
3954
3955fn skip_index_subtree(index: &Index, cursor: &mut usize, dir_prefix: &[u8]) {
3956 while index
3957 .entries
3958 .get(*cursor)
3959 .is_some_and(|ie| index_relative_in_dir(&ie.path, dir_prefix).is_some())
3960 {
3961 *cursor += 1;
3962 }
3963}
3964
3965const CACHE_CHILD_LINEAR_SCAN_MAX: usize = 16;
3967
3968struct CacheChildren<'a> {
3974 children: &'a [CacheTreeNode],
3975 sorted: Vec<&'a CacheTreeNode>,
3976}
3977
3978impl<'a> CacheChildren<'a> {
3979 fn new(node: Option<&'a CacheTreeNode>) -> Self {
3980 let children = node.map_or(&[][..], |n| n.children.as_slice());
3981 let sorted = if children.len() > CACHE_CHILD_LINEAR_SCAN_MAX {
3982 let mut sorted: Vec<&CacheTreeNode> = children.iter().collect();
3983 sorted.sort_by(|a, b| a.name.cmp(&b.name));
3984 sorted
3985 } else {
3986 Vec::new()
3987 };
3988 Self { children, sorted }
3989 }
3990
3991 fn get(&self, name: &[u8]) -> Option<&'a CacheTreeNode> {
3992 if self.sorted.is_empty() {
3993 return self.children.iter().find(|c| c.name == name);
3994 }
3995 let pos = self.sorted.partition_point(|c| c.name.as_slice() < name);
3996 self.sorted.get(pos).copied().filter(|c| c.name == name)
3997 }
3998}
3999
4000struct DiffIndexTreeWalk<'a, 'b> {
4001 odb: &'a Odb,
4002 index: &'a Index,
4003 idx_cursor: &'b mut usize,
4004 ignore_submodules: bool,
4005 result: &'b mut Vec<DiffEntry>,
4006 abort_to_flatten: bool,
4008}
4009
4010fn tree_entries_have_duplicate_names(entries: &[TreeEntry]) -> bool {
4011 let mut seen = std::collections::BTreeSet::<&[u8]>::new();
4012 for entry in entries {
4013 if !seen.insert(entry.name.as_slice()) {
4014 return true;
4015 }
4016 }
4017 false
4018}
4019
4020fn diff_index_tree_at(
4022 walk: &mut DiffIndexTreeWalk<'_, '_>,
4023 tree_oid: &ObjectId,
4024 cache_node: Option<&CacheTreeNode>,
4025 dir_prefix: &[u8],
4026) -> Result<()> {
4027 if !dir_prefix.is_empty() {
4028 let mut path = dir_prefix.to_vec();
4029 path.push(b'/');
4030 if let Some(entry) = walk
4031 .index
4032 .entries
4033 .get(*walk.idx_cursor)
4034 .filter(|entry| entry.path == path && entry.is_sparse_directory_placeholder())
4035 {
4036 if entry.oid == *tree_oid {
4037 *walk.idx_cursor += 1;
4038 return Ok(());
4039 }
4040 let dir = String::from_utf8_lossy(dir_prefix);
4041 let head_flat = flatten_tree(walk.odb, tree_oid, dir.as_ref())?;
4042 let index_flat = flatten_tree(walk.odb, &entry.oid, dir.as_ref())?;
4043 let mut stage0_paths = std::collections::BTreeSet::new();
4044 let mut tree_map = std::collections::BTreeMap::new();
4045 diff_flattened_subtrees(
4046 &head_flat,
4047 &index_flat,
4048 walk.ignore_submodules,
4049 walk.result,
4050 &mut stage0_paths,
4051 &mut tree_map,
4052 );
4053 *walk.idx_cursor += 1;
4054 return Ok(());
4055 }
4056 }
4057 let tree_entries = read_tree(walk.odb, tree_oid)?;
4058 if tree_entries_have_duplicate_names(&tree_entries) {
4059 walk.abort_to_flatten = true;
4060 return Ok(());
4061 }
4062 let cache_children = CacheChildren::new(cache_node);
4063 let mut ti = 0usize;
4064
4065 loop {
4066 let te = tree_entries.get(ti);
4067 let ie_kind = peek_index_level_entry(
4068 walk.index,
4069 walk.idx_cursor,
4070 dir_prefix,
4071 walk.ignore_submodules,
4072 );
4073
4074 match (te, ie_kind) {
4075 (Some(t_entry), Some(ie)) => {
4076 let t_name = &t_entry.name;
4077 let (i_name, i_is_tree) = match &ie {
4078 IndexLevelKind::Blob { name, .. } => (*name, false),
4079 IndexLevelKind::Tree { name } => (*name, true),
4080 };
4081 let t_is_tree = is_tree_mode(t_entry.mode);
4082 let cmp = crate::objects::tree_entry_cmp(t_name, t_is_tree, i_name, i_is_tree);
4083 match cmp {
4084 std::cmp::Ordering::Less => {
4085 emit_index_tree_deleted(
4086 walk.odb,
4087 t_entry,
4088 dir_prefix,
4089 walk.ignore_submodules,
4090 walk.result,
4091 )?;
4092 ti += 1;
4093 }
4094 std::cmp::Ordering::Greater => {
4095 emit_index_tree_added(walk, dir_prefix)?;
4096 }
4097 std::cmp::Ordering::Equal => {
4098 if t_is_tree && i_is_tree {
4099 let full_path = join_index_path(dir_prefix, t_name);
4100 if diff_index_tree_skip_sparse_placeholder(walk, &full_path, t_entry)? {
4101 ti += 1;
4102 continue;
4103 }
4104 let child_cache = cache_children.get(t_name);
4105 if child_cache
4106 .is_some_and(|c| c.is_valid() && c.oid == Some(t_entry.oid))
4107 {
4108 skip_index_subtree(walk.index, walk.idx_cursor, &full_path);
4109 ti += 1;
4110 continue;
4111 }
4112 diff_index_tree_at(walk, &t_entry.oid, child_cache, &full_path)?;
4113 } else if t_is_tree != i_is_tree {
4114 emit_index_tree_deleted(
4115 walk.odb,
4116 t_entry,
4117 dir_prefix,
4118 walk.ignore_submodules,
4119 walk.result,
4120 )?;
4121 emit_index_tree_added(walk, dir_prefix)?;
4122 } else {
4123 let IndexLevelKind::Blob { entry: ie, .. } = ie else {
4124 ti += 1;
4125 continue;
4126 };
4127 *walk.idx_cursor += 1;
4128 if t_entry.oid != ie.oid
4129 || crate::index::canon_mode(t_entry.mode)
4130 != crate::index::canon_mode(ie.mode)
4131 {
4132 let path = String::from_utf8_lossy(&ie.path).into_owned();
4133 let tree_mode = crate::index::canon_mode(t_entry.mode);
4134 let index_mode = crate::index::canon_mode(ie.mode);
4135 walk.result.push(DiffEntry {
4136 status: DiffStatus::for_content_change(tree_mode, index_mode),
4137 old_path: Some(path.clone()),
4138 new_path: Some(path),
4139 old_mode: FileMode::new(tree_mode),
4140 new_mode: FileMode::new(index_mode),
4141 old_oid: t_entry.oid,
4142 new_oid: ie.oid,
4143 score: None,
4144 });
4145 }
4146 }
4147 ti += 1;
4148 }
4149 }
4150 }
4151 (Some(t_entry), None) => {
4152 emit_index_tree_deleted(
4153 walk.odb,
4154 t_entry,
4155 dir_prefix,
4156 walk.ignore_submodules,
4157 walk.result,
4158 )?;
4159 ti += 1;
4160 }
4161 (None, Some(_)) => {
4162 emit_index_tree_added(walk, dir_prefix)?;
4163 }
4164 (None, None) => break,
4165 }
4166 }
4167
4168 Ok(())
4169}
4170
4171fn diff_index_tree_skip_sparse_placeholder(
4172 walk: &mut DiffIndexTreeWalk<'_, '_>,
4173 full_path: &[u8],
4174 t_entry: &TreeEntry,
4175) -> Result<bool> {
4176 let Some(entry) = walk.index.entries.get(*walk.idx_cursor) else {
4177 return Ok(false);
4178 };
4179 if !entry.is_sparse_directory_placeholder() {
4180 return Ok(false);
4181 }
4182 let mut dir = full_path.to_vec();
4183 if !dir.ends_with(b"/") {
4184 dir.push(b'/');
4185 }
4186 if entry.path != dir || entry.oid != t_entry.oid {
4187 return Ok(false);
4188 }
4189 *walk.idx_cursor += 1;
4190 Ok(true)
4191}
4192
4193fn emit_index_tree_deleted(
4194 odb: &Odb,
4195 entry: &TreeEntry,
4196 dir_prefix: &[u8],
4197 ignore_submodules: bool,
4198 result: &mut Vec<DiffEntry>,
4199) -> Result<()> {
4200 if ignore_submodules && entry.mode == 0o160000 {
4201 return Ok(());
4202 }
4203 let path = String::from_utf8_lossy(&join_index_path(dir_prefix, &entry.name)).into_owned();
4204 if is_tree_mode(entry.mode) {
4205 for fe in flatten_tree(odb, &entry.oid, &path)? {
4206 if ignore_submodules && fe.mode == 0o160000 {
4207 continue;
4208 }
4209 result.push(DiffEntry {
4210 status: DiffStatus::Deleted,
4211 old_path: Some(fe.path.clone()),
4212 new_path: None,
4213 old_mode: FileMode::new(fe.mode),
4214 new_mode: FileMode::ABSENT,
4215 old_oid: fe.oid,
4216 new_oid: zero_oid(),
4217 score: None,
4218 });
4219 }
4220 } else {
4221 result.push(DiffEntry {
4222 status: DiffStatus::Deleted,
4223 old_path: Some(path),
4224 new_path: None,
4225 old_mode: FileMode::new(entry.mode),
4226 new_mode: FileMode::ABSENT,
4227 old_oid: entry.oid,
4228 new_oid: zero_oid(),
4229 score: None,
4230 });
4231 }
4232 Ok(())
4233}
4234
4235fn emit_index_tree_added(walk: &mut DiffIndexTreeWalk<'_, '_>, dir_prefix: &[u8]) -> Result<()> {
4236 let ie_kind = peek_index_level_entry(
4237 walk.index,
4238 walk.idx_cursor,
4239 dir_prefix,
4240 walk.ignore_submodules,
4241 )
4242 .ok_or_else(|| Error::IndexError("missing index entry during tree walk".to_owned()))?;
4243 match ie_kind {
4244 IndexLevelKind::Blob { entry: ie, .. } => {
4245 let path = String::from_utf8_lossy(&ie.path).into_owned();
4246 *walk.idx_cursor += 1;
4247 walk.result.push(DiffEntry {
4248 status: DiffStatus::Added,
4249 old_path: None,
4250 new_path: Some(path),
4251 old_mode: FileMode::ABSENT,
4252 new_mode: FileMode::new(ie.mode),
4253 old_oid: zero_oid(),
4254 new_oid: ie.oid,
4255 score: None,
4256 });
4257 }
4258 IndexLevelKind::Tree { name } => {
4259 let full_path = join_index_path(dir_prefix, name);
4260 let mut path_slash = full_path.clone();
4261 path_slash.push(b'/');
4262 if let Some(entry) =
4263 walk.index.entries.get(*walk.idx_cursor).filter(|entry| {
4264 entry.path == path_slash && entry.is_sparse_directory_placeholder()
4265 })
4266 {
4267 let dir = String::from_utf8_lossy(if dir_prefix.is_empty() {
4268 name
4269 } else {
4270 &full_path
4271 });
4272 emit_flattened_subtree_as_added(
4273 walk.odb,
4274 &entry.oid,
4275 dir.as_ref(),
4276 walk.ignore_submodules,
4277 walk.result,
4278 )?;
4279 *walk.idx_cursor += 1;
4280 return Ok(());
4281 }
4282 let start = *walk.idx_cursor;
4283 skip_index_subtree(walk.index, walk.idx_cursor, &full_path);
4284 for ie in &walk.index.entries[start..*walk.idx_cursor] {
4285 if ie.stage() != 0 || ie.intent_to_add() {
4286 continue;
4287 }
4288 if walk.ignore_submodules && ie.mode == 0o160000 {
4289 continue;
4290 }
4291 let path = String::from_utf8_lossy(&ie.path).into_owned();
4292 walk.result.push(DiffEntry {
4293 status: DiffStatus::Added,
4294 old_path: None,
4295 new_path: Some(path),
4296 old_mode: FileMode::ABSENT,
4297 new_mode: FileMode::new(ie.mode),
4298 old_oid: zero_oid(),
4299 new_oid: ie.oid,
4300 score: None,
4301 });
4302 }
4303 }
4304 }
4305 Ok(())
4306}
4307
4308#[derive(Default)]
4310pub(crate) struct HardResetIndexPlan {
4311 pub remove_paths: Vec<Vec<u8>>,
4312 pub updates: Vec<(Vec<u8>, u32, ObjectId)>,
4313 pub insert_entries: Vec<IndexEntry>,
4314}
4315
4316pub(crate) fn plan_hard_reset_index_align(
4321 odb: &Odb,
4322 index: &Index,
4323 target_tree: &ObjectId,
4324) -> Result<HardResetIndexPlan> {
4325 let mut plan = HardResetIndexPlan::default();
4326 let mut idx_cursor = 0usize;
4327 let cache_root = index.cache_tree.as_ref();
4328 let use_cache = cache_root.is_some()
4329 && index
4330 .entries
4331 .iter()
4332 .all(|entry| entry.stage() == 0 && !entry.intent_to_add());
4333 let cache_node = if use_cache { cache_root } else { None };
4334 let retry = {
4335 let mut walk = HardResetAlignWalk {
4336 odb,
4337 index,
4338 idx_cursor: &mut idx_cursor,
4339 ignore_submodules: false,
4340 plan: &mut plan,
4341 retry_without_cache: false,
4342 };
4343 hard_reset_align_index_tree_at(&mut walk, target_tree, cache_node, b"")?;
4344 walk.retry_without_cache
4345 };
4346 if retry {
4347 idx_cursor = 0;
4348 plan = HardResetIndexPlan::default();
4349 let mut walk = HardResetAlignWalk {
4350 odb,
4351 index,
4352 idx_cursor: &mut idx_cursor,
4353 ignore_submodules: false,
4354 plan: &mut plan,
4355 retry_without_cache: false,
4356 };
4357 hard_reset_align_index_tree_at(&mut walk, target_tree, None, b"")?;
4358 }
4359 while idx_cursor < index.entries.len() {
4360 let ie = &index.entries[idx_cursor];
4361 idx_cursor += 1;
4362 if ie.stage() != 0 || ie.intent_to_add() {
4363 continue;
4364 }
4365 plan.remove_paths.push(ie.path.clone());
4366 }
4367 Ok(plan)
4368}
4369
4370pub(crate) fn apply_hard_reset_index_plan(index: &mut Index, plan: HardResetIndexPlan) {
4372 let change_count = plan
4373 .remove_paths
4374 .len()
4375 .saturating_add(plan.updates.len())
4376 .saturating_add(plan.insert_entries.len());
4377 const BULK_HARD_RESET_INDEX_MIN: usize = 64;
4378 let bulk = change_count >= BULK_HARD_RESET_INDEX_MIN;
4379 if bulk {
4380 index.drop_derived_index_extensions();
4381 }
4382 for (path, mode, oid) in plan.updates {
4383 if let Some(pos) = index.find_entry_pos(&path, 0) {
4384 let entry = &mut index.entries[pos];
4385 let content_unchanged = entry.oid == oid && entry.mode == mode;
4386 entry.mode = mode;
4387 entry.oid = oid;
4388 if !content_unchanged {
4389 entry.size = 0;
4390 }
4391 if !bulk {
4392 index.invalidate_cache_tree_for_path(&path);
4393 }
4394 }
4395 }
4396 index.defer_untracked_cache_invalidation = bulk;
4397 index.remove_prefixes_paths_and_insert(
4398 &std::collections::BTreeSet::new(),
4399 plan.remove_paths.iter().map(|p| p.as_slice()),
4400 plan.insert_entries,
4401 );
4402 index.defer_untracked_cache_invalidation = false;
4403 if !bulk {
4404 for path in &plan.remove_paths {
4405 index.invalidate_cache_tree_for_path(path);
4406 }
4407 }
4408 index.sort();
4409}
4410
4411pub(crate) fn rebuild_hard_reset_cache_tree(
4414 odb: &Odb,
4415 index: &mut Index,
4416 target_tree: &ObjectId,
4417) -> Result<()> {
4418 let stage0_count = index
4419 .entries
4420 .iter()
4421 .filter(|entry| entry.stage() == 0 && !entry.intent_to_add())
4422 .count();
4423 let seed_root = index.cache_tree.as_ref();
4424 let mut idx_cursor = 0usize;
4425 let root = rebuild_hard_reset_cache_tree_at(
4426 odb,
4427 index,
4428 &mut idx_cursor,
4429 target_tree,
4430 seed_root,
4431 b"",
4432 Vec::new(),
4433 )?;
4434 if usize::try_from(root.entry_count).ok() == Some(stage0_count) {
4435 index.set_cache_tree(root);
4436 Ok(())
4437 } else {
4438 Err(Error::IndexError(
4439 "hard reset cache-tree entry count does not match index".into(),
4440 ))
4441 }
4442}
4443
4444fn rebuild_hard_reset_cache_tree_at(
4445 odb: &Odb,
4446 index: &Index,
4447 idx_cursor: &mut usize,
4448 tree_oid: &ObjectId,
4449 seed_node: Option<&CacheTreeNode>,
4450 dir_prefix: &[u8],
4451 name: Vec<u8>,
4452) -> Result<CacheTreeNode> {
4453 if !dir_prefix.is_empty() {
4454 let mut path = dir_prefix.to_vec();
4455 path.push(b'/');
4456 if let Some(entry) = index.entries.get(*idx_cursor).filter(|entry| {
4457 entry.path == path && entry.is_sparse_directory_placeholder() && entry.oid == *tree_oid
4458 }) {
4459 let _ = entry;
4460 *idx_cursor += 1;
4461 return Ok(CacheTreeNode::valid(name, 1, *tree_oid, Vec::new()));
4462 }
4463 }
4464 let tree_entries = read_tree(odb, tree_oid)?;
4465 let cache_children = CacheChildren::new(seed_node);
4466 let mut entry_count: i32 = 0;
4467 let mut children: Vec<CacheTreeNode> = Vec::new();
4468 let mut ti = 0usize;
4469
4470 loop {
4471 let te = tree_entries.get(ti);
4472 let ie_kind = peek_index_level_entry(index, idx_cursor, dir_prefix, false);
4473 match (te, ie_kind) {
4474 (Some(t_entry), Some(ie)) => {
4475 let t_name = &t_entry.name;
4476 let (i_name, i_is_tree) = match &ie {
4477 IndexLevelKind::Blob { name, .. } => (*name, false),
4478 IndexLevelKind::Tree { name } => (*name, true),
4479 };
4480 let t_is_tree = is_tree_mode(t_entry.mode);
4481 let cmp = crate::objects::tree_entry_cmp(t_name, t_is_tree, i_name, i_is_tree);
4482 if cmp != std::cmp::Ordering::Equal {
4483 return Err(Error::IndexError(
4484 "hard reset cache-tree rebuild: index out of sync with target tree".into(),
4485 ));
4486 }
4487 if t_is_tree && i_is_tree {
4488 let full_path = join_index_path(dir_prefix, t_name);
4489 if hard_reset_skip_sparse_cache_rebuild(index, idx_cursor, &full_path, t_entry)?
4490 {
4491 entry_count = entry_count.saturating_add(1);
4492 ti += 1;
4493 continue;
4494 }
4495 let child = if let Some(child_cache) = cache_children
4496 .get(t_name)
4497 .filter(|node| node.is_valid() && node.oid == Some(t_entry.oid))
4498 {
4499 skip_index_subtree(index, idx_cursor, &full_path);
4500 let mut reused = (*child_cache).clone();
4501 reused.name = t_name.clone();
4502 reused
4503 } else {
4504 rebuild_hard_reset_cache_tree_at(
4505 odb,
4506 index,
4507 idx_cursor,
4508 &t_entry.oid,
4509 cache_children.get(t_name),
4510 &full_path,
4511 t_name.clone(),
4512 )?
4513 };
4514 entry_count = entry_count.saturating_add(child.entry_count.max(0));
4515 children.push(child);
4516 } else if t_is_tree != i_is_tree {
4517 return Err(Error::IndexError(
4518 "hard reset cache-tree rebuild: tree/index type mismatch".into(),
4519 ));
4520 } else {
4521 *idx_cursor += 1;
4522 entry_count = entry_count.saturating_add(1);
4523 }
4524 ti += 1;
4525 }
4526 (None, None) => break,
4527 _ => {
4528 return Err(Error::IndexError(
4529 "hard reset cache-tree rebuild: index out of sync with target tree".into(),
4530 ));
4531 }
4532 }
4533 }
4534 children.sort_by(|a, b| a.name.cmp(&b.name));
4535 Ok(CacheTreeNode::valid(name, entry_count, *tree_oid, children))
4536}
4537
4538fn hard_reset_skip_sparse_cache_rebuild(
4539 index: &Index,
4540 idx_cursor: &mut usize,
4541 full_path: &[u8],
4542 t_entry: &TreeEntry,
4543) -> Result<bool> {
4544 let Some(entry) = index.entries.get(*idx_cursor) else {
4545 return Ok(false);
4546 };
4547 if !entry.is_sparse_directory_placeholder() {
4548 return Ok(false);
4549 }
4550 let mut dir = full_path.to_vec();
4551 if !dir.ends_with(b"/") {
4552 dir.push(b'/');
4553 }
4554 if entry.path != dir || entry.oid != t_entry.oid {
4555 return Ok(false);
4556 }
4557 *idx_cursor += 1;
4558 Ok(true)
4559}
4560
4561struct HardResetAlignWalk<'a, 'b> {
4562 odb: &'a Odb,
4563 index: &'a Index,
4564 idx_cursor: &'b mut usize,
4565 ignore_submodules: bool,
4566 plan: &'b mut HardResetIndexPlan,
4567 retry_without_cache: bool,
4568}
4569
4570fn hard_reset_align_index_tree_at(
4571 walk: &mut HardResetAlignWalk<'_, '_>,
4572 tree_oid: &ObjectId,
4573 cache_node: Option<&CacheTreeNode>,
4574 dir_prefix: &[u8],
4575) -> Result<()> {
4576 let tree_entries = read_tree(walk.odb, tree_oid)?;
4577 if tree_entries_have_duplicate_names(&tree_entries) {
4578 walk.retry_without_cache = true;
4579 return Ok(());
4580 }
4581 let cache_children = CacheChildren::new(cache_node);
4582 let mut ti = 0usize;
4583
4584 loop {
4585 let te = tree_entries.get(ti);
4586 let ie_kind = peek_index_level_entry(
4587 walk.index,
4588 walk.idx_cursor,
4589 dir_prefix,
4590 walk.ignore_submodules,
4591 );
4592
4593 match (te, ie_kind) {
4594 (Some(t_entry), Some(ie)) => {
4595 let t_name = &t_entry.name;
4596 let (i_name, i_is_tree) = match &ie {
4597 IndexLevelKind::Blob { name, .. } => (*name, false),
4598 IndexLevelKind::Tree { name } => (*name, true),
4599 };
4600 let t_is_tree = is_tree_mode(t_entry.mode);
4601 let cmp = crate::objects::tree_entry_cmp(t_name, t_is_tree, i_name, i_is_tree);
4602 match cmp {
4603 std::cmp::Ordering::Less => {
4604 hard_reset_plan_tree_only(walk, t_entry, dir_prefix)?;
4605 ti += 1;
4606 }
4607 std::cmp::Ordering::Greater => {
4608 hard_reset_plan_index_only(walk, dir_prefix)?;
4609 }
4610 std::cmp::Ordering::Equal => {
4611 if t_is_tree && i_is_tree {
4612 let full_path = join_index_path(dir_prefix, t_name);
4613 if hard_reset_skip_sparse_placeholder(walk, &full_path, t_entry)? {
4614 ti += 1;
4615 continue;
4616 }
4617 let child_cache = cache_children.get(t_name);
4618 if child_cache
4619 .is_some_and(|c| c.is_valid() && c.oid == Some(t_entry.oid))
4620 {
4621 skip_index_subtree(walk.index, walk.idx_cursor, &full_path);
4622 ti += 1;
4623 continue;
4624 }
4625 hard_reset_align_index_tree_at(
4626 walk,
4627 &t_entry.oid,
4628 child_cache,
4629 &full_path,
4630 )?;
4631 } else if t_is_tree != i_is_tree {
4632 hard_reset_plan_tree_only(walk, t_entry, dir_prefix)?;
4633 hard_reset_plan_index_only(walk, dir_prefix)?;
4634 } else {
4635 let IndexLevelKind::Blob { entry: ie, .. } = ie else {
4636 ti += 1;
4637 continue;
4638 };
4639 *walk.idx_cursor += 1;
4640 if t_entry.oid != ie.oid
4641 || crate::index::canon_mode(t_entry.mode)
4642 != crate::index::canon_mode(ie.mode)
4643 {
4644 walk.plan.updates.push((
4645 ie.path.clone(),
4646 crate::index::canon_mode(t_entry.mode),
4647 t_entry.oid,
4648 ));
4649 }
4650 }
4651 ti += 1;
4652 }
4653 }
4654 }
4655 (Some(t_entry), None) => {
4656 hard_reset_plan_tree_only(walk, t_entry, dir_prefix)?;
4657 ti += 1;
4658 }
4659 (None, Some(_)) => {
4660 hard_reset_plan_index_only(walk, dir_prefix)?;
4661 }
4662 (None, None) => break,
4663 }
4664 }
4665 Ok(())
4666}
4667
4668fn hard_reset_skip_sparse_placeholder(
4669 walk: &mut HardResetAlignWalk<'_, '_>,
4670 full_path: &[u8],
4671 t_entry: &TreeEntry,
4672) -> Result<bool> {
4673 let Some(ie) = walk.index.entries.get(*walk.idx_cursor) else {
4674 return Ok(false);
4675 };
4676 if !ie.is_sparse_directory_placeholder() {
4677 return Ok(false);
4678 }
4679 let mut dir = full_path.to_vec();
4680 if !dir.ends_with(b"/") {
4681 dir.push(b'/');
4682 }
4683 if ie.path != dir || ie.oid != t_entry.oid {
4684 return Ok(false);
4685 }
4686 *walk.idx_cursor += 1;
4687 Ok(true)
4688}
4689
4690fn hard_reset_plan_index_only(
4691 walk: &mut HardResetAlignWalk<'_, '_>,
4692 dir_prefix: &[u8],
4693) -> Result<()> {
4694 let ie_kind = peek_index_level_entry(
4695 walk.index,
4696 walk.idx_cursor,
4697 dir_prefix,
4698 walk.ignore_submodules,
4699 )
4700 .ok_or_else(|| Error::IndexError("missing index entry during hard reset align".into()))?;
4701 match ie_kind {
4702 IndexLevelKind::Blob { entry: ie, .. } => {
4703 walk.plan.remove_paths.push(ie.path.clone());
4704 *walk.idx_cursor += 1;
4705 }
4706 IndexLevelKind::Tree { name } => {
4707 let full_path = join_index_path(dir_prefix, name);
4708 let start = *walk.idx_cursor;
4709 skip_index_subtree(walk.index, walk.idx_cursor, &full_path);
4710 for ie in &walk.index.entries[start..*walk.idx_cursor] {
4711 if ie.stage() != 0 || ie.intent_to_add() {
4712 continue;
4713 }
4714 walk.plan.remove_paths.push(ie.path.clone());
4715 }
4716 }
4717 }
4718 Ok(())
4719}
4720
4721fn hard_reset_plan_tree_only(
4722 walk: &mut HardResetAlignWalk<'_, '_>,
4723 entry: &TreeEntry,
4724 dir_prefix: &[u8],
4725) -> Result<()> {
4726 if walk.ignore_submodules && entry.mode == 0o160000 {
4727 return Ok(());
4728 }
4729 let path = join_index_path(dir_prefix, &entry.name);
4730 if is_tree_mode(entry.mode) {
4731 hard_reset_append_entries_from_tree(
4732 walk.odb,
4733 &entry.oid,
4734 &path,
4735 &mut walk.plan.insert_entries,
4736 )?;
4737 } else {
4738 walk.plan
4739 .insert_entries
4740 .push(hard_reset_index_tree_row(path, entry.mode, entry.oid));
4741 }
4742 Ok(())
4743}
4744
4745fn hard_reset_index_tree_row(path: Vec<u8>, mode: u32, oid: ObjectId) -> IndexEntry {
4746 IndexEntry {
4747 ctime_sec: 0,
4748 ctime_nsec: 0,
4749 mtime_sec: 0,
4750 mtime_nsec: 0,
4751 dev: 0,
4752 ino: 0,
4753 mode: crate::index::canon_mode(mode),
4754 uid: 0,
4755 gid: 0,
4756 size: 0,
4757 oid,
4758 flags: path.len().min(0xFFF) as u16,
4759 flags_extended: None,
4760 path,
4761 base_index_pos: 0,
4762 }
4763}
4764
4765fn hard_reset_append_entries_from_tree(
4766 odb: &Odb,
4767 tree_oid: &ObjectId,
4768 prefix: &[u8],
4769 out: &mut Vec<IndexEntry>,
4770) -> Result<()> {
4771 for te in read_tree(odb, tree_oid)? {
4772 let mut path = prefix.to_vec();
4773 if !path.is_empty() {
4774 path.push(b'/');
4775 }
4776 path.extend_from_slice(&te.name);
4777 if is_tree_mode(te.mode) {
4778 hard_reset_append_entries_from_tree(odb, &te.oid, &path, out)?;
4779 } else {
4780 out.push(hard_reset_index_tree_row(path, te.mode, te.oid));
4781 }
4782 }
4783 Ok(())
4784}
4785
4786pub fn diff_index_to_worktree(
4811 odb: &Odb,
4812 index: &mut Index,
4813 work_tree: &Path,
4814 ignore_submodule_untracked: bool,
4815 simplify_gitlinks: bool,
4816) -> Result<Vec<DiffEntry>> {
4817 diff_index_to_worktree_with_options(
4818 odb,
4819 index,
4820 work_tree,
4821 DiffIndexToWorktreeOptions {
4822 ignore_submodule_untracked,
4823 simplify_gitlinks,
4824 ..DiffIndexToWorktreeOptions::default()
4825 },
4826 )
4827 .map(|(entries, _)| entries)
4828}
4829
4830#[derive(Clone)]
4832pub struct DiffIndexToWorktreeOptions {
4833 pub index_mtime: Option<(u32, u32)>,
4839 pub ignore_submodule_untracked: bool,
4841 pub simplify_gitlinks: bool,
4843 pub error_on_broken_gitlinks: bool,
4846 pub repository_git_dir: Option<std::path::PathBuf>,
4850 pub refresh_index_stat_in_pass: bool,
4853 pub config: Option<std::sync::Arc<crate::config::ConfigSet>>,
4855 pub stat_parallel_threads: Option<usize>,
4859 pub thread_policy: Option<crate::parallel::ThreadPolicy>,
4861 pub worktree_rules:
4863 Option<std::sync::Arc<std::sync::Mutex<crate::worktree_rules::WorktreeRules>>>,
4864 pub for_status: bool,
4871 pub refresh_fsmonitor: bool,
4873 pub command_runner: Option<std::sync::Arc<dyn crate::command_runner::CommandRunner>>,
4875 pub fsmonitor_query_boundary: Option<crate::fsmonitor::FsmonitorQueryBoundary>,
4877 pub restrict_to_paths: Option<std::sync::Arc<std::collections::BTreeSet<String>>>,
4884 pub hash_blobs_without_write: bool,
4886 pub cancel_watch: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
4889 pub diagnostics: Option<crate::diagnostics::DiagnosticsHandle>,
4892}
4893
4894impl Default for DiffIndexToWorktreeOptions {
4895 fn default() -> Self {
4896 Self {
4897 index_mtime: None,
4898 ignore_submodule_untracked: false,
4899 simplify_gitlinks: false,
4900 error_on_broken_gitlinks: false,
4901 repository_git_dir: None,
4902 refresh_index_stat_in_pass: false,
4903 config: None,
4904 stat_parallel_threads: None,
4905 thread_policy: None,
4906 worktree_rules: None,
4907 for_status: false,
4908 refresh_fsmonitor: true,
4909 command_runner: None,
4910 fsmonitor_query_boundary: None,
4911 restrict_to_paths: None,
4912 hash_blobs_without_write: false,
4913 cancel_watch: None,
4914 diagnostics: None,
4915 }
4916 }
4917}
4918
4919#[inline]
4920fn poll_diff_cancel_watch(
4921 watch: Option<&std::sync::Arc<std::sync::atomic::AtomicBool>>,
4922) -> crate::error::Result<()> {
4923 if let Some(flag) = watch {
4924 crate::progress::poll_watch(flag)?;
4925 }
4926 Ok(())
4927}
4928
4929#[inline]
4930fn poll_diff_cancel(
4931 options: &DiffIndexToWorktreeOptions,
4932 owner_poll: Option<&std::sync::Arc<crate::progress::StackCancelPoll>>,
4933) -> crate::error::Result<()> {
4934 if let Some(poll) = owner_poll {
4935 poll.poll_owner()?;
4936 } else {
4937 poll_diff_cancel_watch(options.cancel_watch.as_ref())?;
4938 }
4939 Ok(())
4940}
4941
4942struct StagingHashJob {
4947 entry_index: usize,
4948 worktree_mode: u32,
4950 file_attrs: Option<Box<crate::crlf::FileAttrs>>,
4952}
4953
4954impl StagingHashJob {
4955 fn new(entry_index: usize, worktree_mode: u32, file_attrs: crate::crlf::FileAttrs) -> Self {
4956 let file_attrs =
4957 (file_attrs != crate::crlf::FileAttrs::default()).then(|| Box::new(file_attrs));
4958 Self {
4959 entry_index,
4960 worktree_mode,
4961 file_attrs,
4962 }
4963 }
4964}
4965
4966struct StagingHashSource<'a> {
4968 index: &'a Index,
4969 work_tree: &'a Path,
4970 precompose_unicode: bool,
4971 ignorecase: bool,
4972}
4973
4974#[allow(clippy::too_many_arguments)]
4975fn parallel_process_blob_worktree_jobs(
4976 odb: &Odb,
4977 source: &StagingHashSource<'_>,
4978 jobs: &[StagingHashJob],
4979 thread_policy: &crate::parallel::ThreadPolicy,
4980 config: &crate::config::ConfigSet,
4981 materialize_dirty_blobs: bool,
4982 hash_blobs_without_write: bool,
4983 for_status: bool,
4984 diagnostics: Option<&crate::diagnostics::DiagnosticsHandle>,
4985 conv: &crate::crlf::ConversionConfig,
4986 filter_process: Option<&crate::filter_process::FilterProcessState>,
4987 _icase_collisions: Option<&crate::path_icase::IcaseCheckoutCollisionMap>,
4988 result: &mut Vec<DiffEntry>,
4989) -> Result<()> {
4990 use crate::parallel::{try_par_map, ParallelHashError, ParallelOp};
4991
4992 if jobs.is_empty() {
4993 return Ok(());
4994 }
4995 let mut par_opts = thread_policy.resolve(ParallelOp::IndexRefresh, config);
4996 par_opts.min_items = 0;
4997 par_opts.min_total_bytes = None;
4998 let est_bytes = jobs.len().saturating_mul(8192);
4999 let store_dirty_blobs = (materialize_dirty_blobs && !hash_blobs_without_write)
5000 .then(|| odb.bulk_stage_write_options());
5001 let stream_from = if store_dirty_blobs.is_some() {
5002 odb.big_file_threshold()?
5003 } else {
5004 hash_stream_threshold(odb)?
5005 };
5006 let default_attrs = crate::crlf::FileAttrs::default();
5007
5008 let new_oids = try_par_map::<StagingHashJob, Option<ObjectId>, Error, _>(
5011 jobs,
5012 par_opts,
5013 est_bytes,
5014 |job| {
5015 let entry = &source.index.entries[job.entry_index];
5016 let rel_path = crate::repo_relpath::storage_string(&entry.path);
5017 let modified = |new_oid| Ok(Some(new_oid));
5018 let file_path = index_entry_worktree_abs_bytes(
5019 source.work_tree,
5020 &entry.path,
5021 source.precompose_unicode,
5022 source.ignorecase,
5023 );
5024 let content = match read_regular_worktree_blob(
5025 odb,
5026 &file_path,
5027 conv,
5028 job.file_attrs.as_deref().unwrap_or(&default_attrs),
5029 &rel_path,
5030 Some(entry),
5031 filter_process,
5032 stream_from,
5033 ) {
5034 Ok(content) => content,
5035 Err(err)
5036 if status_skips_inaccessible_worktree_error(
5037 &err,
5038 for_status,
5039 materialize_dirty_blobs,
5040 ) =>
5041 {
5042 warn_and_skip_status_inaccessible(diagnostics, rel_path.as_str(), &err);
5043 return Ok(None);
5044 }
5045 Err(err) if unreadable_worktree_blob_is_modified(&err, materialize_dirty_blobs) => {
5046 return modified(zero_oid());
5047 }
5048 Err(err) => return Err(err),
5049 };
5050 let eff_oid = content.object_id(odb, store_dirty_blobs)?;
5051 if eff_oid != entry.oid || job.worktree_mode != entry.mode {
5052 let new_oid = if eff_oid != entry.oid && !materialize_dirty_blobs {
5053 zero_oid()
5054 } else {
5055 eff_oid
5056 };
5057 modified(new_oid)
5058 } else {
5059 Ok(None)
5060 }
5061 },
5062 )
5063 .map_err(|e| match e {
5064 ParallelHashError::Task(err) => err,
5065 })?;
5066
5067 let mut changed: Vec<(usize, u32, ObjectId)> = jobs
5068 .iter()
5069 .zip(new_oids)
5070 .filter_map(|(job, new_oid)| new_oid.map(|oid| (job.entry_index, job.worktree_mode, oid)))
5071 .collect();
5072 changed.sort_by_key(|(idx, _, _)| *idx);
5073 result.extend(
5074 changed
5075 .into_iter()
5076 .map(|(entry_index, worktree_mode, new_oid)| {
5077 let entry = &source.index.entries[entry_index];
5078 let rel_path = crate::repo_relpath::storage_string(&entry.path);
5079 worktree_content_change_entry(entry, &rel_path, worktree_mode, new_oid)
5080 }),
5081 );
5082 Ok(())
5083}
5084
5085fn worktree_content_change_entry(
5087 ie: &IndexEntry,
5088 path: &str,
5089 worktree_mode: u32,
5090 new_oid: ObjectId,
5091) -> DiffEntry {
5092 DiffEntry {
5093 status: DiffStatus::for_content_change(ie.mode, worktree_mode),
5094 old_path: Some(path.to_owned()),
5095 new_path: Some(path.to_owned()),
5096 old_mode: FileMode::new(ie.mode),
5097 new_mode: FileMode::new(worktree_mode),
5098 old_oid: ie.oid,
5099 new_oid,
5100 score: None,
5101 }
5102}
5103
5104fn unreadable_worktree_blob_is_modified(err: &Error, materialize_dirty_blobs: bool) -> bool {
5111 !materialize_dirty_blobs
5112 && matches!(
5113 err,
5114 Error::WorktreePathIo {
5115 operation: crate::worktree::WorktreePathOperation::Read,
5116 ..
5117 }
5118 )
5119}
5120
5121fn warn_worktree_access_failed(
5122 diagnostics: Option<&crate::diagnostics::DiagnosticsHandle>,
5123 path: &str,
5124 err: &std::io::Error,
5125) {
5126 if let Some(handle) = diagnostics {
5127 crate::diagnostics::warn(
5128 handle,
5129 crate::diagnostics::Warning::WorktreeAccessFailed {
5130 path: path.to_owned(),
5131 detail: err.to_string(),
5132 },
5133 );
5134 }
5135}
5136
5137fn blob_disk_lookup_io_result(
5138 diagnostics: Option<&crate::diagnostics::DiagnosticsHandle>,
5139 path: &str,
5140 err: std::io::Error,
5141 for_status: bool,
5142 materialize_dirty_blobs: bool,
5143) -> Result<()> {
5144 if for_status && !materialize_dirty_blobs {
5145 warn_worktree_access_failed(diagnostics, path, &err);
5146 return Ok(());
5147 }
5148 Err(Error::Io(err))
5149}
5150
5151fn io_err_is_permission_denied(err: &std::io::Error) -> bool {
5152 err.kind() == std::io::ErrorKind::PermissionDenied
5153}
5154
5155fn status_skips_inaccessible_worktree_error(
5156 err: &Error,
5157 for_status: bool,
5158 materialize_dirty_blobs: bool,
5159) -> bool {
5160 if !for_status || materialize_dirty_blobs {
5161 return false;
5162 }
5163 match err {
5164 Error::Io(e) => io_err_is_permission_denied(e),
5165 Error::WorktreePathIo {
5166 operation: crate::worktree::WorktreePathOperation::Metadata,
5167 source,
5168 ..
5169 } => io_err_is_permission_denied(source),
5170 _ => false,
5171 }
5172}
5173
5174fn warn_and_skip_status_inaccessible(
5175 diagnostics: Option<&crate::diagnostics::DiagnosticsHandle>,
5176 path: &str,
5177 err: &Error,
5178) -> bool {
5179 let detail = match err {
5180 Error::Io(e) => e.to_string(),
5181 Error::WorktreePathIo { source, .. } => source.to_string(),
5182 _ => return false,
5183 };
5184 if let Some(handle) = diagnostics {
5185 crate::diagnostics::warn(
5186 handle,
5187 crate::diagnostics::Warning::WorktreeAccessFailed {
5188 path: path.to_owned(),
5189 detail,
5190 },
5191 );
5192 }
5193 true
5194}
5195
5196fn symlink_metadata_worktree_diff(
5197 file_path: &Path,
5198 path_str: &str,
5199 diagnostics: Option<&crate::diagnostics::DiagnosticsHandle>,
5200 for_status: bool,
5201 materialize_dirty_blobs: bool,
5202) -> Result<Option<fs::Metadata>> {
5203 match fs::symlink_metadata(file_path) {
5204 Ok(meta) => Ok(Some(meta)),
5205 Err(e)
5206 if e.kind() == std::io::ErrorKind::NotFound
5207 || e.raw_os_error() == Some(20) =>
5208 {
5209 Ok(None)
5210 }
5211 Err(e) => {
5212 if for_status && !materialize_dirty_blobs && io_err_is_permission_denied(&e) {
5213 warn_worktree_access_failed(diagnostics, path_str, &e);
5214 Ok(None)
5215 } else {
5216 Err(Error::Io(e))
5217 }
5218 }
5219 }
5220}
5221
5222fn status_size_change_proves_modified(
5231 ie: &IndexEntry,
5232 worktree_size: u32,
5233 for_status: bool,
5234 conversion_is_identity: impl FnOnce() -> bool,
5235) -> bool {
5236 for_status && !ie.stat_cache_unset() && worktree_size != ie.size && conversion_is_identity()
5237}
5238
5239#[allow(clippy::too_many_arguments)]
5242fn try_push_status_modified_on_size_mismatch(
5243 odb: &Odb,
5244 ie: &IndexEntry,
5245 path_str_ref: &str,
5246 worktree_size: u32,
5247 worktree_mode: u32,
5248 raw_worktree_len: u64,
5249 for_status: bool,
5250 conversion_is_identity: impl FnOnce() -> bool,
5251 result: &mut Vec<DiffEntry>,
5252) -> Result<bool> {
5253 let identity = conversion_is_identity();
5254 let size_differs = worktree_size != ie.size;
5255 if !size_differs || !identity {
5256 return Ok(false);
5257 }
5258 let threshold = odb.big_file_threshold()?;
5259 let big_file = raw_worktree_len > threshold;
5260 let proves_modified = if big_file {
5261 true
5262 } else {
5263 status_size_change_proves_modified(ie, worktree_size, for_status, || identity)
5264 };
5265 if !proves_modified {
5266 return Ok(false);
5267 }
5268 result.push(worktree_content_change_entry(
5269 ie,
5270 path_str_ref,
5271 worktree_mode,
5272 zero_oid(),
5273 ));
5274 Ok(true)
5275}
5276
5277#[allow(clippy::too_many_arguments)]
5278fn push_index_blob_worktree_diff(
5279 odb: &Odb,
5280 ie: &IndexEntry,
5281 path_str_ref: &str,
5282 file_path: &Path,
5283 meta: fs::Metadata,
5284 index_mtime: Option<(u32, u32)>,
5285 mode_policy: crate::index::WorktreeModePolicy,
5286 stat_policy: crate::index::StatMatchPolicy,
5287 file_attrs_for: &dyn Fn(&str) -> crate::crlf::FileAttrs,
5288 conv: &crate::crlf::ConversionConfig,
5289 materialize_dirty_blobs: bool,
5290 hash_blobs_without_write: bool,
5291 for_status: bool,
5292 diagnostics: Option<&crate::diagnostics::DiagnosticsHandle>,
5293 filter_process: Option<&crate::filter_process::FilterProcessState>,
5294 icase_collisions: Option<&crate::path_icase::IcaseCheckoutCollisionMap>,
5295 result: &mut Vec<DiffEntry>,
5296) -> Result<()> {
5297 if meta.is_dir() {
5298 if file_path.join(".git").exists() {
5299 let head = read_submodule_head_oid(file_path).unwrap_or_else(zero_oid);
5300 let path_owned = path_str_ref.to_owned();
5301 result.push(DiffEntry {
5302 status: DiffStatus::TypeChanged,
5303 old_path: Some(path_owned.clone()),
5304 new_path: Some(path_owned),
5305 old_mode: FileMode::new(ie.mode),
5306 new_mode: FileMode::new(0o160000),
5307 old_oid: ie.oid,
5308 new_oid: head,
5309 score: None,
5310 });
5311 return Ok(());
5312 }
5313 result.push(DiffEntry {
5314 status: DiffStatus::Deleted,
5315 old_path: Some(path_str_ref.to_owned()),
5316 new_path: None,
5317 old_mode: FileMode::new(ie.mode),
5318 new_mode: FileMode::ABSENT,
5319 old_oid: ie.oid,
5320 new_oid: zero_oid(),
5321 score: None,
5322 });
5323 return Ok(());
5324 }
5325
5326 let worktree_mode =
5327 mode_policy.index_mode_for_worktree(mode_from_metadata(&meta), Some(ie.mode));
5328 let stat_same = stat_matches_with_policy(ie, &meta, stat_policy);
5329 if stat_same && worktree_mode != ie.mode {
5330 result.push(worktree_content_change_entry(
5331 ie,
5332 path_str_ref,
5333 worktree_mode,
5334 ie.oid,
5335 ));
5336 return Ok(());
5337 }
5338
5339 if stat_same
5340 && index_entry_skips_worktree_content_hash_after_stat_match(ie, index_mtime, stat_policy)
5341 {
5342 return Ok(());
5343 }
5344
5345 let file_attrs = file_attrs_for(path_str_ref);
5346 if !icase_collisions.is_some_and(|map| map.diff_requires_worktree_hash(ie.path.as_slice()))
5347 && try_push_status_modified_on_size_mismatch(
5348 odb,
5349 ie,
5350 path_str_ref,
5351 crate::index::munge_index_size(meta.len()),
5352 worktree_mode,
5353 meta.len(),
5354 for_status,
5355 || crate::crlf::git_conversion_is_identity(conv, &file_attrs),
5356 result,
5357 )?
5358 {
5359 return Ok(());
5360 }
5361
5362 let store = (materialize_dirty_blobs && !hash_blobs_without_write)
5363 .then(|| odb.bulk_stage_write_options());
5364 let stream_from = if store.is_some() {
5365 odb.big_file_threshold()?
5366 } else {
5367 hash_stream_threshold(odb)?
5368 };
5369 let content = match worktree_blob_content_from(
5370 odb,
5371 file_path,
5372 &meta,
5373 conv,
5374 &file_attrs,
5375 path_str_ref,
5376 Some(ie),
5377 filter_process,
5378 stream_from,
5379 ) {
5380 Ok(content) => content,
5381 Err(err)
5382 if status_skips_inaccessible_worktree_error(
5383 &err,
5384 for_status,
5385 materialize_dirty_blobs,
5386 ) =>
5387 {
5388 warn_and_skip_status_inaccessible(diagnostics, path_str_ref, &err);
5389 return Ok(());
5390 }
5391 Err(err) if unreadable_worktree_blob_is_modified(&err, materialize_dirty_blobs) => {
5392 result.push(worktree_content_change_entry(
5393 ie,
5394 path_str_ref,
5395 worktree_mode,
5396 zero_oid(),
5397 ));
5398 return Ok(());
5399 }
5400 Err(err) => return Err(err),
5401 };
5402 let eff_oid = content.object_id(odb, store)?;
5403
5404 if eff_oid != ie.oid || worktree_mode != ie.mode {
5405 let new_oid = if eff_oid != ie.oid && !materialize_dirty_blobs {
5408 zero_oid()
5409 } else {
5410 eff_oid
5411 };
5412 result.push(worktree_content_change_entry(
5413 ie,
5414 path_str_ref,
5415 worktree_mode,
5416 new_oid,
5417 ));
5418 }
5419 Ok(())
5420}
5421
5422pub fn diff_index_to_worktree_with_options(
5432 odb: &Odb,
5433 index: &mut Index,
5434 work_tree: &Path,
5435 options: DiffIndexToWorktreeOptions,
5436) -> Result<(Vec<DiffEntry>, bool)> {
5437 let default_git_dir = work_tree.join(".git");
5438 let git_dir = options
5439 .repository_git_dir
5440 .clone()
5441 .unwrap_or(default_git_dir);
5442 diff_index_to_worktree_inner(odb, index, work_tree, &git_dir, options, false, None)
5443 .map(|(entries, changed, _)| (entries, changed))
5444}
5445
5446pub(crate) fn diff_index_to_worktree_with_owner_poll(
5449 odb: &Odb,
5450 index: &mut Index,
5451 work_tree: &Path,
5452 options: DiffIndexToWorktreeOptions,
5453 owner_poll: Option<std::sync::Arc<crate::progress::StackCancelPoll>>,
5454) -> Result<(Vec<DiffEntry>, bool)> {
5455 let default_git_dir = work_tree.join(".git");
5456 let git_dir = options
5457 .repository_git_dir
5458 .clone()
5459 .unwrap_or(default_git_dir);
5460 diff_index_to_worktree_inner(odb, index, work_tree, &git_dir, options, false, owner_poll)
5461 .map(|(entries, changed, _)| (entries, changed))
5462}
5463
5464pub fn diff_index_to_worktree_for_staging(
5470 odb: &Odb,
5471 repository_git_dir: &Path,
5472 index: &mut Index,
5473 work_tree: &Path,
5474 options: DiffIndexToWorktreeOptions,
5475) -> Result<(
5476 Vec<DiffEntry>,
5477 bool,
5478 std::collections::BTreeMap<String, crate::index::IndexStatFields>,
5479)> {
5480 diff_index_to_worktree_inner(
5481 odb,
5482 index,
5483 work_tree,
5484 repository_git_dir,
5485 options,
5486 true,
5487 None,
5488 )
5489}
5490
5491#[allow(clippy::too_many_arguments)]
5493fn worktree_diff_pass1_index_entry(
5494 odb: &Odb,
5495 work_tree: &Path,
5496 ie: &IndexEntry,
5497 path_str_ref: &str,
5498 simplify_gitlinks: bool,
5499 ignore_submodule_untracked: bool,
5500 error_on_broken_gitlinks: bool,
5501 materialize_dirty_blobs: bool,
5502 filter_fp: Option<&crate::filter_process::FilterProcessState>,
5503 conv: &crate::crlf::ConversionConfig,
5504 file_attrs_for: &dyn Fn(&str) -> crate::crlf::FileAttrs,
5505 precompose_unicode: bool,
5506 ignorecase: bool,
5507 result: &mut Vec<DiffEntry>,
5508) -> Result<()> {
5509 if ie.skip_worktree() || ie.assume_unchanged() {
5510 return Ok(());
5511 }
5512 let is_intent_to_add = ie.intent_to_add();
5513
5514 if ie.mode == 0o160000 {
5515 let sub_dir =
5516 index_entry_worktree_abs_bytes(work_tree, &ie.path, precompose_unicode, ignorecase);
5517 match fs::symlink_metadata(&sub_dir) {
5518 Err(e)
5519 if e.kind() == std::io::ErrorKind::NotFound
5520 || e.raw_os_error() == Some(20) =>
5521 {
5522 if !simplify_gitlinks {
5523 let path_owned = path_str_ref.to_owned();
5524 result.push(DiffEntry {
5525 status: DiffStatus::Deleted,
5526 old_path: Some(path_owned.clone()),
5527 new_path: Some(path_owned),
5528 old_mode: FileMode::new(ie.mode),
5529 new_mode: FileMode::ABSENT,
5530 old_oid: ie.oid,
5531 new_oid: zero_oid(),
5532 score: None,
5533 });
5534 }
5535 return Ok(());
5536 }
5537 Err(e) => return Err(Error::Io(e)),
5538 Ok(meta) if !meta.is_dir() => {
5539 let worktree_mode = mode_from_metadata(&meta);
5540 let file_attrs = file_attrs_for(path_str_ref);
5541 let worktree_oid = worktree_file_oid(
5542 odb,
5543 &sub_dir,
5544 &meta,
5545 conv,
5546 &file_attrs,
5547 path_str_ref,
5548 None,
5549 materialize_dirty_blobs,
5550 filter_fp,
5551 )?;
5552 let path_owned = path_str_ref.to_owned();
5553 result.push(DiffEntry {
5554 status: DiffStatus::TypeChanged,
5555 old_path: Some(path_owned.clone()),
5556 new_path: Some(path_owned),
5557 old_mode: FileMode::new(ie.mode),
5558 new_mode: FileMode::new(worktree_mode),
5559 old_oid: ie.oid,
5560 new_oid: worktree_oid,
5561 score: None,
5562 });
5563 return Ok(());
5564 }
5565 Ok(_) => {}
5566 }
5567 let sub_head_oid = read_submodule_head_oid(&sub_dir);
5568 let ref_matches = if let Some(oid) = sub_head_oid {
5569 oid == ie.oid
5570 } else {
5571 if error_on_broken_gitlinks && submodule_embedded_git_dir(&sub_dir).is_some() {
5572 return Err(Error::Config(
5573 format!("could not read submodule HEAD for '{path_str_ref}'").into(),
5574 ));
5575 }
5576 uninitialized_gitlink_worktree_matches_index(&sub_dir)
5577 };
5578 if simplify_gitlinks {
5579 if !ref_matches {
5580 let path_owned = path_str_ref.to_owned();
5581 let new_oid = sub_head_oid.unwrap_or_else(zero_oid);
5582 result.push(DiffEntry {
5583 status: DiffStatus::Modified,
5584 old_path: Some(path_owned.clone()),
5585 new_path: Some(path_owned),
5586 old_mode: FileMode::new(ie.mode),
5587 new_mode: FileMode::new(ie.mode),
5588 old_oid: ie.oid,
5589 new_oid,
5590 score: None,
5591 });
5592 }
5593 return Ok(());
5594 }
5595 if sub_head_oid.is_some() && submodule_head_object_broken(&sub_dir) {
5596 return Err(Error::Config(
5597 format!("'git status --porcelain=2' failed in submodule {path_str_ref}").into(),
5598 ));
5599 }
5600 let mut flags = submodule_porcelain_flags(work_tree, path_str_ref, ie.oid);
5601 if ignore_submodule_untracked {
5602 flags.untracked = false;
5603 }
5604 let inner_dirty = flags.modified || flags.untracked;
5605 if !ref_matches || inner_dirty {
5606 let path_owned = path_str_ref.to_owned();
5607 let new_oid = if !ref_matches {
5608 sub_head_oid.unwrap_or_else(zero_oid)
5609 } else {
5610 zero_oid()
5611 };
5612 result.push(DiffEntry {
5613 status: DiffStatus::Modified,
5614 old_path: Some(path_owned.clone()),
5615 new_path: Some(path_owned),
5616 old_mode: FileMode::new(ie.mode),
5617 new_mode: FileMode::new(ie.mode),
5618 old_oid: ie.oid,
5619 new_oid,
5620 score: None,
5621 });
5622 }
5623 return Ok(());
5624 }
5625
5626 let file_path =
5627 index_entry_worktree_abs_bytes(work_tree, &ie.path, precompose_unicode, ignorecase);
5628 let path_match = crate::repo_relpath::as_utf8_for_matching(path_str_ref);
5629
5630 if is_intent_to_add {
5631 match fs::symlink_metadata(&file_path) {
5632 Ok(meta) => {
5633 let file_attrs = file_attrs_for(path_match.as_ref());
5634 let worktree_oid = worktree_file_oid(
5635 odb,
5636 &file_path,
5637 &meta,
5638 conv,
5639 &file_attrs,
5640 path_match.as_ref(),
5641 None,
5642 materialize_dirty_blobs,
5643 filter_fp,
5644 )?;
5645 let worktree_mode = mode_from_metadata(&meta);
5646 result.push(DiffEntry {
5647 status: DiffStatus::Added,
5648 old_path: None,
5649 new_path: Some(path_str_ref.to_owned()),
5650 old_mode: FileMode::ABSENT,
5651 new_mode: FileMode::new(worktree_mode),
5652 old_oid: zero_oid(),
5653 new_oid: worktree_oid,
5654 score: None,
5655 });
5656 }
5657 Err(e)
5658 if e.kind() == std::io::ErrorKind::NotFound
5659 || e.raw_os_error() == Some(20) =>
5660 {
5661 result.push(DiffEntry {
5662 status: DiffStatus::Deleted,
5663 old_path: Some(path_str_ref.to_owned()),
5664 new_path: None,
5665 old_mode: FileMode::new(ie.mode),
5666 new_mode: FileMode::ABSENT,
5667 old_oid: ie.oid,
5668 new_oid: zero_oid(),
5669 score: None,
5670 });
5671 }
5672 Err(e) => return Err(Error::Io(e)),
5673 }
5674 }
5675 Ok(())
5676}
5677
5678#[inline]
5679fn index_entry_needs_worktree_diff_pass1(ie: &IndexEntry) -> bool {
5680 ie.mode == crate::index::MODE_GITLINK || ie.intent_to_add()
5681}
5682
5683fn diff_index_to_worktree_inner(
5684 odb: &Odb,
5685 index: &mut Index,
5686 work_tree: &Path,
5687 repository_git_dir: &Path,
5688 options: DiffIndexToWorktreeOptions,
5689 materialize_dirty_blobs: bool,
5690 owner_poll: Option<std::sync::Arc<crate::progress::StackCancelPoll>>,
5691) -> Result<(
5692 Vec<DiffEntry>,
5693 bool,
5694 std::collections::BTreeMap<String, crate::index::IndexStatFields>,
5695)> {
5696 use crate::config::ConfigSet;
5697 use crate::crlf;
5698 use std::collections::BTreeMap;
5699
5700 let mut staging_stat_cache = BTreeMap::new();
5701 poll_diff_cancel(&options, owner_poll.as_ref())?;
5702 let index_mtime = index_mtime_for_diff(index, options.index_mtime);
5703
5704 let ignore_submodule_untracked = options.ignore_submodule_untracked;
5705 let simplify_gitlinks = options.simplify_gitlinks;
5706
5707 let rules_lock = options
5708 .worktree_rules
5709 .as_ref()
5710 .map(|shared| {
5711 shared
5712 .lock()
5713 .map_err(|e| Error::Message(format!("worktree rules lock poisoned: {e}")))
5714 })
5715 .transpose()?;
5716 let config = if let Some(rules) = rules_lock.as_ref() {
5717 rules.config().clone()
5718 } else if let Some(c) = options.config.as_ref() {
5719 c.as_ref().clone()
5720 } else {
5721 ConfigSet::load(
5722 &crate::environment::Environment::empty(),
5723 Some(repository_git_dir),
5724 true,
5725 )
5726 .unwrap_or_else(|_| ConfigSet::new())
5727 };
5728 let conv = rules_lock
5729 .as_ref()
5730 .map(|r| r.conversion().clone())
5731 .unwrap_or_else(|| crlf::ConversionConfig::from_config(&config));
5732 let legacy_attrs = if rules_lock.is_none() {
5733 Some(crlf::load_gitattributes_parsed(work_tree))
5734 } else {
5735 None
5736 };
5737 let file_attrs_for = |path: &str| -> crlf::FileAttrs {
5738 if let Some(rules) = rules_lock.as_ref() {
5739 rules.file_attrs(path, false)
5740 } else if let Some(parsed) = legacy_attrs.as_ref() {
5741 crlf::file_attrs_from_gitattributes(&parsed.rules, &parsed.macros, path, false, &config)
5742 } else {
5743 crlf::FileAttrs::default()
5744 }
5745 };
5746 let filter_fp = rules_lock.as_ref().map(|r| r.filter_process());
5747 let precompose_unicode = config
5748 .get_bool("core.precomposeunicode")
5749 .and_then(|r| r.ok())
5750 .unwrap_or(false);
5751 let mode_policy = crate::index::WorktreeModePolicy::from_config(&config);
5752 let stat_policy = crate::index::StatMatchPolicy::from_config(Some(&config));
5753 let ignorecase = config
5754 .get_bool("core.ignorecase")
5755 .and_then(|r| r.ok())
5756 .unwrap_or(false);
5757 let icase_collisions = ignorecase
5758 .then(|| crate::path_icase::IcaseCheckoutCollisionMap::from_index(work_tree, index, true));
5759 let hash_blobs_without_write = options.hash_blobs_without_write;
5760
5761 let mut result = Vec::new();
5762 let refresh_index_stat_in_pass = options.refresh_index_stat_in_pass;
5763 let mut index_stat_refresh_changed = false;
5764 let mut fsmonitor_valid_updated = false;
5765 let mut honor_fsmonitor_valid = false;
5766 if options.refresh_fsmonitor {
5767 let default_runner = crate::command_runner::system_command_runner();
5768 let runner = options
5769 .command_runner
5770 .as_deref()
5771 .unwrap_or(default_runner.as_ref());
5772 let boundary = options.fsmonitor_query_boundary.unwrap_or_else(|| {
5773 crate::fsmonitor::FsmonitorQueryBoundary(
5774 crate::environment::wall_clock_unix_timestamp_nanos(),
5775 )
5776 });
5777 let refresh_opts = crate::fsmonitor::FsmonitorRefreshOptions::with_query_boundary(boundary);
5778 let refresh = crate::fsmonitor::refresh_index(
5779 index,
5780 repository_git_dir,
5781 work_tree,
5782 &config,
5783 Some(runner),
5784 &refresh_opts,
5785 )?;
5786 if refresh.index_changed {
5787 index_stat_refresh_changed = true;
5788 }
5789 honor_fsmonitor_valid = refresh.honor_valid_bits;
5790 }
5791 let use_fsmonitor_stat_skip = honor_fsmonitor_valid;
5792 let mut unmerged_base: std::collections::BTreeMap<String, (u8, IndexEntry)> =
5793 std::collections::BTreeMap::new();
5794
5795 let error_on_broken_gitlinks = options.error_on_broken_gitlinks;
5796 if options.restrict_to_paths.is_none() {
5797 for ie in &index.entries {
5798 poll_diff_cancel(&options, owner_poll.as_ref())?;
5799 if ie.stage() != 0 {
5800 let path = String::from_utf8_lossy(&ie.path).to_string();
5801 let rank = match ie.stage() {
5802 2 => 0u8,
5803 3 => 1u8,
5804 1 => 2u8,
5805 _ => 3u8,
5806 };
5807 match unmerged_base.get(&path) {
5808 Some((existing_rank, _)) if *existing_rank <= rank => {}
5809 _ => {
5810 unmerged_base.insert(path, (rank, ie.clone()));
5811 }
5812 }
5813 }
5814 }
5815 }
5816
5817 if let Some(limit) = options.restrict_to_paths.as_ref() {
5818 for path in limit.iter() {
5819 let Some(ie) = index.get(path.as_bytes(), 0) else {
5820 continue;
5821 };
5822 if !index_entry_needs_worktree_diff_pass1(ie) {
5823 continue;
5824 }
5825 worktree_diff_pass1_index_entry(
5826 odb,
5827 work_tree,
5828 ie,
5829 path.as_str(),
5830 simplify_gitlinks,
5831 ignore_submodule_untracked,
5832 error_on_broken_gitlinks,
5833 materialize_dirty_blobs,
5834 filter_fp,
5835 &conv,
5836 &file_attrs_for,
5837 precompose_unicode,
5838 ignorecase,
5839 &mut result,
5840 )?;
5841 }
5842 } else {
5843 for ie in &index.entries {
5844 poll_diff_cancel(&options, owner_poll.as_ref())?;
5845 if ie.stage() != 0 {
5846 continue;
5847 }
5848 if !index_entry_needs_worktree_diff_pass1(ie) {
5849 continue;
5850 }
5851 let path_stored = crate::repo_relpath::storage_string(&ie.path);
5852 worktree_diff_pass1_index_entry(
5853 odb,
5854 work_tree,
5855 ie,
5856 &path_stored,
5857 simplify_gitlinks,
5858 ignore_submodule_untracked,
5859 error_on_broken_gitlinks,
5860 materialize_dirty_blobs,
5861 filter_fp,
5862 &conv,
5863 &file_attrs_for,
5864 precompose_unicode,
5865 ignorecase,
5866 &mut result,
5867 )?;
5868 }
5869 }
5870
5871 use crate::worktree_scan::{
5872 collect_blob_lookup_batches_with_literal_paths, group_blob_entries_by_dir,
5873 stat_scan_thread_count, visit_blob_lookup_batches, BlobDiskLookup, BlobScanSparseMode,
5874 WorktreeBlobScanOptions, PARALLEL_STAT_MIN_ENTRIES,
5875 };
5876 let preload_index = config
5877 .get_bool("core.preloadIndex")
5878 .and_then(|r| r.ok())
5879 .unwrap_or(true);
5880 let blob_scan = WorktreeBlobScanOptions {
5881 preload_index,
5882 stat_parallel_threads: options.stat_parallel_threads,
5883 ..WorktreeBlobScanOptions::default()
5884 };
5885 let blob_dirs = if let Some(limit) = options.restrict_to_paths.as_ref() {
5886 crate::worktree_scan::group_blob_entries_by_dir_for_paths(index, limit)
5887 } else {
5888 group_blob_entries_by_dir(index, honor_fsmonitor_valid)
5889 };
5890 let work_tree_owned = work_tree.to_path_buf();
5891 let dir_abs = move |dir: &[u8]| {
5892 if dir.is_empty() {
5893 work_tree_owned.clone()
5894 } else {
5895 index_entry_worktree_abs_bytes(&work_tree_owned, dir, precompose_unicode, ignorecase)
5896 }
5897 };
5898 let work_tree_for_files = work_tree.to_path_buf();
5899 let file_abs = move |rel: &[u8]| {
5900 index_entry_worktree_abs_bytes(&work_tree_for_files, rel, precompose_unicode, ignorecase)
5901 };
5902 let blob_count = blob_dirs.len();
5903 let thread_policy = options.thread_policy.unwrap_or_default();
5904 let hash_threads = stat_scan_thread_count(&thread_policy, &config, blob_scan, blob_count);
5905 let parallel_blob_work = blob_count >= PARALLEL_STAT_MIN_ENTRIES && hash_threads > 1;
5906 let mut staging_hash_jobs: Vec<StagingHashJob> = Vec::new();
5907 let mut refresh_content_verified = vec![false; index.entries.len()];
5909 let sparse = if parallel_blob_work {
5914 BlobScanSparseMode::StatusDiff {
5915 stat_policy,
5916 index_mtime,
5917 mode_policy,
5918 for_status: options.for_status,
5919 refresh_index_stat_in_pass,
5920 parallel_blob_work,
5921 honor_fsmonitor_stat_skip: use_fsmonitor_stat_skip,
5922 }
5923 } else {
5924 BlobScanSparseMode::Dense
5925 };
5926 let blob_stat_batches = collect_blob_lookup_batches_with_literal_paths(
5927 index,
5928 &blob_dirs,
5929 blob_scan,
5930 &thread_policy,
5931 &config,
5932 dir_abs,
5933 file_abs,
5934 !precompose_unicode && !ignorecase,
5935 || {
5936 let mut dir_symlinks = SymlinkDirCache::default();
5937 move |rel_path: &[u8]| {
5938 dir_symlinks.has_symlink_in_path_bytes(
5939 work_tree,
5940 rel_path,
5941 precompose_unicode,
5942 ignorecase,
5943 )
5944 }
5945 },
5946 sparse,
5947 options.cancel_watch.clone(),
5948 owner_poll.clone(),
5949 )?;
5950 let identity_by_entry = std::cell::RefCell::new(std::collections::HashMap::new());
5953 let conversion_is_identity = |entry_index: usize, path: &str| -> bool {
5954 *identity_by_entry
5955 .borrow_mut()
5956 .entry(entry_index)
5957 .or_insert_with(|| crlf::git_conversion_is_identity(&conv, &file_attrs_for(path)))
5958 };
5959 let refresh_checks = if refresh_index_stat_in_pass {
5960 let par_opts =
5961 refresh_hash_parallel_options(&thread_policy, &config, options.stat_parallel_threads);
5962 refresh_content_checks(
5963 odb,
5964 index,
5965 &blob_stat_batches,
5966 index_mtime,
5967 stat_policy,
5968 &|entry_index, ie, stat| {
5969 !(use_fsmonitor_stat_skip && crate::fsmonitor::entry_skips_worktree_stat(ie, index))
5970 && worktree_diff_refreshes_entry(
5971 ie,
5972 stat,
5973 index_mtime,
5974 stat_policy,
5975 options.for_status,
5976 || {
5977 conversion_is_identity(
5978 entry_index,
5979 &crate::repo_relpath::storage_string(&ie.path),
5980 )
5981 },
5982 )
5983 },
5984 &WorktreeHashContext {
5985 work_tree,
5986 precompose_unicode,
5987 ignorecase,
5988 conv: &conv,
5989 file_attrs_for: &file_attrs_for,
5990 filter_process: filter_fp,
5991 },
5992 par_opts,
5993 options.cancel_watch.as_ref(),
5994 )?
5995 } else {
5996 RefreshChecks::default()
5997 };
5998 poll_diff_cancel(&options, owner_poll.as_ref())?;
5999 visit_blob_lookup_batches(blob_stat_batches, |entry_index, lookup| {
6000 poll_diff_cancel(&options, owner_poll.as_ref())?;
6001 let path_owned = crate::repo_relpath::storage_string(&index.entries[entry_index].path);
6002 let path_str_ref = path_owned.as_str();
6003 if use_fsmonitor_stat_skip {
6004 let ie_skip = &index.entries[entry_index];
6005 if crate::fsmonitor::entry_skips_worktree_stat(ie_skip, index) {
6006 return Ok(());
6007 }
6008 }
6009 match lookup {
6010 BlobDiskLookup::Missing => {
6011 if icase_collisions.as_ref().is_some_and(|map| {
6012 map.preserve_missing_collision_loser(&index.entries[entry_index].path)
6013 }) {
6014 return Ok(());
6015 }
6016 let ie = &index.entries[entry_index];
6017 result.push(DiffEntry {
6018 status: DiffStatus::Deleted,
6019 old_path: Some(path_str_ref.to_owned()),
6020 new_path: None,
6021 old_mode: FileMode::new(ie.mode),
6022 new_mode: FileMode::ABSENT,
6023 old_oid: ie.oid,
6024 new_oid: zero_oid(),
6025 score: None,
6026 });
6027 }
6028 BlobDiskLookup::Io(e) => {
6029 blob_disk_lookup_io_result(
6030 options.diagnostics.as_ref(),
6031 path_str_ref,
6032 e,
6033 options.for_status,
6034 materialize_dirty_blobs,
6035 )?;
6036 }
6037 BlobDiskLookup::Present(stat) => {
6038 if materialize_dirty_blobs {
6039 staging_stat_cache.insert(path_str_ref.to_owned(), stat.fields);
6040 }
6041 if refresh_index_stat_in_pass
6042 && worktree_diff_refreshes_entry(
6043 &index.entries[entry_index],
6044 &stat,
6045 index_mtime,
6046 stat_policy,
6047 options.for_status,
6048 || conversion_is_identity(entry_index, path_str_ref),
6049 )
6050 {
6051 let content_matches = RefreshContent::matches(
6052 refresh_checks.get(entry_index),
6053 &index.entries[entry_index].oid,
6054 );
6055 let refresh = refresh_index_blob_stat_from_scan(
6056 &mut index.entries[entry_index],
6057 &stat,
6058 index_mtime,
6059 stat_policy,
6060 content_matches,
6061 );
6062 if refresh.index_changed {
6063 index_stat_refresh_changed = true;
6064 }
6065 if refresh.content_verified {
6066 refresh_content_verified[entry_index] = true;
6067 let path = index.entries[entry_index].path.clone();
6068 let oid = index.entries[entry_index].oid;
6069 index.mark_content_verified(&path, oid);
6070 }
6071 }
6072 let ie = &index.entries[entry_index];
6073 let worktree_mode =
6074 mode_policy.index_mode_for_worktree(stat.git_mode, Some(ie.mode));
6075 if !materialize_dirty_blobs
6078 && !stat.is_dir
6079 && stat.git_mode != crate::index::MODE_SYMLINK
6080 && RefreshContent::differs(refresh_checks.get(entry_index), &ie.oid)
6081 {
6082 result.push(worktree_content_change_entry(
6083 ie,
6084 path_str_ref,
6085 worktree_mode,
6086 zero_oid(),
6087 ));
6088 return Ok(());
6089 }
6090 if parallel_blob_work && !stat.is_dir && stat.git_mode != crate::index::MODE_SYMLINK
6094 {
6095 let stat_same = stat_matches_blob_stat(ie, &stat, stat_policy);
6096 if stat_same
6097 && worktree_mode == ie.mode
6098 && index_entry_skips_worktree_content_hash_after_stat_match(
6099 ie,
6100 index_mtime,
6101 stat_policy,
6102 )
6103 {
6104 if crate::fsmonitor::mark_fsmonitor_valid_proven_clean(index, entry_index) {
6105 fsmonitor_valid_updated = true;
6106 }
6107 return Ok(());
6108 }
6109 if stat_same && worktree_mode != ie.mode {
6110 result.push(worktree_content_change_entry(
6111 ie,
6112 path_str_ref,
6113 worktree_mode,
6114 ie.oid,
6115 ));
6116 return Ok(());
6117 }
6118 if !icase_collisions.as_ref().is_some_and(|map| {
6119 map.diff_requires_worktree_hash(&index.entries[entry_index].path)
6120 }) && try_push_status_modified_on_size_mismatch(
6121 odb,
6122 ie,
6123 path_str_ref,
6124 stat.worktree_size(),
6125 worktree_mode,
6126 stat.raw_size,
6127 options.for_status,
6128 || conversion_is_identity(entry_index, path_str_ref),
6129 &mut result,
6130 )? {
6131 return Ok(());
6132 }
6133 if refresh_content_verified[entry_index] {
6134 if worktree_mode == ie.mode {
6135 if crate::fsmonitor::mark_fsmonitor_valid_proven_clean(
6136 index,
6137 entry_index,
6138 ) {
6139 fsmonitor_valid_updated = true;
6140 }
6141 } else {
6142 result.push(worktree_content_change_entry(
6143 ie,
6144 path_str_ref,
6145 worktree_mode,
6146 ie.oid,
6147 ));
6148 }
6149 } else {
6150 staging_hash_jobs.push(StagingHashJob::new(
6151 entry_index,
6152 worktree_mode,
6153 file_attrs_for(path_str_ref),
6154 ));
6155 }
6156 } else {
6157 let same_file_type = (worktree_mode == crate::index::MODE_SYMLINK)
6161 == (ie.mode == crate::index::MODE_SYMLINK);
6162 if !stat.is_dir
6163 && worktree_mode == ie.mode
6164 && ((refresh_content_verified[entry_index] && same_file_type)
6165 || (stat_matches_blob_stat(ie, &stat, stat_policy)
6166 && index_entry_skips_worktree_content_hash_after_stat_match(
6167 ie,
6168 index_mtime,
6169 stat_policy,
6170 )))
6171 {
6172 if crate::fsmonitor::mark_fsmonitor_valid_proven_clean(index, entry_index) {
6173 fsmonitor_valid_updated = true;
6174 }
6175 return Ok(());
6176 }
6177 let file_path = index_entry_worktree_abs_bytes(
6178 work_tree,
6179 &index.entries[entry_index].path,
6180 precompose_unicode,
6181 ignorecase,
6182 );
6183 let Some(meta) = symlink_metadata_worktree_diff(
6184 &file_path,
6185 path_str_ref,
6186 options.diagnostics.as_ref(),
6187 options.for_status,
6188 materialize_dirty_blobs,
6189 )?
6190 else {
6191 return Ok(());
6192 };
6193 push_index_blob_worktree_diff(
6194 odb,
6195 ie,
6196 path_str_ref,
6197 &file_path,
6198 meta,
6199 index_mtime,
6200 mode_policy,
6201 stat_policy,
6202 &file_attrs_for,
6203 &conv,
6204 materialize_dirty_blobs,
6205 hash_blobs_without_write,
6206 options.for_status,
6207 options.diagnostics.as_ref(),
6208 filter_fp,
6209 icase_collisions.as_ref(),
6210 &mut result,
6211 )?;
6212 }
6213 }
6214 }
6215 Ok(())
6216 })?;
6217 if parallel_blob_work && !staging_hash_jobs.is_empty() {
6218 parallel_process_blob_worktree_jobs(
6219 odb,
6220 &StagingHashSource {
6221 index,
6222 work_tree,
6223 precompose_unicode,
6224 ignorecase,
6225 },
6226 &staging_hash_jobs,
6227 &thread_policy,
6228 &config,
6229 materialize_dirty_blobs,
6230 hash_blobs_without_write,
6231 options.for_status,
6232 options.diagnostics.as_ref(),
6233 &conv,
6234 filter_fp,
6235 icase_collisions.as_ref(),
6236 &mut result,
6237 )?;
6238 }
6239
6240 for (path, (_, base_entry)) in unmerged_base {
6241 let file_path = index_entry_worktree_abs(work_tree, &path, precompose_unicode, ignorecase);
6242 let wt_meta = match fs::symlink_metadata(&file_path) {
6243 Ok(meta) => Some(meta),
6244 Err(e)
6245 if e.kind() == std::io::ErrorKind::NotFound
6246 || e.raw_os_error() == Some(20) =>
6247 {
6248 None
6249 }
6250 Err(e) => return Err(Error::Io(e)),
6251 };
6252
6253 let new_mode = wt_meta.as_ref().map_or_else(
6254 || FileMode::ABSENT,
6255 |meta| FileMode::new(mode_from_metadata(meta)),
6256 );
6257 result.push(DiffEntry {
6258 status: DiffStatus::Unmerged,
6259 old_path: Some(path.clone()),
6260 new_path: Some(path.clone()),
6261 old_mode: FileMode::ABSENT,
6262 new_mode,
6263 old_oid: zero_oid(),
6264 new_oid: zero_oid(),
6265 score: None,
6266 });
6267
6268 if !options.for_status {
6269 if let Some(meta) = wt_meta {
6270 let file_attrs = file_attrs_for(&path);
6271 let wt_oid = worktree_file_oid(
6272 odb,
6273 &file_path,
6274 &meta,
6275 &conv,
6276 &file_attrs,
6277 &path,
6278 Some(&base_entry),
6279 materialize_dirty_blobs,
6280 filter_fp,
6281 )?;
6282 let wt_mode = mode_from_metadata(&meta);
6283 if wt_oid != base_entry.oid || wt_mode != base_entry.mode {
6284 result.push(DiffEntry {
6285 status: DiffStatus::for_content_change(base_entry.mode, wt_mode),
6286 old_path: Some(path.clone()),
6287 new_path: Some(path),
6288 old_mode: FileMode::new(base_entry.mode),
6289 new_mode: FileMode::new(wt_mode),
6290 old_oid: base_entry.oid,
6291 new_oid: wt_oid,
6292 score: None,
6293 });
6294 }
6295 }
6296 }
6297 }
6298
6299 if fsmonitor_valid_updated {
6300 index.fsmonitor_validity_changed();
6301 index_stat_refresh_changed = true;
6302 }
6303
6304 Ok((result, index_stat_refresh_changed, staging_stat_cache))
6305}
6306
6307#[derive(Default)]
6314pub(crate) struct SymlinkDirCache {
6315 real_dirs: Vec<u8>,
6318 blocked: Option<(Vec<u8>, bool)>,
6321 #[cfg(all(target_os = "linux", target_pointer_width = "64"))]
6323 chain: Option<crate::worktree_scan::RealDirChain>,
6324 #[cfg(test)]
6326 fs_queries: usize,
6327}
6328
6329fn path_within_dir(path: &[u8], dir: &[u8]) -> bool {
6331 path.starts_with(dir) && path.get(dir.len()).is_none_or(|&b| b == b'/')
6332}
6333
6334fn common_dir_prefix_len(a: &[u8], b: &[u8]) -> usize {
6336 let same = a.iter().zip(b).take_while(|(x, y)| x == y).count();
6337 let ends_component = |p: &[u8]| p.get(same).is_none_or(|&c| c == b'/');
6338 if ends_component(a) && ends_component(b) {
6339 return same;
6340 }
6341 a[..same].iter().rposition(|&c| c == b'/').unwrap_or(0)
6342}
6343
6344pub(crate) fn index_entry_worktree_abs(
6345 work_tree: &Path,
6346 index_relpath: &str,
6347 precompose_unicode: bool,
6348 ignorecase: bool,
6349) -> PathBuf {
6350 index_entry_worktree_abs_bytes(
6351 work_tree,
6352 index_relpath.as_bytes(),
6353 precompose_unicode,
6354 ignorecase,
6355 )
6356}
6357
6358pub(crate) fn index_entry_worktree_abs_bytes(
6359 work_tree: &Path,
6360 index_relpath: &[u8],
6361 precompose_unicode: bool,
6362 ignorecase: bool,
6363) -> PathBuf {
6364 if precompose_unicode || ignorecase {
6365 if let Ok(s) = std::str::from_utf8(index_relpath) {
6366 if precompose_unicode {
6367 return crate::unicode_normalization::resolve_worktree_path_for_staging(
6368 work_tree, s, true,
6369 )
6370 .abs;
6371 }
6372 return crate::path_icase::worktree_path_for_index_entry(work_tree, s, ignorecase);
6373 }
6374 }
6375 crate::index::worktree_path_from_index_rel(work_tree, index_relpath)
6376}
6377
6378#[must_use]
6383pub fn index_mtime_for_diff(index: &Index, explicit: Option<(u32, u32)>) -> Option<(u32, u32)> {
6384 explicit.or(index.source_mtime)
6385}
6386
6387impl SymlinkDirCache {
6388 pub(crate) fn has_symlink_in_path_bytes(
6393 &mut self,
6394 work_tree: &Path,
6395 rel_path: &[u8],
6396 precompose_unicode: bool,
6397 ignorecase: bool,
6398 ) -> bool {
6399 use crate::worktree_scan::{ancestor_kind, AncestorKind};
6400
6401 let Some(dir_len) = rel_path.iter().rposition(|&b| b == b'/') else {
6402 return false;
6403 };
6404 let dir = &rel_path[..dir_len];
6405 if let Some((blocked, is_symlink)) = &self.blocked {
6406 if path_within_dir(dir, blocked) {
6407 return *is_symlink;
6408 }
6409 }
6410 let verified = common_dir_prefix_len(dir, &self.real_dirs);
6411 self.real_dirs.truncate(verified);
6412 #[cfg(all(target_os = "linux", target_pointer_width = "64"))]
6413 if !precompose_unicode
6414 && !ignorecase
6415 && dir[verified..]
6416 .split(|&b| b == b'/')
6417 .filter(|component| !component.is_empty())
6418 .nth(1)
6419 .is_some()
6420 {
6421 let chain = match &mut self.chain {
6422 Some(chain) if chain.work_tree() == work_tree => chain,
6423 slot => slot.insert(crate::worktree_scan::RealDirChain::open(work_tree)),
6424 };
6425 #[cfg(test)]
6426 {
6427 self.fs_queries += 1;
6428 }
6429 if chain.confirms(dir) {
6430 self.real_dirs.clear();
6431 self.real_dirs.extend_from_slice(dir);
6432 return false;
6433 }
6434 }
6435 let mut start = verified;
6436 while start < dir.len() {
6437 if dir[start] == b'/' {
6438 start += 1;
6439 continue;
6440 }
6441 let end = dir[start..]
6442 .iter()
6443 .position(|&b| b == b'/')
6444 .map_or(dir.len(), |i| start + i);
6445 let prefix = &dir[..end];
6446 let abs =
6447 index_entry_worktree_abs_bytes(work_tree, prefix, precompose_unicode, ignorecase);
6448 #[cfg(test)]
6449 {
6450 self.fs_queries += 1;
6451 }
6452 match ancestor_kind(&abs) {
6453 AncestorKind::Directory => {
6454 let known = self.real_dirs.len();
6455 self.real_dirs.extend_from_slice(&prefix[known..]);
6456 }
6457 AncestorKind::Symlink => {
6458 self.blocked = Some((prefix.to_vec(), true));
6459 return true;
6460 }
6461 AncestorKind::Other => {
6462 self.blocked = Some((prefix.to_vec(), false));
6463 return false;
6464 }
6465 }
6466 start = end;
6467 }
6468 false
6469 }
6470}
6471
6472#[must_use]
6478pub fn entry_is_racy(ie: &IndexEntry, index_mtime: Option<(u32, u32)>) -> bool {
6479 entry_is_racy_with_policy(ie, index_mtime, crate::index::StatMatchPolicy::default())
6480}
6481
6482#[must_use]
6488pub fn entry_is_racy_with_policy(
6489 ie: &IndexEntry,
6490 index_mtime: Option<(u32, u32)>,
6491 policy: crate::index::StatMatchPolicy,
6492) -> bool {
6493 let Some((index_mtime_sec, index_mtime_nsec)) = index_mtime else {
6494 return false;
6495 };
6496 if index_mtime_sec == 0 {
6497 return false;
6498 }
6499 if policy.use_nanosec && policy.check_stat {
6500 index_mtime_sec < ie.mtime_sec
6501 || (index_mtime_sec == ie.mtime_sec && index_mtime_nsec <= ie.mtime_nsec)
6502 } else {
6503 index_mtime_sec <= ie.mtime_sec
6504 }
6505}
6506
6507pub fn smudge_racily_clean_entries(
6524 odb: &Odb,
6525 git_dir: &Path,
6526 index: &mut Index,
6527 work_tree: &Path,
6528 index_mtime: Option<(u32, u32)>,
6529 config: Option<&ConfigSet>,
6530 filter_process: Option<&crate::filter_process::FilterProcessState>,
6531) -> bool {
6532 use crate::crlf;
6533 use crate::index::{MODE_EXECUTABLE, MODE_REGULAR, MODE_SYMLINK};
6534
6535 let Some(index_mtime) = index_mtime else {
6536 return false;
6537 };
6538
6539 let config = config.cloned().unwrap_or_else(|| {
6540 ConfigSet::load(
6541 &crate::environment::Environment::empty(),
6542 Some(git_dir),
6543 true,
6544 )
6545 .unwrap_or_default()
6546 });
6547 let conv = crlf::ConversionConfig::from_config(&config);
6548 let attrs = crlf::load_gitattributes(work_tree);
6549 let verified = std::mem::take(&mut index.content_verified);
6550
6551 let mut changed = false;
6552 for ie in &mut index.entries {
6553 let marked_verified = ie.take_content_verified();
6554 if ie.stage() != 0 || ie.skip_worktree() || ie.assume_unchanged() || ie.intent_to_add() {
6555 continue;
6556 }
6557 if ie.mode != MODE_REGULAR && ie.mode != MODE_EXECUTABLE && ie.mode != MODE_SYMLINK {
6558 continue;
6559 }
6560 if ie.size == 0 {
6561 continue;
6562 }
6563 if !entry_is_racy(ie, Some(index_mtime)) {
6564 continue;
6565 }
6566 if marked_verified || verified.get(&ie.path) == Some(&ie.oid) {
6567 continue;
6568 }
6569 let Ok(path) = std::str::from_utf8(&ie.path) else {
6570 continue;
6571 };
6572 let abs = work_tree.join(path);
6573 let Ok(meta) = fs::symlink_metadata(&abs) else {
6574 continue;
6575 };
6576 if !stat_matches(ie, &meta) {
6577 continue;
6578 }
6579 let file_attrs = crlf::get_file_attrs(&attrs, path, false, &config);
6580 let content_matches = hash_worktree_file(
6581 odb,
6582 &abs,
6583 &meta,
6584 &conv,
6585 &file_attrs,
6586 path,
6587 Some(ie),
6588 filter_process,
6589 )
6590 .map(|oid| oid == ie.oid)
6591 .unwrap_or(false);
6592 if content_matches {
6593 continue;
6594 }
6595 ie.size = 0;
6596 changed = true;
6597 }
6598 changed
6599}
6600pub fn worktree_differs_from_index_entry(
6608 odb: &Odb,
6609 repository_git_dir: &Path,
6610 work_tree: &Path,
6611 ie: &IndexEntry,
6612 ignore_submodule_untracked: bool,
6613 config: Option<&ConfigSet>,
6614 filter_process: Option<&crate::filter_process::FilterProcessState>,
6615) -> Result<bool> {
6616 WorktreeEntryComparer::new(repository_git_dir, work_tree, config).differs(
6617 odb,
6618 ie,
6619 ignore_submodule_untracked,
6620 filter_process,
6621 )
6622}
6623
6624pub(crate) struct WorktreeEntryComparer<'a> {
6628 work_tree: &'a Path,
6629 config: ConfigSet,
6630 mode_policy: crate::index::WorktreeModePolicy,
6631 precompose_unicode: bool,
6632 ignorecase: bool,
6633 conv: std::cell::OnceCell<crate::crlf::ConversionConfig>,
6634 attrs: std::cell::OnceCell<Vec<crate::crlf::AttrRule>>,
6635}
6636
6637impl<'a> WorktreeEntryComparer<'a> {
6638 pub(crate) fn new(
6640 repository_git_dir: &Path,
6641 work_tree: &'a Path,
6642 config: Option<&ConfigSet>,
6643 ) -> Self {
6644 let config = config.cloned().unwrap_or_else(|| {
6645 ConfigSet::load(
6646 &crate::environment::Environment::empty(),
6647 Some(repository_git_dir),
6648 true,
6649 )
6650 .unwrap_or_else(|_| ConfigSet::new())
6651 });
6652 let flag =
6653 |key: &str, default: bool| config.get_bool(key).and_then(|r| r.ok()).unwrap_or(default);
6654 Self {
6655 work_tree,
6656 mode_policy: crate::index::WorktreeModePolicy::from_config(&config),
6657 precompose_unicode: flag("core.precomposeunicode", false),
6658 ignorecase: flag("core.ignorecase", false),
6659 config,
6660 conv: std::cell::OnceCell::new(),
6661 attrs: std::cell::OnceCell::new(),
6662 }
6663 }
6664
6665 pub(crate) fn differs(
6667 &self,
6668 odb: &Odb,
6669 ie: &IndexEntry,
6670 ignore_submodule_untracked: bool,
6671 filter_process: Option<&crate::filter_process::FilterProcessState>,
6672 ) -> Result<bool> {
6673 use crate::crlf;
6674
6675 let work_tree = self.work_tree;
6676 let path_stored = crate::repo_relpath::storage_string(&ie.path);
6677 let path_str_ref = crate::repo_relpath::as_utf8_for_matching(&path_stored);
6678 let file_path = index_entry_worktree_abs_bytes(
6679 work_tree,
6680 &ie.path,
6681 self.precompose_unicode,
6682 self.ignorecase,
6683 );
6684
6685 if ie.mode == 0o160000 {
6686 let sub_head_oid = read_submodule_head(&file_path);
6687 let ref_matches = match sub_head_oid {
6688 Some(oid) => oid == ie.oid,
6689 None => uninitialized_gitlink_worktree_matches_index(&file_path),
6690 };
6691 let mut flags = submodule_porcelain_flags(work_tree, path_str_ref.as_ref(), ie.oid);
6692 if ignore_submodule_untracked {
6693 flags.untracked = false;
6694 }
6695 return Ok(!ref_matches || flags.modified || flags.untracked);
6696 }
6697
6698 let meta = match fs::symlink_metadata(&file_path) {
6699 Ok(m) => m,
6700 Err(e)
6701 if e.kind() == std::io::ErrorKind::NotFound
6702 || e.raw_os_error() == Some(20) =>
6703 {
6704 return Ok(true);
6705 }
6706 Err(e) => return Err(Error::Io(e)),
6707 };
6708
6709 if meta.is_dir() {
6710 return Ok(true);
6711 }
6712
6713 if !self
6714 .mode_policy
6715 .worktree_mode_matches(mode_from_metadata(&meta), ie.mode)
6716 {
6717 return Ok(true);
6718 }
6719
6720 let conv = self
6721 .conv
6722 .get_or_init(|| crlf::ConversionConfig::from_config(&self.config));
6723 let attrs = self
6724 .attrs
6725 .get_or_init(|| crlf::load_gitattributes(work_tree));
6726 let file_attrs = crlf::get_file_attrs(attrs, path_str_ref.as_ref(), false, &self.config);
6727 let worktree_oid = hash_worktree_file(
6728 odb,
6729 &file_path,
6730 &meta,
6731 conv,
6732 &file_attrs,
6733 path_str_ref.as_ref(),
6734 Some(ie),
6735 filter_process,
6736 )?;
6737
6738 let mut eff_oid = worktree_oid;
6739 if eff_oid != ie.oid {
6740 if let Ok(raw) = fs::read(&file_path) {
6741 let raw_oid = odb.hash(ObjectKind::Blob, &raw)?;
6742 if raw_oid == ie.oid {
6743 eff_oid = ie.oid;
6744 }
6745 }
6746 }
6747
6748 Ok(eff_oid != ie.oid)
6749 }
6750}
6751
6752pub struct WorktreeAddRefreshParams<'a> {
6754 pub odb: &'a Odb,
6756 pub ie: &'a IndexEntry,
6758 pub meta: &'a fs::Metadata,
6760 pub abs_path: &'a Path,
6762 pub rel_path: &'a str,
6764 pub conv: &'a crate::crlf::ConversionConfig,
6766 pub file_attrs: &'a crate::crlf::FileAttrs,
6768 pub index_mtime: Option<(u32, u32)>,
6770 pub staged_mode: u32,
6773 pub filter_process: Option<&'a crate::filter_process::FilterProcessState>,
6775}
6776
6777#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6779pub enum WorktreeAddRefresh {
6780 UpToDate,
6782 ModeOnly {
6784 mode: u32,
6786 },
6787 StatOnly,
6789 NeedsRestage,
6791}
6792
6793#[must_use]
6795pub(crate) fn index_stat_cache_trustworthy(index_mtime: Option<(u32, u32)>) -> bool {
6796 match index_mtime {
6797 None => false,
6798 Some((0, _)) => false,
6799 Some(_) => true,
6800 }
6801}
6802
6803#[must_use]
6812pub(crate) fn index_entry_skips_worktree_content_hash_after_stat_match(
6813 ie: &IndexEntry,
6814 index_mtime: Option<(u32, u32)>,
6815 stat_policy: crate::index::StatMatchPolicy,
6816) -> bool {
6817 if ie.stat_cache_unset() || (ie.size == 0 && !ie.oid.is_empty_blob()) {
6818 return false;
6819 }
6820 if !index_stat_cache_trustworthy(index_mtime) {
6821 return false;
6822 }
6823 !entry_is_racy_with_policy(ie, index_mtime, stat_policy)
6824}
6825
6826pub fn classify_worktree_entry_for_add(
6840 params: &WorktreeAddRefreshParams<'_>,
6841) -> Result<WorktreeAddRefresh> {
6842 use crate::index::{MODE_EXECUTABLE, MODE_REGULAR, MODE_SYMLINK};
6843
6844 let ie = params.ie;
6845 let meta = params.meta;
6846 let staged_mode = params.staged_mode;
6847
6848 if ie.intent_to_add() {
6849 return Ok(WorktreeAddRefresh::NeedsRestage);
6850 }
6851
6852 let racy = entry_is_racy(ie, params.index_mtime);
6853 let stat_same = stat_matches(ie, meta);
6854 let trust_stat = index_stat_cache_trustworthy(params.index_mtime);
6855
6856 if ie.mode == MODE_SYMLINK {
6857 if !meta.file_type().is_symlink() && staged_mode != MODE_SYMLINK {
6860 return Ok(WorktreeAddRefresh::NeedsRestage);
6861 }
6862 if stat_same && !racy && trust_stat && staged_mode == ie.mode {
6863 return Ok(WorktreeAddRefresh::UpToDate);
6864 }
6865 if stat_same && !racy && trust_stat && staged_mode != ie.mode {
6866 return Ok(WorktreeAddRefresh::ModeOnly { mode: staged_mode });
6867 }
6868 return worktree_effective_oid_matches_index(params);
6869 }
6870
6871 if ie.mode != MODE_REGULAR && ie.mode != MODE_EXECUTABLE {
6872 return Ok(WorktreeAddRefresh::NeedsRestage);
6873 }
6874
6875 if meta.file_type().is_symlink() || meta.is_dir() {
6876 return Ok(WorktreeAddRefresh::NeedsRestage);
6877 }
6878
6879 if stat_same && !racy && trust_stat {
6880 if staged_mode == ie.mode {
6881 return Ok(WorktreeAddRefresh::UpToDate);
6882 }
6883 return Ok(WorktreeAddRefresh::ModeOnly { mode: staged_mode });
6884 }
6885
6886 worktree_effective_oid_matches_index(params)
6887}
6888
6889fn worktree_effective_oid_matches_index(
6890 params: &WorktreeAddRefreshParams<'_>,
6891) -> Result<WorktreeAddRefresh> {
6892 let WorktreeAddRefreshParams {
6893 odb,
6894 ie,
6895 meta,
6896 abs_path,
6897 rel_path,
6898 conv,
6899 file_attrs,
6900 staged_mode,
6901 ..
6902 } = params;
6903 let worktree_oid = hash_worktree_file(
6904 odb,
6905 abs_path,
6906 meta,
6907 conv,
6908 file_attrs,
6909 rel_path,
6910 Some(ie),
6911 params.filter_process,
6912 )?;
6913 let mut eff_oid = worktree_oid;
6914 if eff_oid != ie.oid {
6915 if let Ok(raw) = fs::read(abs_path) {
6916 let raw_oid = odb.hash(ObjectKind::Blob, &raw)?;
6917 if raw_oid == ie.oid {
6918 eff_oid = ie.oid;
6919 }
6920 }
6921 }
6922 if eff_oid != ie.oid {
6923 return Ok(WorktreeAddRefresh::NeedsRestage);
6924 }
6925 if *staged_mode != ie.mode {
6926 return Ok(WorktreeAddRefresh::ModeOnly { mode: *staged_mode });
6927 }
6928 if stat_matches(ie, meta) {
6929 Ok(WorktreeAddRefresh::UpToDate)
6930 } else {
6931 Ok(WorktreeAddRefresh::StatOnly)
6932 }
6933}
6934
6935#[must_use]
6937pub fn stat_matches(ie: &IndexEntry, meta: &fs::Metadata) -> bool {
6938 stat_matches_with_policy(ie, meta, crate::index::StatMatchPolicy::default())
6939}
6940
6941pub(crate) fn stat_matches_blob_stat(
6943 ie: &IndexEntry,
6944 stat: &crate::worktree_scan::BlobDiskStat,
6945 policy: crate::index::StatMatchPolicy,
6946) -> bool {
6947 stat.fields.matches_index_entry(ie, policy)
6948}
6949
6950#[must_use]
6952pub fn stat_matches_with_policy(
6953 ie: &IndexEntry,
6954 meta: &fs::Metadata,
6955 policy: crate::index::StatMatchPolicy,
6956) -> bool {
6957 if crate::index::munge_index_size(meta.len()) != ie.size {
6958 return false;
6959 }
6960 #[cfg(unix)]
6961 {
6962 use std::os::unix::fs::MetadataExt;
6963 if meta.mtime() as u32 != ie.mtime_sec {
6964 return false;
6965 }
6966 if policy.use_nanosec && policy.check_stat && meta.mtime_nsec() as u32 != ie.mtime_nsec {
6967 return false;
6968 }
6969 if policy.trust_ctime && policy.check_stat {
6970 if meta.ctime() as u32 != ie.ctime_sec {
6971 return false;
6972 }
6973 if policy.use_nanosec && meta.ctime_nsec() as u32 != ie.ctime_nsec {
6974 return false;
6975 }
6976 }
6977 if policy.check_stat && meta.ino() as u32 != ie.ino {
6978 return false;
6979 }
6980 }
6981 #[cfg(not(unix))]
6982 {
6983 let ((ctime_sec, ctime_nsec), (mtime_sec, mtime_nsec)) = crate::index::metadata_times(meta);
6984 if mtime_sec != ie.mtime_sec {
6985 return false;
6986 }
6987 if policy.use_nanosec && policy.check_stat && mtime_nsec != ie.mtime_nsec {
6988 return false;
6989 }
6990 if policy.trust_ctime && policy.check_stat {
6991 if ctime_sec != ie.ctime_sec {
6992 return false;
6993 }
6994 if policy.use_nanosec && ctime_nsec != ie.ctime_nsec {
6995 return false;
6996 }
6997 }
6998 }
6999 true
7000}
7001
7002pub fn refresh_index_stat_content_verified(
7025 odb: &Odb,
7026 git_dir: &Path,
7027 index: &mut Index,
7028 work_tree: &Path,
7029 index_mtime: Option<(u32, u32)>,
7030 config: Option<&ConfigSet>,
7031 _stat_parallel_threads: Option<usize>,
7032) -> Result<bool> {
7033 let _ = _stat_parallel_threads;
7034 refresh_index_stat_content_verified_with_rules(
7035 odb,
7036 git_dir,
7037 index,
7038 work_tree,
7039 index_mtime,
7040 config,
7041 _stat_parallel_threads,
7042 None,
7043 )
7044}
7045
7046#[allow(clippy::too_many_arguments)]
7054pub fn refresh_index_stat_content_verified_with_rules(
7055 odb: &Odb,
7056 git_dir: &Path,
7057 index: &mut Index,
7058 work_tree: &Path,
7059 index_mtime: Option<(u32, u32)>,
7060 config: Option<&ConfigSet>,
7061 stat_parallel_threads: Option<usize>,
7062 worktree_rules: Option<&std::sync::Arc<std::sync::Mutex<crate::worktree_rules::WorktreeRules>>>,
7063) -> Result<bool> {
7064 use crate::config::ConfigSet;
7065 use crate::crlf;
7066 use crate::worktree_scan::{
7067 collect_blob_lookup_batches, group_blob_entries_by_dir, visit_blob_lookup_batches,
7068 BlobDiskLookup, BlobScanSparseMode, WorktreeBlobScanOptions,
7069 };
7070
7071 let index_mtime = index_mtime_for_diff(index, index_mtime);
7072 let rules_lock = worktree_rules
7073 .map(|shared| {
7074 shared
7075 .lock()
7076 .map_err(|e| Error::Message(format!("worktree rules lock poisoned: {e}")))
7077 })
7078 .transpose()?;
7079 let config = rules_lock
7080 .as_ref()
7081 .map(|r| r.config().clone())
7082 .unwrap_or_else(|| {
7083 config.cloned().unwrap_or_else(|| {
7084 ConfigSet::load(
7085 &crate::environment::Environment::empty(),
7086 Some(git_dir),
7087 true,
7088 )
7089 .unwrap_or_default()
7090 })
7091 });
7092 let preload_index = config
7093 .get_bool("core.preloadIndex")
7094 .and_then(|r| r.ok())
7095 .unwrap_or(true);
7096 let blob_scan = WorktreeBlobScanOptions {
7097 preload_index,
7098 stat_parallel_threads,
7099 ..WorktreeBlobScanOptions::default()
7100 };
7101 let conv = rules_lock
7102 .as_ref()
7103 .map(|r| r.conversion().clone())
7104 .unwrap_or_else(|| crlf::ConversionConfig::from_config(&config));
7105 let legacy_attrs = if rules_lock.is_none() {
7106 Some(crlf::load_gitattributes_parsed(work_tree))
7107 } else {
7108 None
7109 };
7110 let file_attrs_for = |path: &str| -> crlf::FileAttrs {
7111 if let Some(rules) = rules_lock.as_ref() {
7112 rules.file_attrs(path, false)
7113 } else if let Some(parsed) = legacy_attrs.as_ref() {
7114 crlf::file_attrs_from_gitattributes(&parsed.rules, &parsed.macros, path, false, &config)
7115 } else {
7116 crlf::FileAttrs::default()
7117 }
7118 };
7119 let precompose_unicode = config
7120 .get_bool("core.precomposeunicode")
7121 .and_then(|r| r.ok())
7122 .unwrap_or(false);
7123 let ignorecase = config
7124 .get_bool("core.ignorecase")
7125 .and_then(|r| r.ok())
7126 .unwrap_or(false);
7127 let stat_policy = crate::index::StatMatchPolicy::from_config(Some(&config));
7128
7129 let blob_dirs = group_blob_entries_by_dir(index, false);
7130 let work_tree_owned = work_tree.to_path_buf();
7131 let dir_abs = move |dir: &[u8]| {
7132 if dir.is_empty() {
7133 work_tree_owned.clone()
7134 } else {
7135 index_entry_worktree_abs_bytes(&work_tree_owned, dir, precompose_unicode, ignorecase)
7136 }
7137 };
7138 let work_tree_for_files = work_tree.to_path_buf();
7139 let file_abs = move |rel: &[u8]| {
7140 index_entry_worktree_abs_bytes(&work_tree_for_files, rel, precompose_unicode, ignorecase)
7141 };
7142 let mut changed = false;
7143 let filter_fp = rules_lock.as_ref().map(|r| r.filter_process());
7144 let thread_policy = crate::parallel::ThreadPolicy::default();
7145 let refresh_batches = collect_blob_lookup_batches(
7147 index,
7148 &blob_dirs,
7149 blob_scan,
7150 &thread_policy,
7151 &config,
7152 dir_abs,
7153 file_abs,
7154 || {
7155 let mut dir_symlinks = SymlinkDirCache::default();
7156 move |rel_path: &[u8]| {
7157 dir_symlinks.has_symlink_in_path_bytes(
7158 work_tree,
7159 rel_path,
7160 precompose_unicode,
7161 ignorecase,
7162 )
7163 }
7164 },
7165 BlobScanSparseMode::StatRefresh {
7166 stat_policy,
7167 index_mtime,
7168 },
7169 )?;
7170 let par_opts = refresh_hash_parallel_options(&thread_policy, &config, stat_parallel_threads);
7171 let refresh_checks = refresh_content_checks(
7172 odb,
7173 index,
7174 &refresh_batches,
7175 index_mtime,
7176 stat_policy,
7177 &|_, _, _| true,
7178 &WorktreeHashContext {
7179 work_tree,
7180 precompose_unicode,
7181 ignorecase,
7182 conv: &conv,
7183 file_attrs_for: &file_attrs_for,
7184 filter_process: filter_fp,
7185 },
7186 par_opts,
7187 None,
7188 )?;
7189 visit_blob_lookup_batches(refresh_batches, |entry_index, lookup| {
7190 let BlobDiskLookup::Present(stat) = lookup else {
7191 return Ok(());
7192 };
7193 let content_matches = RefreshContent::matches(
7194 refresh_checks.get(entry_index),
7195 &index.entries[entry_index].oid,
7196 );
7197 let refresh = refresh_index_blob_stat_from_scan(
7198 &mut index.entries[entry_index],
7199 &stat,
7200 index_mtime,
7201 stat_policy,
7202 content_matches,
7203 );
7204 if refresh.index_changed {
7205 changed = true;
7206 }
7207 if refresh.content_verified {
7208 let path = index.entries[entry_index].path.clone();
7209 let oid = index.entries[entry_index].oid;
7210 index.mark_content_verified(&path, oid);
7211 }
7212 Ok(())
7213 })?;
7214 Ok(changed)
7215}
7216
7217struct BlobStatRefreshOutcome {
7219 index_changed: bool,
7221 content_verified: bool,
7223}
7224
7225fn refresh_index_entry_eligible(ie: &IndexEntry) -> bool {
7227 use crate::index::{MODE_EXECUTABLE, MODE_REGULAR, MODE_SYMLINK};
7228 ie.stage() == 0
7229 && !ie.skip_worktree()
7230 && !ie.assume_unchanged()
7231 && !ie.intent_to_add()
7232 && matches!(ie.mode, MODE_REGULAR | MODE_EXECUTABLE | MODE_SYMLINK)
7233}
7234
7235fn refresh_needs_content_check(
7238 ie: &IndexEntry,
7239 stat: &crate::worktree_scan::BlobDiskStat,
7240 index_mtime: Option<(u32, u32)>,
7241 stat_policy: crate::index::StatMatchPolicy,
7242) -> bool {
7243 refresh_index_entry_eligible(ie)
7244 && !stat.is_dir
7245 && (!stat_matches_blob_stat(ie, stat, stat_policy)
7246 || entry_is_racy_with_policy(ie, index_mtime, stat_policy))
7247}
7248
7249pub(crate) fn worktree_diff_refreshes_entry(
7256 ie: &IndexEntry,
7257 stat: &crate::worktree_scan::BlobDiskStat,
7258 index_mtime: Option<(u32, u32)>,
7259 stat_policy: crate::index::StatMatchPolicy,
7260 for_status: bool,
7261 conversion_is_identity: impl FnOnce() -> bool,
7262) -> bool {
7263 if !for_status {
7264 return true;
7265 }
7266 if stat_matches_blob_stat(ie, stat, stat_policy) {
7267 return entry_is_racy_with_policy(ie, index_mtime, stat_policy);
7268 }
7269 !status_size_change_proves_modified(
7270 ie,
7271 stat.worktree_size(),
7272 for_status,
7273 conversion_is_identity,
7274 )
7275}
7276
7277fn refresh_hash_parallel_options(
7284 thread_policy: &crate::parallel::ThreadPolicy,
7285 config: &ConfigSet,
7286 stat_parallel_threads: Option<usize>,
7287) -> crate::parallel::ParallelOptions {
7288 let policy = crate::parallel::ThreadPolicy {
7289 min_items_override: thread_policy.min_items_override.or(Some(0)),
7290 min_total_bytes_override: thread_policy.min_total_bytes_override.or(Some(0)),
7291 ..*thread_policy
7292 };
7293 let mut par_opts = policy.resolve(crate::parallel::ParallelOp::IndexRefresh, config);
7294 if let Some(cap) = stat_parallel_threads {
7295 par_opts.threads = par_opts
7296 .threads
7297 .min(std::num::NonZeroUsize::new(cap).unwrap_or(std::num::NonZeroUsize::MIN));
7298 }
7299 par_opts.min_items = 0;
7300 par_opts.min_total_bytes = None;
7301 par_opts
7302}
7303
7304struct WorktreeHashContext<'a> {
7306 work_tree: &'a Path,
7307 precompose_unicode: bool,
7308 ignorecase: bool,
7309 conv: &'a crate::crlf::ConversionConfig,
7310 file_attrs_for: &'a dyn Fn(&str) -> crate::crlf::FileAttrs,
7311 filter_process: Option<&'a crate::filter_process::FilterProcessState>,
7312}
7313
7314#[allow(clippy::too_many_arguments)]
7324fn refresh_content_checks(
7325 odb: &Odb,
7326 index: &Index,
7327 batches: &[Vec<(usize, crate::worktree_scan::BlobDiskLookup)>],
7328 index_mtime: Option<(u32, u32)>,
7329 stat_policy: crate::index::StatMatchPolicy,
7330 include: &dyn Fn(usize, &IndexEntry, &crate::worktree_scan::BlobDiskStat) -> bool,
7331 ctx: &WorktreeHashContext<'_>,
7332 par_opts: crate::parallel::ParallelOptions,
7333 cancel_watch: Option<&std::sync::Arc<std::sync::atomic::AtomicBool>>,
7334) -> Result<RefreshChecks> {
7335 use crate::progress::{poll_watch, CANCEL_POLL_STRIDE};
7336 use crate::worktree_scan::BlobDiskLookup;
7337
7338 struct ContentCheck {
7340 entry_index: usize,
7341 worktree_mode: u32,
7342 file_attrs: crate::crlf::FileAttrs,
7343 }
7344
7345 let mut jobs = Vec::new();
7346 let mut total_bytes = 0usize;
7347 let mut cancel_stride = 0usize;
7348 for (entry_index, lookup) in batches.iter().flatten() {
7349 cancel_stride = cancel_stride.wrapping_add(1);
7350 if cancel_stride.is_multiple_of(CANCEL_POLL_STRIDE) {
7351 if let Some(w) = cancel_watch {
7352 poll_watch(w)?;
7353 }
7354 }
7355 let BlobDiskLookup::Present(stat) = lookup else {
7356 continue;
7357 };
7358 let ie = &index.entries[*entry_index];
7359 if !refresh_needs_content_check(ie, stat, index_mtime, stat_policy)
7360 || !include(*entry_index, ie, stat)
7361 {
7362 continue;
7363 }
7364 total_bytes = total_bytes.saturating_add(ie.size as usize);
7365 jobs.push(ContentCheck {
7366 entry_index: *entry_index,
7367 worktree_mode: stat.git_mode,
7368 file_attrs: (ctx.file_attrs_for)(&crate::repo_relpath::storage_string(&ie.path)),
7369 });
7370 }
7371 if jobs.is_empty() {
7372 return Ok(RefreshChecks::default());
7373 }
7374 let mut checks = vec![None; index.entries.len()];
7375 let (work_tree, precompose_unicode, ignorecase) =
7376 (ctx.work_tree, ctx.precompose_unicode, ctx.ignorecase);
7377 let (conv, filter_process) = (ctx.conv, ctx.filter_process);
7378 let entries = index.entries.as_slice();
7379 let watch_for_hash = cancel_watch.cloned();
7380 let hash_one = |job: &ContentCheck| -> Result<RefreshContent> {
7381 if job.entry_index.is_multiple_of(CANCEL_POLL_STRIDE) {
7382 if let Some(w) = watch_for_hash.as_ref() {
7383 poll_watch(w)?;
7384 }
7385 }
7386 let ie = &entries[job.entry_index];
7387 let file_path =
7388 index_entry_worktree_abs_bytes(work_tree, &ie.path, precompose_unicode, ignorecase);
7389 Ok(RefreshContent::from(hash_worktree_file_with_mode(
7390 odb,
7391 &file_path,
7392 job.worktree_mode,
7393 conv,
7394 &job.file_attrs,
7395 &crate::repo_relpath::storage_string(&ie.path),
7396 Some(ie),
7397 filter_process,
7398 )))
7399 };
7400 let matches = if par_opts.threads.get() > 1 && jobs.len() > 1 {
7401 crate::parallel::try_par_map_force(&jobs, par_opts, hash_one).map_err(|e| match e {
7402 crate::parallel::ParallelHashError::Task(err) => err,
7403 })?
7404 } else {
7405 let mut out = Vec::with_capacity(jobs.len());
7406 let mut serial_stride = 0usize;
7407 for job in &jobs {
7408 serial_stride = serial_stride.wrapping_add(1);
7409 if serial_stride.is_multiple_of(CANCEL_POLL_STRIDE) {
7410 if let Some(w) = cancel_watch {
7411 poll_watch(w)?;
7412 }
7413 }
7414 out.push(hash_one(job)?);
7415 }
7416 out
7417 };
7418 for (job, content) in jobs.iter().zip(matches) {
7419 checks[job.entry_index] = Some(content);
7420 }
7421 Ok(RefreshChecks(checks))
7422}
7423
7424#[derive(Default)]
7429struct RefreshChecks(Vec<Option<RefreshContent>>);
7430
7431impl RefreshChecks {
7432 fn get(&self, entry_index: usize) -> Option<RefreshContent> {
7434 self.0.get(entry_index).copied().flatten()
7435 }
7436}
7437
7438#[derive(Clone, Copy)]
7440enum RefreshContent {
7441 Failed,
7443 Hashed(ObjectId),
7445}
7446
7447impl From<Result<ObjectId>> for RefreshContent {
7448 fn from(hashed: Result<ObjectId>) -> Self {
7449 hashed.map_or(Self::Failed, Self::Hashed)
7450 }
7451}
7452
7453impl RefreshContent {
7454 fn matches(check: Option<Self>, oid: &ObjectId) -> bool {
7456 matches!(check, Some(Self::Hashed(hashed)) if hashed == *oid)
7457 }
7458
7459 fn differs(check: Option<Self>, oid: &ObjectId) -> bool {
7461 matches!(check, Some(Self::Hashed(hashed)) if hashed != *oid)
7462 }
7463}
7464
7465fn refresh_index_blob_stat_from_scan(
7471 ie: &mut IndexEntry,
7472 stat: &crate::worktree_scan::BlobDiskStat,
7473 index_mtime: Option<(u32, u32)>,
7474 stat_policy: crate::index::StatMatchPolicy,
7475 content_matches: bool,
7476) -> BlobStatRefreshOutcome {
7477 const UNCHANGED: BlobStatRefreshOutcome = BlobStatRefreshOutcome {
7478 index_changed: false,
7479 content_verified: false,
7480 };
7481 if !refresh_index_entry_eligible(ie) || stat.is_dir {
7482 return UNCHANGED;
7483 }
7484 let cached = crate::index::IndexStatFields::of_entry(ie);
7485 let stat_same = cached.equivalent_to(&stat.fields, stat_policy);
7486 let stat_matches = stat_matches_blob_stat(ie, stat, stat_policy);
7487 if !stat_matches || entry_is_racy_with_policy(ie, index_mtime, stat_policy) {
7488 if !content_matches {
7489 if stat_matches {
7490 invalidate_index_stat_cache(ie);
7491 return BlobStatRefreshOutcome {
7492 index_changed: true,
7493 content_verified: false,
7494 };
7495 }
7496 return UNCHANGED;
7497 }
7498 if !stat_same {
7499 stat.fields.store_in(ie);
7500 }
7501 return BlobStatRefreshOutcome {
7502 index_changed: !stat_same,
7503 content_verified: true,
7504 };
7505 }
7506 if stat_same {
7507 return BlobStatRefreshOutcome {
7508 index_changed: false,
7509 content_verified: true,
7510 };
7511 }
7512 stat.fields.store_in(ie);
7513 BlobStatRefreshOutcome {
7514 index_changed: true,
7515 content_verified: false,
7516 }
7517}
7518
7519#[allow(clippy::too_many_arguments)]
7531pub(crate) fn path_checkout_skip_blob_write_when_up_to_date_with_config(
7532 odb: &Odb,
7533 git_dir: &Path,
7534 work_tree: &Path,
7535 index: &Index,
7536 entry: &IndexEntry,
7537 expected_oid: &ObjectId,
7538 expected_mode: u32,
7539 rel_path: &str,
7540 filter_process: Option<&crate::filter_process::FilterProcessState>,
7541 preloaded_config: Option<&crate::config::ConfigSet>,
7542) -> Result<bool> {
7543 use crate::config::ConfigSet;
7544 use crate::crlf;
7545 use crate::index::{MODE_EXECUTABLE, MODE_REGULAR};
7546
7547 if entry.oid != *expected_oid || entry.mode != expected_mode {
7548 return Ok(false);
7549 }
7550 if expected_mode != MODE_REGULAR && expected_mode != MODE_EXECUTABLE {
7551 return Ok(false);
7552 }
7553 let abs_path = crate::repo_relpath::worktree_abs(work_tree, rel_path);
7554 let meta = match fs::symlink_metadata(&abs_path) {
7555 Ok(m) => m,
7556 Err(_) => return Ok(false),
7557 };
7558 if !meta.is_file() || !stat_matches(entry, &meta) {
7559 return Ok(false);
7560 }
7561 let owned_config;
7562 let config = match preloaded_config {
7563 Some(cfg) => cfg,
7564 None => {
7565 owned_config = ConfigSet::load(
7566 &crate::environment::Environment::empty(),
7567 Some(git_dir),
7568 true,
7569 )
7570 .unwrap_or_default();
7571 &owned_config
7572 }
7573 };
7574 if !crate::index::WorktreeModePolicy::from_config(config)
7575 .worktree_mode_matches(mode_from_metadata(&meta), expected_mode)
7576 {
7577 return Ok(false);
7578 }
7579 let index_mtime = index_mtime_for_diff(index, None);
7580 if !entry_is_racy(entry, index_mtime) {
7581 return Ok(true);
7582 }
7583 let conv = crlf::ConversionConfig::from_config(config);
7584 let attrs = crlf::load_gitattributes(work_tree);
7585 let file_attrs = crlf::get_file_attrs(&attrs, rel_path, false, config);
7586 let wt_oid = hash_worktree_file(
7587 odb,
7588 &abs_path,
7589 &meta,
7590 &conv,
7591 &file_attrs,
7592 rel_path,
7593 Some(entry),
7594 filter_process,
7595 )?;
7596 Ok(wt_oid == *expected_oid)
7597}
7598
7599fn invalidate_index_stat_cache(ie: &mut IndexEntry) {
7601 ie.ctime_sec = 0;
7602 ie.ctime_nsec = 0;
7603 ie.mtime_sec = 0;
7604 ie.mtime_nsec = 0;
7605 ie.dev = 0;
7606 ie.ino = 0;
7607 ie.size = 0;
7608}
7609
7610pub fn read_worktree_bytes_for_mode(path: &Path, mode: u32) -> std::io::Result<Vec<u8>> {
7621 if mode == MODE_SYMLINK && fs::symlink_metadata(path)?.file_type().is_symlink() {
7622 let target = fs::read_link(path)?;
7623 return Ok(symlink_target_bytes(&target));
7624 }
7625 fs::read(path)
7626}
7627
7628pub fn read_worktree_side_bytes_for_diff(
7638 odb: &Odb,
7639 work_tree: &Path,
7640 rel_path: &str,
7641 mode: u32,
7642 config: &crate::config::ConfigSet,
7643 index_entry: Option<&IndexEntry>,
7644 worktree_rules: Option<&crate::worktree_rules::WorktreeRules>,
7645) -> Result<Vec<u8>> {
7646 Ok(load_worktree_side_for_diff(
7647 odb,
7648 work_tree,
7649 rel_path,
7650 mode,
7651 config,
7652 index_entry,
7653 worktree_rules,
7654 false,
7655 )?
7656 .0
7657 .data)
7658}
7659
7660#[allow(clippy::too_many_arguments)]
7670pub fn load_worktree_side_for_diff(
7671 odb: &Odb,
7672 work_tree: &Path,
7673 rel_path: &str,
7674 mode: u32,
7675 config: &crate::config::ConfigSet,
7676 index_entry: Option<&IndexEntry>,
7677 worktree_rules: Option<&crate::worktree_rules::WorktreeRules>,
7678 force_read_content: bool,
7679) -> Result<(super::structured::SideBytes, bool)> {
7680 use super::tree_content::{side_bytes_after_blob_read, treat_side_as_binary_without_reading};
7681 let file_path = crate::repo_relpath::worktree_abs(work_tree, rel_path);
7682 let rel_match = crate::repo_relpath::as_utf8_for_matching(rel_path);
7683 let meta = fs::symlink_metadata(&file_path).map_err(|source| {
7684 crate::worktree::map_worktree_io(
7685 rel_match.as_ref(),
7686 crate::worktree::WorktreePathOperation::Metadata,
7687 source,
7688 )
7689 })?;
7690 if mode == MODE_SYMLINK && meta.file_type().is_symlink() {
7691 let target = fs::read_link(&file_path).map_err(|source| {
7692 crate::worktree::map_worktree_io(
7693 rel_match.as_ref(),
7694 crate::worktree::WorktreePathOperation::Read,
7695 source,
7696 )
7697 })?;
7698 return Ok((
7699 super::structured::SideBytes::from_bytes(symlink_target_bytes(&target)),
7700 false,
7701 ));
7702 }
7703 if !force_read_content
7704 && treat_side_as_binary_without_reading(odb, meta.len(), Some(rel_match.as_ref()), false)?
7705 {
7706 return Ok((super::structured::SideBytes::from_bytes(Vec::new()), true));
7707 }
7708 let data = if let Some(rules) = worktree_rules {
7709 worktree_blob_bytes(
7710 odb,
7711 &file_path,
7712 &meta,
7713 rules.conversion(),
7714 &rules.file_attrs(rel_match.as_ref(), meta.is_dir()),
7715 rel_match.as_ref(),
7716 index_entry,
7717 Some(rules.filter_process()),
7718 )?
7719 } else {
7720 let conv = crate::crlf::ConversionConfig::from_config(config);
7721 let attrs = crate::crlf::load_gitattributes(work_tree);
7722 let file_attrs =
7723 crate::crlf::get_file_attrs(&attrs, rel_match.as_ref(), meta.is_dir(), config);
7724 worktree_blob_bytes(
7725 odb,
7726 &file_path,
7727 &meta,
7728 &conv,
7729 &file_attrs,
7730 rel_match.as_ref(),
7731 index_entry,
7732 None,
7733 )?
7734 };
7735 Ok(side_bytes_after_blob_read(data, force_read_content))
7736}
7737
7738pub(crate) fn symlink_target_bytes(target: &Path) -> Vec<u8> {
7744 #[cfg(unix)]
7745 {
7746 use std::os::unix::ffi::OsStrExt as _;
7747 target.as_os_str().as_bytes().to_vec()
7748 }
7749 #[cfg(not(unix))]
7750 {
7751 target.as_os_str().to_string_lossy().as_bytes().to_vec()
7752 }
7753}
7754
7755pub(crate) enum WorktreeBlobContent {
7762 Bytes(Vec<u8>),
7764 Stream {
7766 file: fs::File,
7768 size: u64,
7770 },
7771}
7772
7773impl WorktreeBlobContent {
7774 pub(crate) fn hash(self, odb: &Odb) -> Result<ObjectId> {
7776 match self {
7777 Self::Bytes(data) => odb.hash(ObjectKind::Blob, &data),
7778 Self::Stream { mut file, size } => odb.hash_stream(ObjectKind::Blob, size, &mut file),
7779 }
7780 }
7781
7782 pub(crate) fn store(self, odb: &Odb, options: crate::odb::WriteOptions) -> Result<ObjectId> {
7784 match self {
7785 Self::Bytes(data) => odb.write_with_options(ObjectKind::Blob, &data, options),
7786 Self::Stream { mut file, size } => {
7787 odb.write_stream(ObjectKind::Blob, size, &mut file, options)
7788 }
7789 }
7790 }
7791
7792 fn object_id(self, odb: &Odb, store: Option<crate::odb::WriteOptions>) -> Result<ObjectId> {
7794 match store {
7795 Some(options) => self.store(odb, options),
7796 None => self.hash(odb),
7797 }
7798 }
7799}
7800
7801#[allow(clippy::too_many_arguments)]
7803fn read_regular_worktree_bytes(
7804 odb: &Odb,
7805 mut file: fs::File,
7806 size: u64,
7807 conv: &crate::crlf::ConversionConfig,
7808 file_attrs: &crate::crlf::FileAttrs,
7809 rel_path: &str,
7810 index_entry: Option<&IndexEntry>,
7811 filter_process: Option<&crate::filter_process::FilterProcessState>,
7812) -> Result<Vec<u8>> {
7813 let needs_index_blob =
7814 index_entry.is_some_and(|_| crate::crlf::clean_uses_autocrlf_index_guard(file_attrs, conv));
7815 let prior_blob: Option<Vec<u8>> = if needs_index_blob {
7816 index_entry
7817 .filter(|e| e.oid != zero_oid())
7818 .and_then(|e| odb.read(&e.oid).ok().map(|o| o.data))
7819 } else {
7820 None
7821 };
7822 let mut raw = Vec::with_capacity(usize::try_from(size).unwrap_or_default());
7823 std::io::Read::read_to_end(&mut file, &mut raw).map_err(|source| {
7824 crate::worktree::map_worktree_io(
7825 rel_path,
7826 crate::worktree::WorktreePathOperation::Read,
7827 source,
7828 )
7829 })?;
7830 #[cfg(test)]
7831 odb.hot_path_metrics().record_blob_content_read();
7832 let opts = crate::crlf::ConvertToGitOpts {
7833 index_blob: prior_blob.as_deref(),
7834 renormalize: false,
7835 check_safecrlf: false,
7836 filter_process,
7837 command_runner: None,
7838 };
7839 Ok(crate::crlf::convert_to_git_owned(
7840 raw, rel_path, conv, file_attrs, opts,
7841 )?)
7842}
7843
7844fn open_with_size(path: &Path, rel_path: &str) -> Result<(fs::File, u64)> {
7845 let file = fs::File::open(path).map_err(|source| {
7846 crate::worktree::map_worktree_io(
7847 rel_path,
7848 crate::worktree::WorktreePathOperation::Read,
7849 source,
7850 )
7851 })?;
7852 let size = file
7853 .metadata()
7854 .map_err(|source| {
7855 crate::worktree::map_worktree_io(
7856 rel_path,
7857 crate::worktree::WorktreePathOperation::Metadata,
7858 source,
7859 )
7860 })?
7861 .len();
7862 Ok((file, size))
7863}
7864
7865#[allow(clippy::too_many_arguments)]
7868fn read_regular_worktree_blob(
7869 odb: &Odb,
7870 path: &Path,
7871 conv: &crate::crlf::ConversionConfig,
7872 file_attrs: &crate::crlf::FileAttrs,
7873 rel_path: &str,
7874 index_entry: Option<&IndexEntry>,
7875 filter_process: Option<&crate::filter_process::FilterProcessState>,
7876 stream_from: u64,
7877) -> Result<WorktreeBlobContent> {
7878 let (mut file, size) = open_with_size(path, rel_path)?;
7879 if size >= stream_from
7880 && crate::crlf::worktree_allows_raw_byte_stream(
7881 conv,
7882 file_attrs,
7883 index_entry,
7884 &mut file,
7885 size,
7886 )?
7887 {
7888 #[cfg(test)]
7889 odb.hot_path_metrics().record_blob_content_read();
7890 return Ok(WorktreeBlobContent::Stream { file, size });
7891 }
7892 read_regular_worktree_bytes(
7893 odb,
7894 file,
7895 size,
7896 conv,
7897 file_attrs,
7898 rel_path,
7899 index_entry,
7900 filter_process,
7901 )
7902 .map(WorktreeBlobContent::Bytes)
7903}
7904
7905#[allow(clippy::too_many_arguments)]
7908pub(crate) fn worktree_blob_content(
7909 odb: &Odb,
7910 path: &Path,
7911 meta: &fs::Metadata,
7912 conv: &crate::crlf::ConversionConfig,
7913 file_attrs: &crate::crlf::FileAttrs,
7914 rel_path: &str,
7915 index_entry: Option<&IndexEntry>,
7916 filter_process: Option<&crate::filter_process::FilterProcessState>,
7917) -> Result<WorktreeBlobContent> {
7918 worktree_blob_content_from(
7919 odb,
7920 path,
7921 meta,
7922 conv,
7923 file_attrs,
7924 rel_path,
7925 index_entry,
7926 filter_process,
7927 hash_stream_threshold(odb)?,
7928 )
7929}
7930
7931#[allow(clippy::too_many_arguments)]
7933fn worktree_blob_content_from(
7934 odb: &Odb,
7935 path: &Path,
7936 meta: &fs::Metadata,
7937 conv: &crate::crlf::ConversionConfig,
7938 file_attrs: &crate::crlf::FileAttrs,
7939 rel_path: &str,
7940 index_entry: Option<&IndexEntry>,
7941 filter_process: Option<&crate::filter_process::FilterProcessState>,
7942 stream_from: u64,
7943) -> Result<WorktreeBlobContent> {
7944 if meta.file_type().is_symlink() {
7945 let target = fs::read_link(path).map_err(|source| {
7946 crate::worktree::map_worktree_io(
7947 rel_path,
7948 crate::worktree::WorktreePathOperation::Read,
7949 source,
7950 )
7951 })?;
7952 return Ok(WorktreeBlobContent::Bytes(symlink_target_bytes(&target)));
7953 }
7954 if meta.is_dir() {
7955 return Ok(WorktreeBlobContent::Bytes(Vec::new()));
7956 }
7957 read_regular_worktree_blob(
7958 odb,
7959 path,
7960 conv,
7961 file_attrs,
7962 rel_path,
7963 index_entry,
7964 filter_process,
7965 stream_from,
7966 )
7967}
7968
7969#[allow(clippy::too_many_arguments)]
7972pub(crate) fn worktree_blob_bytes(
7973 odb: &Odb,
7974 path: &Path,
7975 meta: &fs::Metadata,
7976 conv: &crate::crlf::ConversionConfig,
7977 file_attrs: &crate::crlf::FileAttrs,
7978 rel_path: &str,
7979 index_entry: Option<&IndexEntry>,
7980 filter_process: Option<&crate::filter_process::FilterProcessState>,
7981) -> Result<Vec<u8>> {
7982 if meta.file_type().is_symlink() {
7983 let target = fs::read_link(path).map_err(|source| {
7984 crate::worktree::map_worktree_io(
7985 rel_path,
7986 crate::worktree::WorktreePathOperation::Read,
7987 source,
7988 )
7989 })?;
7990 return Ok(symlink_target_bytes(&target));
7991 }
7992 if meta.is_dir() {
7993 return Ok(Vec::new());
7994 }
7995 let (file, size) = open_with_size(path, rel_path)?;
7996 read_regular_worktree_bytes(
7997 odb,
7998 file,
7999 size,
8000 conv,
8001 file_attrs,
8002 rel_path,
8003 index_entry,
8004 filter_process,
8005 )
8006}
8007
8008#[allow(clippy::too_many_arguments)]
8009fn worktree_file_oid(
8010 odb: &Odb,
8011 path: &Path,
8012 meta: &fs::Metadata,
8013 conv: &crate::crlf::ConversionConfig,
8014 file_attrs: &crate::crlf::FileAttrs,
8015 rel_path: &str,
8016 index_entry: Option<&IndexEntry>,
8017 materialize: bool,
8018 filter_process: Option<&crate::filter_process::FilterProcessState>,
8019) -> Result<ObjectId> {
8020 let stream_from = hash_stream_threshold(odb)?;
8021 worktree_blob_content_from(
8022 odb,
8023 path,
8024 meta,
8025 conv,
8026 file_attrs,
8027 rel_path,
8028 index_entry,
8029 filter_process,
8030 stream_from,
8031 )?
8032 .object_id(odb, materialize.then(|| odb.bulk_stage_write_options()))
8033}
8034
8035#[allow(clippy::too_many_arguments)]
8046pub fn hash_worktree_file(
8047 odb: &Odb,
8048 path: &Path,
8049 meta: &fs::Metadata,
8050 conv: &crate::crlf::ConversionConfig,
8051 file_attrs: &crate::crlf::FileAttrs,
8052 rel_path: &str,
8053 index_entry: Option<&IndexEntry>,
8054 filter_process: Option<&crate::filter_process::FilterProcessState>,
8055) -> Result<ObjectId> {
8056 worktree_file_oid(
8057 odb,
8058 path,
8059 meta,
8060 conv,
8061 file_attrs,
8062 rel_path,
8063 index_entry,
8064 false,
8065 filter_process,
8066 )
8067}
8068
8069const STREAMED_HASH_MIN_SIZE: u64 = 1024 * 1024;
8072
8073fn hash_stream_threshold(odb: &Odb) -> Result<u64> {
8075 Ok(odb.big_file_threshold()?.min(STREAMED_HASH_MIN_SIZE))
8076}
8077
8078#[allow(clippy::too_many_arguments)]
8082pub(crate) fn hash_worktree_file_with_mode(
8083 odb: &Odb,
8084 path: &Path,
8085 worktree_mode: u32,
8086 conv: &crate::crlf::ConversionConfig,
8087 file_attrs: &crate::crlf::FileAttrs,
8088 rel_path: &str,
8089 index_entry: Option<&IndexEntry>,
8090 filter_process: Option<&crate::filter_process::FilterProcessState>,
8091) -> Result<ObjectId> {
8092 if worktree_mode == MODE_SYMLINK {
8093 let data = read_worktree_bytes_for_mode(path, worktree_mode)?;
8094 return odb.hash(ObjectKind::Blob, &data);
8095 }
8096 read_regular_worktree_blob(
8097 odb,
8098 path,
8099 conv,
8100 file_attrs,
8101 rel_path,
8102 index_entry,
8103 filter_process,
8104 hash_stream_threshold(odb)?,
8105 )?
8106 .hash(odb)
8107}
8108
8109#[allow(clippy::too_many_arguments)]
8117pub fn materialize_worktree_blob(
8118 odb: &Odb,
8119 path: &Path,
8120 meta: &fs::Metadata,
8121 conv: &crate::crlf::ConversionConfig,
8122 file_attrs: &crate::crlf::FileAttrs,
8123 rel_path: &str,
8124 index_entry: Option<&IndexEntry>,
8125 filter_process: Option<&crate::filter_process::FilterProcessState>,
8126) -> Result<ObjectId> {
8127 worktree_file_oid(
8128 odb,
8129 path,
8130 meta,
8131 conv,
8132 file_attrs,
8133 rel_path,
8134 index_entry,
8135 true,
8136 filter_process,
8137 )
8138}
8139
8140pub fn mode_from_metadata(meta: &fs::Metadata) -> u32 {
8142 if meta.file_type().is_symlink() {
8143 0o120000
8144 } else {
8145 #[cfg(unix)]
8146 {
8147 if meta.mode() & 0o111 != 0 {
8148 return 0o100755;
8149 }
8150 }
8151 0o100644
8152 }
8153}
8154
8155pub fn diff_tree_to_worktree(
8172 odb: &Odb,
8173 tree_oid: Option<&ObjectId>,
8174 work_tree: &Path,
8175 index: &Index,
8176) -> Result<Vec<DiffEntry>> {
8177 diff_tree_to_worktree_with_git_dir(
8178 odb,
8179 tree_oid,
8180 work_tree,
8181 &work_tree.join(".git"),
8182 index,
8183 None,
8184 )
8185}
8186
8187pub fn diff_tree_to_worktree_with_git_dir(
8191 odb: &Odb,
8192 tree_oid: Option<&ObjectId>,
8193 work_tree: &Path,
8194 repository_git_dir: &Path,
8195 index: &Index,
8196 config: Option<&ConfigSet>,
8197) -> Result<Vec<DiffEntry>> {
8198 diff_tree_to_worktree_with_git_dir_and_rules(
8199 odb,
8200 tree_oid,
8201 work_tree,
8202 repository_git_dir,
8203 index,
8204 config,
8205 None,
8206 )
8207}
8208
8209pub fn diff_tree_to_worktree_with_git_dir_and_rules(
8211 odb: &Odb,
8212 tree_oid: Option<&ObjectId>,
8213 work_tree: &Path,
8214 repository_git_dir: &Path,
8215 index: &Index,
8216 config: Option<&ConfigSet>,
8217 worktree_rules: Option<&crate::worktree_rules::WorktreeRules>,
8218) -> Result<Vec<DiffEntry>> {
8219 use crate::config::ConfigSet;
8220 use crate::crlf;
8221
8222 if worktree_rules.is_none() {
8223 if let Some(tree_oid) = tree_oid {
8224 if index.cache_tree_root.as_ref() == Some(tree_oid)
8225 && crate::write_tree::cache_tree_fully_valid(odb, index.cache_tree.as_ref())
8226 {
8227 let mut idx = index.clone();
8228 let options = DiffIndexToWorktreeOptions {
8229 repository_git_dir: Some(repository_git_dir.to_path_buf()),
8230 refresh_index_stat_in_pass: false,
8231 for_status: false,
8235 config: config.cloned().map(std::sync::Arc::new),
8236 ..DiffIndexToWorktreeOptions::default()
8237 };
8238 let (entries, _, _) = diff_index_to_worktree_inner(
8239 odb,
8240 &mut idx,
8241 work_tree,
8242 repository_git_dir,
8243 options,
8244 false,
8245 None,
8246 )?;
8247 return Ok(entries);
8248 }
8249 }
8250 }
8251
8252 let config = worktree_rules
8253 .map(|r| r.config().clone())
8254 .unwrap_or_else(|| {
8255 config.cloned().unwrap_or_else(|| {
8256 ConfigSet::load(
8257 &crate::environment::Environment::empty(),
8258 Some(repository_git_dir),
8259 true,
8260 )
8261 .unwrap_or_else(|_| ConfigSet::new())
8262 })
8263 });
8264 let conv = worktree_rules
8265 .map(|r| r.conversion().clone())
8266 .unwrap_or_else(|| crlf::ConversionConfig::from_config(&config));
8267 let legacy_attrs = if worktree_rules.is_none() {
8268 Some(crlf::load_gitattributes_parsed(work_tree))
8269 } else {
8270 None
8271 };
8272 let file_attrs_for = |path: &str| -> crlf::FileAttrs {
8273 if let Some(rules) = worktree_rules {
8274 rules.file_attrs(path, false)
8275 } else if let Some(parsed) = legacy_attrs.as_ref() {
8276 crlf::file_attrs_from_gitattributes(&parsed.rules, &parsed.macros, path, false, &config)
8277 } else {
8278 crlf::FileAttrs::default()
8279 }
8280 };
8281 let filter_fp = worktree_rules.map(|r| r.filter_process());
8282
8283 let sparse_index = index.has_sparse_directory_placeholders();
8284 let tree_flat = match tree_oid {
8286 Some(oid) if sparse_index => flatten_tree_with_index(odb, oid, "", Some(index))?,
8287 Some(oid) => flatten_tree(odb, oid, "")?,
8288 None => Vec::new(),
8289 };
8290 let tree_map: std::collections::BTreeMap<String, &FlatEntry> =
8291 tree_flat.iter().map(|e| (e.path.clone(), e)).collect();
8292
8293 let mut index_entries: std::collections::BTreeMap<&[u8], &IndexEntry> =
8295 std::collections::BTreeMap::new();
8296 let mut index_paths: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
8297 let mut stage0_paths: std::collections::BTreeSet<Vec<u8>> = std::collections::BTreeSet::new();
8298 for ie in &index.entries {
8299 if ie.stage() != 0 {
8300 continue;
8301 }
8302 let path = String::from_utf8_lossy(&ie.path).to_string();
8303 index_entries.insert(&ie.path, ie);
8304 index_paths.insert(path);
8305 stage0_paths.insert(ie.path.clone());
8306 }
8307
8308 let mut unmerged_only_paths: std::collections::BTreeSet<String> =
8311 std::collections::BTreeSet::new();
8312 for ie in &index.entries {
8313 if !(1..=3).contains(&ie.stage()) {
8314 continue;
8315 }
8316 if stage0_paths.contains(&ie.path) {
8317 continue;
8318 }
8319 unmerged_only_paths.insert(String::from_utf8_lossy(&ie.path).into_owned());
8320 }
8321
8322 let mut all_paths: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
8324 all_paths.extend(tree_map.keys().cloned());
8325 all_paths.extend(index_paths.iter().cloned());
8326 all_paths.extend(unmerged_only_paths.iter().cloned());
8327
8328 let index_mtime = index_mtime_for_diff(index, None);
8329 let stat_policy = crate::index::StatMatchPolicy::from_config(Some(&config));
8330 let mode_policy = crate::index::WorktreeModePolicy::from_config(&config);
8331 let mut result = Vec::new();
8332
8333 for path in &all_paths {
8334 if index_entries
8335 .get(path.as_bytes())
8336 .is_some_and(|ie| ie.skip_worktree())
8337 {
8338 continue;
8341 }
8342 if index_entries
8343 .get(path.as_bytes())
8344 .is_some_and(|ie| ie.assume_unchanged())
8345 {
8346 continue;
8347 }
8348
8349 let tree_entry = tree_map.get(path.as_str());
8350
8351 let is_gitlink = tree_entry.is_some_and(|te| te.mode == 0o160000)
8353 || index_entries
8354 .get(path.as_bytes())
8355 .is_some_and(|ie| ie.mode == 0o160000);
8356 if is_gitlink {
8357 let sub_dir = work_tree.join(path);
8358 let index_gitlink_oid = index_entries
8359 .get(path.as_bytes())
8360 .filter(|ie| ie.mode == 0o160000)
8361 .map(|ie| ie.oid);
8362 match (tree_entry, index_gitlink_oid) {
8363 (Some(te), _) => {
8364 let sub_head = read_submodule_head_oid(&sub_dir);
8365 if sub_head.is_none() && !sub_dir.exists() {
8369 result.push(DiffEntry {
8370 status: DiffStatus::Deleted,
8371 old_path: Some(path.clone()),
8372 new_path: Some(path.clone()),
8373 old_mode: FileMode::new(te.mode),
8374 new_mode: FileMode::ABSENT,
8375 old_oid: te.oid,
8376 new_oid: zero_oid(),
8377 score: None,
8378 });
8379 continue;
8380 }
8381 let ref_matches = if let Some(head) = sub_head {
8382 head == te.oid
8383 } else {
8384 uninitialized_gitlink_worktree_matches_index(&sub_dir)
8385 };
8386 let index_matches_tree = index_gitlink_oid.is_some_and(|oid| oid == te.oid);
8387 let sub_flags = submodule_porcelain_flags(work_tree, path, te.oid);
8390 let dirty_while_aligned =
8391 index_matches_tree && ref_matches && sub_flags.modified;
8392 if !ref_matches || dirty_while_aligned {
8393 let new_oid = if !ref_matches {
8397 sub_head.unwrap_or_else(zero_oid)
8398 } else {
8399 zero_oid()
8400 };
8401 result.push(DiffEntry {
8402 status: DiffStatus::Modified,
8403 old_path: Some(path.clone()),
8404 new_path: Some(path.clone()),
8405 old_mode: FileMode::new(te.mode),
8406 new_mode: FileMode::new(te.mode),
8407 old_oid: te.oid,
8408 new_oid,
8409 score: None,
8410 });
8411 }
8412 }
8413 (None, Some(idx_oid)) => {
8414 let new_oid = read_submodule_head_oid(&sub_dir).unwrap_or(idx_oid);
8419 result.push(DiffEntry {
8420 status: DiffStatus::Added,
8421 old_path: Some(path.clone()),
8422 new_path: Some(path.clone()),
8423 old_mode: FileMode::ABSENT,
8424 new_mode: FileMode::new(0o160000),
8425 old_oid: zero_oid(),
8426 new_oid,
8427 score: None,
8428 });
8429 }
8430 (None, None) => {}
8431 }
8432 continue;
8433 }
8434
8435 let file_path = work_tree.join(path);
8436
8437 let wt_meta = match fs::symlink_metadata(&file_path) {
8438 Ok(m) => Some(m),
8439 Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
8440 Err(e) => return Err(Error::Io(e)),
8441 };
8442
8443 if unmerged_only_paths.contains(path) {
8444 if let (Some(te), Some(meta)) = (tree_entry, wt_meta.as_ref()) {
8445 let tree_mode = crate::index::canon_mode(te.mode);
8446 let file_attrs = file_attrs_for(path);
8447 let wt_oid = hash_worktree_file(
8448 odb,
8449 &file_path,
8450 meta,
8451 &conv,
8452 &file_attrs,
8453 path,
8454 None,
8455 filter_fp,
8456 )?;
8457 let wt_mode =
8458 mode_policy.index_mode_for_worktree(mode_from_metadata(meta), Some(tree_mode));
8459 if wt_oid != te.oid || wt_mode != tree_mode {
8460 result.push(DiffEntry {
8461 status: DiffStatus::for_content_change(tree_mode, wt_mode),
8462 old_path: Some(path.clone()),
8463 new_path: Some(path.clone()),
8464 old_mode: FileMode::new(tree_mode),
8465 new_mode: FileMode::new(wt_mode),
8466 old_oid: te.oid,
8467 new_oid: wt_oid,
8468 score: None,
8469 });
8470 }
8471 }
8472 continue;
8473 }
8474
8475 match (tree_entry, wt_meta) {
8476 (Some(te), Some(ref meta)) => {
8477 let tree_mode = crate::index::canon_mode(te.mode);
8478 let Some(ie) = index_entries.get(path.as_bytes()) else {
8479 continue;
8480 };
8481 let wt_mode =
8482 mode_policy.index_mode_for_worktree(mode_from_metadata(meta), Some(ie.mode));
8483
8484 let index_matches_tree = ie.oid == te.oid && ie.mode == tree_mode;
8485
8486 let stat_trusted = stat_matches_with_policy(ie, meta, stat_policy)
8488 && !entry_is_racy_with_policy(ie, index_mtime, stat_policy);
8489 if index_matches_tree && stat_trusted && wt_mode == tree_mode {
8490 continue;
8491 }
8492
8493 let file_attrs = file_attrs_for(path);
8494 let idx_ent = index_entries.get(path.as_bytes()).copied();
8495
8496 if ie.oid == te.oid && ie.mode != tree_mode && stat_trusted && wt_mode == ie.mode {
8501 result.push(DiffEntry {
8502 status: DiffStatus::for_content_change(tree_mode, ie.mode),
8503 old_path: Some(path.clone()),
8504 new_path: Some(path.clone()),
8505 old_mode: FileMode::new(tree_mode),
8506 new_mode: FileMode::new(ie.mode),
8507 old_oid: te.oid,
8508 new_oid: te.oid,
8509 score: None,
8510 });
8511 continue;
8512 }
8513
8514 if index_matches_tree {
8517 let wt_oid = hash_worktree_file(
8518 odb,
8519 &file_path,
8520 meta,
8521 &conv,
8522 &file_attrs,
8523 path,
8524 None,
8525 filter_fp,
8526 )?;
8527 if wt_oid != te.oid {
8528 result.push(DiffEntry {
8529 status: DiffStatus::for_content_change(tree_mode, wt_mode),
8530 old_path: Some(path.clone()),
8531 new_path: Some(path.clone()),
8532 old_mode: FileMode::new(tree_mode),
8533 new_mode: FileMode::new(wt_mode),
8534 old_oid: te.oid,
8535 new_oid: zero_oid(),
8538 score: None,
8539 });
8540 } else if wt_mode != tree_mode {
8541 result.push(DiffEntry {
8542 status: DiffStatus::for_content_change(tree_mode, wt_mode),
8543 old_path: Some(path.clone()),
8544 new_path: Some(path.clone()),
8545 old_mode: FileMode::new(tree_mode),
8546 new_mode: FileMode::new(wt_mode),
8547 old_oid: te.oid,
8548 new_oid: te.oid,
8549 score: None,
8550 });
8551 }
8552 continue;
8553 }
8554
8555 let wt_oid = hash_worktree_file(
8557 odb,
8558 &file_path,
8559 meta,
8560 &conv,
8561 &file_attrs,
8562 path,
8563 idx_ent,
8564 filter_fp,
8565 )?;
8566 if wt_oid != te.oid || wt_mode != tree_mode {
8567 let new_oid = if wt_oid != te.oid { zero_oid() } else { te.oid };
8568 result.push(DiffEntry {
8569 status: DiffStatus::for_content_change(tree_mode, wt_mode),
8570 old_path: Some(path.clone()),
8571 new_path: Some(path.clone()),
8572 old_mode: FileMode::new(tree_mode),
8573 new_mode: FileMode::new(wt_mode),
8574 old_oid: te.oid,
8575 new_oid,
8576 score: None,
8577 });
8578 }
8579 }
8580 (Some(te), None) => {
8581 let tree_mode = crate::index::canon_mode(te.mode);
8582 result.push(DiffEntry {
8584 status: DiffStatus::Deleted,
8585 old_path: Some(path.clone()),
8586 new_path: None,
8587 old_mode: FileMode::new(tree_mode),
8588 new_mode: FileMode::ABSENT,
8589 old_oid: te.oid,
8590 new_oid: zero_oid(),
8591 score: None,
8592 });
8593 }
8594 (None, Some(ref meta)) => {
8595 let file_attrs = file_attrs_for(path);
8597 let idx_ent = index_entries.get(path.as_bytes()).copied();
8598 let wt_oid = materialize_worktree_blob(
8599 odb,
8600 &file_path,
8601 meta,
8602 &conv,
8603 &file_attrs,
8604 path,
8605 idx_ent,
8606 filter_fp,
8607 )?;
8608 let wt_mode = mode_from_metadata(meta);
8609 result.push(DiffEntry {
8610 status: DiffStatus::Added,
8611 old_path: None,
8612 new_path: Some(path.clone()),
8613 old_mode: FileMode::ABSENT,
8614 new_mode: FileMode::new(wt_mode),
8615 old_oid: zero_oid(),
8616 new_oid: wt_oid,
8617 score: None,
8618 });
8619 }
8620 (None, None) => {
8621 }
8623 }
8624 }
8625
8626 sort_entries_by_path(&mut result);
8627 Ok(result)
8628}
8629
8630fn read_added_entry_bytes(
8633 odb: &Odb,
8634 entry: &DiffEntry,
8635 work_root: Option<&Path>,
8636) -> Option<Vec<u8>> {
8637 if entry.new_oid != zero_oid() {
8638 if let Ok(obj) = odb.read(&entry.new_oid) {
8639 return Some(obj.data);
8640 }
8641 }
8642 let path = entry.new_path.as_deref()?;
8643 let root = work_root?;
8644 fs::read(root.join(path)).ok()
8645}
8646
8647#[allow(clippy::expect_used)]
8659pub fn detect_renames(
8660 odb: &Odb,
8661 work_root: Option<&Path>,
8662 entries: Vec<DiffEntry>,
8663 threshold: u32,
8664) -> Vec<DiffEntry> {
8665 let config = ConfigSet::new();
8666 let policy = ThreadPolicy::default();
8667 detect_renames_with_policy(odb, work_root, entries, threshold, &config, &policy, None)
8668 .expect("rename detection")
8669}
8670
8671#[inline]
8672fn poll_rename_cancel(
8673 cancel_watch: Option<&std::sync::atomic::AtomicBool>,
8674) -> crate::error::Result<()> {
8675 if let Some(watch) = cancel_watch {
8676 crate::progress::poll_watch(watch)?;
8677 }
8678 Ok(())
8679}
8680
8681pub fn detect_renames_with_policy(
8692 odb: &Odb,
8693 work_root: Option<&Path>,
8694 entries: Vec<DiffEntry>,
8695 threshold: u32,
8696 config: &ConfigSet,
8697 thread_policy: &ThreadPolicy,
8698 cancel_watch: Option<&std::sync::atomic::AtomicBool>,
8699) -> Result<Vec<DiffEntry>> {
8700 poll_rename_cancel(cancel_watch)?;
8701 let has_deleted = entries.iter().any(|e| e.status == DiffStatus::Deleted);
8702 let has_added = entries.iter().any(|e| e.status == DiffStatus::Added);
8703 if (!has_deleted || !has_added) && entries_strictly_sorted_by_path(&entries) {
8704 return Ok(entries);
8706 }
8707 let mut deleted: Vec<DiffEntry> = Vec::new();
8709 let mut added: Vec<DiffEntry> = Vec::new();
8710 let mut others: Vec<DiffEntry> = Vec::new();
8711
8712 for entry in entries {
8713 match entry.status {
8714 DiffStatus::Deleted => deleted.push(entry),
8715 DiffStatus::Added => added.push(entry),
8716 _ => others.push(entry),
8717 }
8718 }
8719
8720 if deleted.is_empty() || added.is_empty() {
8721 let mut result = others;
8723 result.extend(deleted);
8724 result.extend(added);
8725 sort_entries_by_path(&mut result);
8726 return Ok(result);
8727 }
8728
8729 fn is_regularish_mode(mode: FileMode) -> bool {
8730 mode == FileMode::REGULAR || mode == FileMode::EXECUTABLE
8731 }
8732
8733 fn same_path_same_blob(del: &DiffEntry, add: &DiffEntry) -> bool {
8734 del.old_path == add.new_path && del.old_oid == add.new_oid && del.old_mode == add.new_mode
8735 }
8736
8737 fn sort_rename_scores(
8738 scores: &mut [(u32, usize, usize)],
8739 deleted: &[DiffEntry],
8740 added: &[DiffEntry],
8741 ) {
8742 scores.sort_by(|a, b| {
8743 let a_noop = same_path_same_blob(&deleted[a.1], &added[a.2]);
8744 let b_noop = same_path_same_blob(&deleted[b.1], &added[b.2]);
8745 let a_same = same_basename(&deleted[a.1], &added[a.2]);
8746 let b_same = same_basename(&deleted[b.1], &added[b.2]);
8747 a_noop
8748 .cmp(&b_noop)
8749 .then_with(|| b_same.cmp(&a_same))
8750 .then_with(|| b.0.cmp(&a.0))
8751 });
8752 }
8753
8754 fn apply_rename_scores(
8755 scores: &[(u32, usize, usize)],
8756 deleted: &[DiffEntry],
8757 added: &[DiffEntry],
8758 used_deleted: &mut [bool],
8759 used_added: &mut [bool],
8760 renames: &mut Vec<DiffEntry>,
8761 ) {
8762 for (score, di, ai) in scores {
8763 if used_deleted[*di] || used_added[*ai] {
8764 continue;
8765 }
8766 used_deleted[*di] = true;
8767 used_added[*ai] = true;
8768
8769 let del = &deleted[*di];
8770 let add = &added[*ai];
8771 if same_path_same_blob(del, add) {
8772 continue;
8773 }
8774
8775 renames.push(DiffEntry {
8776 status: DiffStatus::Renamed,
8777 old_path: del.old_path.clone(),
8778 new_path: add.new_path.clone(),
8779 old_mode: del.old_mode,
8780 new_mode: add.new_mode,
8781 old_oid: del.old_oid,
8782 new_oid: add.new_oid,
8783 score: Some(*score),
8784 });
8785 }
8786 }
8787
8788 let mut used_deleted = vec![false; deleted.len()];
8789 let mut used_added = vec![false; added.len()];
8790 let mut renames: Vec<DiffEntry> = Vec::new();
8791
8792 let mut exact_scores: Vec<(u32, usize, usize)> = Vec::new();
8794 for (di, del) in deleted.iter().enumerate() {
8795 poll_rename_cancel(cancel_watch)?;
8796 for (ai, add) in added.iter().enumerate() {
8797 if del.old_oid == add.new_oid {
8798 exact_scores.push((100, di, ai));
8799 }
8800 }
8801 }
8802 sort_rename_scores(&mut exact_scores, &deleted, &added);
8803 apply_rename_scores(
8804 &exact_scores,
8805 &deleted,
8806 &added,
8807 &mut used_deleted,
8808 &mut used_added,
8809 &mut renames,
8810 );
8811
8812 let remaining_deleted: Vec<usize> = used_deleted
8813 .iter()
8814 .enumerate()
8815 .filter_map(|(i, used)| (!*used).then_some(i))
8816 .collect();
8817 let remaining_added: Vec<usize> = used_added
8818 .iter()
8819 .enumerate()
8820 .filter_map(|(i, used)| (!*used).then_some(i))
8821 .collect();
8822
8823 let rename_limit = parse_rename_limit(config);
8824 if !remaining_deleted.is_empty()
8825 && !remaining_added.is_empty()
8826 && !inexact_matrix_exceeds_limit(
8827 remaining_added.len(),
8828 remaining_deleted.len(),
8829 rename_limit,
8830 )
8831 {
8832 let par_opts = diff_rename_parallel_options(config, thread_policy);
8833
8834 let mut prefetch_entries: Vec<DiffEntry> =
8835 Vec::with_capacity(remaining_deleted.len() + remaining_added.len());
8836 for &di in &remaining_deleted {
8837 prefetch_entries.push(deleted[di].clone());
8838 }
8839 for &ai in &remaining_added {
8840 prefetch_entries.push(added[ai].clone());
8841 }
8842 super::tree_content::prefetch_tree_diff_blobs(odb, &prefetch_entries)?;
8843
8844 let deleted_sizes: Vec<Option<usize>> = remaining_deleted
8845 .iter()
8846 .map(|&di| blob_payload_size(odb, &deleted[di].old_oid))
8847 .collect();
8848 let added_sizes: Vec<Option<usize>> = remaining_added
8849 .iter()
8850 .map(|&ai| added_entry_payload_size(odb, &added[ai], work_root))
8851 .collect();
8852
8853 let mut load_deleted = vec![false; remaining_deleted.len()];
8854 let mut load_added = vec![false; remaining_added.len()];
8855 let mut candidate_pairs: Vec<(usize, usize)> = Vec::new();
8856 for (row, &di) in remaining_deleted.iter().enumerate() {
8857 poll_rename_cancel(cancel_watch)?;
8858 let del = &deleted[di];
8859 if !is_regularish_mode(del.old_mode) {
8860 continue;
8861 }
8862 let Some(ds) = deleted_sizes[row] else {
8863 continue;
8864 };
8865 for (col, &ai) in remaining_added.iter().enumerate() {
8866 let add = &added[ai];
8867 if !is_regularish_mode(add.new_mode) {
8868 continue;
8869 }
8870 let Some(asz) = added_sizes[col] else {
8871 continue;
8872 };
8873 if rename_pair_size_reachable(ds, asz, threshold) {
8874 load_deleted[row] = true;
8875 load_added[col] = true;
8876 candidate_pairs.push((row, col));
8877 }
8878 }
8879 }
8880
8881 if !candidate_pairs.is_empty() {
8882 let loads_needed = load_deleted.iter().filter(|&&b| b).count()
8883 + load_added.iter().filter(|&&b| b).count();
8884 let load_deleted_blob = |i: usize| -> Option<Vec<u8>> {
8885 if !load_deleted[i] {
8886 return None;
8887 }
8888 odb.read(&deleted[remaining_deleted[i]].old_oid)
8889 .ok()
8890 .map(|obj| obj.data)
8891 };
8892 let load_added_blob = |i: usize| -> Option<Vec<u8>> {
8893 if !load_added[i] {
8894 return None;
8895 }
8896 read_added_entry_bytes(odb, &added[remaining_added[i]], work_root)
8897 };
8898 let (deleted_contents, added_contents) = if loads_needed <= 32 {
8899 (
8900 (0..remaining_deleted.len())
8901 .map(load_deleted_blob)
8902 .collect(),
8903 (0..remaining_added.len()).map(load_added_blob).collect(),
8904 )
8905 } else {
8906 (
8907 parallel_load_optional_blobs(
8908 odb,
8909 remaining_deleted.len(),
8910 par_opts,
8911 0,
8912 load_deleted_blob,
8913 ),
8914 parallel_load_optional_blobs(
8915 odb,
8916 remaining_added.len(),
8917 par_opts,
8918 0,
8919 load_added_blob,
8920 ),
8921 )
8922 };
8923
8924 let est_table_bytes: usize = deleted_contents
8925 .iter()
8926 .chain(added_contents.iter())
8927 .filter_map(|opt| opt.as_ref().map(|b| b.len()))
8928 .sum();
8929 let build_deleted_table = |row: usize| -> std::result::Result<
8930 Option<RenameLineTable>,
8931 std::convert::Infallible,
8932 > {
8933 Ok(deleted_contents[row]
8934 .as_ref()
8935 .map(|d| RenameLineTable::from_blob(d)))
8936 };
8937 let build_added_table = |col: usize| -> std::result::Result<
8938 Option<RenameLineTable>,
8939 std::convert::Infallible,
8940 > {
8941 Ok(added_contents[col]
8942 .as_ref()
8943 .map(|d| RenameLineTable::from_blob(d)))
8944 };
8945 let deleted_tables: Vec<Option<RenameLineTable>> = if loads_needed <= 32 {
8946 (0..remaining_deleted.len())
8947 .map(|row| build_deleted_table(row).unwrap())
8948 .collect()
8949 } else {
8950 parallel_map_indices(
8951 odb,
8952 remaining_deleted.len(),
8953 par_opts,
8954 est_table_bytes,
8955 build_deleted_table,
8956 )
8957 .expect("deleted rename line tables")
8958 };
8959 let added_tables: Vec<Option<RenameLineTable>> = if loads_needed <= 32 {
8960 (0..remaining_added.len())
8961 .map(|col| build_added_table(col).unwrap())
8962 .collect()
8963 } else {
8964 parallel_map_indices(
8965 odb,
8966 remaining_added.len(),
8967 par_opts,
8968 est_table_bytes,
8969 build_added_table,
8970 )
8971 .expect("added rename line tables")
8972 };
8973
8974 let mut inexact_scores: Vec<(u32, usize, usize)> = Vec::new();
8975 for (row, col) in candidate_pairs {
8976 poll_rename_cancel(cancel_watch)?;
8977 let di = remaining_deleted[row];
8978 let ai = remaining_added[col];
8979 let (Some(old_table), Some(new_table)) = (&deleted_tables[row], &added_tables[col])
8980 else {
8981 continue;
8982 };
8983 let score = RenameLineTable::similarity_score(old_table, new_table);
8984 if score >= threshold {
8985 inexact_scores.push((score, di, ai));
8986 }
8987 }
8988 sort_rename_scores(&mut inexact_scores, &deleted, &added);
8989 apply_rename_scores(
8990 &inexact_scores,
8991 &deleted,
8992 &added,
8993 &mut used_deleted,
8994 &mut used_added,
8995 &mut renames,
8996 );
8997 }
8998 }
8999
9000 let mut result = others;
9002 result.extend(renames);
9003 for (i, entry) in deleted.into_iter().enumerate() {
9004 if !used_deleted[i] {
9005 result.push(entry);
9006 }
9007 }
9008 for (i, entry) in added.into_iter().enumerate() {
9009 if !used_added[i] {
9010 result.push(entry);
9011 }
9012 }
9013
9014 result = prune_break_deletions_obviated_by_renames(result);
9015 sort_entries_by_path(&mut result);
9016 Ok(result)
9017}
9018
9019pub fn detect_copies(
9034 odb: &Odb,
9035 work_root: Option<&Path>,
9036 entries: Vec<DiffEntry>,
9037 threshold: u32,
9038 find_copies_harder: bool,
9039 source_tree_entries: &[(String, FileMode, ObjectId)],
9040) -> Result<Vec<DiffEntry>> {
9041 let config = ConfigSet::new();
9042 let policy = ThreadPolicy::default();
9043 detect_copies_with_policy(
9044 odb,
9045 work_root,
9046 entries,
9047 threshold,
9048 find_copies_harder,
9049 source_tree_entries,
9050 &config,
9051 &policy,
9052 )
9053}
9054
9055#[expect(clippy::too_many_arguments)]
9061pub fn detect_copies_with_policy(
9062 odb: &Odb,
9063 work_root: Option<&Path>,
9064 entries: Vec<DiffEntry>,
9065 threshold: u32,
9066 find_copies_harder: bool,
9067 source_tree_entries: &[(String, FileMode, ObjectId)],
9068 config: &ConfigSet,
9069 thread_policy: &ThreadPolicy,
9070) -> Result<Vec<DiffEntry>> {
9071 use std::collections::{HashMap, HashSet};
9072
9073 let mut deleted: Vec<DiffEntry> = Vec::new();
9075 let mut added: Vec<DiffEntry> = Vec::new();
9076 let mut others: Vec<DiffEntry> = Vec::new();
9077
9078 for entry in entries {
9079 match entry.status {
9080 DiffStatus::Deleted => deleted.push(entry),
9081 DiffStatus::Added => added.push(entry),
9082 _ => others.push(entry),
9083 }
9084 }
9085
9086 let mut sources: Vec<(String, ObjectId, bool)> = Vec::new(); let mut deleted_source_idx: HashMap<String, usize> = HashMap::new();
9090
9091 for entry in &deleted {
9092 if let Some(ref path) = entry.old_path {
9093 deleted_source_idx.insert(path.clone(), sources.len());
9094 sources.push((path.clone(), entry.old_oid, true));
9095 }
9096 }
9097
9098 for entry in &others {
9101 if matches!(entry.status, DiffStatus::Modified | DiffStatus::TypeChanged) {
9102 if let Some(ref old_path) = entry.old_path {
9103 if !sources.iter().any(|(p, _, _)| p == old_path) {
9104 sources.push((old_path.clone(), entry.old_oid, false));
9105 }
9106 }
9107 }
9108 }
9109
9110 if find_copies_harder {
9112 for (path, _mode, oid) in source_tree_entries {
9113 if !sources.iter().any(|(p, _, _)| p == path) {
9114 sources.push((path.clone(), *oid, false));
9115 }
9116 }
9117 }
9118
9119 let mut source_modes: Vec<(String, FileMode, ObjectId)> = source_tree_entries.to_vec();
9122 for entry in deleted.iter().chain(&others) {
9123 if let Some(ref path) = entry.old_path {
9124 if !source_modes.iter().any(|(p, _, _)| p == path) {
9125 source_modes.push((path.clone(), entry.old_mode, entry.old_oid));
9126 }
9127 }
9128 }
9129
9130 if sources.is_empty() {
9131 let mut result = others;
9132 result.extend(deleted);
9133 result.extend(added);
9134 sort_entries_by_path(&mut result);
9135 return Ok(result);
9136 }
9137
9138 let par_opts = diff_rename_parallel_options(config, thread_policy);
9139 let rename_limit = parse_rename_limit(config);
9140
9141 fn apply_copy_score_assignments(
9142 scores: &[(u32, usize, usize)],
9143 sources: &[(String, ObjectId, bool)],
9144 added: &[DiffEntry],
9145 source_tree_entries: &[(String, FileMode, ObjectId)],
9146 used_added2: &mut [bool],
9147 renamed_deleted: &mut HashSet<usize>,
9148 result_entries: &mut Vec<DiffEntry>,
9149 ) {
9150 let mut used_added = vec![false; added.len()];
9151 let mut source_to_added: HashMap<usize, Vec<(usize, u32)>> = HashMap::new();
9152 for &(score, si, ai) in scores {
9153 if used_added[ai] {
9154 continue;
9155 }
9156 used_added[ai] = true;
9157 source_to_added.entry(si).or_default().push((ai, score));
9158 }
9159
9160 for (&si, assignments_for_src) in &source_to_added {
9161 let (_, _, is_deleted) = &sources[si];
9162 if *is_deleted && !assignments_for_src.is_empty() {
9163 let rename_ai = assignments_for_src
9164 .iter()
9165 .max_by_key(|(ai, _score)| added[*ai].path().to_string())
9166 .map(|(ai, _)| *ai);
9167
9168 for &(ai, score) in assignments_for_src {
9169 let (ref src_path, _, _) = sources[si];
9170 let add = &added[ai];
9171 let src_mode = source_tree_entries
9172 .iter()
9173 .find(|(p, _, _)| p == src_path)
9174 .map_or(add.old_mode, |(_, m, _)| *m);
9175
9176 let is_rename = Some(ai) == rename_ai;
9177 result_entries.push(DiffEntry {
9178 status: if is_rename {
9179 DiffStatus::Renamed
9180 } else {
9181 DiffStatus::Copied
9182 },
9183 old_path: Some(src_path.clone()),
9184 new_path: add.new_path.clone(),
9185 old_mode: src_mode,
9186 new_mode: add.new_mode,
9187 old_oid: sources[si].1,
9188 new_oid: add.new_oid,
9189 score: Some(score),
9190 });
9191 used_added2[ai] = true;
9192 }
9193 renamed_deleted.insert(si);
9194 } else {
9195 for &(ai, score) in assignments_for_src {
9196 let (ref src_path, _, _) = sources[si];
9197 let add = &added[ai];
9198 let src_mode = source_tree_entries
9199 .iter()
9200 .find(|(p, _, _)| p == src_path)
9201 .map_or(add.old_mode, |(_, m, _)| *m);
9202
9203 result_entries.push(DiffEntry {
9204 status: DiffStatus::Copied,
9205 old_path: Some(src_path.clone()),
9206 new_path: add.new_path.clone(),
9207 old_mode: src_mode,
9208 new_mode: add.new_mode,
9209 old_oid: sources[si].1,
9210 new_oid: add.new_oid,
9211 score: Some(score),
9212 });
9213 used_added2[ai] = true;
9214 }
9215 }
9216 }
9217 }
9218
9219 fn added_entry_oid(
9220 odb: &Odb,
9221 add: &DiffEntry,
9222 added_contents: &[Option<Vec<u8>>],
9223 ai: usize,
9224 ) -> Result<ObjectId> {
9225 if add.new_oid != zero_oid() {
9226 Ok(add.new_oid)
9227 } else if let Some(ref data) = added_contents[ai] {
9228 odb.hash(ObjectKind::Blob, data)
9229 } else {
9230 Ok(zero_oid())
9231 }
9232 }
9233
9234 let mut result_entries: Vec<DiffEntry> = Vec::new();
9235 let mut renamed_deleted: HashSet<usize> = HashSet::new();
9236 let mut used_added2 = vec![false; added.len()];
9237 let mut source_contents = vec![None; sources.len()];
9238
9239 if !added.is_empty() {
9240 let added_contents = parallel_load_optional_blobs(odb, added.len(), par_opts, 0, |ai| {
9241 read_added_entry_bytes(odb, &added[ai], work_root)
9242 });
9243
9244 let mut exact_scores: Vec<(u32, usize, usize)> = Vec::new();
9246 for (si, (src_path, src_oid, _)) in sources.iter().enumerate() {
9247 for (ai, add) in added.iter().enumerate() {
9248 if add.new_path.as_deref() == Some(src_path.as_str()) {
9249 continue;
9250 }
9251 let add_oid = added_entry_oid(odb, add, &added_contents, ai)?;
9252 if *src_oid == add_oid && add_oid != zero_oid() {
9253 exact_scores.push((100, si, ai));
9254 }
9255 }
9256 }
9257 exact_scores.sort_by_key(|a| std::cmp::Reverse(a.0));
9258 apply_copy_score_assignments(
9259 &exact_scores,
9260 &sources,
9261 &added,
9262 &source_modes,
9263 &mut used_added2,
9264 &mut renamed_deleted,
9265 &mut result_entries,
9266 );
9267
9268 let remaining_added: Vec<usize> = used_added2
9269 .iter()
9270 .enumerate()
9271 .filter_map(|(i, used)| (!*used).then_some(i))
9272 .collect();
9273 let remaining_source_count = sources
9274 .iter()
9275 .enumerate()
9276 .filter(|(si, (_, _, is_deleted))| !*is_deleted || !renamed_deleted.contains(si))
9277 .count();
9278
9279 let skip_inexact = remaining_added.is_empty()
9280 || inexact_matrix_exceeds_limit(
9281 remaining_added.len(),
9282 remaining_source_count,
9283 rename_limit,
9284 );
9285
9286 if !skip_inexact {
9287 source_contents = parallel_load_optional_blobs(odb, sources.len(), par_opts, 0, |si| {
9288 odb.read(&sources[si].1).ok().map(|obj| obj.data)
9289 });
9290
9291 let est_bytes: usize = source_contents
9292 .iter()
9293 .chain(added_contents.iter())
9294 .filter_map(|opt| opt.as_ref().map(|b| b.len()))
9295 .sum();
9296
9297 let source_tables: Vec<Option<RenameLineTable>> = parallel_map_indices(
9298 odb,
9299 sources.len(),
9300 par_opts,
9301 est_bytes,
9302 |si| -> std::result::Result<Option<RenameLineTable>, std::convert::Infallible> {
9303 Ok(source_contents[si]
9304 .as_ref()
9305 .map(|d| RenameLineTable::from_blob(d)))
9306 },
9307 )
9308 .expect("copy source line tables");
9309 let added_tables: Vec<Option<RenameLineTable>> = parallel_map_indices(
9310 odb,
9311 added.len(),
9312 par_opts,
9313 est_bytes,
9314 |ai| -> std::result::Result<Option<RenameLineTable>, std::convert::Infallible> {
9315 Ok(added_contents[ai]
9316 .as_ref()
9317 .map(|d| RenameLineTable::from_blob(d)))
9318 },
9319 )
9320 .expect("copy added line tables");
9321
9322 let score_rows: Vec<Vec<(u32, usize, usize)>> = parallel_map_indices(
9323 odb,
9324 sources.len(),
9325 par_opts,
9326 est_bytes,
9327 |si| -> std::result::Result<Vec<(u32, usize, usize)>, std::convert::Infallible> {
9328 if renamed_deleted.contains(&si) {
9329 return Ok(Vec::new());
9330 }
9331 let (src_path, _, _) = &sources[si];
9332 let mut row: Vec<(u32, usize, usize)> = Vec::new();
9333 for &ai in &remaining_added {
9334 let add = &added[ai];
9335 if add.new_path.as_deref() == Some(src_path.as_str()) {
9336 continue;
9337 }
9338 let score = match (&source_tables[si], &added_tables[ai]) {
9339 (Some(old_table), Some(new_table)) => {
9340 RenameLineTable::similarity_score(old_table, new_table)
9341 }
9342 _ => 0,
9343 };
9344 if score >= threshold {
9345 row.push((score, si, ai));
9346 }
9347 }
9348 Ok(row)
9349 },
9350 )
9351 .expect("copy score rows");
9352 let mut inexact_scores: Vec<(u32, usize, usize)> =
9353 score_rows.into_iter().flatten().collect();
9354 inexact_scores.sort_by_key(|a| std::cmp::Reverse(a.0));
9355 apply_copy_score_assignments(
9356 &inexact_scores,
9357 &sources,
9358 &added,
9359 &source_modes,
9360 &mut used_added2,
9361 &mut renamed_deleted,
9362 &mut result_entries,
9363 );
9364 }
9365 }
9366
9367 for entry in deleted.into_iter() {
9369 if let Some(ref path) = entry.old_path {
9370 if let Some(&si) = deleted_source_idx.get(path) {
9371 if renamed_deleted.contains(&si) {
9372 continue;
9374 }
9375 }
9376 }
9377 result_entries.push(entry);
9378 }
9379
9380 let mut result = others;
9381 result.extend(result_entries);
9382 for (i, entry) in added.into_iter().enumerate() {
9384 if !used_added2[i] {
9385 result.push(entry);
9386 }
9387 }
9388
9389 let mut final_result = result;
9393 sort_entries_by_path(&mut final_result);
9394 Ok(final_result)
9395}
9396
9397#[derive(Debug, Clone, Copy)]
9399pub struct DiffPostprocessOptions {
9400 pub break_score: Option<u64>,
9403 pub rename_threshold: u32,
9405 pub copy_threshold: Option<u32>,
9407 pub find_copies_harder: bool,
9409 pub materialize_break_worktree_new_oid: bool,
9412}
9413
9414impl Default for DiffPostprocessOptions {
9415 fn default() -> Self {
9416 Self {
9417 break_score: None,
9418 rename_threshold: 100,
9419 copy_threshold: None,
9420 find_copies_harder: false,
9421 materialize_break_worktree_new_oid: false,
9422 }
9423 }
9424}
9425
9426#[derive(Debug, Clone, Copy)]
9429pub struct DiffPostprocessWorktree<'a> {
9430 pub repository_git_dir: &'a Path,
9432 pub index: &'a Index,
9434}
9435
9436fn blob_bytes(odb: &Odb, oid: &ObjectId) -> Result<Vec<u8>> {
9437 Ok(odb.read(oid)?.data)
9438}
9439
9440fn read_new_side_bytes(
9445 odb: &Odb,
9446 entry: &DiffEntry,
9447 work_root: Option<&Path>,
9448 worktree: Option<&DiffPostprocessWorktree<'_>>,
9449 config: &ConfigSet,
9450) -> Result<Vec<u8>> {
9451 if entry.new_oid != zero_oid() {
9452 return blob_bytes(odb, &entry.new_oid);
9453 }
9454 let path = entry.new_path.as_deref().ok_or_else(|| {
9455 Error::ObjectNotFound("modified entry missing new_path for worktree read".into())
9456 })?;
9457 let root = work_root.ok_or_else(|| {
9458 Error::ObjectNotFound(format!(
9459 "worktree bytes required for {} but no work_root was provided",
9460 path
9461 ))
9462 })?;
9463 if let Some(wctx) = worktree {
9464 let mode = entry.new_mode.bits();
9465 let index_entry = wctx.index.get(path.as_bytes(), 0);
9466 return read_worktree_side_bytes_for_diff(odb, root, path, mode, config, index_entry, None);
9467 }
9468 fs::read(root.join(path)).map_err(Error::Io)
9469}
9470
9471fn worktree_side_differs_after_normalize(
9473 odb: &Odb,
9474 entry: &DiffEntry,
9475 work_root: &Path,
9476 worktree: &DiffPostprocessWorktree<'_>,
9477 config: &ConfigSet,
9478) -> Result<bool> {
9479 let path = entry.new_path.as_deref().ok_or_else(|| {
9480 Error::ObjectNotFound("modified entry missing new_path for worktree read".into())
9481 })?;
9482 let raw = fs::read(work_root.join(path)).map_err(Error::Io)?;
9483 let canonical = read_new_side_bytes(odb, entry, Some(work_root), Some(worktree), config)?;
9484 Ok(raw != canonical)
9485}
9486
9487fn split_broken_modifications(
9488 odb: &Odb,
9489 work_root: Option<&Path>,
9490 entries: Vec<DiffEntry>,
9491 break_score: u64,
9492 worktree: Option<&DiffPostprocessWorktree<'_>>,
9493 config: &ConfigSet,
9494) -> Result<Vec<DiffEntry>> {
9495 let mut out = Vec::with_capacity(entries.len());
9496 for e in entries {
9497 if e.status != DiffStatus::Modified {
9498 out.push(e);
9499 continue;
9500 }
9501 let old = blob_bytes(odb, &e.old_oid)?;
9502 let new = read_new_side_bytes(odb, &e, work_root, worktree, config)?;
9503 if should_break_rewrite_pair(&old, &new, break_score) {
9504 let path = e.path().to_owned();
9505 out.push(DiffEntry {
9506 status: DiffStatus::Deleted,
9507 old_path: Some(path.clone()),
9508 new_path: None,
9509 old_mode: e.old_mode,
9510 new_mode: FileMode::ABSENT,
9511 old_oid: e.old_oid,
9512 new_oid: zero_oid(),
9513 score: None,
9514 });
9515 out.push(DiffEntry {
9516 status: DiffStatus::Added,
9517 old_path: None,
9518 new_path: Some(path),
9519 old_mode: FileMode::ABSENT,
9520 new_mode: e.new_mode,
9521 old_oid: zero_oid(),
9522 new_oid: e.new_oid,
9523 score: None,
9524 });
9525 } else {
9526 out.push(e);
9527 }
9528 }
9529 Ok(out)
9530}
9531
9532fn annotate_broken_modifications(
9533 odb: &Odb,
9534 work_root: Option<&Path>,
9535 entries: Vec<DiffEntry>,
9536 break_score: u64,
9537 worktree: Option<&DiffPostprocessWorktree<'_>>,
9538 config: &ConfigSet,
9539 materialize_worktree_oid: bool,
9540) -> Result<Vec<DiffEntry>> {
9541 let mut out = Vec::with_capacity(entries.len());
9542 for mut e in entries {
9543 if e.status == DiffStatus::Modified {
9544 let old = blob_bytes(odb, &e.old_oid)?;
9545 let new = read_new_side_bytes(odb, &e, work_root, worktree, config)?;
9546 if should_break_rewrite_pair(&old, &new, break_score) {
9547 let show_score = if e.new_oid != zero_oid() {
9551 true
9552 } else if materialize_worktree_oid {
9553 match (work_root, worktree) {
9554 (Some(root), Some(wctx)) => {
9555 worktree_side_differs_after_normalize(odb, &e, root, wctx, config)?
9556 }
9557 _ => false,
9558 }
9559 } else {
9560 false
9561 };
9562 if show_score {
9563 e.score = rewrite_dissimilarity_index_percent(&old, &new);
9564 }
9565 }
9566 }
9567 out.push(e);
9568 }
9569 Ok(out)
9570}
9571
9572fn materialize_break_worktree_new_oids(
9573 odb: &Odb,
9574 work_root: Option<&Path>,
9575 entries: &mut [DiffEntry],
9576 worktree: Option<&DiffPostprocessWorktree<'_>>,
9577 config: &ConfigSet,
9578) -> Result<()> {
9579 for e in entries {
9580 if e.new_oid != zero_oid() {
9581 continue;
9582 }
9583 if !matches!(e.status, DiffStatus::Modified | DiffStatus::Added) {
9584 continue;
9585 }
9586 let bytes = read_new_side_bytes(odb, e, work_root, worktree, config)?;
9587 e.new_oid = odb.hash(ObjectKind::Blob, &bytes)?;
9588 }
9589 Ok(())
9590}
9591
9592fn prune_break_deletions_obviated_by_renames(entries: Vec<DiffEntry>) -> Vec<DiffEntry> {
9595 use std::collections::HashSet;
9596 let rename_targets: HashSet<String> = entries
9597 .iter()
9598 .filter(|e| e.status == DiffStatus::Renamed)
9599 .filter_map(|e| e.new_path.clone())
9600 .collect();
9601 if rename_targets.is_empty() {
9602 return entries;
9603 }
9604 entries
9605 .into_iter()
9606 .filter(|e| {
9607 if e.status != DiffStatus::Deleted {
9608 return true;
9609 }
9610 e.old_path
9611 .as_ref()
9612 .is_none_or(|p| !rename_targets.contains(p))
9613 })
9614 .collect()
9615}
9616
9617#[allow(clippy::too_many_arguments)]
9627pub fn postprocess_diff_entries(
9628 odb: &Odb,
9629 entries: Vec<DiffEntry>,
9630 source_tree_oid: Option<&ObjectId>,
9631 work_root: Option<&Path>,
9632 worktree: Option<&DiffPostprocessWorktree<'_>>,
9633 config: &ConfigSet,
9634 thread_policy: &ThreadPolicy,
9635 opts: &DiffPostprocessOptions,
9636) -> Result<Vec<DiffEntry>> {
9637 let mut entries = entries;
9638 if let Some(break_score) = opts.break_score {
9639 let rename_or_copy = opts.rename_threshold < 100 || opts.copy_threshold.is_some();
9640 entries = if rename_or_copy {
9641 split_broken_modifications(odb, work_root, entries, break_score, worktree, config)?
9642 } else {
9643 annotate_broken_modifications(
9644 odb,
9645 work_root,
9646 entries,
9647 break_score,
9648 worktree,
9649 config,
9650 opts.materialize_break_worktree_new_oid,
9651 )?
9652 };
9653 if opts.materialize_break_worktree_new_oid {
9654 materialize_break_worktree_new_oids(odb, work_root, &mut entries, worktree, config)?;
9655 }
9656 }
9657
9658 let source_tree_entries = source_tree_oid
9659 .map(|t| flatten_tree_entries(odb, t))
9660 .transpose()?
9661 .unwrap_or_default();
9662
9663 if opts.copy_threshold.is_some() || opts.find_copies_harder {
9664 let threshold = opts.copy_threshold.unwrap_or(opts.rename_threshold.min(99));
9665 detect_copies_with_policy(
9666 odb,
9667 work_root,
9668 entries,
9669 threshold,
9670 opts.find_copies_harder,
9671 &source_tree_entries,
9672 config,
9673 thread_policy,
9674 )
9675 } else if opts.rename_threshold < 100 {
9676 detect_renames_with_policy(
9677 odb,
9678 work_root,
9679 entries,
9680 opts.rename_threshold,
9681 config,
9682 thread_policy,
9683 None,
9684 )
9685 } else {
9686 Ok(entries)
9687 }
9688}
9689
9690pub fn flatten_tree_entries(
9692 odb: &Odb,
9693 tree_oid: &ObjectId,
9694) -> Result<Vec<(String, FileMode, ObjectId)>> {
9695 fn walk(
9696 odb: &Odb,
9697 tree_oid: &ObjectId,
9698 prefix: &str,
9699 out: &mut Vec<(String, FileMode, ObjectId)>,
9700 ) -> Result<()> {
9701 let obj = odb.read(tree_oid)?;
9702 let oid_len = odb.hash_algo().len();
9703 for e in TreeRefIter::with_oid_len(&obj.data, oid_len) {
9704 let e = e?;
9705 let name = String::from_utf8_lossy(e.name);
9706 let path = if prefix.is_empty() {
9707 name.to_string()
9708 } else {
9709 format!("{prefix}/{name}")
9710 };
9711 if e.mode == 0o040000 {
9712 walk(odb, &e.oid, &path, out)?;
9713 } else {
9714 out.push((path, FileMode::new(e.mode), e.oid));
9715 }
9716 }
9717 Ok(())
9718 }
9719 let mut out = Vec::new();
9720 walk(odb, tree_oid, "", &mut out)?;
9721 Ok(out)
9722}
9723
9724#[allow(clippy::too_many_arguments)]
9734pub fn status_apply_rename_copy_detection(
9735 odb: &Odb,
9736 unstaged_raw: Vec<DiffEntry>,
9737 threshold: u32,
9738 copies: bool,
9739 head_tree: Option<&ObjectId>,
9740 config: &ConfigSet,
9741 thread_policy: &ThreadPolicy,
9742 cancel_watch: Option<&std::sync::atomic::AtomicBool>,
9743) -> Result<Vec<DiffEntry>> {
9744 if !copies {
9745 return detect_renames_with_policy(
9746 odb,
9747 None,
9748 unstaged_raw,
9749 threshold,
9750 config,
9751 thread_policy,
9752 cancel_watch,
9753 );
9754 }
9755 let source_tree_entries: Vec<(String, FileMode, ObjectId)> = match head_tree {
9756 Some(oid) => flatten_tree(odb, oid, "")?
9757 .into_iter()
9758 .map(|e| (e.path, FileMode::new(e.mode), e.oid))
9759 .collect(),
9760 None => Vec::new(),
9761 };
9762 detect_copies_with_policy(
9763 odb,
9764 None,
9765 unstaged_raw,
9766 threshold,
9767 false,
9768 &source_tree_entries,
9769 config,
9770 thread_policy,
9771 )
9772}
9773
9774fn same_basename(del: &DiffEntry, add: &DiffEntry) -> bool {
9776 let old = del.old_path.as_deref().unwrap_or("");
9777 let new = add.new_path.as_deref().unwrap_or("");
9778 let old_base = old.rsplit('/').next().unwrap_or(old);
9779 let new_base = new.rsplit('/').next().unwrap_or(new);
9780 old_base == new_base && !old_base.is_empty()
9781}
9782
9783const RENAME_SCORE_MAX: u32 = 100;
9784
9785fn blob_payload_size(odb: &Odb, oid: &ObjectId) -> Option<usize> {
9787 if oid.is_zero() {
9788 return None;
9789 }
9790 odb.read_info(oid).ok().map(|info| info.size as usize)
9791}
9792
9793fn added_entry_payload_size(odb: &Odb, add: &DiffEntry, work_root: Option<&Path>) -> Option<usize> {
9795 if !add.new_oid.is_zero() {
9796 if let Some(size) = blob_payload_size(odb, &add.new_oid) {
9797 return Some(size);
9798 }
9799 }
9800 read_added_entry_bytes(odb, add, work_root).map(|data| data.len())
9801}
9802
9803fn rename_pair_size_reachable(src_size: usize, dst_size: usize, minimum_score: u32) -> bool {
9805 let max_size = src_size.max(dst_size);
9806 let min_size = src_size.min(dst_size);
9807 if max_size == 0 {
9808 return minimum_score <= RENAME_SCORE_MAX;
9809 }
9810 (max_size - min_size).saturating_mul(RENAME_SCORE_MAX as usize)
9811 <= max_size.saturating_mul((RENAME_SCORE_MAX - minimum_score) as usize)
9812}
9813
9814fn compute_similarity(old: &[u8], new: &[u8]) -> u32 {
9819 RenameLineTable::similarity_score(
9820 &RenameLineTable::from_blob(old),
9821 &RenameLineTable::from_blob(new),
9822 )
9823}
9824
9825#[must_use]
9830pub fn rename_similarity_score(old: &[u8], new: &[u8]) -> u32 {
9831 compute_similarity(old, new)
9832}
9833
9834pub fn format_raw(entry: &DiffEntry) -> String {
9840 let path = match entry.status {
9841 DiffStatus::Renamed | DiffStatus::Copied => {
9842 format!(
9843 "{}\t{}",
9844 entry.old_path.as_deref().unwrap_or(""),
9845 entry.new_path.as_deref().unwrap_or("")
9846 )
9847 }
9848 _ => entry.path().to_owned(),
9849 };
9850
9851 let status_str = match (entry.status, entry.score) {
9852 (DiffStatus::Renamed, Some(s)) => format!("R{:03}", s),
9853 (DiffStatus::Copied, Some(s)) => format!("C{:03}", s),
9854 (DiffStatus::Modified, Some(s)) => format!("M{s:03}"),
9855 (DiffStatus::TypeChanged, Some(s)) => format!("T{s:03}"),
9856 _ => entry.status.letter().to_string(),
9857 };
9858
9859 let (old_hex, new_hex) = raw_oid_hex_pair(&entry.old_oid, &entry.new_oid);
9860 format!(
9861 ":{} {} {} {} {}\t{}",
9862 entry.old_mode, entry.new_mode, old_hex, new_hex, status_str, path
9863 )
9864}
9865
9866fn raw_oid_hex_pair(old: &ObjectId, new: &ObjectId) -> (String, String) {
9874 let width = if !old.is_zero() {
9875 old.algo().hex_len()
9876 } else if !new.is_zero() {
9877 new.algo().hex_len()
9878 } else {
9879 old.algo().hex_len()
9880 };
9881 let render = |oid: &ObjectId| {
9882 if oid.is_zero() {
9883 "0".repeat(width)
9884 } else {
9885 oid.to_hex()
9886 }
9887 };
9888 (render(old), render(new))
9889}
9890
9891pub fn format_raw_abbrev(entry: &DiffEntry, abbrev_len: usize) -> String {
9893 let ellipsis = if crate::environment::Environment::empty()
9894 .var("GIT_PRINT_SHA1_ELLIPSIS")
9895 .as_deref()
9896 == Some("yes")
9897 {
9898 "..."
9899 } else {
9900 ""
9901 };
9902 let old_hex = format!("{}", entry.old_oid);
9903 let new_hex = format!("{}", entry.new_oid);
9904 let old_abbrev = &old_hex[..abbrev_len.min(old_hex.len())];
9905 let new_abbrev = &new_hex[..abbrev_len.min(new_hex.len())];
9906
9907 let path = match entry.status {
9909 DiffStatus::Renamed | DiffStatus::Copied => format!(
9910 "{}\t{}",
9911 entry.old_path.as_deref().unwrap_or(""),
9912 entry.new_path.as_deref().unwrap_or("")
9913 ),
9914 _ => entry.path().to_owned(),
9915 };
9916 let status_str = match (entry.status, entry.score) {
9917 (DiffStatus::Renamed, Some(s)) => format!("R{s:03}"),
9918 (DiffStatus::Copied, Some(s)) => format!("C{s:03}"),
9919 (DiffStatus::Modified, Some(s)) => format!("M{s:03}"),
9920 (DiffStatus::TypeChanged, Some(s)) => format!("T{s:03}"),
9921 _ => entry.status.letter().to_string(),
9922 };
9923
9924 format!(
9925 ":{} {} {}{} {}{} {}\t{}",
9926 entry.old_mode,
9927 entry.new_mode,
9928 old_abbrev,
9929 ellipsis,
9930 new_abbrev,
9931 ellipsis,
9932 status_str,
9933 path
9934 )
9935}
9936
9937pub fn unified_diff(
9952 old_content: &str,
9953 new_content: &str,
9954 old_path: &str,
9955 new_path: &str,
9956 context_lines: usize,
9957 indent_heuristic: bool,
9958 quote_path_fully: bool,
9959) -> String {
9960 unified_diff_with_prefix(
9961 old_content,
9962 new_content,
9963 old_path,
9964 new_path,
9965 context_lines,
9966 0,
9967 "a/",
9968 "b/",
9969 indent_heuristic,
9970 quote_path_fully,
9971 )
9972}
9973
9974#[allow(clippy::too_many_arguments)] pub fn unified_diff_with_prefix(
9980 old_content: &str,
9981 new_content: &str,
9982 old_path: &str,
9983 new_path: &str,
9984 context_lines: usize,
9985 inter_hunk_context: usize,
9986 src_prefix: &str,
9987 dst_prefix: &str,
9988 indent_heuristic: bool,
9989 quote_path_fully: bool,
9990) -> String {
9991 unified_diff_with_prefix_and_funcname(
9992 old_content,
9993 new_content,
9994 old_path,
9995 new_path,
9996 context_lines,
9997 inter_hunk_context,
9998 src_prefix,
9999 dst_prefix,
10000 None,
10001 indent_heuristic,
10002 quote_path_fully,
10003 )
10004}
10005
10006#[allow(clippy::too_many_arguments)]
10009pub fn unified_diff_with_prefix_and_funcname(
10010 old_content: &str,
10011 new_content: &str,
10012 old_path: &str,
10013 new_path: &str,
10014 context_lines: usize,
10015 inter_hunk_context: usize,
10016 src_prefix: &str,
10017 dst_prefix: &str,
10018 funcname_matcher: Option<&FuncnameMatcher>,
10019 indent_heuristic: bool,
10020 quote_path_fully: bool,
10021) -> String {
10022 unified_diff_with_prefix_and_funcname_and_algorithm(
10023 old_content,
10024 new_content,
10025 old_path,
10026 new_path,
10027 context_lines,
10028 inter_hunk_context,
10029 src_prefix,
10030 dst_prefix,
10031 funcname_matcher,
10032 similar::Algorithm::Myers,
10033 false,
10034 false,
10035 indent_heuristic,
10036 quote_path_fully,
10037 )
10038}
10039
10040#[allow(clippy::too_many_arguments)]
10046pub fn write_unified_diff_with_prefix_and_funcname_and_algorithm<W: std::io::Write>(
10047 out: &mut W,
10048 old_content: &str,
10049 new_content: &str,
10050 old_path: &str,
10051 new_path: &str,
10052 context_lines: usize,
10053 inter_hunk_context: usize,
10054 src_prefix: &str,
10055 dst_prefix: &str,
10056 funcname_matcher: Option<&FuncnameMatcher>,
10057 algorithm: similar::Algorithm,
10058 function_context: bool,
10059 use_git_histogram: bool,
10060 indent_heuristic: bool,
10061 quote_path_fully: bool,
10062) -> Result<()> {
10063 if function_context {
10064 let patch = unified_diff_with_function_context(
10065 old_content,
10066 new_content,
10067 old_path,
10068 new_path,
10069 context_lines,
10070 inter_hunk_context,
10071 src_prefix,
10072 dst_prefix,
10073 funcname_matcher,
10074 algorithm,
10075 indent_heuristic,
10076 quote_path_fully,
10077 );
10078 out.write_all(patch.as_bytes())?;
10079 return Ok(());
10080 }
10081
10082 if use_git_histogram {
10083 let patch = unified_diff_histogram_with_prefix_and_funcname_indent(
10084 old_content,
10085 new_content,
10086 old_path,
10087 new_path,
10088 context_lines,
10089 inter_hunk_context,
10090 src_prefix,
10091 dst_prefix,
10092 funcname_matcher,
10093 indent_heuristic,
10094 quote_path_fully,
10095 );
10096 out.write_all(patch.as_bytes())?;
10097 return Ok(());
10098 }
10099
10100 if algorithm == similar::Algorithm::Myers && !function_context {
10101 let patch = unified_diff_git_myers_with_prefix_and_funcname(
10102 old_content,
10103 new_content,
10104 old_path,
10105 new_path,
10106 context_lines,
10107 inter_hunk_context,
10108 src_prefix,
10109 dst_prefix,
10110 funcname_matcher,
10111 indent_heuristic,
10112 quote_path_fully,
10113 );
10114 out.write_all(patch.as_bytes())?;
10115 return Ok(());
10116 }
10117
10118 if algorithm == similar::Algorithm::Lcs && !function_context {
10119 let patch = unified_diff_git_minimal_with_prefix_and_funcname(
10120 old_content,
10121 new_content,
10122 old_path,
10123 new_path,
10124 context_lines,
10125 inter_hunk_context,
10126 src_prefix,
10127 dst_prefix,
10128 funcname_matcher,
10129 indent_heuristic,
10130 quote_path_fully,
10131 );
10132 out.write_all(patch.as_bytes())?;
10133 return Ok(());
10134 }
10135
10136 if algorithm == similar::Algorithm::Patience && !function_context {
10137 let patch = unified_diff_git_patience_with_prefix_and_funcname(
10138 old_content,
10139 new_content,
10140 old_path,
10141 new_path,
10142 context_lines,
10143 inter_hunk_context,
10144 src_prefix,
10145 dst_prefix,
10146 funcname_matcher,
10147 indent_heuristic,
10148 quote_path_fully,
10149 );
10150 out.write_all(patch.as_bytes())?;
10151 return Ok(());
10152 }
10153
10154 use crate::quote_path::format_diff_path_with_prefix;
10155 use similar::{udiff::UnifiedDiffHunk, TextDiff};
10156
10157 let diff = TextDiff::configure()
10158 .algorithm(algorithm)
10159 .diff_lines(old_content, new_content);
10160 let compacted_ops = diff_indent_heuristic::diff_lines_ops_compacted(
10161 old_content,
10162 new_content,
10163 algorithm,
10164 indent_heuristic,
10165 );
10166
10167 if old_path == "/dev/null" {
10168 out.write_all(b"--- /dev/null\n")?;
10169 } else if src_prefix.is_empty() {
10170 writeln!(out, "--- {old_path}")?;
10171 } else {
10172 out.write_all(b"--- ")?;
10173 out.write_all(
10174 format_diff_path_with_prefix(src_prefix, old_path, quote_path_fully).as_bytes(),
10175 )?;
10176 out.write_all(b"\n")?;
10177 }
10178 if new_path == "/dev/null" {
10179 out.write_all(b"+++ /dev/null\n")?;
10180 } else if dst_prefix.is_empty() {
10181 writeln!(out, "+++ {new_path}")?;
10182 } else {
10183 out.write_all(b"+++ ")?;
10184 out.write_all(
10185 format_diff_path_with_prefix(dst_prefix, new_path, quote_path_fully).as_bytes(),
10186 )?;
10187 out.write_all(b"\n")?;
10188 }
10189
10190 let old_lines: Vec<&str> = old_content.lines().collect();
10191 let max_common_gap = context_lines
10192 .saturating_mul(2)
10193 .saturating_add(inter_hunk_context);
10194 let op_groups = group_diff_ops_gap(compacted_ops, context_lines, max_common_gap);
10195
10196 let mut hunk_buf = String::new();
10197 let mut eof_annot = UnifiedDiffNoNewlineAnnotator::new(old_content, new_content);
10198 for ops in op_groups {
10199 if ops.is_empty() {
10200 continue;
10201 }
10202 let hunk = UnifiedDiffHunk::new(ops, &diff, true);
10203 hunk_buf.clear();
10204 use std::fmt::Write as FmtWrite;
10205 let _ = FmtWrite::write_fmt(&mut hunk_buf, format_args!("{hunk}"));
10206 let mut fragment = hunk_buf.clone();
10207 if let Some(first_newline) = hunk_buf.find('\n') {
10208 let header_line = &hunk_buf[..first_newline];
10209 let rest = &hunk_buf[first_newline..];
10210 if let Some(func_ctx) =
10211 extract_function_context(header_line, &old_lines, funcname_matcher, Some(old_path))
10212 {
10213 fragment.clear();
10214 fragment.push_str(header_line);
10215 fragment.push(' ');
10216 fragment.push_str(&func_ctx);
10217 fragment.push_str(rest);
10218 }
10219 }
10220 eof_annot.write_hunk(out, &fragment)?;
10221 }
10222 Ok(())
10223}
10224
10225#[allow(clippy::too_many_arguments)]
10231pub fn unified_diff_with_prefix_and_funcname_and_algorithm(
10232 old_content: &str,
10233 new_content: &str,
10234 old_path: &str,
10235 new_path: &str,
10236 context_lines: usize,
10237 inter_hunk_context: usize,
10238 src_prefix: &str,
10239 dst_prefix: &str,
10240 funcname_matcher: Option<&FuncnameMatcher>,
10241 algorithm: similar::Algorithm,
10242 function_context: bool,
10243 use_git_histogram: bool,
10244 indent_heuristic: bool,
10245 quote_path_fully: bool,
10246) -> String {
10247 let mut output = Vec::new();
10248 if write_unified_diff_with_prefix_and_funcname_and_algorithm(
10249 &mut output,
10250 old_content,
10251 new_content,
10252 old_path,
10253 new_path,
10254 context_lines,
10255 inter_hunk_context,
10256 src_prefix,
10257 dst_prefix,
10258 funcname_matcher,
10259 algorithm,
10260 function_context,
10261 use_git_histogram,
10262 indent_heuristic,
10263 quote_path_fully,
10264 )
10265 .is_err()
10266 {
10267 output.clear();
10268 }
10269 String::from_utf8_lossy(&output).into_owned()
10270}
10271
10272pub(crate) fn group_diff_ops_gap(
10278 mut ops: Vec<similar::DiffOp>,
10279 context: usize,
10280 max_common_gap: usize,
10281) -> Vec<Vec<similar::DiffOp>> {
10282 use similar::DiffOp;
10283 if ops.is_empty() {
10284 return vec![];
10285 }
10286
10287 let mut pending_group = Vec::new();
10288 let mut rv = Vec::new();
10289
10290 if let Some(DiffOp::Equal {
10291 old_index,
10292 new_index,
10293 len,
10294 }) = ops.first_mut()
10295 {
10296 let offset = (*len).saturating_sub(context);
10297 *old_index += offset;
10298 *new_index += offset;
10299 *len -= offset;
10300 }
10301
10302 if let Some(DiffOp::Equal { len, .. }) = ops.last_mut() {
10303 *len -= (*len).saturating_sub(context);
10304 }
10305
10306 for op in ops.into_iter() {
10307 if let DiffOp::Equal {
10308 old_index,
10309 new_index,
10310 len,
10311 } = op
10312 {
10313 if len > max_common_gap {
10316 pending_group.push(DiffOp::Equal {
10317 old_index,
10318 new_index,
10319 len: context,
10320 });
10321 rv.push(pending_group);
10322 let offset = len.saturating_sub(context);
10323 pending_group = vec![DiffOp::Equal {
10324 old_index: old_index + offset,
10325 new_index: new_index + offset,
10326 len: len - offset,
10327 }];
10328 continue;
10329 }
10330 }
10331 pending_group.push(op);
10332 }
10333
10334 match &pending_group[..] {
10335 &[] | &[similar::DiffOp::Equal { .. }] => {}
10336 _ => rv.push(pending_group),
10337 }
10338
10339 rv
10340}
10341#[expect(clippy::too_many_arguments)]
10342fn unified_diff_with_function_context(
10344 old_content: &str,
10345 new_content: &str,
10346 old_path: &str,
10347 new_path: &str,
10348 context_lines: usize,
10349 inter_hunk_context: usize,
10350 src_prefix: &str,
10351 dst_prefix: &str,
10352 funcname_matcher: Option<&FuncnameMatcher>,
10353 algorithm: similar::Algorithm,
10354 indent_heuristic: bool,
10355 quote_path_fully: bool,
10356) -> String {
10357 use crate::quote_path::format_diff_path_with_prefix;
10358 use similar::{udiff::UnifiedDiffHunk, TextDiff};
10359
10360 let diff = TextDiff::configure()
10361 .algorithm(algorithm)
10362 .diff_lines(old_content, new_content);
10363
10364 let old_lines: Vec<&str> = old_content.lines().collect();
10365 let new_lines: Vec<&str> = new_content.lines().collect();
10366 let n_old = old_lines.len();
10367 let n_new = new_lines.len();
10368
10369 let max_common_gap = context_lines
10376 .saturating_mul(2)
10377 .saturating_add(inter_hunk_context);
10378 let all_ops = diff.ops().to_vec();
10379 let op_groups = group_diff_ops_gap(all_ops.clone(), context_lines, max_common_gap);
10380
10381 let mut ranges: Vec<(usize, usize, usize, usize)> = Vec::new();
10382
10383 for ops in op_groups {
10384 if ops.is_empty() {
10385 continue;
10386 }
10387 let i1_anchor = func_context_old_anchor(&ops, n_old);
10388 let i1_end = hunk_old_change_end_exclusive(&ops);
10389 let skip_preimage_pull =
10390 append_with_whole_function_added(&ops, n_old, n_new, &new_lines, funcname_matcher);
10391 let hunk = UnifiedDiffHunk::new(ops, &diff, true);
10392 let hunk_str = format!("{hunk}");
10393 let header_line = hunk_str
10394 .lines()
10395 .next()
10396 .unwrap_or("")
10397 .trim_end_matches(['\r', '\n']);
10398 let Some((base_s1, _base_e1, _base_s2, _base_e2)) =
10399 parse_unified_hunk_header_ranges(header_line)
10400 else {
10401 continue;
10402 };
10403
10404 let ctx = context_lines;
10405 let (s1, e1, s2, e2) = if skip_preimage_pull {
10406 let s = n_old.saturating_sub(ctx);
10407 let s2 = map_old_line_to_new(&all_ops, s, n_new).min(n_new);
10408 (s, n_old, s2, n_new)
10409 } else {
10410 let mut s1 = base_s1.saturating_sub(ctx);
10411 let mut s2 = map_old_line_to_new(&all_ops, s1, n_new).min(n_new);
10412
10413 let base_pre_s1 = i1_anchor.saturating_sub(ctx);
10414 if base_pre_s1 < s1 {
10415 s1 = base_pre_s1;
10416 s2 = map_old_line_to_new(&all_ops, s1, n_new).min(n_new);
10417 }
10418
10419 let fs1 = expand_func_pre_start(s1, i1_anchor, n_old, &old_lines, funcname_matcher);
10420 if fs1 < s1 {
10421 s1 = fs1;
10422 s2 = map_old_line_to_new(&all_ops, s1, n_new).min(n_new);
10423 }
10424
10425 let mut e1 = (i1_end + ctx).min(n_old);
10431 let mut e2 = map_old_line_to_new(&all_ops, e1, n_new).min(n_new);
10432 let fe1 = expand_func_post_end(e1, i1_end, n_old, &old_lines, funcname_matcher);
10433 if fe1 > e1 {
10434 e1 = fe1;
10435 e2 = map_old_line_to_new(&all_ops, e1, n_new).min(n_new);
10436 }
10437 (s1, e1, s2, e2)
10438 };
10439
10440 ranges.push((s1, e1, s2, e2));
10441 }
10442
10443 ranges.sort_by_key(|r| (r.0, r.2));
10446 let mut merged: Vec<(usize, usize, usize, usize)> = Vec::with_capacity(ranges.len());
10447 for (s1, e1, s2, e2) in ranges {
10448 if let Some(last) = merged.last_mut() {
10449 if s1 < last.1 {
10450 last.1 = last.1.max(e1);
10451 last.3 = last.3.max(e2);
10452 continue;
10453 }
10454 }
10455 merged.push((s1, e1, s2, e2));
10456 }
10457 let ranges = merged;
10458
10459 let mut output = String::new();
10460 if old_path == "/dev/null" {
10461 output.push_str("--- /dev/null\n");
10462 } else if src_prefix.is_empty() {
10463 output.push_str(&format!("--- {old_path}\n"));
10464 } else {
10465 output.push_str("--- ");
10466 output.push_str(&format_diff_path_with_prefix(
10467 src_prefix,
10468 old_path,
10469 quote_path_fully,
10470 ));
10471 output.push('\n');
10472 }
10473 if new_path == "/dev/null" {
10474 output.push_str("+++ /dev/null\n");
10475 } else if dst_prefix.is_empty() {
10476 output.push_str(&format!("+++ {new_path}\n"));
10477 } else {
10478 output.push_str("+++ ");
10479 output.push_str(&format_diff_path_with_prefix(
10480 dst_prefix,
10481 new_path,
10482 quote_path_fully,
10483 ));
10484 output.push('\n');
10485 }
10486
10487 for (s1, e1, s2, e2) in ranges {
10488 if s1 >= e1 && s2 >= e2 {
10489 continue;
10490 }
10491 let old_seg =
10492 line_slice_for_diff_with_eof_nl(&old_lines, s1, e1, old_content.ends_with('\n'));
10493 let new_seg =
10494 line_slice_for_diff_with_eof_nl(&new_lines, s2, e2, new_content.ends_with('\n'));
10495 let inner_ctx = old_seg.lines().count().max(new_seg.lines().count()).max(1);
10496 let piece = unified_diff_with_prefix_and_funcname_and_algorithm(
10497 &old_seg,
10498 &new_seg,
10499 old_path,
10500 new_path,
10501 inner_ctx,
10502 0,
10503 src_prefix,
10504 dst_prefix,
10505 funcname_matcher,
10506 algorithm,
10507 false,
10508 false,
10509 indent_heuristic,
10510 quote_path_fully,
10511 );
10512 let shifted = shift_unified_hunk_headers_to_full_file(&piece, s1, s2);
10513 let with_func =
10514 enrich_unified_hunk_headers_funcname(&shifted, &old_lines, funcname_matcher);
10515 for line in with_func.lines() {
10516 if line.starts_with("--- ") || line.starts_with("+++ ") {
10517 continue;
10518 }
10519 output.push_str(line);
10520 output.push('\n');
10521 }
10522 }
10523
10524 output
10525}
10526
10527fn shift_unified_hunk_headers_to_full_file(
10532 patch: &str,
10533 delta_old: usize,
10534 delta_new: usize,
10535) -> String {
10536 if delta_old == 0 && delta_new == 0 {
10537 return patch.to_owned();
10538 }
10539 let mut out = String::with_capacity(patch.len());
10540 for line in patch.lines() {
10541 if let Some(shifted) = shift_one_unified_hunk_header(line, delta_old, delta_new) {
10542 out.push_str(&shifted);
10543 } else {
10544 out.push_str(line);
10545 }
10546 out.push('\n');
10547 }
10548 out
10549}
10550
10551fn shift_one_unified_hunk_header(line: &str, delta_old: usize, delta_new: usize) -> Option<String> {
10552 let rest = line.strip_prefix("@@ ")?;
10553 let (old_chunk, after_plus) = rest.split_once(" +")?;
10554 let old_spec = old_chunk.strip_prefix('-')?;
10555 let (new_spec, suffix) = after_plus.split_once(" @@")?;
10556 let shifted_old = shift_unified_range_spec(old_spec, delta_old)?;
10557 let shifted_new = shift_unified_range_spec(new_spec, delta_new)?;
10558 Some(format!("@@ -{shifted_old} +{shifted_new} @@{suffix}"))
10559}
10560
10561fn shift_unified_range_spec(spec: &str, delta: usize) -> Option<String> {
10562 let spec = spec.trim();
10563 if let Some((start_s, count_s)) = spec.split_once(',') {
10564 let start: usize = start_s.parse().ok()?;
10565 let count: usize = count_s.parse().ok()?;
10566 Some(format!("{},{}", start.saturating_add(delta), count))
10567 } else {
10568 let start: usize = spec.parse().ok()?;
10569 Some(format!("{}", start.saturating_add(delta)))
10570 }
10571}
10572
10573fn enrich_unified_hunk_headers_funcname(
10575 patch: &str,
10576 full_old_lines: &[&str],
10577 funcname_matcher: Option<&FuncnameMatcher>,
10578) -> String {
10579 let mut out = String::with_capacity(patch.len());
10580 for line in patch.lines() {
10581 if let Some(fixed) = enrich_one_hunk_header_funcname(line, full_old_lines, funcname_matcher)
10582 {
10583 out.push_str(&fixed);
10584 } else {
10585 out.push_str(line);
10586 }
10587 out.push('\n');
10588 }
10589 out
10590}
10591
10592fn enrich_one_hunk_header_funcname(
10593 line: &str,
10594 full_old_lines: &[&str],
10595 funcname_matcher: Option<&FuncnameMatcher>,
10596) -> Option<String> {
10597 let after_at = line.strip_prefix("@@ ")?;
10598 let idx = after_at.find(" @@")?;
10599 let mid = after_at[..idx].trim();
10600 let tail = after_at[idx + 3..].trim_start();
10601 let header_for_parse = format!("@@ {mid} @@");
10602 let func = extract_function_context(&header_for_parse, full_old_lines, funcname_matcher, None);
10603 Some(if let Some(f) = func {
10604 format!("@@ {mid} @@ {f}")
10605 } else if !tail.is_empty() {
10606 format!("@@ {mid} @@ {tail}")
10607 } else {
10608 format!("@@ {mid} @@")
10609 })
10610}
10611
10612fn line_slice_for_diff_with_eof_nl(
10613 lines: &[&str],
10614 start: usize,
10615 end: usize,
10616 full_file_ends_with_newline: bool,
10617) -> String {
10618 if start >= end {
10619 return String::new();
10620 }
10621 let mut s = lines[start..end].join("\n");
10622 let slice_is_suffix_of_file = end == lines.len();
10623 let need_trailing_nl = if slice_is_suffix_of_file {
10624 full_file_ends_with_newline
10625 } else {
10626 true
10627 };
10628 if need_trailing_nl && !s.ends_with('\n') {
10629 s.push('\n');
10630 }
10631 s
10632}
10633
10634fn map_old_line_to_new(ops: &[similar::DiffOp], old_line: usize, n_new: usize) -> usize {
10637 use similar::DiffOp;
10638 let mut n = 0usize;
10639 for op in ops {
10640 match *op {
10641 DiffOp::Equal {
10642 old_index,
10643 new_index,
10644 len,
10645 } => {
10646 if old_index + len <= old_line {
10647 n = new_index + len;
10648 continue;
10649 }
10650 if old_index < old_line {
10651 let take = old_line - old_index;
10652 return (new_index + take).min(n_new);
10653 }
10654 return new_index.min(n_new);
10655 }
10656 DiffOp::Delete {
10657 old_index,
10658 old_len,
10659 new_index,
10660 } => {
10661 if old_index + old_len <= old_line {
10662 n = new_index;
10663 continue;
10664 }
10665 if old_index < old_line {
10666 return new_index.min(n_new);
10667 }
10668 }
10669 DiffOp::Insert {
10670 old_index,
10671 new_index,
10672 new_len,
10673 } => {
10674 if old_index < old_line {
10675 n = new_index + new_len;
10676 continue;
10677 }
10678 if old_index == old_line {
10679 return (new_index + new_len).min(n_new);
10682 }
10683 return new_index.min(n_new);
10684 }
10685 DiffOp::Replace {
10686 old_index,
10687 old_len,
10688 new_index,
10689 new_len,
10690 } => {
10691 if old_index + old_len <= old_line {
10692 n = new_index + new_len;
10693 continue;
10694 }
10695 if old_index < old_line {
10696 let into_old = old_line - old_index;
10697 let mapped = new_index + into_old.min(new_len);
10698 return mapped.min(n_new);
10699 }
10700 return new_index.min(n_new);
10701 }
10702 }
10703 }
10704 n.min(n_new)
10705}
10706
10707fn parse_unified_hunk_header_ranges(header: &str) -> Option<(usize, usize, usize, usize)> {
10709 let rest = header.strip_prefix("@@ ")?;
10710 let (old_tok, rest2) = rest.split_once(" +")?;
10711 let old_tok = old_tok.strip_prefix('-')?;
10712 let new_tok = rest2.split_once(" @@").map(|(a, _)| a)?;
10713
10714 fn parse_side(spec: &str) -> Option<(usize, usize)> {
10715 let spec = spec.trim();
10716 let (start_one_based, count) = if let Some((a, b)) = spec.split_once(',') {
10717 (a.parse::<usize>().ok()?, b.parse::<usize>().ok()?)
10718 } else {
10719 let s = spec.parse::<usize>().ok()?;
10720 (s, 1usize)
10721 };
10722 let s0 = start_one_based.saturating_sub(1);
10723 let e0 = s0.saturating_add(count);
10724 Some((s0, e0))
10725 }
10726
10727 let (os, oe) = parse_side(old_tok)?;
10728 let (ns, ne) = parse_side(new_tok)?;
10729 Some((os, oe, ns, ne))
10730}
10731
10732fn append_with_whole_function_added(
10735 ops: &[similar::DiffOp],
10736 n_old: usize,
10737 n_new: usize,
10738 new_lines: &[&str],
10739 matcher: Option<&FuncnameMatcher>,
10740) -> bool {
10741 use similar::DiffOp;
10742 if n_old == 0 {
10743 return false;
10744 }
10745 let mut only_ins_or_eq = true;
10746 let mut min_new_ins = usize::MAX;
10747 for op in ops {
10748 match *op {
10749 DiffOp::Equal { .. } => {}
10750 DiffOp::Insert {
10751 new_index, new_len, ..
10752 } => {
10753 min_new_ins = min_new_ins.min(new_index);
10754 if new_len == 0 {
10755 only_ins_or_eq = false;
10756 }
10757 }
10758 DiffOp::Delete { .. } | DiffOp::Replace { .. } => {
10759 only_ins_or_eq = false;
10760 }
10761 }
10762 }
10763 let mut insert_at_eof = false;
10764 for op in ops {
10765 if let DiffOp::Insert { old_index, .. } = *op {
10766 if old_index == n_old {
10767 insert_at_eof = true;
10768 break;
10769 }
10770 }
10771 }
10772 let append_at_eof = min_new_ins == n_old || insert_at_eof;
10773 if !only_ins_or_eq || !append_at_eof || min_new_ins == usize::MAX {
10774 return false;
10775 }
10776 let mut j = min_new_ins;
10781 while j < n_new {
10782 let line = new_lines[j];
10783 if line.trim().is_empty() {
10784 j += 1;
10785 continue;
10786 }
10787 if let Some(m) = matcher {
10788 if m.match_line(line).is_some() {
10789 return true;
10790 }
10791 } else if inserted_block_starts_with_c_like_function_definition(line) {
10792 return true;
10793 }
10794 j += 1;
10795 }
10796 false
10797}
10798
10799fn inserted_block_starts_with_c_like_function_definition(line: &str) -> bool {
10800 let t = line.trim_start();
10801 let Some(open_paren) = t.find('(') else {
10802 return false;
10803 };
10804 let head = &t[..open_paren];
10805 let tokens: Vec<&str> = head.split_whitespace().collect();
10806 if tokens.len() < 2 {
10807 return false;
10809 }
10810 let nameish = tokens.last().copied().unwrap_or("");
10811 let name = nameish.trim_end_matches(['*', '&']);
10812 if name.is_empty() || !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
10813 return false;
10814 }
10815 let type_or_modifier = |tok: &str| {
10816 matches!(
10817 tok,
10818 "static"
10819 | "extern"
10820 | "inline"
10821 | "void"
10822 | "int"
10823 | "char"
10824 | "short"
10825 | "long"
10826 | "float"
10827 | "double"
10828 | "unsigned"
10829 | "signed"
10830 | "struct"
10831 | "enum"
10832 | "union"
10833 | "const"
10834 | "volatile"
10835 | "typedef"
10836 )
10837 };
10838 tokens[..tokens.len() - 1]
10839 .iter()
10840 .any(|tok| type_or_modifier(tok))
10841}
10842
10843fn hunk_old_change_end_exclusive(ops: &[similar::DiffOp]) -> usize {
10844 use similar::DiffOp;
10845 let mut max_o = 0usize;
10846 for op in ops {
10847 match *op {
10848 DiffOp::Delete {
10849 old_index, old_len, ..
10850 } => {
10851 max_o = max_o.max(old_index + old_len);
10852 }
10853 DiffOp::Replace {
10854 old_index, old_len, ..
10855 } => {
10856 max_o = max_o.max(old_index + old_len);
10857 }
10858 DiffOp::Insert { old_index, .. } => {
10859 max_o = max_o.max(old_index);
10862 }
10863 DiffOp::Equal { .. } => {}
10864 }
10865 }
10866 max_o
10867}
10868
10869fn func_context_old_anchor(ops: &[similar::DiffOp], n_old: usize) -> usize {
10870 use similar::DiffOp;
10871 let mut has_delete_or_replace = false;
10872 let mut min_del = usize::MAX;
10873 let mut min_ins_old = usize::MAX;
10874
10875 for op in ops {
10876 match *op {
10877 DiffOp::Delete {
10878 old_index, old_len, ..
10879 } => {
10880 has_delete_or_replace = true;
10881 min_del = min_del.min(old_index);
10882 min_del = min_del.min(old_index + old_len.saturating_sub(1));
10883 }
10884 DiffOp::Replace {
10885 old_index, old_len, ..
10886 } => {
10887 has_delete_or_replace = true;
10888 min_del = min_del.min(old_index);
10889 min_del = min_del.min(old_index + old_len.saturating_sub(1));
10890 }
10891 DiffOp::Insert { old_index, .. } => {
10892 min_ins_old = min_ins_old.min(old_index);
10893 }
10894 DiffOp::Equal { .. } => {}
10895 }
10896 }
10897
10898 let mut i1 = if has_delete_or_replace {
10899 min_del
10900 } else if min_ins_old != usize::MAX {
10901 min_ins_old
10902 } else {
10903 0
10904 };
10905
10906 let pure_insert = ops
10907 .iter()
10908 .all(|op| matches!(op, DiffOp::Insert { .. } | DiffOp::Equal { .. }))
10909 && ops.iter().any(|op| matches!(op, DiffOp::Insert { .. }));
10910
10911 if pure_insert && i1 >= n_old && n_old > 0 {
10912 i1 = n_old - 1;
10913 }
10914
10915 i1.min(n_old.saturating_sub(1))
10916}
10917
10918fn expand_func_pre_start(
10919 s1: usize,
10920 i1: usize,
10921 n_old: usize,
10922 old_lines: &[&str],
10923 matcher: Option<&FuncnameMatcher>,
10924) -> usize {
10925 if n_old == 0 {
10926 return s1;
10927 }
10928 let i1 = i1.min(n_old.saturating_sub(1));
10929 let mut fs1 = get_func_line_backward(old_lines, i1, matcher).unwrap_or(i1);
10930 while fs1 > 0
10931 && !is_line_empty_for_func_context(old_lines[fs1 - 1])
10932 && !is_func_line(old_lines[fs1 - 1], matcher)
10933 {
10934 fs1 -= 1;
10935 }
10936 s1.min(fs1)
10937}
10938
10939fn expand_func_post_end(
10940 e1: usize,
10941 i1_end: usize,
10942 n_old: usize,
10943 old_lines: &[&str],
10944 matcher: Option<&FuncnameMatcher>,
10945) -> usize {
10946 let from = i1_end.min(n_old);
10947 let fe1 = get_func_line_forward(old_lines, from, matcher).unwrap_or(n_old);
10948 let mut fe1_adj = fe1;
10949 while fe1_adj > 0 && is_line_empty_for_func_context(old_lines[fe1_adj - 1]) {
10950 fe1_adj -= 1;
10951 }
10952 e1.max(fe1_adj).min(n_old)
10953}
10954
10955fn is_line_empty_for_func_context(line: &str) -> bool {
10956 line.chars().all(|c| c.is_whitespace())
10957}
10958
10959pub(crate) fn is_func_line(line: &str, matcher: Option<&FuncnameMatcher>) -> bool {
10960 if let Some(m) = matcher {
10961 return m.match_line(line).is_some();
10962 }
10963 let t = line.trim_end_matches(['\n', '\r']);
10964 if t.is_empty() {
10965 return false;
10966 }
10967 let b = t.as_bytes()[0];
10968 if !(b.is_ascii_alphabetic() || b == b'_' || b == b'$') {
10969 return false;
10970 }
10971 true
10972}
10973
10974fn get_func_line_backward(
10975 old_lines: &[&str],
10976 start: usize,
10977 matcher: Option<&FuncnameMatcher>,
10978) -> Option<usize> {
10979 let mut l = start.min(old_lines.len().saturating_sub(1));
10980 if old_lines.is_empty() {
10981 return None;
10982 }
10983 loop {
10984 if is_func_line(old_lines[l], matcher) {
10985 return Some(l);
10986 }
10987 if l == 0 {
10988 break;
10989 }
10990 l -= 1;
10991 }
10992 None
10993}
10994
10995fn get_func_line_forward(
10996 old_lines: &[&str],
10997 start: usize,
10998 matcher: Option<&FuncnameMatcher>,
10999) -> Option<usize> {
11000 let mut l = start;
11001 while l < old_lines.len() {
11002 if is_func_line(old_lines[l], matcher) {
11003 return Some(l);
11004 }
11005 l += 1;
11006 }
11007 None
11008}
11009#[expect(clippy::too_many_arguments)]
11010pub fn anchored_unified_diff(
11020 old_content: &str,
11021 new_content: &str,
11022 old_path: &str,
11023 new_path: &str,
11024 context_lines: usize,
11025 anchors: &[String],
11026 algorithm: similar::Algorithm,
11027 use_git_histogram: bool,
11028 indent_heuristic: bool,
11029 quote_path_fully: bool,
11030) -> String {
11031 use crate::quote_path::format_diff_path_with_prefix;
11032 use similar::TextDiff;
11033
11034 let old_lines: Vec<&str> = old_content.lines().collect();
11035 let new_lines: Vec<&str> = new_content.lines().collect();
11036
11037 let mut anchor_pairs: Vec<(usize, usize)> = Vec::new(); for anchor in anchors {
11041 let anchor_str = anchor.as_str();
11042
11043 let old_positions: Vec<usize> = old_lines
11045 .iter()
11046 .enumerate()
11047 .filter(|(_, l)| l.trim_end() == anchor_str)
11048 .map(|(i, _)| i)
11049 .collect();
11050
11051 let new_positions: Vec<usize> = new_lines
11053 .iter()
11054 .enumerate()
11055 .filter(|(_, l)| l.trim_end() == anchor_str)
11056 .map(|(i, _)| i)
11057 .collect();
11058
11059 if old_positions.len() == 1 && new_positions.len() == 1 {
11061 anchor_pairs.push((old_positions[0], new_positions[0]));
11062 }
11063 }
11064
11065 if anchor_pairs.is_empty() {
11067 return unified_diff_with_prefix_and_funcname_and_algorithm(
11068 old_content,
11069 new_content,
11070 old_path,
11071 new_path,
11072 context_lines,
11073 0,
11074 "a/",
11075 "b/",
11076 None,
11077 algorithm,
11078 false,
11079 use_git_histogram,
11080 indent_heuristic,
11081 quote_path_fully,
11082 );
11083 }
11084
11085 anchor_pairs.sort_by_key(|&(old_idx, _)| old_idx);
11087
11088 let mut filtered: Vec<(usize, usize)> = Vec::new();
11091 for &pair in &anchor_pairs {
11092 if filtered.is_empty() || filtered.last().is_some_and(|last| pair.1 > last.1) {
11093 filtered.push(pair);
11094 }
11095 }
11096 let anchor_pairs = filtered;
11097
11098 struct LineDiffOp {
11107 tag: char, line: String,
11109 }
11110
11111 let append_segment_diff =
11112 |ops: &mut Vec<LineDiffOp>, old_seg_input: &str, new_seg_input: &str| {
11113 use similar::ChangeTag;
11114 let old_ls: Vec<&str> = old_seg_input.lines().collect();
11115 let new_ls: Vec<&str> = new_seg_input.lines().collect();
11116 if old_ls.is_empty() && new_ls.is_empty() {
11117 return;
11118 }
11119 let seg_diff = TextDiff::configure()
11120 .algorithm(algorithm)
11121 .diff_slices(&old_ls, &new_ls);
11122 let raw = seg_diff.ops().to_vec();
11123 let compacted = diff_indent_heuristic::apply_change_compact_to_ops(
11124 &raw,
11125 &old_ls,
11126 &new_ls,
11127 indent_heuristic,
11128 );
11129 for op in &compacted {
11130 for ch in op.iter_changes(&old_ls, &new_ls) {
11131 let t = match ch.tag() {
11132 ChangeTag::Equal => ' ',
11133 ChangeTag::Delete => '-',
11134 ChangeTag::Insert => '+',
11135 };
11136 ops.push(LineDiffOp {
11137 tag: t,
11138 line: ch.value().to_string(),
11139 });
11140 }
11141 }
11142 };
11143
11144 let mut ops: Vec<LineDiffOp> = Vec::new();
11145 let mut old_pos = 0usize;
11146 let mut new_pos = 0usize;
11147
11148 for &(old_anchor, new_anchor) in &anchor_pairs {
11149 let old_segment: Vec<&str> = old_lines[old_pos..old_anchor].to_vec();
11151 let new_segment: Vec<&str> = new_lines[new_pos..new_anchor].to_vec();
11152
11153 let old_seg_text = old_segment.join("\n");
11154 let new_seg_text = new_segment.join("\n");
11155
11156 if !old_seg_text.is_empty() || !new_seg_text.is_empty() {
11157 let old_seg_input = if old_seg_text.is_empty() {
11158 String::new()
11159 } else {
11160 format!("{}\n", old_seg_text)
11161 };
11162 let new_seg_input = if new_seg_text.is_empty() {
11163 String::new()
11164 } else {
11165 format!("{}\n", new_seg_text)
11166 };
11167 append_segment_diff(&mut ops, &old_seg_input, &new_seg_input);
11168 }
11169
11170 ops.push(LineDiffOp {
11172 tag: ' ',
11173 line: old_lines[old_anchor].to_string(),
11174 });
11175
11176 old_pos = old_anchor + 1;
11177 new_pos = new_anchor + 1;
11178 }
11179
11180 let old_segment: Vec<&str> = old_lines[old_pos..].to_vec();
11182 let new_segment: Vec<&str> = new_lines[new_pos..].to_vec();
11183 let old_seg_text = old_segment.join("\n");
11184 let new_seg_text = new_segment.join("\n");
11185
11186 if !old_seg_text.is_empty() || !new_seg_text.is_empty() {
11187 let old_seg_input = if old_seg_text.is_empty() {
11188 String::new()
11189 } else {
11190 format!("{}\n", old_seg_text)
11191 };
11192 let new_seg_input = if new_seg_text.is_empty() {
11193 String::new()
11194 } else {
11195 format!("{}\n", new_seg_text)
11196 };
11197 append_segment_diff(&mut ops, &old_seg_input, &new_seg_input);
11198 }
11199
11200 let mut output = String::new();
11202 if old_path == "/dev/null" {
11203 output.push_str("--- /dev/null\n");
11204 } else {
11205 output.push_str("--- ");
11206 output.push_str(&format_diff_path_with_prefix(
11207 "a/",
11208 old_path,
11209 quote_path_fully,
11210 ));
11211 output.push('\n');
11212 }
11213 if new_path == "/dev/null" {
11214 output.push_str("+++ /dev/null\n");
11215 } else {
11216 output.push_str("+++ ");
11217 output.push_str(&format_diff_path_with_prefix(
11218 "b/",
11219 new_path,
11220 quote_path_fully,
11221 ));
11222 output.push('\n');
11223 }
11224
11225 let total_ops = ops.len();
11227 if total_ops == 0 {
11228 return output;
11229 }
11230
11231 let mut hunks: Vec<(usize, usize)> = Vec::new(); let mut i = 0;
11234 while i < total_ops {
11235 if ops[i].tag != ' ' {
11236 let start = i.saturating_sub(context_lines);
11237 let mut end = i;
11238 while end < total_ops {
11240 if ops[end].tag != ' ' {
11241 end += 1;
11242 continue;
11243 }
11244 let mut next_change = end;
11246 while next_change < total_ops && ops[next_change].tag == ' ' {
11247 next_change += 1;
11248 }
11249 if next_change < total_ops && next_change - end <= context_lines * 2 {
11250 end = next_change + 1;
11251 } else {
11252 end = (end + context_lines).min(total_ops);
11253 break;
11254 }
11255 }
11256 if let Some(last) = hunks.last_mut() {
11258 if start <= last.1 {
11259 last.1 = end;
11260 } else {
11261 hunks.push((start, end));
11262 }
11263 } else {
11264 hunks.push((start, end));
11265 }
11266 i = end;
11267 } else {
11268 i += 1;
11269 }
11270 }
11271
11272 for (start, end) in hunks {
11274 let mut old_start = 1usize;
11276 let mut new_start = 1usize;
11277 for op in &ops[..start] {
11279 match op.tag {
11280 ' ' => {
11281 old_start += 1;
11282 new_start += 1;
11283 }
11284 '-' => {
11285 old_start += 1;
11286 }
11287 '+' => {
11288 new_start += 1;
11289 }
11290 _ => {}
11291 }
11292 }
11293 let mut old_count = 0usize;
11294 let mut new_count = 0usize;
11295 for op in &ops[start..end] {
11296 match op.tag {
11297 ' ' => {
11298 old_count += 1;
11299 new_count += 1;
11300 }
11301 '-' => {
11302 old_count += 1;
11303 }
11304 '+' => {
11305 new_count += 1;
11306 }
11307 _ => {}
11308 }
11309 }
11310
11311 output.push_str(&format!(
11312 "@@ -{},{} +{},{} @@\n",
11313 old_start, old_count, new_start, new_count
11314 ));
11315 for op in &ops[start..end] {
11316 output.push(op.tag);
11317 output.push_str(&op.line);
11318 output.push('\n');
11319 }
11320 }
11321
11322 output
11323}
11324
11325fn func_context_line_matches(line: &str, funcname_matcher: Option<&FuncnameMatcher>) -> bool {
11331 if let Some(m) = funcname_matcher {
11332 return m.match_line(line).is_some();
11333 }
11334 is_func_line(line, None)
11335}
11336
11337fn extract_function_context(
11338 header: &str,
11339 old_lines: &[&str],
11340 funcname_matcher: Option<&FuncnameMatcher>,
11341 _source_path: Option<&str>,
11342) -> Option<String> {
11343 let at_pos = header.find("-")?;
11345 let rest = &header[at_pos + 1..];
11346 let comma_or_space = rest.find([',', ' '])?;
11347 let start_str = &rest[..comma_or_space];
11348 let start_line: usize = start_str.parse().ok()?;
11349
11350 let old_token_end = rest.find([' ', '\t']).unwrap_or(rest.len());
11354 let old_token = &rest[..old_token_end];
11355 let old_count: usize = if let Some(comma) = old_token.find(',') {
11356 old_token[comma + 1..].parse().unwrap_or(1)
11357 } else {
11358 1
11359 };
11360
11361 if start_line == 0 {
11362 return None;
11363 }
11364
11365 if start_line == 0 {
11372 return None;
11373 }
11374 let search_end = if old_count == 0 {
11375 start_line.min(old_lines.len())
11376 } else {
11377 if start_line <= 1 {
11378 return None;
11379 }
11380 (start_line - 1).min(old_lines.len())
11381 };
11382 let truncate = |line: &str| {
11383 let mut end = line.len().min(80);
11384 while end > 0 && !line.is_char_boundary(end) {
11385 end -= 1;
11386 }
11387 let mut out = line[..end].to_owned();
11388 let tail = line[end..].trim_start();
11389 if tail.is_empty()
11394 || (!tail.starts_with('"')
11395 && !tail.starts_with('\'')
11396 && !line.starts_with("msgstr")
11397 && !line.starts_with("msgid"))
11398 {
11399 out = out.trim_end().to_owned();
11400 }
11401 out
11402 };
11403
11404 for i in (0..search_end).rev() {
11405 let line = old_lines[i];
11406 if line.is_empty() {
11407 continue;
11408 }
11409 if func_context_line_matches(line, funcname_matcher) {
11410 if let Some(matcher) = funcname_matcher {
11411 if let Some(matched) = matcher.match_line(line) {
11412 return Some(truncate(&matched));
11413 }
11414 continue;
11415 }
11416 return Some(truncate(line));
11417 }
11418 }
11419 None
11420}
11421
11422fn last_line_index_without_trailing_newline(text: &str) -> Option<usize> {
11423 LineSide::from_text(text).incomplete_last_line_index()
11424}
11425
11426fn parse_unified_hunk_starts(header: &str) -> Option<(usize, usize)> {
11427 let rest = header.strip_prefix("@@ ")?;
11428 let (old_part, new_part) = rest.split_once(" +")?;
11429 let old_start = parse_unified_range_start(old_part.strip_prefix('-')?)?;
11430 let new_start = parse_unified_range_start(new_part.split(" @@").next()?)?;
11431 Some((old_start, new_start))
11432}
11433
11434fn parse_unified_range_start(spec: &str) -> Option<usize> {
11435 let spec = spec.trim();
11436 if let Some((start, _)) = spec.split_once(',') {
11437 start.parse().ok()
11438 } else {
11439 spec.parse().ok()
11440 }
11441}
11442
11443struct UnifiedDiffNoNewlineAnnotator {
11445 old_last: Option<usize>,
11446 new_last: Option<usize>,
11447 old_line: usize,
11448 new_line: usize,
11449 passthrough: bool,
11450}
11451
11452impl UnifiedDiffNoNewlineAnnotator {
11453 fn new(old: &str, new: &str) -> Self {
11454 let old_last = last_line_index_without_trailing_newline(old);
11455 let new_last = last_line_index_without_trailing_newline(new);
11456 Self {
11457 old_last,
11458 new_last,
11459 old_line: 1,
11460 new_line: 1,
11461 passthrough: old_last.is_none() && new_last.is_none(),
11462 }
11463 }
11464
11465 fn write_hunk<W: std::io::Write>(&mut self, out: &mut W, patch: &str) -> Result<()> {
11466 if self.passthrough {
11467 out.write_all(patch.as_bytes())?;
11468 return Ok(());
11469 }
11470 let mut lines = patch.lines().peekable();
11471 while let Some(line) = lines.next() {
11472 self.write_annotated_line(out, line, &mut lines)?;
11473 }
11474 Ok(())
11475 }
11476
11477 fn write_annotated_line<W: std::io::Write>(
11478 &mut self,
11479 out: &mut W,
11480 line: &str,
11481 lines: &mut std::iter::Peekable<std::str::Lines<'_>>,
11482 ) -> Result<()> {
11483 if let Some((o, n)) = line
11484 .strip_prefix("@@ ")
11485 .and_then(|_| parse_unified_hunk_starts(line))
11486 {
11487 self.old_line = o;
11488 self.new_line = n;
11489 writeln!(out, "{line}")?;
11490 return Ok(());
11491 }
11492 if line.starts_with("--- ")
11493 || line.starts_with("+++ ")
11494 || line.starts_with("diff --git ")
11495 || line.starts_with("diff --cc ")
11496 || line.starts_with("diff --combined ")
11497 {
11498 writeln!(out, "{line}")?;
11499 return Ok(());
11500 }
11501 if line.starts_with('-') {
11502 writeln!(out, "{line}")?;
11503 let already_marked = lines
11504 .peek()
11505 .is_some_and(|next| *next == "\\ No newline at end of file");
11506 if !already_marked && self.old_last == Some(self.old_line.saturating_sub(1)) {
11507 writeln!(out, "\\ No newline at end of file")?;
11508 }
11509 self.old_line += 1;
11510 return Ok(());
11511 }
11512 if line.starts_with('+') {
11513 writeln!(out, "{line}")?;
11514 let already_marked = lines
11515 .peek()
11516 .is_some_and(|next| *next == "\\ No newline at end of file");
11517 if !already_marked && self.new_last == Some(self.new_line.saturating_sub(1)) {
11518 writeln!(out, "\\ No newline at end of file")?;
11519 }
11520 self.new_line += 1;
11521 return Ok(());
11522 }
11523 if line.starts_with(' ') {
11524 writeln!(out, "{line}")?;
11525 let already_marked = lines
11526 .peek()
11527 .is_some_and(|next| *next == "\\ No newline at end of file");
11528 if !already_marked {
11529 let old_idx = self.old_line.saturating_sub(1);
11530 let new_idx = self.new_line.saturating_sub(1);
11531 if self.old_last == Some(old_idx) || self.new_last == Some(new_idx) {
11532 writeln!(out, "\\ No newline at end of file")?;
11533 }
11534 }
11535 self.old_line += 1;
11536 self.new_line += 1;
11537 return Ok(());
11538 }
11539 writeln!(out, "{line}")?;
11540 Ok(())
11541 }
11542}
11543
11544#[must_use]
11547pub fn annotate_unified_diff_no_newline_at_eof(patch: &str, old: &str, new: &str) -> String {
11548 let mut annotator = UnifiedDiffNoNewlineAnnotator::new(old, new);
11549 if annotator.passthrough {
11550 return patch.to_string();
11551 }
11552 let mut out = Vec::with_capacity(patch.len() + 64);
11553 let mut lines = patch.lines().peekable();
11554 while let Some(line) = lines.next() {
11555 if annotator
11556 .write_annotated_line(&mut out, line, &mut lines)
11557 .is_err()
11558 {
11559 return patch.to_string();
11560 }
11561 }
11562 String::from_utf8(out).unwrap_or_else(|_| patch.to_string())
11563}
11564
11565#[must_use]
11567pub fn normalize_ignore_space_change_line(line: &str) -> String {
11568 let mut result = String::with_capacity(line.len());
11569 let mut in_space = false;
11570 for c in line.chars() {
11571 if c.is_whitespace() {
11572 if !in_space {
11573 result.push(' ');
11574 in_space = true;
11575 }
11576 } else {
11577 result.push(c);
11578 in_space = false;
11579 }
11580 }
11581 while result.ends_with(' ') {
11582 result.pop();
11583 }
11584 result
11585}
11586
11587#[must_use]
11593pub fn normalize_ignore_space_change(content: &str) -> String {
11594 content
11595 .lines()
11596 .map(normalize_ignore_space_change_line)
11597 .collect::<Vec<_>>()
11598 .join("\n")
11599}
11600
11601#[must_use]
11605pub fn count_changes_with_algorithm(
11606 old_content: &str,
11607 new_content: &str,
11608 algorithm: similar::Algorithm,
11609 use_git_histogram: bool,
11610) -> (usize, usize) {
11611 use crate::diff::options::{DiffAlgorithm, DiffOptions};
11612 let diff_algorithm = if use_git_histogram {
11613 DiffAlgorithm::Histogram
11614 } else {
11615 match algorithm {
11616 similar::Algorithm::Myers => DiffAlgorithm::Myers,
11617 similar::Algorithm::Lcs => DiffAlgorithm::Minimal,
11618 similar::Algorithm::Patience => DiffAlgorithm::Patience,
11619 _ => DiffAlgorithm::Myers,
11620 }
11621 };
11622 count_line_changes_with_options(
11623 old_content,
11624 new_content,
11625 &DiffOptions {
11626 algorithm: diff_algorithm,
11627 ..DiffOptions::default()
11628 },
11629 )
11630}
11631
11632#[must_use]
11634pub fn count_changes_git_histogram(old_content: &str, new_content: &str) -> (usize, usize) {
11635 count_changes_with_algorithm(old_content, new_content, similar::Algorithm::Myers, true)
11636}
11637
11638#[must_use]
11643pub fn count_git_lines(data: &[u8]) -> usize {
11644 if data.is_empty() {
11645 return 0;
11646 }
11647 let mut count = 0usize;
11648 let mut nl_just_seen = false;
11649 for &ch in data {
11650 if ch == b'\n' {
11651 count += 1;
11652 nl_just_seen = true;
11653 } else {
11654 nl_just_seen = false;
11655 }
11656 }
11657 if !nl_just_seen {
11658 count += 1;
11659 }
11660 count
11661}
11662
11663pub const GIT_DIFF_MAX_SCORE: u64 = 60_000;
11665const DIFF_MAX_SCORE: u64 = GIT_DIFF_MAX_SCORE;
11666const DIFF_MINIMUM_BREAK_SIZE: usize = 400;
11667const DIFF_DEFAULT_BREAK_SCORE: u64 = 30_000;
11668pub const GIT_DIFF_DEFAULT_BREAK_SCORE: u64 = DIFF_DEFAULT_BREAK_SCORE;
11670const DIFF_HASHBASE: u32 = 107_927;
11671
11672#[derive(Clone, Copy, Default)]
11673struct SpanSlot {
11674 hashval: u32,
11675 cnt: u32,
11676}
11677
11678struct SpanHashTop {
11679 alloc_log2: u8,
11680 free_slots: i32,
11681 data: Vec<SpanSlot>,
11682}
11683
11684impl SpanHashTop {
11685 fn new(initial_log2: u8) -> Self {
11686 let cap = 1usize << initial_log2;
11687 Self {
11688 alloc_log2: initial_log2,
11689 free_slots: initial_free(initial_log2),
11690 data: vec![SpanSlot::default(); cap],
11691 }
11692 }
11693
11694 fn len(&self) -> usize {
11695 1usize << self.alloc_log2
11696 }
11697
11698 fn add_span(&mut self, hashval: u32, cnt: u32) {
11699 loop {
11700 let lim = self.len();
11701 let mut bucket = (hashval as usize) & (lim - 1);
11702 loop {
11703 let h = &mut self.data[bucket];
11704 if h.cnt == 0 {
11705 h.hashval = hashval;
11706 h.cnt = cnt;
11707 self.free_slots -= 1;
11708 if self.free_slots < 0 {
11709 self.rehash();
11710 break;
11711 }
11712 return;
11713 }
11714 if h.hashval == hashval {
11715 h.cnt = h.cnt.saturating_add(cnt);
11716 return;
11717 }
11718 bucket += 1;
11719 if bucket >= lim {
11720 bucket = 0;
11721 }
11722 }
11723 }
11724 }
11725
11726 fn rehash(&mut self) {
11727 let old = std::mem::take(&mut self.data);
11728 let old_log = self.alloc_log2;
11729 self.alloc_log2 = old_log.saturating_add(1);
11730 let new_len = 1usize << self.alloc_log2;
11731 self.free_slots = initial_free(self.alloc_log2);
11732 self.data = vec![SpanSlot::default(); new_len];
11733 let old_sz = 1usize << old_log;
11734 for o in old.iter().take(old_sz) {
11735 let o = *o;
11736 if o.cnt == 0 {
11737 continue;
11738 }
11739 self.add_span_after_rehash(o.hashval, o.cnt);
11740 }
11741 }
11742
11743 fn add_span_after_rehash(&mut self, hashval: u32, cnt: u32) {
11744 loop {
11745 let lim = self.len();
11746 let mut bucket = (hashval as usize) & (lim - 1);
11747 loop {
11748 let h = &mut self.data[bucket];
11749 if h.cnt == 0 {
11750 h.hashval = hashval;
11751 h.cnt = cnt;
11752 self.free_slots -= 1;
11753 if self.free_slots < 0 {
11754 self.rehash();
11755 break;
11756 }
11757 return;
11758 }
11759 if h.hashval == hashval {
11760 h.cnt = h.cnt.saturating_add(cnt);
11761 return;
11762 }
11763 bucket += 1;
11764 if bucket >= lim {
11765 bucket = 0;
11766 }
11767 }
11768 }
11769 }
11770
11771 fn sort_by_hashval(&mut self) {
11772 let sz = self.len();
11773 self.data[..sz].sort_by(|a, b| {
11774 if a.cnt == 0 {
11775 return std::cmp::Ordering::Greater;
11776 }
11777 if b.cnt == 0 {
11778 return std::cmp::Ordering::Less;
11779 }
11780 a.hashval.cmp(&b.hashval)
11781 });
11782 }
11783}
11784
11785fn initial_free(sz_log2: u8) -> i32 {
11786 let sz = sz_log2 as i32;
11787 ((1i32 << sz_log2) * (sz - 3) / sz).max(0)
11788}
11789
11790fn hash_blob_spans(buf: &[u8], is_text: bool) -> SpanHashTop {
11791 let mut hash = SpanHashTop::new(9);
11792 let mut n = 0u32;
11793 let mut accum1: u32 = 0;
11794 let mut accum2: u32 = 0;
11795 let mut i = 0usize;
11796 while i < buf.len() {
11797 let c = buf[i] as u32;
11798 let old_1 = accum1;
11799 i += 1;
11800
11801 if is_text && c == b'\r' as u32 && i < buf.len() && buf[i] == b'\n' {
11802 continue;
11803 }
11804
11805 accum1 = accum1.wrapping_shl(7) ^ accum2.wrapping_shr(25);
11806 accum2 = accum2.wrapping_shl(7) ^ old_1.wrapping_shr(25);
11807 accum1 = accum1.wrapping_add(c);
11808 n += 1;
11809 if n < 64 && c != b'\n' as u32 {
11810 continue;
11811 }
11812 let hashval = (accum1.wrapping_add(accum2.wrapping_mul(0x61))) % DIFF_HASHBASE;
11813 hash.add_span(hashval, n);
11814 n = 0;
11815 accum1 = 0;
11816 accum2 = 0;
11817 }
11818 if n > 0 {
11819 let hashval = (accum1.wrapping_add(accum2.wrapping_mul(0x61))) % DIFF_HASHBASE;
11820 hash.add_span(hashval, n);
11821 }
11822 hash.sort_by_hashval();
11823 hash
11824}
11825
11826#[must_use]
11831pub fn diffcore_count_changes(old: &[u8], new: &[u8]) -> (u64, u64) {
11832 crate::trace::trace_span!("grit.diff.blob");
11833 let src_is_text = !crate::merge_file::is_binary(old);
11834 let dst_is_text = !crate::merge_file::is_binary(new);
11835 let src_count = hash_blob_spans(old, src_is_text);
11836 let dst_count = hash_blob_spans(new, dst_is_text);
11837 let mut sc: u64 = 0;
11838 let mut la: u64 = 0;
11839 let mut si = 0usize;
11840 let mut di = 0usize;
11841 let src_len = src_count.len();
11842 let dst_len = dst_count.len();
11843 loop {
11844 if si >= src_len || src_count.data[si].cnt == 0 {
11845 break;
11846 }
11847 let s_hash = src_count.data[si].hashval;
11848 let s_cnt = u64::from(src_count.data[si].cnt);
11849 while di < dst_len && dst_count.data[di].cnt != 0 && dst_count.data[di].hashval < s_hash {
11850 la += u64::from(dst_count.data[di].cnt);
11851 di += 1;
11852 }
11853 let mut dst_cnt = 0u64;
11854 if di < dst_len && dst_count.data[di].cnt != 0 && dst_count.data[di].hashval == s_hash {
11855 dst_cnt = u64::from(dst_count.data[di].cnt);
11856 di += 1;
11857 }
11858 if s_cnt < dst_cnt {
11859 la += dst_cnt - s_cnt;
11860 sc += s_cnt;
11861 } else {
11862 sc += dst_cnt;
11863 }
11864 si += 1;
11865 }
11866 while di < dst_len && dst_count.data[di].cnt != 0 {
11867 la += u64::from(dst_count.data[di].cnt);
11868 di += 1;
11869 }
11870 (sc, la)
11871}
11872
11873#[must_use]
11877pub fn should_break_rewrite_pair(old: &[u8], new: &[u8], break_score: u64) -> bool {
11878 should_break_rewrite_inner(old, new, break_score)
11879}
11880
11881#[must_use]
11883pub fn apply_diff_filter(
11884 entries: Vec<DiffEntry>,
11885 filter: Option<super::filter::DiffFilter>,
11886) -> Vec<DiffEntry> {
11887 match filter {
11888 Some(f) => f.filter_entries(entries),
11889 None => entries,
11890 }
11891}
11892
11893#[must_use]
11895pub fn parse_diff_rename_threshold_percent(arg: &str) -> Option<u8> {
11896 let token = if arg.is_empty() { "50" } else { arg };
11897 let internal = parse_diff_rename_score_token(token)?;
11898 Some((internal.saturating_mul(100) / GIT_DIFF_MAX_SCORE).min(100) as u8)
11899}
11900
11901pub fn parse_diff_rename_score_token(arg: &str) -> Option<u64> {
11904 let mut num: u64 = 0;
11905 let mut scale: u64 = 1;
11906 let mut dot = false;
11907 let mut saw_digit = false;
11908 for ch in arg.chars() {
11909 if !dot && ch == '.' {
11910 scale = 1;
11911 dot = true;
11912 continue;
11913 }
11914 if ch == '%' {
11915 scale = if dot { scale.saturating_mul(100) } else { 100 };
11916 break;
11917 }
11918 if ch.is_ascii_digit() {
11919 saw_digit = true;
11920 if scale < 100_000 {
11921 scale = scale.saturating_mul(10);
11922 num = num.saturating_mul(10) + u64::from(ch as u8 - b'0');
11923 }
11924 } else {
11925 break;
11926 }
11927 }
11928 if !saw_digit {
11929 return None;
11930 }
11931 Some(if num >= scale {
11932 GIT_DIFF_MAX_SCORE
11933 } else {
11934 GIT_DIFF_MAX_SCORE * num / scale
11935 })
11936}
11937
11938#[must_use]
11941pub fn rewrite_merge_score(old: &[u8], new: &[u8]) -> Option<u64> {
11942 if old.is_empty() {
11943 return None;
11944 }
11945 let max_size = old.len().max(new.len());
11946 if max_size < DIFF_MINIMUM_BREAK_SIZE {
11947 return None;
11948 }
11949 let (src_copied, _) = diffcore_count_changes(old, new);
11950 let src_copied = src_copied.min(old.len() as u64);
11951 let src_removed = (old.len() as u64).saturating_sub(src_copied);
11952 Some(src_removed * DIFF_MAX_SCORE / old.len() as u64)
11953}
11954
11955#[must_use]
11957pub fn rewrite_dissimilarity_index_percent(old: &[u8], new: &[u8]) -> Option<u32> {
11958 let score = rewrite_merge_score(old, new)?;
11959 Some((score * 100 / DIFF_MAX_SCORE).min(100) as u32)
11960}
11961
11962fn should_break_rewrite_inner(src: &[u8], dst: &[u8], break_score: u64) -> bool {
11963 if src.is_empty() {
11964 return false;
11965 }
11966 let max_size = src.len().max(dst.len());
11967 if max_size < DIFF_MINIMUM_BREAK_SIZE {
11968 return false;
11969 }
11970 let (src_copied, literal_added) = diffcore_count_changes(src, dst);
11971 let src_copied = src_copied.min(src.len() as u64);
11972 let mut literal_added = literal_added;
11973 let dst_len = dst.len() as u64;
11974 if src_copied < dst_len && literal_added + src_copied > dst_len {
11975 literal_added = dst_len.saturating_sub(src_copied);
11976 }
11977 let src_removed = (src.len() as u64).saturating_sub(src_copied);
11978 let merge_score = src_removed * DIFF_MAX_SCORE / src.len() as u64;
11979 if merge_score > break_score {
11980 return true;
11981 }
11982 let delta_size = src_removed.saturating_add(literal_added);
11983 if delta_size * DIFF_MAX_SCORE / (max_size as u64) < break_score {
11984 return false;
11985 }
11986 let s = src.len() as u64;
11987 if (s * break_score < src_removed * DIFF_MAX_SCORE)
11988 && (literal_added * 20 < src_removed)
11989 && (literal_added * 20 < src_copied)
11990 {
11991 return false;
11992 }
11993 true
11994}
11995
11996struct FlatEntry {
12000 path: String,
12001 mode: u32,
12002 oid: ObjectId,
12003}
12004
12005fn flatten_tree(odb: &Odb, tree_oid: &ObjectId, prefix: &str) -> Result<Vec<FlatEntry>> {
12006 flatten_tree_with_index(odb, tree_oid, prefix, None)
12007}
12008
12009fn sparse_index_skips_tree_descent(index: &Index, dir_path: &str, tree_oid: &ObjectId) -> bool {
12010 let mut storage = crate::repo_relpath::storage_bytes(dir_path).into_owned();
12011 if !storage.ends_with(b"/") {
12012 storage.push(b'/');
12013 }
12014 index.find_entry_pos(&storage, 0).is_some_and(|pos| {
12015 let entry = &index.entries[pos];
12016 entry.is_sparse_directory_placeholder() && entry.oid == *tree_oid
12017 })
12018}
12019
12020fn subtree_oid_at_path(odb: &Odb, root: &ObjectId, path: &str) -> Result<Option<ObjectId>> {
12022 let mut oid = *root;
12023 for component in path.split('/').filter(|c| !c.is_empty()) {
12024 let entries = read_tree(odb, &oid)?;
12025 let Some(entry) = entries
12026 .iter()
12027 .find(|e| e.name.as_slice() == component.as_bytes())
12028 else {
12029 return Ok(None);
12030 };
12031 if !is_tree_mode(entry.mode) {
12032 return Ok(None);
12033 }
12034 oid = entry.oid;
12035 }
12036 Ok(Some(oid))
12037}
12038
12039fn emit_flattened_subtree_as_added(
12041 odb: &Odb,
12042 subtree: &ObjectId,
12043 prefix: &str,
12044 ignore_submodules: bool,
12045 result: &mut Vec<DiffEntry>,
12046) -> Result<()> {
12047 for entry in flatten_tree(odb, subtree, prefix)? {
12048 if ignore_submodules && entry.mode == 0o160000 {
12049 continue;
12050 }
12051 result.push(DiffEntry {
12052 status: DiffStatus::Added,
12053 old_path: None,
12054 new_path: Some(entry.path),
12055 old_mode: FileMode::ABSENT,
12056 new_mode: FileMode::new(entry.mode),
12057 old_oid: zero_oid(),
12058 new_oid: entry.oid,
12059 score: None,
12060 });
12061 }
12062 Ok(())
12063}
12064
12065fn diff_flattened_subtrees(
12067 head: &[FlatEntry],
12068 index: &[FlatEntry],
12069 ignore_submodules: bool,
12070 result: &mut Vec<DiffEntry>,
12071 stage0_paths: &mut std::collections::BTreeSet<String>,
12072 tree_map: &mut std::collections::BTreeMap<&str, &FlatEntry>,
12073) {
12074 let mut head_map: std::collections::BTreeMap<&str, &FlatEntry> = head
12075 .iter()
12076 .map(|entry| (entry.path.as_str(), entry))
12077 .collect();
12078 for entry in index {
12079 stage0_paths.insert(entry.path.clone());
12080 tree_map.remove(entry.path.as_str());
12081 match head_map.remove(entry.path.as_str()) {
12082 Some(te) => {
12083 if te.oid != entry.oid || te.mode != entry.mode {
12084 result.push(DiffEntry {
12085 status: DiffStatus::for_content_change(te.mode, entry.mode),
12086 old_path: Some(entry.path.clone()),
12087 new_path: Some(entry.path.clone()),
12088 old_mode: FileMode::new(te.mode),
12089 new_mode: FileMode::new(entry.mode),
12090 old_oid: te.oid,
12091 new_oid: entry.oid,
12092 score: None,
12093 });
12094 }
12095 }
12096 None => {
12097 result.push(DiffEntry {
12098 status: DiffStatus::Added,
12099 old_path: None,
12100 new_path: Some(entry.path.clone()),
12101 old_mode: FileMode::ABSENT,
12102 new_mode: FileMode::new(entry.mode),
12103 old_oid: zero_oid(),
12104 new_oid: entry.oid,
12105 score: None,
12106 });
12107 }
12108 }
12109 }
12110 for (path, te) in head_map {
12111 if ignore_submodules && te.mode == 0o160000 {
12112 continue;
12113 }
12114 tree_map.remove(path);
12115 result.push(DiffEntry {
12116 status: DiffStatus::Deleted,
12117 old_path: Some(path.to_owned()),
12118 new_path: None,
12119 old_mode: FileMode::new(te.mode),
12120 new_mode: FileMode::ABSENT,
12121 old_oid: te.oid,
12122 new_oid: zero_oid(),
12123 score: None,
12124 });
12125 }
12126}
12127
12128fn flatten_tree_with_index(
12129 odb: &Odb,
12130 tree_oid: &ObjectId,
12131 prefix: &str,
12132 index: Option<&Index>,
12133) -> Result<Vec<FlatEntry>> {
12134 let entries = read_tree(odb, tree_oid)?;
12135 let mut result = Vec::new();
12136
12137 for entry in entries {
12138 let name_str = String::from_utf8_lossy(&entry.name);
12139 let path = format_path(prefix, &name_str);
12140 if is_tree_mode(entry.mode) {
12141 if index.is_some_and(|idx| sparse_index_skips_tree_descent(idx, &path, &entry.oid)) {
12142 continue;
12143 }
12144 let nested = flatten_tree_with_index(odb, &entry.oid, &path, index)?;
12145 result.extend(nested);
12146 } else {
12147 result.push(FlatEntry {
12148 path,
12149 mode: entry.mode,
12150 oid: entry.oid,
12151 });
12152 }
12153 }
12154
12155 Ok(result)
12156}
12157
12158pub fn head_path_states(
12160 odb: &Odb,
12161 head_tree: Option<&ObjectId>,
12162) -> Result<std::collections::BTreeMap<String, (u32, ObjectId)>> {
12163 let mut m = std::collections::BTreeMap::new();
12164 let Some(t) = head_tree else {
12165 return Ok(m);
12166 };
12167 for fe in flatten_tree(odb, t, "")? {
12168 m.insert(fe.path, (fe.mode, fe.oid));
12169 }
12170 Ok(m)
12171}
12172
12173fn is_tree_mode(mode: u32) -> bool {
12175 crate::index::is_tree_mode(mode)
12176}
12177
12178fn path_order(a: &DiffEntry, b: &DiffEntry) -> std::cmp::Ordering {
12181 use crate::repo_relpath::storage_bytes;
12182 storage_bytes(a.path()).cmp(&storage_bytes(b.path()))
12183}
12184
12185fn entries_sorted_by_path(entries: &[DiffEntry]) -> bool {
12186 entries.is_sorted_by(|a, b| path_order(a, b).is_le())
12187}
12188
12189fn entries_strictly_sorted_by_path(entries: &[DiffEntry]) -> bool {
12190 entries.is_sorted_by(|a, b| path_order(a, b).is_lt())
12191}
12192
12193pub fn sort_entries_by_path(entries: &mut [DiffEntry]) {
12204 if entries_sorted_by_path(entries) {
12205 return;
12206 }
12207 entries.sort_by(path_order);
12208}
12209
12210fn format_path(prefix: &str, name: &str) -> String {
12212 if prefix.is_empty() {
12213 name.to_owned()
12214 } else {
12215 let mut path = String::with_capacity(prefix.len() + 1 + name.len());
12216 path.push_str(prefix);
12217 path.push('/');
12218 path.push_str(name);
12219 path
12220 }
12221}
12222
12223pub fn format_mode(mode: u32) -> String {
12225 format!("{mode:06o}")
12226}
12227
12228#[must_use]
12232pub fn read_submodule_head_for_checkout(sub_dir: &Path) -> Option<ObjectId> {
12233 read_submodule_head(sub_dir)
12234}
12235
12236#[must_use]
12241pub fn submodule_commit_subject_line(c: &CommitData) -> String {
12242 let enc = c.encoding.as_deref().unwrap_or("UTF-8");
12243 let is_latin1 = enc.eq_ignore_ascii_case("ISO8859-1")
12244 || enc.eq_ignore_ascii_case("ISO-8859-1")
12245 || enc.eq_ignore_ascii_case("LATIN1")
12246 || enc.eq_ignore_ascii_case("ISO-8859-15");
12247 if let Some(raw) = c.raw_message.as_deref() {
12248 let line = raw.split(|b| *b == b'\n').next().unwrap_or(raw);
12249 if is_latin1 {
12250 return line
12251 .iter()
12252 .map(|&b| b as char)
12253 .collect::<String>()
12254 .trim()
12255 .to_owned();
12256 }
12257 return String::from_utf8_lossy(line).trim().to_string();
12258 }
12259 c.message.lines().next().unwrap_or("").trim().to_owned()
12260}
12261
12262fn uninitialized_gitlink_worktree_matches_index(sub_dir: &Path) -> bool {
12266 submodule_embedded_git_dir(sub_dir).is_none()
12267}
12268
12269fn read_submodule_head(sub_dir: &Path) -> Option<ObjectId> {
12270 read_submodule_head_oid(sub_dir)
12271}
12272
12273#[must_use]
12275pub fn submodule_embedded_git_dir(sub_dir: &Path) -> Option<PathBuf> {
12276 let gitfile = sub_dir.join(".git");
12277 if gitfile.is_file() {
12278 let content = fs::read_to_string(&gitfile).ok()?;
12279 let gitdir = content
12280 .lines()
12281 .find_map(|l| l.strip_prefix("gitdir: "))?
12282 .trim();
12283 Some(if Path::new(gitdir).is_absolute() {
12284 PathBuf::from(gitdir)
12285 } else {
12286 sub_dir.join(gitdir)
12287 })
12288 } else if gitfile.is_dir() {
12289 Some(gitfile)
12290 } else {
12291 None
12292 }
12293}
12294
12295#[must_use]
12301pub(crate) fn is_unregistered_embedded_git_repo(index: &Index, rel: &str, abs: &Path) -> bool {
12302 if rel.is_empty() {
12303 return false;
12304 }
12305 if index
12306 .entries
12307 .iter()
12308 .any(|e| e.path.as_slice() == rel.as_bytes() && e.mode == crate::index::MODE_GITLINK)
12309 {
12310 return false;
12311 }
12312 let Some(git_dir) = submodule_embedded_git_dir(abs) else {
12313 return false;
12314 };
12315 crate::submodule_gitdir::is_valid_git_dir(&git_dir)
12316}
12317
12318pub(crate) enum EmbeddedGitRepoStage {
12320 NotEmbedded,
12322 Gitlink(crate::objects::ObjectId),
12324}
12325
12326pub(crate) fn classify_embedded_git_repo_for_stage(
12335 _index: &Index,
12336 rel: &str,
12337 abs: &Path,
12338) -> crate::error::Result<EmbeddedGitRepoStage> {
12339 if rel.is_empty() {
12340 return Ok(EmbeddedGitRepoStage::NotEmbedded);
12341 }
12342 let Some(git_dir) = submodule_embedded_git_dir(abs) else {
12343 return Ok(EmbeddedGitRepoStage::NotEmbedded);
12344 };
12345 if !crate::submodule_gitdir::is_valid_git_dir(&git_dir) {
12346 return Ok(EmbeddedGitRepoStage::NotEmbedded);
12347 }
12348 let path = rel.trim_end_matches('/').to_owned();
12349 match read_submodule_head_oid_for_stage(abs) {
12350 Some(oid) => Ok(EmbeddedGitRepoStage::Gitlink(oid)),
12351 None => Err(crate::error::Error::EmbeddedRepoWithoutCommit { path }),
12352 }
12353}
12354
12355fn find_superproject_git(sub_dir: &Path) -> Option<(PathBuf, PathBuf)> {
12357 let mut cur = sub_dir.parent()?;
12358 loop {
12359 let git_path = cur.join(".git");
12360 if git_path.exists() {
12361 let gd = if git_path.is_file() {
12362 let content = fs::read_to_string(&git_path).ok()?;
12363 let line = content
12364 .lines()
12365 .find_map(|l| l.strip_prefix("gitdir: "))?
12366 .trim();
12367 if Path::new(line).is_absolute() {
12368 PathBuf::from(line)
12369 } else {
12370 cur.join(line)
12371 }
12372 } else {
12373 git_path
12374 };
12375 return Some((cur.to_path_buf(), gd));
12376 }
12377 cur = cur.parent()?;
12378 }
12379}
12380
12381fn submodule_git_dir_for_head_read(sub_dir: &Path) -> Option<PathBuf> {
12382 let mut git_dir = submodule_embedded_git_dir(sub_dir)?;
12383 if let Some((super_wt, super_git_dir)) = find_superproject_git(sub_dir) {
12384 let rel = sub_dir.strip_prefix(&super_wt).ok()?;
12385 let rel_str = rel.to_string_lossy().replace('\\', "/");
12386 let local_mod = super_git_dir
12387 .join("modules")
12388 .join(rel_str.trim_start_matches('/'));
12389 if local_mod.join("HEAD").exists() {
12390 let sg = super_git_dir
12391 .canonicalize_simplified()
12392 .unwrap_or(super_git_dir);
12393 let cur = git_dir
12394 .canonicalize_simplified()
12395 .unwrap_or_else(|_| git_dir.clone());
12396 if !cur.starts_with(&sg) {
12397 git_dir = local_mod;
12398 }
12399 }
12400 }
12401 Some(git_dir)
12402}
12403
12404fn read_submodule_head_oid_from_git_dir(
12405 git_dir: &Path,
12406 allow_branch_fallback: bool,
12407) -> Option<ObjectId> {
12408 let head_content = fs::read_to_string(git_dir.join("HEAD")).ok()?;
12409 let head_trimmed = head_content.trim();
12410 if head_trimmed.starts_with("ref: ") {
12411 match crate::refs::resolve_ref(git_dir, "HEAD") {
12412 Ok(oid) => Some(oid),
12413 Err(_) if allow_branch_fallback => {
12414 for branch in ["main", "master"] {
12415 let p = git_dir.join("refs/heads").join(branch);
12416 if let Ok(s) = fs::read_to_string(&p) {
12417 if let Ok(o) = ObjectId::from_hex(s.trim()) {
12418 return Some(o);
12419 }
12420 }
12421 }
12422 None
12423 }
12424 Err(_) => None,
12425 }
12426 } else {
12427 ObjectId::from_hex(head_trimmed).ok()
12428 }
12429}
12430
12431pub fn read_submodule_head_oid(sub_dir: &Path) -> Option<ObjectId> {
12437 let git_dir = submodule_git_dir_for_head_read(sub_dir)?;
12438 read_submodule_head_oid_from_git_dir(&git_dir, true)
12439}
12440
12441pub(crate) fn read_submodule_head_oid_for_stage(sub_dir: &Path) -> Option<ObjectId> {
12444 let git_dir = submodule_git_dir_for_head_read(sub_dir)?;
12445 read_submodule_head_oid_from_git_dir(&git_dir, false)
12446}
12447
12448#[must_use]
12460pub fn submodule_head_object_broken(sub_dir: &Path) -> bool {
12461 let Some(sub_git_dir) = submodule_embedded_git_dir(sub_dir) else {
12462 return false;
12463 };
12464 let Some(head_oid) = read_submodule_head_oid(sub_dir) else {
12465 return false;
12466 };
12467 let odb = Odb::new(&sub_git_dir.join("objects"));
12468 match odb.read(&head_oid) {
12469 Ok(obj) => parse_commit(&obj.data).is_err(),
12472 Err(_) => true,
12473 }
12474}
12475
12476#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
12478pub struct SubmodulePorcelainFlags {
12479 pub new_commits: bool,
12481 pub modified: bool,
12483 pub untracked: bool,
12485}
12486
12487pub fn submodule_porcelain_flags(
12493 super_worktree: &Path,
12494 rel_path: &str,
12495 recorded_oid: ObjectId,
12496) -> SubmodulePorcelainFlags {
12497 let sub_dir = super_worktree.join(rel_path);
12498 let Some(sub_git_dir) = submodule_embedded_git_dir(&sub_dir) else {
12499 return SubmodulePorcelainFlags::default();
12500 };
12501 let Some(sub_head) = read_submodule_head_oid(&sub_dir) else {
12502 return SubmodulePorcelainFlags::default();
12503 };
12504
12505 let new_commits = sub_head != recorded_oid;
12506
12507 let index_path = sub_git_dir.join("index");
12508 let mut sub_index = match crate::index::Index::load(&index_path) {
12509 Ok(ix) => ix,
12510 Err(_) => {
12511 return SubmodulePorcelainFlags {
12512 new_commits,
12513 ..Default::default()
12514 }
12515 }
12516 };
12517
12518 let tracked: std::collections::BTreeSet<String> = sub_index
12519 .entries
12520 .iter()
12521 .filter(|e| e.stage() == 0)
12522 .map(|e| String::from_utf8_lossy(&e.path).into_owned())
12523 .collect();
12524 let untracked = submodule_dir_has_untracked_inner(&sub_dir, &sub_dir, &tracked, &sub_index);
12525
12526 let objects_dir = sub_git_dir.join("objects");
12527 let odb = Odb::new(&objects_dir);
12528
12529 let sub_head_tree = (|| -> Option<ObjectId> {
12530 let h = fs::read_to_string(sub_git_dir.join("HEAD")).ok()?;
12531 let h_str = h.trim();
12532 let commit_oid = if let Some(r) = h_str.strip_prefix("ref: ") {
12533 let oid_hex = fs::read_to_string(sub_git_dir.join(r)).ok()?;
12534 ObjectId::from_hex(oid_hex.trim()).ok()?
12535 } else {
12536 ObjectId::from_hex(h_str).ok()?
12537 };
12538 let obj = odb.read(&commit_oid).ok()?;
12539 let commit = parse_commit(&obj.data).ok()?;
12540 Some(commit.tree)
12541 })();
12542
12543 let staged_dirty = sub_head_tree
12544 .as_ref()
12545 .map(|t| diff_index_to_tree(&odb, &sub_index, Some(t), false).map(|v| !v.is_empty()))
12546 .unwrap_or(Ok(false));
12547 let staged_dirty = staged_dirty.unwrap_or(false);
12548
12549 let unstaged_dirty = diff_index_to_worktree(&odb, &mut sub_index, &sub_dir, false, true)
12550 .map(|v| !v.is_empty())
12551 .unwrap_or(false);
12552
12553 let mut modified = staged_dirty || unstaged_dirty;
12554
12555 for e in &sub_index.entries {
12560 if e.stage() != 0 || e.mode != 0o160000 {
12561 continue;
12562 }
12563 let child = String::from_utf8_lossy(&e.path).into_owned();
12564 let full_rel = if rel_path.is_empty() {
12565 child
12566 } else {
12567 format!("{rel_path}/{child}")
12568 };
12569 let nested = submodule_porcelain_flags(super_worktree, &full_rel, e.oid);
12570 modified |= nested.modified;
12571 }
12572
12573 SubmodulePorcelainFlags {
12574 new_commits,
12575 modified,
12576 untracked,
12577 }
12578}
12579
12580fn submodule_dir_has_untracked_inner(
12581 dir: &Path,
12582 root: &Path,
12583 tracked: &std::collections::BTreeSet<String>,
12584 owning_index: &Index,
12585) -> bool {
12586 let entries = match fs::read_dir(dir) {
12587 Ok(e) => e,
12588 Err(_) => return false,
12589 };
12590 let mut sorted: Vec<_> = entries.filter_map(|e| e.ok()).collect();
12591 sorted.sort_by_key(|e| e.file_name());
12592
12593 for entry in sorted {
12594 let name = entry.file_name().to_string_lossy().to_string();
12595 if name == ".git" {
12596 continue;
12597 }
12598 let path = entry.path();
12599 let rel = path
12600 .strip_prefix(root)
12601 .map(|p| p.to_string_lossy().to_string())
12602 .unwrap_or_else(|_| name.clone());
12603
12604 let is_dir = entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false);
12605 if is_dir {
12606 let is_gitlink = owning_index
12607 .get(rel.as_bytes(), 0)
12608 .is_some_and(|e| e.mode == 0o160000);
12609 if is_gitlink {
12610 let Some(nested_git) = submodule_embedded_git_dir(&path) else {
12611 continue;
12612 };
12613 let nested_index_path = nested_git.join("index");
12614 let Ok(nested_ix) = crate::index::Index::load(&nested_index_path) else {
12615 continue;
12616 };
12617 let nested_tracked: std::collections::BTreeSet<String> = nested_ix
12618 .entries
12619 .iter()
12620 .filter(|e| e.stage() == 0)
12621 .map(|e| String::from_utf8_lossy(&e.path).into_owned())
12622 .collect();
12623 if submodule_dir_has_untracked_inner(&path, &path, &nested_tracked, &nested_ix) {
12624 return true;
12625 }
12626 } else if submodule_dir_has_untracked_inner(&path, root, tracked, owning_index) {
12627 return true;
12628 }
12629 } else if !tracked.contains(&rel) {
12630 return true;
12631 }
12632 }
12633 false
12634}
12635
12636#[cfg(test)]
12637mod line_compare_key_tests {
12638 use super::*;
12639 use crate::diff::unified_emit::line_diff_token;
12640 use crate::diff::WhitespaceMode;
12641
12642 #[test]
12643 fn borrowed_keys_match_owned_line_tokens() {
12644 let texts = [
12645 "",
12646 "\n",
12647 "a",
12648 "a\n",
12649 "a\nb",
12650 "a\r\nb\r\n",
12651 "a\r\nb\r",
12652 "\n\n\nlast",
12653 "x\n\r\n\ry\n",
12654 ];
12655 for text in texts {
12656 let side = LineSide::from_text(text);
12657 let mut normalized = Vec::new();
12658 let keys = line_compare_keys(text, &side, WhitespaceMode::default(), &mut normalized);
12659 let expected: Vec<String> = side.lines.iter().map(line_diff_token).collect();
12660 assert_eq!(keys, expected, "text {text:?}");
12661 let source = text.as_bytes().as_ptr_range();
12662 assert!(
12663 keys.iter()
12664 .all(|key| source.contains(&key.as_ptr()) || key.is_empty()),
12665 "text {text:?}"
12666 );
12667 }
12668 }
12669
12670 #[test]
12671 fn whitespace_modes_compare_normalized_bodies() {
12672 let text = "a b\nab \n";
12673 let side = LineSide::from_text(text);
12674 let ws = WhitespaceMode {
12675 ignore_all_space: true,
12676 ..WhitespaceMode::default()
12677 };
12678 let mut normalized = Vec::new();
12679 let keys = line_compare_keys(text, &side, ws, &mut normalized);
12680 assert_eq!(keys, vec!["ab", "ab"]);
12681 }
12682
12683 #[test]
12684 fn xdiff_marks_only_edited_lines_in_large_file() {
12685 let old: String = (0..5_000).map(|i| format!("line{i}\n")).collect();
12686 let new: String = (0..5_000)
12687 .map(|i| {
12688 if i % 100 == 0 {
12689 format!("LINE{i}\n")
12690 } else {
12691 format!("line{i}\n")
12692 }
12693 })
12694 .collect();
12695 let body =
12696 git_xdiff_unified_body_raw(&old, &new, 3, 0, true, false, WhitespaceMode::default());
12697 assert_eq!(body.matches("\n@@ ").count() + 1, 50);
12698 assert_eq!(body.lines().filter(|l| l.starts_with('-')).count(), 50);
12699 assert_eq!(body.lines().filter(|l| l.starts_with('+')).count(), 50);
12700 assert!(body.starts_with("@@ -1,4 +1,4 @@\n-line0\n+LINE0\n line1\n"));
12701 }
12702}
12703
12704#[cfg(test)]
12705mod entry_order_tests {
12706 use super::*;
12707
12708 fn entry(status: DiffStatus, path: &str, score: Option<u32>) -> DiffEntry {
12709 let mode = |present: bool| {
12710 if present {
12711 FileMode::REGULAR
12712 } else {
12713 FileMode::ABSENT
12714 }
12715 };
12716 DiffEntry {
12717 status,
12718 old_path: (status != DiffStatus::Added).then(|| path.to_owned()),
12719 new_path: (status != DiffStatus::Deleted).then(|| path.to_owned()),
12720 old_mode: mode(status != DiffStatus::Added),
12721 new_mode: mode(status != DiffStatus::Deleted),
12722 old_oid: ObjectId::zero(),
12723 new_oid: ObjectId::zero(),
12724 score,
12725 }
12726 }
12727
12728 #[test]
12729 fn sort_entries_by_path_orders_by_path_and_keeps_equal_paths_in_place() {
12730 let mut entries = vec![
12731 entry(DiffStatus::Modified, "b", None),
12732 entry(DiffStatus::Deleted, "a/x", Some(1)),
12733 entry(DiffStatus::Added, "a/x", Some(2)),
12734 entry(DiffStatus::Added, "a", None),
12735 ];
12736 sort_entries_by_path(&mut entries);
12737 let order: Vec<_> = entries.iter().map(|e| (e.path(), e.score)).collect();
12738 assert_eq!(
12739 order,
12740 [("a", None), ("a/x", Some(1)), ("a/x", Some(2)), ("b", None)]
12741 );
12742 let sorted = entries.clone();
12743 sort_entries_by_path(&mut entries);
12744 assert_eq!(entries, sorted);
12745 sort_entries_by_path(&mut []);
12746 }
12747
12748 #[test]
12749 fn sort_entries_by_path_compares_non_utf8_paths_by_raw_bytes() {
12750 let raw = crate::repo_relpath::format_path_stored("", b"raw-\xff");
12751 assert!(raw.starts_with("grit+pathhex:"));
12752 let mut entries = vec![
12753 entry(DiffStatus::Modified, &raw, None),
12754 entry(DiffStatus::Modified, "node/leaf", None),
12755 entry(DiffStatus::Modified, "zeta", None),
12756 ];
12757 sort_entries_by_path(&mut entries);
12758 let paths: Vec<_> = entries.iter().map(DiffEntry::path).collect();
12759 assert_eq!(paths, ["node/leaf", raw.as_str(), "zeta"]);
12760 }
12761
12762 #[test]
12763 fn rename_pass_returns_unpairable_lists_in_path_order() {
12764 let config = ConfigSet::new();
12765 let policy = ThreadPolicy::default();
12766 let odb = Odb::new(Path::new("/nonexistent-objects"));
12767 let run = |entries: Vec<DiffEntry>| {
12768 detect_renames_with_policy(&odb, None, entries, 50, &config, &policy, None)
12769 .expect("no blob is read without both adds and deletes")
12770 };
12771 let sorted = vec![
12772 entry(DiffStatus::Deleted, "a", None),
12773 entry(DiffStatus::Modified, "b", None),
12774 entry(DiffStatus::Deleted, "c", None),
12775 ];
12776 assert_eq!(run(sorted.clone()), sorted);
12777 let unsorted = vec![
12778 entry(DiffStatus::Added, "z", None),
12779 entry(DiffStatus::Modified, "m", None),
12780 entry(DiffStatus::Added, "a", None),
12781 ];
12782 let paths: Vec<_> = run(unsorted).iter().map(|e| e.path().to_owned()).collect();
12783 assert_eq!(paths, ["a", "m", "z"]);
12784 let duplicate = vec![
12786 entry(DiffStatus::Added, "p", None),
12787 entry(DiffStatus::Modified, "p", None),
12788 ];
12789 let statuses: Vec<_> = run(duplicate).iter().map(|e| e.status).collect();
12790 assert_eq!(statuses, [DiffStatus::Modified, DiffStatus::Added]);
12791 }
12792}
12793
12794#[cfg(test)]
12795mod diff_index_to_tree_cache_tree_tests {
12796 use std::rc::Rc;
12797 use std::sync::atomic::{AtomicUsize, Ordering};
12798
12799 use super::*;
12800 use crate::index::{Index, IndexEntry, MODE_EXECUTABLE, MODE_GITLINK, MODE_REGULAR};
12801 use crate::objects::ObjectKind;
12802 use crate::write_tree::{build_cache_tree_from_index, write_tree_from_index};
12803 use tempfile::TempDir;
12804
12805 struct TreeReadCounter {
12806 counter: Rc<AtomicUsize>,
12807 }
12808
12809 impl TreeReadCounter {
12810 fn start() -> Self {
12811 let counter = Rc::new(AtomicUsize::new(0));
12812 set_diff_tree_read_hook_for_tests(Some(counter.clone()));
12813 Self { counter }
12814 }
12815
12816 fn get(&self) -> usize {
12817 self.counter.load(Ordering::Relaxed)
12818 }
12819 }
12820
12821 impl Drop for TreeReadCounter {
12822 fn drop(&mut self) {
12823 set_diff_tree_read_hook_for_tests(None);
12824 }
12825 }
12826
12827 fn diff_index_to_tree_flatten_reference(
12828 odb: &Odb,
12829 index: &Index,
12830 head_tree: &ObjectId,
12831 ignore_submodules: bool,
12832 ) -> Result<Vec<DiffEntry>> {
12833 let mut flat_index = index.clone();
12834 flat_index.clear_cache_tree();
12835 diff_index_to_tree(odb, &flat_index, Some(head_tree), ignore_submodules)
12836 }
12837
12838 fn index_entry(path: &str, mode: u32, oid: ObjectId) -> IndexEntry {
12839 IndexEntry {
12840 ctime_sec: 0,
12841 ctime_nsec: 0,
12842 mtime_sec: 0,
12843 mtime_nsec: 0,
12844 dev: 0,
12845 ino: 0,
12846 mode,
12847 uid: 0,
12848 gid: 0,
12849 size: 0,
12850 oid,
12851 flags: path.len().min(0xfff) as u16,
12852 flags_extended: None,
12853 path: path.as_bytes().to_vec(),
12854 base_index_pos: 0,
12855 }
12856 }
12857
12858 fn repo_with_tree(depth: usize, width: usize) -> (TempDir, Odb, ObjectId, Index) {
12859 let temp = TempDir::new().expect("tempdir");
12860 let odb = Odb::new(temp.path());
12861 let mut index = Index::new();
12862 for d in 0..depth {
12863 for w in 0..width {
12864 let path = format!("unchanged/d{d}/f{w}.txt");
12865 let oid = odb
12866 .write(ObjectKind::Blob, format!("body-{d}-{w}").as_bytes())
12867 .expect("blob");
12868 index.add_or_replace(index_entry(&path, MODE_REGULAR, oid));
12869 }
12870 }
12871 let head_tree = write_tree_from_index(&odb, &index, "").expect("write tree");
12872 let cache = build_cache_tree_from_index(&odb, &index).expect("cache-tree");
12873 index.set_cache_tree(cache);
12874 index.cache_tree_root = Some(head_tree);
12875 (temp, odb, head_tree, index)
12876 }
12877
12878 #[test]
12879 fn diff_index_to_tree_cache_tree_fast_path_matches_slow() {
12880 let (_temp, odb, head_tree, mut index) = repo_with_tree(4, 5);
12881 assert!(index.cache_tree.as_ref().is_some_and(|c| c.is_valid()));
12882
12883 let mut state: u64 = 0xDEAD_BEEF;
12884 for _ in 0..24 {
12885 state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
12886 let d = (state % 4) as usize;
12887 state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
12888 let w = (state % 5) as usize;
12889 let path = format!("unchanged/d{d}/f{w}.txt");
12890 index.invalidate_cache_tree_for_path(path.as_bytes());
12891 assert!(
12892 index.cache_tree_root.is_none(),
12893 "partial invalidation clears cache_tree_root like Index::add"
12894 );
12895 assert!(
12896 index.cache_tree.as_ref().is_some_and(|r| !r.is_valid()),
12897 "root cache node must be invalid after partial invalidation"
12898 );
12899 if let Some(e) = index.get_mut(path.as_bytes(), 0) {
12900 let new_oid = odb
12901 .write(ObjectKind::Blob, format!("new-{d}-{w}").as_bytes())
12902 .expect("blob");
12903 e.oid = new_oid;
12904 }
12905 let fast = diff_index_to_tree(&odb, &index, Some(&head_tree), false).expect("fast");
12906 let flat = diff_index_to_tree_flatten_reference(&odb, &index, &head_tree, false)
12907 .expect("flat");
12908 assert_eq!(fast, flat, "mismatch after mutating {path}");
12909 }
12910 }
12911
12912 #[test]
12913 fn diff_index_to_tree_skips_unchanged_subtrees() {
12914 let (_temp, odb, head_tree, index) = repo_with_tree(5, 6);
12915 let reads = TreeReadCounter::start();
12916 diff_index_to_tree(&odb, &index, Some(&head_tree), false).expect("cached diff");
12917 let cached_reads = reads.get();
12918
12919 let mut no_cache = index.clone();
12920 no_cache.clear_cache_tree();
12921 let reads = TreeReadCounter::start();
12922 diff_index_to_tree(&odb, &no_cache, Some(&head_tree), false).expect("flat diff");
12923 let flat_reads = reads.get();
12924
12925 assert!(
12926 cached_reads < flat_reads,
12927 "expected fewer tree reads with cache-tree (cached={cached_reads}, flat={flat_reads})"
12928 );
12929 assert_eq!(
12930 cached_reads, 1,
12931 "unchanged repo should only read the root tree"
12932 );
12933 }
12934
12935 #[test]
12936 fn diff_index_to_tree_partial_invalidation_skips_sibling_subtree() {
12937 let (_temp, odb, head_tree, mut index) = repo_with_tree(5, 6);
12938 let path = "unchanged/d1/f2.txt";
12939 index.invalidate_cache_tree_for_path(path.as_bytes());
12940 let new_oid = odb.write(ObjectKind::Blob, b"changed").expect("blob");
12941 index.get_mut(path.as_bytes(), 0).expect("entry").oid = new_oid;
12942
12943 let flat =
12944 diff_index_to_tree_flatten_reference(&odb, &index, &head_tree, false).expect("flat");
12945 let reads = TreeReadCounter::start();
12946 let fast = diff_index_to_tree(&odb, &index, Some(&head_tree), false).expect("fast");
12947 let cached_reads = reads.get();
12948 assert_eq!(fast, flat);
12949
12950 let reads = TreeReadCounter::start();
12951 diff_index_to_tree_flatten_reference(&odb, &index, &head_tree, false).expect("flat");
12952 let flat_reads = reads.get();
12953
12954 assert!(
12955 cached_reads < flat_reads,
12956 "partial cache-tree should skip sibling dirs (cached={cached_reads}, flat={flat_reads})"
12957 );
12958 }
12959
12960 fn cache_leaf(name: &str) -> CacheTreeNode {
12961 CacheTreeNode::valid(name.as_bytes().to_vec(), 1, ObjectId::zero(), Vec::new())
12962 }
12963
12964 #[test]
12965 fn cache_children_lookup_matches_linear_scan_on_wide_unsorted_nodes() {
12966 let mut children: Vec<CacheTreeNode> = (0..CACHE_CHILD_LINEAR_SCAN_MAX * 3)
12967 .rev()
12968 .map(|i| cache_leaf(&format!("d{i:03}")))
12969 .collect();
12970 let mut first_dup = cache_leaf("d007");
12971 first_dup.entry_count = 7;
12972 children.insert(3, first_dup);
12973 let root = CacheTreeNode::valid(Vec::new(), 0, ObjectId::zero(), children);
12974
12975 let lookup = CacheChildren::new(Some(&root));
12976 assert!(
12977 !lookup.sorted.is_empty(),
12978 "wide nodes must use the sorted lookup"
12979 );
12980 for name in ["d000", "d007", "d020", "d047", "zzz", "d", ""] {
12981 let linear = root.children.iter().find(|c| c.name == name.as_bytes());
12982 let fast = lookup.get(name.as_bytes());
12983 assert_eq!(
12984 fast.map(|c| (&c.name, c.entry_count)),
12985 linear.map(|c| (&c.name, c.entry_count)),
12986 "lookup of {name:?}"
12987 );
12988 }
12989 assert_eq!(lookup.get(b"d007").map(|c| c.entry_count), Some(7));
12990
12991 let narrow = CacheTreeNode::valid(
12992 Vec::new(),
12993 0,
12994 ObjectId::zero(),
12995 vec![cache_leaf("b"), cache_leaf("a")],
12996 );
12997 let lookup = CacheChildren::new(Some(&narrow));
12998 assert!(lookup.sorted.is_empty());
12999 assert_eq!(lookup.get(b"a").map(|c| c.name.as_slice()), Some(&b"a"[..]));
13000 assert!(lookup.get(b"c").is_none());
13001 assert!(CacheChildren::new(None).get(b"a").is_none());
13002 }
13003
13004 #[test]
13005 fn index_relative_in_dir_requires_a_component_boundary() {
13006 assert_eq!(index_relative_in_dir(b"a/b", b""), Some(&b"a/b"[..]));
13007 assert_eq!(index_relative_in_dir(b"a/b", b"a"), Some(&b"b"[..]));
13008 assert_eq!(index_relative_in_dir(b"a", b"a"), Some(&b""[..]));
13009 assert_eq!(index_relative_in_dir(b"ab/c", b"a"), None);
13010 assert_eq!(index_relative_in_dir(b"a.c", b"a"), None);
13011 assert_eq!(index_relative_in_dir(b"b", b"a"), None);
13012 assert_eq!(join_index_path(b"", b"x"), b"x".to_vec());
13013 assert_eq!(join_index_path(b"a/b", b"x"), b"a/b/x".to_vec());
13014 }
13015
13016 #[test]
13019 fn diff_index_to_tree_wide_unsorted_cache_tree_matches_flatten() {
13020 let temp = TempDir::new().expect("tempdir");
13021 let odb = Odb::new(temp.path());
13022 let mut index = Index::new();
13023 let width = CACHE_CHILD_LINEAR_SCAN_MAX * 4;
13024 for d in 0..width {
13025 for f in 0..2 {
13026 let path = format!("dir{d:03}/sub/f{f}.txt");
13027 let oid = odb.write(ObjectKind::Blob, path.as_bytes()).expect("blob");
13028 index.add_or_replace(index_entry(&path, MODE_REGULAR, oid));
13029 }
13030 }
13031 let head_tree = write_tree_from_index(&odb, &index, "").expect("write tree");
13032 let mut cache = build_cache_tree_from_index(&odb, &index).expect("cache-tree");
13033 cache.children.reverse();
13034 index.set_cache_tree(cache);
13035 index.cache_tree_root = Some(head_tree);
13036
13037 let changed = [
13038 "dir003/sub/f1.txt",
13039 "dir031/sub/f0.txt",
13040 "dir063/sub/f1.txt",
13041 ];
13042 for path in changed {
13043 index.invalidate_cache_tree_for_path(path.as_bytes());
13044 let oid = odb.write(ObjectKind::Blob, b"changed").expect("blob");
13045 index.get_mut(path.as_bytes(), 0).expect("entry").oid = oid;
13046 }
13047 for path in ["dir010/sub/new.txt", "dir999/new.txt", "top.txt"] {
13048 index.invalidate_cache_tree_for_path(path.as_bytes());
13049 let oid = odb.write(ObjectKind::Blob, path.as_bytes()).expect("blob");
13050 index.add_or_replace(index_entry(path, MODE_REGULAR, oid));
13051 }
13052 for path in [
13053 "dir020/sub/f0.txt",
13054 "dir020/sub/f1.txt",
13055 "dir021/sub/f0.txt",
13056 ] {
13057 index.invalidate_cache_tree_for_path(path.as_bytes());
13058 index.remove(path.as_bytes());
13059 }
13060
13061 let flat =
13062 diff_index_to_tree_flatten_reference(&odb, &index, &head_tree, false).expect("flat");
13063 let reads = TreeReadCounter::start();
13064 let fast = diff_index_to_tree(&odb, &index, Some(&head_tree), false).expect("fast");
13065 let cached_reads = reads.get();
13066 assert_eq!(fast, flat);
13067 assert_eq!(fast.len(), changed.len() + 3 + 3);
13068 assert!(
13069 cached_reads <= 1 + 2 * 6,
13070 "unchanged siblings must be skipped through the cache-tree (reads={cached_reads})"
13071 );
13072 }
13073
13074 #[test]
13075 fn diff_index_to_tree_cache_tree_handles_non_utf8_directory_names() {
13076 let temp = TempDir::new().expect("tempdir");
13077 let odb = Odb::new(temp.path());
13078 let mut index = Index::new();
13079 let raw_dirs: [&[u8]; 3] = [b"caf\xe9", b"caf\xef\xbf\xbd", b"plain"];
13080 for dir in raw_dirs {
13081 for f in [&b"a.txt"[..], b"b.txt"] {
13082 let path = [dir, b"/", f].concat();
13083 let oid = odb.write(ObjectKind::Blob, &path).expect("blob");
13084 let mut entry = index_entry("x", MODE_REGULAR, oid);
13085 entry.flags = path.len().min(0xfff) as u16;
13086 entry.path = path;
13087 index.add_or_replace(entry);
13088 }
13089 }
13090 let head_tree = write_tree_from_index(&odb, &index, "").expect("write tree");
13091 let cache = build_cache_tree_from_index(&odb, &index).expect("cache-tree");
13092 index.set_cache_tree(cache);
13093 index.cache_tree_root = Some(head_tree);
13094
13095 let changed: &[u8] = b"caf\xe9/b.txt";
13096 index.invalidate_cache_tree_for_path(changed);
13097 let oid = odb.write(ObjectKind::Blob, b"changed").expect("blob");
13098 index.get_mut(changed, 0).expect("entry").oid = oid;
13099
13100 let fast = diff_index_to_tree(&odb, &index, Some(&head_tree), false).expect("fast");
13103 assert_eq!(fast.len(), 1, "{fast:?}");
13104 assert_eq!(fast[0].status, DiffStatus::Modified);
13105 assert_eq!(
13106 fast[0].old_oid,
13107 odb.write(ObjectKind::Blob, changed).expect("blob")
13108 );
13109 assert_eq!(fast[0].new_oid, oid);
13110 }
13111
13112 #[test]
13113 fn diff_index_to_tree_reports_mode_gitlink_and_intent_to_add() {
13114 let temp = TempDir::new().expect("tempdir");
13115 let odb = Odb::new(temp.path());
13116 let blob = odb.write(ObjectKind::Blob, b"x").expect("blob");
13117 let mut index = Index::new();
13118 index.add_or_replace(index_entry("tracked.txt", MODE_REGULAR, blob));
13119 let head_tree = write_tree_from_index(&odb, &index, "").expect("tree");
13120 let cache = build_cache_tree_from_index(&odb, &index).expect("cache");
13121 index.set_cache_tree(cache);
13122
13123 let mode_entry = index_entry("tracked.txt", MODE_EXECUTABLE, blob);
13124 index.add_or_replace(mode_entry);
13125 index.invalidate_cache_tree_for_path(b"tracked.txt");
13126
13127 let diffs = diff_index_to_tree(&odb, &index, Some(&head_tree), false).expect("diff");
13128 assert_eq!(diffs.len(), 1);
13129 assert_eq!(diffs[0].status, DiffStatus::Modified);
13130 assert_eq!(diffs[0].old_mode, FileMode::new(MODE_REGULAR));
13131 assert_eq!(diffs[0].new_mode, FileMode::new(MODE_EXECUTABLE));
13132
13133 let mut ita = index_entry("new.txt", MODE_REGULAR, blob);
13135 ita.set_intent_to_add(true);
13136 index.add_or_replace(ita);
13137 let diffs = diff_index_to_tree(&odb, &index, Some(&head_tree), false).expect("diff");
13138 assert!(
13139 diffs.iter().all(|d| d.path() != "new.txt"),
13140 "intent-to-add must not appear in staged diff"
13141 );
13142
13143 let gitlink: ObjectId = "855827c583bc30645ba427885caa40c5b81764d2"
13145 .parse()
13146 .expect("oid");
13147 let mut sub_index = Index::new();
13148 sub_index.add_or_replace(index_entry("sub", MODE_GITLINK, gitlink));
13149 let sub_tree = write_tree_from_index(&odb, &sub_index, "").expect("tree");
13150 let sub_cache = build_cache_tree_from_index(&odb, &sub_index).expect("cache");
13151 sub_index.set_cache_tree(sub_cache);
13152 let diffs =
13153 diff_index_to_tree(&odb, &sub_index, Some(&sub_tree), true).expect("gitlink diff");
13154 assert!(
13155 diffs.is_empty(),
13156 "ignored gitlink should not diff: {diffs:?}"
13157 );
13158 }
13159
13160 #[test]
13161 fn ignore_submodules_skips_gitlink_in_added_directory_cache_walk() {
13162 let temp = TempDir::new().expect("tempdir");
13163 let odb = Odb::new(temp.path());
13164 let gitlink: ObjectId = "855827c583bc30645ba427885caa40c5b81764d2"
13165 .parse()
13166 .expect("oid");
13167 let head_tree = write_tree_from_index(&odb, &Index::new(), "").expect("empty tree");
13168 let mut index = Index::new();
13169 index.add_or_replace(index_entry("newdir/sub", MODE_GITLINK, gitlink));
13170 let cache = build_cache_tree_from_index(&odb, &index).expect("cache");
13171 index.set_cache_tree(cache);
13172 index.invalidate_cache_tree_for_path(b"newdir/sub");
13173
13174 let fast = diff_index_to_tree(&odb, &index, Some(&head_tree), true).expect("fast");
13175 let flat =
13176 diff_index_to_tree_flatten_reference(&odb, &index, &head_tree, true).expect("flat");
13177 assert_eq!(fast, flat);
13178 assert!(
13179 fast.is_empty(),
13180 "nested gitlinks in added dirs must be ignored: {fast:?}"
13181 );
13182 }
13183
13184 #[test]
13185 fn ignore_submodules_skips_gitlink_in_deleted_directory_cache_walk() {
13186 let temp = TempDir::new().expect("tempdir");
13187 let odb = Odb::new(temp.path());
13188 let blob = odb.write(ObjectKind::Blob, b"keep").expect("blob");
13189 let gitlink: ObjectId = "855827c583bc30645ba427885caa40c5b81764d2"
13190 .parse()
13191 .expect("oid");
13192 let mut index = Index::new();
13193 index.add_or_replace(index_entry("keep.txt", MODE_REGULAR, blob));
13194 index.add_or_replace(index_entry("olddir/sub", MODE_GITLINK, gitlink));
13195 let head_tree = write_tree_from_index(&odb, &index, "").expect("head");
13196 let cache = build_cache_tree_from_index(&odb, &index).expect("cache");
13197 index.set_cache_tree(cache);
13198 index
13199 .entries
13200 .retain(|e| !String::from_utf8_lossy(&e.path).starts_with("olddir/"));
13201 index.invalidate_cache_tree_for_path(b"olddir/sub");
13202
13203 let fast = diff_index_to_tree(&odb, &index, Some(&head_tree), true).expect("fast");
13204 let flat =
13205 diff_index_to_tree_flatten_reference(&odb, &index, &head_tree, true).expect("flat");
13206 assert_eq!(fast, flat);
13207 assert!(
13208 fast.is_empty(),
13209 "nested gitlinks in deleted dirs must be ignored: {fast:?}"
13210 );
13211 }
13212
13213 #[test]
13214 fn diff_trees_skips_identical_subtree_oids() {
13215 let temp = TempDir::new().expect("tempdir");
13216 let odb = Odb::new(temp.path());
13217 let leaf = odb.write(ObjectKind::Blob, b"leaf").expect("blob");
13218 let mut deep = Index::new();
13219 for i in 0..20 {
13220 deep.add_or_replace(index_entry(&format!("deep/n{i}.txt"), MODE_REGULAR, leaf));
13221 }
13222 let tree = write_tree_from_index(&odb, &deep, "").expect("tree");
13223 let reads = TreeReadCounter::start();
13224 let entries = diff_trees(&odb, Some(&tree), Some(&tree), "").expect("diff");
13225 assert!(entries.is_empty());
13226 assert_eq!(
13227 reads.get(),
13228 0,
13229 "identical tree oids should short-circuit without reading tree objects"
13230 );
13231 }
13232
13233 #[test]
13234 fn diff_tree_path_change_count_matches_diff_trees() {
13235 let temp = TempDir::new().expect("tempdir");
13236 let odb = Odb::new(temp.path());
13237 let a = odb.write(ObjectKind::Blob, b"a").expect("blob");
13238 let b = odb.write(ObjectKind::Blob, b"b").expect("blob");
13239 let mut left = Index::new();
13240 left.add_or_replace(index_entry("f.txt", MODE_REGULAR, a));
13241 let mut right = Index::new();
13242 right.add_or_replace(index_entry("f.txt", MODE_REGULAR, b));
13243 right.add_or_replace(index_entry("g.txt", MODE_REGULAR, a));
13244 let old_tree = write_tree_from_index(&odb, &left, "").expect("old");
13245 let new_tree = write_tree_from_index(&odb, &right, "").expect("new");
13246 let entries = diff_trees(&odb, Some(&old_tree), Some(&new_tree), "").expect("diff");
13247 let count =
13248 diff_tree_path_change_count(&odb, Some(&old_tree), Some(&new_tree), "").expect("count");
13249 assert_eq!(count, entries.len());
13250 assert_eq!(count, 2);
13251 }
13252
13253 #[test]
13254 fn diff_trees_matches_whether_trees_were_cached_parsed_or_raw() {
13255 let temp = TempDir::new().expect("tempdir");
13256 let a = Odb::new(temp.path())
13257 .write(ObjectKind::Blob, b"a")
13258 .expect("blob");
13259 let b = Odb::new(temp.path())
13260 .write(ObjectKind::Blob, b"b")
13261 .expect("blob");
13262 let mut left = Index::new();
13263 let mut right = Index::new();
13264 for i in 0..50 {
13265 left.add_or_replace(index_entry(&format!("same{i:02}.txt"), MODE_REGULAR, a));
13266 right.add_or_replace(index_entry(&format!("same{i:02}.txt"), MODE_REGULAR, a));
13267 }
13268 left.add_or_replace(index_entry("dir/changed.txt", MODE_REGULAR, a));
13269 left.add_or_replace(index_entry("dir/kept.txt", MODE_REGULAR, a));
13270 left.add_or_replace(index_entry("gone/x.txt", MODE_REGULAR, a));
13271 left.add_or_replace(index_entry("mode.sh", MODE_REGULAR, a));
13272 left.add_or_replace(index_entry("swap", MODE_REGULAR, a));
13273 right.add_or_replace(index_entry("dir/changed.txt", MODE_REGULAR, b));
13274 right.add_or_replace(index_entry("dir/kept.txt", MODE_REGULAR, a));
13275 right.add_or_replace(index_entry("mode.sh", MODE_EXECUTABLE, a));
13276 right.add_or_replace(index_entry("new/y.txt", MODE_REGULAR, b));
13277 right.add_or_replace(index_entry("swap/inner.txt", MODE_REGULAR, b));
13278 let setup = Odb::new(temp.path());
13279 let old_tree = write_tree_from_index(&setup, &left, "").expect("old");
13280 let new_tree = write_tree_from_index(&setup, &right, "").expect("new");
13281
13282 let raw = diff_trees(&Odb::new(temp.path()), Some(&old_tree), Some(&new_tree), "")
13283 .expect("raw diff");
13284 let changed: Vec<(DiffStatus, &str)> = raw.iter().map(|e| (e.status, e.path())).collect();
13285 assert_eq!(
13286 changed,
13287 vec![
13288 (DiffStatus::Modified, "dir/changed.txt"),
13289 (DiffStatus::Deleted, "gone/x.txt"),
13290 (DiffStatus::Modified, "mode.sh"),
13291 (DiffStatus::Added, "new/y.txt"),
13292 (DiffStatus::Deleted, "swap"),
13293 (DiffStatus::Added, "swap/inner.txt"),
13294 ]
13295 );
13296
13297 let parsed = Odb::new(temp.path());
13298 parsed.cached_tree_entries(&old_tree).expect("parse old");
13299 parsed.cached_tree_entries(&new_tree).expect("parse new");
13300 assert_eq!(
13301 diff_trees(&parsed, Some(&old_tree), Some(&new_tree), "").expect("parsed diff"),
13302 raw
13303 );
13304
13305 let mixed = Odb::new(temp.path());
13306 mixed.cached_tree_entries(&new_tree).expect("parse new");
13307 assert_eq!(
13308 diff_trees(&mixed, Some(&old_tree), Some(&new_tree), "").expect("mixed diff"),
13309 raw
13310 );
13311 let reparsed = mixed
13312 .cached_tree_entries(&old_tree)
13313 .expect("parse cached raw");
13314 assert_eq!(
13315 reparsed.as_slice(),
13316 crate::objects::parse_tree(&mixed.read(&old_tree).expect("read").data)
13317 .expect("parse")
13318 .as_slice(),
13319 "a tree cached raw by a diff parses to the same entries"
13320 );
13321 }
13322
13323 #[test]
13324 fn diff_trees_reports_a_corrupt_tree() {
13325 let temp = TempDir::new().expect("tempdir");
13326 let odb = Odb::new(temp.path());
13327 let good = write_tree_from_index(&odb, &Index::new(), "").expect("empty tree");
13328 let corrupt = odb
13329 .write(ObjectKind::Tree, b"100644 name-without-oid")
13330 .expect("tree");
13331 let err = diff_trees(&odb, Some(&good), Some(&corrupt), "").expect_err("corrupt");
13332 assert!(matches!(err, Error::CorruptObject(_)), "{err:?}");
13333 }
13334}
13335
13336#[cfg(test)]
13337mod changed_flags_shared_tests {
13338 use super::git_xdiff;
13339 use crate::line_interner::LineInterner;
13340
13341 #[test]
13344 fn shared_counts_stand_in_for_records_left_out() {
13345 let mut old: Vec<String> = Vec::new();
13348 let mut new: Vec<String> = Vec::new();
13349 for i in 0..60 {
13350 for side in [&mut old, &mut new] {
13351 side.push(format!("fn f{i}() {{\n"));
13352 side.push("}\n".to_owned());
13353 }
13354 }
13355 old.extend(
13356 [
13357 "u1\n", "u2\n", "u3\n", "u4\n", "}\n", "u5\n", "u6\n", "u7\n",
13358 ]
13359 .map(str::to_owned),
13360 );
13361 new.extend(
13362 [
13363 "v1\n", "v2\n", "v3\n", "v4\n", "}\n", "v5\n", "v6\n", "v7\n",
13364 ]
13365 .map(str::to_owned),
13366 );
13367 for i in 0..40 {
13368 for side in [&mut old, &mut new] {
13369 side.push(format!("fn g{i}() {{\n"));
13370 side.push("}\n".to_owned());
13371 }
13372 }
13373 let old: Vec<&str> = old.iter().map(String::as_str).collect();
13374 let new: Vec<&str> = new.iter().map(String::as_str).collect();
13375 let (head, tail) = (120, 80);
13376
13377 let mut interner = LineInterner::with_capacity(old.len() + new.len());
13378 let ids1 = interner.intern_all(&old);
13379 let ids2 = interner.intern_all(&new);
13380 let (full1, full2) = git_xdiff::changed_flags(&ids1, &ids2, false);
13381
13382 let mut cut = LineInterner::default();
13383 let cut1 = cut.intern_all(&old[head..old.len() - tail]);
13384 let cut2 = cut.intern_all(&new[head..new.len() - tail]);
13385 let mut shared = vec![0u32; cut.len()];
13386 for line in old[..head].iter().chain(&old[old.len() - tail..]) {
13387 if let Some(id) = cut.get(line) {
13388 shared[id as usize] += 1;
13389 }
13390 }
13391 let (part1, part2) =
13392 git_xdiff::changed_flags_with_shared(&cut1, &cut2, &shared, head + tail, false);
13393 assert_eq!(part1, full1[head..old.len() - tail]);
13394 assert_eq!(part2, full2[head..new.len() - tail]);
13395 assert!(full1[..head]
13396 .iter()
13397 .chain(&full1[old.len() - tail..])
13398 .all(|c| !c));
13399 assert!(full1[head..old.len() - tail].iter().all(|&c| c));
13402 let (alone1, _) = git_xdiff::changed_flags(&cut1, &cut2, false);
13403 assert!(
13404 !alone1[4],
13405 "counted alone, the cut keeps the `}}` as a match"
13406 );
13407 }
13408}
13409
13410#[cfg(test)]
13411mod gitlink_tree_worktree_tests {
13412 use std::fs;
13413
13414 use crate::index::{Index, IndexEntry, MODE_GITLINK};
13415 use crate::objects::{ObjectId, ObjectKind};
13416 use crate::repo::init_repository;
13417
13418 use super::diff_tree_to_worktree;
13419
13420 fn encode_tree_gitlink(name: &str, oid: &ObjectId) -> Vec<u8> {
13421 let mut data = format!("160000 {name}\0").into_bytes();
13422 data.extend_from_slice(oid.as_bytes());
13423 data
13424 }
13425
13426 #[test]
13427 fn unpopulated_gitlink_matching_index_produces_no_diff_entry() {
13428 let dir = tempfile::tempdir().expect("tempdir");
13429 let work_tree = dir.path();
13430 fs::create_dir(work_tree.join("sub")).expect("sub placeholder");
13431
13432 let repo = init_repository(
13433 work_tree,
13434 false,
13435 "main",
13436 None,
13437 crate::RefStorageFormat::Files,
13438 )
13439 .expect("init");
13440 let gitlink: ObjectId = "855827c583bc30645ba427885caa40c5b81764d2"
13441 .parse()
13442 .expect("gitlink oid");
13443 let tree_oid = repo
13444 .odb()
13445 .write(ObjectKind::Tree, &encode_tree_gitlink("sub", &gitlink))
13446 .expect("write tree");
13447
13448 let path = b"sub".to_vec();
13449 let mut index = Index::new();
13450 index.add_or_replace(IndexEntry {
13451 ctime_sec: 0,
13452 ctime_nsec: 0,
13453 mtime_sec: 0,
13454 mtime_nsec: 0,
13455 dev: 0,
13456 ino: 0,
13457 mode: MODE_GITLINK,
13458 uid: 0,
13459 gid: 0,
13460 size: 0,
13461 oid: gitlink,
13462 flags: (path.len().min(0xfff)) as u16,
13463 flags_extended: None,
13464 path,
13465 base_index_pos: 0,
13466 });
13467
13468 let entries = diff_tree_to_worktree(&repo.odb(), Some(&tree_oid), work_tree, &index)
13469 .expect("diff_tree_to_worktree");
13470 assert!(
13471 entries.is_empty(),
13472 "clean uninitialized gitlink should not appear in tree→worktree diff: {entries:?}"
13473 );
13474 }
13475
13476 #[test]
13477 fn unpopulated_gitlink_with_stray_file_produces_no_diff_entry() {
13478 let dir = tempfile::tempdir().expect("tempdir");
13479 let work_tree = dir.path();
13480 let sub = work_tree.join("sub");
13481 fs::create_dir(&sub).expect("sub placeholder");
13482 fs::write(sub.join("x.txt"), b"stray\n").expect("stray file");
13483
13484 let repo = init_repository(
13485 work_tree,
13486 false,
13487 "main",
13488 None,
13489 crate::RefStorageFormat::Files,
13490 )
13491 .expect("init");
13492 let gitlink: ObjectId = "855827c583bc30645ba427885caa40c5b81764d2"
13493 .parse()
13494 .expect("gitlink oid");
13495 let tree_oid = repo
13496 .odb()
13497 .write(ObjectKind::Tree, &encode_tree_gitlink("sub", &gitlink))
13498 .expect("write tree");
13499
13500 let path = b"sub".to_vec();
13501 let mut index = Index::new();
13502 index.add_or_replace(IndexEntry {
13503 ctime_sec: 0,
13504 ctime_nsec: 0,
13505 mtime_sec: 0,
13506 mtime_nsec: 0,
13507 dev: 0,
13508 ino: 0,
13509 mode: MODE_GITLINK,
13510 uid: 0,
13511 gid: 0,
13512 size: 0,
13513 oid: gitlink,
13514 flags: (path.len().min(0xfff)) as u16,
13515 flags_extended: None,
13516 path,
13517 base_index_pos: 0,
13518 });
13519
13520 let tree_entries = diff_tree_to_worktree(&repo.odb(), Some(&tree_oid), work_tree, &index)
13521 .expect("diff_tree_to_worktree");
13522 assert!(
13523 tree_entries.is_empty(),
13524 "uninitialized gitlink with stray files should not diff: {tree_entries:?}"
13525 );
13526
13527 let wt_entries =
13528 super::diff_index_to_worktree(&repo.odb(), &mut index.clone(), work_tree, false, true)
13529 .expect("diff_index_to_worktree");
13530 assert!(
13531 wt_entries.is_empty(),
13532 "uninitialized gitlink with stray files should not diff: {wt_entries:?}"
13533 );
13534
13535 let ie = index.entries()[0].clone();
13536 assert!(
13537 !super::worktree_differs_from_index_entry(
13538 repo.odb(),
13539 repo.git_dir(),
13540 work_tree,
13541 &ie,
13542 false,
13543 None,
13544 None,
13545 )
13546 .expect("differs"),
13547 "stray files in uninitialized submodule should not differ from index"
13548 );
13549 }
13550
13551 #[cfg_attr(
13552 not(feature = "full-tests"),
13553 ignore = "starts system git or is heavy; run with --features full-tests"
13554 )]
13555 #[test]
13556 fn untracked_only_in_checked_out_submodule_produces_no_diff_entry() {
13557 use std::process::Command;
13558
13559 let dir = tempfile::tempdir().expect("tempdir");
13560 let work_tree = dir.path();
13561 fs::create_dir(work_tree.join("sub")).expect("sub dir");
13562
13563 let repo = init_repository(
13564 work_tree,
13565 false,
13566 "main",
13567 None,
13568 crate::RefStorageFormat::Files,
13569 )
13570 .expect("init");
13571 let sub = work_tree.join("sub");
13572 for args in [
13573 &["init", "-q"][..],
13574 &["config", "user.name", "T"][..],
13575 &["config", "user.email", "t@e.com"][..],
13576 ] {
13577 let status = Command::new("git")
13578 .current_dir(&sub)
13579 .args(args)
13580 .status()
13581 .expect("git");
13582 assert!(status.success(), "git {args:?} failed");
13583 }
13584 fs::write(sub.join("tracked.txt"), b"v1\n").expect("write");
13585 for args in [&["add", "tracked.txt"][..], &["commit", "-qm", "sub"][..]] {
13586 let status = Command::new("git")
13587 .current_dir(&sub)
13588 .args(args)
13589 .status()
13590 .expect("git");
13591 assert!(status.success());
13592 }
13593 let head_out = Command::new("git")
13594 .current_dir(&sub)
13595 .args(["rev-parse", "HEAD"])
13596 .output()
13597 .expect("rev-parse");
13598 let gitlink = std::str::from_utf8(&head_out.stdout)
13599 .expect("utf8")
13600 .trim()
13601 .parse::<ObjectId>()
13602 .expect("oid");
13603
13604 let tree_oid = repo
13605 .odb()
13606 .write(ObjectKind::Tree, &encode_tree_gitlink("sub", &gitlink))
13607 .expect("tree");
13608 let path = b"sub".to_vec();
13609 let mut index = Index::new();
13610 index.add_or_replace(IndexEntry {
13611 ctime_sec: 0,
13612 ctime_nsec: 0,
13613 mtime_sec: 0,
13614 mtime_nsec: 0,
13615 dev: 0,
13616 ino: 0,
13617 mode: MODE_GITLINK,
13618 uid: 0,
13619 gid: 0,
13620 size: 0,
13621 oid: gitlink,
13622 flags: (path.len().min(0xfff)) as u16,
13623 flags_extended: None,
13624 path,
13625 base_index_pos: 0,
13626 });
13627
13628 fs::write(sub.join("untracked.txt"), b"extra\n").expect("untracked");
13629
13630 let entries = diff_tree_to_worktree(&repo.odb(), Some(&tree_oid), work_tree, &index)
13631 .expect("diff_tree_to_worktree");
13632 assert!(
13633 entries.is_empty(),
13634 "untracked-only submodule dirt must not appear in default tree→worktree diff: {entries:?}"
13635 );
13636 }
13637}
13638
13639#[cfg(test)]
13640mod racy_index_mtime_diff_tests {
13641 use super::*;
13642 use crate::index::{entry_from_metadata, Index, MODE_REGULAR};
13643 use crate::objects::{HashAlgo, ObjectKind};
13644 use filetime::FileTime;
13645 use std::fs;
13646 use std::path::PathBuf;
13647
13648 const INDEX_MTIME: (u32, u32) = (1_700_000_000, 123_456_789);
13649
13650 fn pin_mtime(path: &Path, sec: u32, nsec: u32) {
13651 filetime::set_file_mtime(path, FileTime::from_unix_time(i64::from(sec), nsec))
13652 .expect("set mtime");
13653 }
13654
13655 fn minimal_sha256_repo(work_tree: &Path) -> (PathBuf, Odb) {
13656 let git_dir = work_tree.join(".git");
13657 fs::create_dir_all(git_dir.join("objects")).expect("objects dir");
13658 fs::write(
13659 git_dir.join("config"),
13660 "[extensions]\n\tobjectformat = sha256\n",
13661 )
13662 .expect("config");
13663 let odb = Odb::new(&git_dir.join("objects")).with_config_git_dir(git_dir.clone());
13664 (git_dir, odb)
13665 }
13666
13667 #[test]
13668 fn detects_same_size_change_when_entry_mtime_matches_index_mtime() {
13669 let td = tempfile::tempdir().expect("tempdir");
13670 let wt = td.path();
13671 let content_a = b"same length!!";
13672 let content_b = b"same length??";
13673 assert_eq!(content_a.len(), content_b.len());
13674
13675 fs::write(wt.join("f.txt"), content_a).expect("write");
13676 pin_mtime(&wt.join("f.txt"), INDEX_MTIME.0, INDEX_MTIME.1);
13677 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
13678 let git_dir = wt.join(".git");
13679 fs::create_dir_all(git_dir.join("objects")).expect("objects dir");
13680 let odb = Odb::new(&git_dir.join("objects"));
13681 let oid_a = odb.hash(ObjectKind::Blob, content_a).expect("hash");
13682 let entry = entry_from_metadata(&meta, b"f.txt", oid_a, MODE_REGULAR);
13683
13684 let mut index = Index::new();
13685 index.source_mtime = Some(INDEX_MTIME);
13686 index.entries.push(entry);
13687
13688 fs::write(wt.join("f.txt"), content_b).expect("rewrite");
13689 pin_mtime(&wt.join("f.txt"), INDEX_MTIME.0, INDEX_MTIME.1);
13690
13691 let diff = diff_index_to_worktree(&odb, &mut index, wt, false, false).expect("diff");
13692 assert_eq!(
13693 diff.len(),
13694 1,
13695 "racy entry must be re-hashed, not stat-trusted"
13696 );
13697 assert_eq!(diff[0].path(), "f.txt");
13698 }
13699
13700 #[test]
13701 #[cfg(unix)]
13702 fn non_utf8_symlink_clean_racy_equal_mtime_reports_no_diff() {
13703 use crate::index::MODE_SYMLINK;
13704 use std::os::unix::ffi::OsStrExt as _;
13705 use std::os::unix::fs::symlink;
13706
13707 let td = tempfile::tempdir().expect("tempdir");
13708 let wt = td.path();
13709 let target = std::ffi::OsStr::from_bytes(b"bad-\xff-target");
13710 let link_path = wt.join("link");
13711 symlink(target, &link_path).expect("symlink");
13712
13713 let ft = FileTime::from_unix_time(i64::from(INDEX_MTIME.0), INDEX_MTIME.1);
13714 filetime::set_symlink_file_times(&link_path, ft, ft).expect("set symlink mtime");
13715 let meta = fs::symlink_metadata(&link_path).expect("stat");
13716 let git_dir = wt.join(".git");
13717 fs::create_dir_all(git_dir.join("objects")).expect("objects dir");
13718 let odb = Odb::new(&git_dir.join("objects"));
13719 let oid = odb.hash(ObjectKind::Blob, target.as_bytes()).expect("hash");
13720 let entry = entry_from_metadata(&meta, b"link", oid, MODE_SYMLINK);
13721
13722 let mut index = Index::new();
13723 index.source_mtime = Some(INDEX_MTIME);
13724 index.entries.push(entry);
13725
13726 let diff = diff_index_to_worktree(&odb, &mut index, wt, false, false).expect("diff");
13727 assert!(
13728 diff.is_empty(),
13729 "clean non-UTF-8 symlink must not be reported modified: {diff:?}"
13730 );
13731 }
13732
13733 #[test]
13734 fn sha256_clean_racy_equal_mtime_reports_no_diff() {
13735 let td = tempfile::tempdir().expect("tempdir");
13736 let wt = td.path();
13737 let (_git_dir, odb) = minimal_sha256_repo(wt);
13738 assert_eq!(odb.hash_algo(), HashAlgo::Sha256);
13739
13740 let bytes = b"sha256 width!!";
13741 fs::write(wt.join("f.txt"), bytes).expect("write");
13742 pin_mtime(&wt.join("f.txt"), INDEX_MTIME.0, INDEX_MTIME.1);
13743 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
13744 let oid = odb.hash(ObjectKind::Blob, bytes).expect("hash");
13745 let entry = entry_from_metadata(&meta, b"f.txt", oid, MODE_REGULAR);
13746
13747 let mut index = Index::new();
13748 index.hash_algo = HashAlgo::Sha256;
13749 index.source_mtime = Some(INDEX_MTIME);
13750 index.entries.push(entry);
13751
13752 let diff = diff_index_to_worktree(&odb, &mut index, wt, false, false).expect("diff");
13753 assert!(
13754 diff.is_empty(),
13755 "clean SHA-256 racy entry must not be reported modified: {diff:?}"
13756 );
13757 }
13758}
13759
13760#[cfg(test)]
13761mod smudge_racily_clean_tests {
13762 use super::*;
13763 use crate::config::ConfigSet;
13764 use crate::crlf::{self, FileAttrs};
13765 use crate::index::{entry_from_metadata, Index, MODE_REGULAR};
13766 use crate::objects::{HashAlgo, ObjectKind};
13767 use filetime::FileTime;
13768 use std::fs;
13769 use std::path::PathBuf;
13770
13771 const INDEX_MTIME_PIN: (u32, u32) = (1_700_000_000, 123_456_789);
13772
13773 fn mtime_from_metadata(meta: &fs::Metadata) -> (u32, u32) {
13774 #[cfg(unix)]
13775 {
13776 use std::os::unix::fs::MetadataExt;
13777 (meta.mtime() as u32, meta.mtime_nsec() as u32)
13778 }
13779 #[cfg(not(unix))]
13780 {
13781 use std::time::UNIX_EPOCH;
13782 let mtime = meta
13783 .modified()
13784 .ok()
13785 .and_then(|t| t.duration_since(UNIX_EPOCH).ok())
13786 .unwrap_or_default();
13787 (mtime.as_secs() as u32, mtime.subsec_nanos())
13788 }
13789 }
13790
13791 fn pin_mtime_and_sample(path: &Path, sec: u32, nsec: u32) -> (u32, u32) {
13793 filetime::set_file_mtime(path, FileTime::from_unix_time(i64::from(sec), nsec))
13794 .expect("set mtime");
13795 let meta = fs::symlink_metadata(path).expect("stat file");
13796 mtime_from_metadata(&meta)
13797 }
13798
13799 fn setup_index_mtime(work_tree: &Path) -> (u32, u32) {
13801 let probe = work_tree.join(".grit-index-mtime-probe");
13802 fs::write(&probe, b"").expect("probe file");
13803 pin_mtime_and_sample(&probe, INDEX_MTIME_PIN.0, INDEX_MTIME_PIN.1)
13804 }
13805
13806 fn minimal_repo(work_tree: &Path, config: &str) -> (PathBuf, Odb) {
13807 let git_dir = work_tree.join(".git");
13808 fs::create_dir_all(git_dir.join("objects")).expect("objects dir");
13809 fs::write(git_dir.join("config"), config).expect("config");
13810 let odb = Odb::new(&git_dir.join("objects")).with_config_git_dir(git_dir.clone());
13811 (git_dir, odb)
13812 }
13813
13814 fn staged_entry_for_file(
13815 odb: &Odb,
13816 work_tree: &Path,
13817 rel: &str,
13818 bytes: &[u8],
13819 mtime: (u32, u32),
13820 ) -> IndexEntry {
13821 let path = work_tree.join(rel);
13822 fs::write(&path, bytes).expect("write file");
13823 pin_mtime_and_sample(&path, mtime.0, mtime.1);
13824 let meta = fs::symlink_metadata(&path).expect("stat file");
13825 let oid = odb.hash(ObjectKind::Blob, bytes).expect("hash");
13826 entry_from_metadata(&meta, rel.as_bytes(), oid, MODE_REGULAR)
13827 }
13828
13829 fn staged_entry_with_oid(
13830 work_tree: &Path,
13831 rel: &str,
13832 worktree_bytes: &[u8],
13833 oid: crate::objects::ObjectId,
13834 mtime: (u32, u32),
13835 ) -> IndexEntry {
13836 let path = work_tree.join(rel);
13837 fs::write(&path, worktree_bytes).expect("write file");
13838 pin_mtime_and_sample(&path, mtime.0, mtime.1);
13839 let meta = fs::symlink_metadata(&path).expect("stat file");
13840 entry_from_metadata(&meta, rel.as_bytes(), oid, MODE_REGULAR)
13841 }
13842
13843 #[test]
13844 fn index_stat_cache_trustworthy_rejects_unknown_mtime() {
13845 assert!(!super::index_stat_cache_trustworthy(None));
13846 assert!(!super::index_stat_cache_trustworthy(Some((0, 0))));
13847 assert!(super::index_stat_cache_trustworthy(Some((1, 0))));
13848 }
13849
13850 #[test]
13851 fn index_entry_skips_content_hash_after_stat_match_when_trusted_and_not_racy() {
13852 use crate::index::StatMatchPolicy;
13853 let td = tempfile::tempdir().expect("tempdir");
13854 let wt = td.path();
13855 let (git_dir, odb) = minimal_repo(wt, "[core]\n\tfilemode = true\n");
13856 let rel = "trusted.txt";
13857 let file_mtime = (1_700_000_000, 0);
13858 let mut entry = staged_entry_for_file(&odb, wt, rel, b"payload\n", file_mtime);
13859 let index_mtime = (file_mtime.0 + 1, 0);
13860 let policy = StatMatchPolicy::from_config(None);
13861 assert!(
13862 super::index_entry_skips_worktree_content_hash_after_stat_match(
13863 &entry,
13864 Some(index_mtime),
13865 policy,
13866 )
13867 );
13868 entry.size = 0;
13869 entry.mtime_sec = 0;
13870 entry.mtime_nsec = 0;
13871 assert!(
13872 !super::index_entry_skips_worktree_content_hash_after_stat_match(
13873 &entry,
13874 Some(index_mtime),
13875 policy,
13876 )
13877 );
13878 let _ = git_dir;
13879 }
13880
13881 #[cfg(unix)]
13882 #[test]
13883 fn symlink_dir_cache_detects_symlinked_ancestors_across_threads() {
13884 let td = tempfile::tempdir().expect("tempdir");
13885 let wt = td.path();
13886 fs::create_dir_all(wt.join("real/deep")).expect("mkdir");
13887 fs::create_dir_all(wt.join("target/deep")).expect("mkdir");
13888 std::os::unix::fs::symlink(wt.join("target"), wt.join("linked")).expect("symlink");
13889 std::thread::scope(|scope| {
13890 for _ in 0..4 {
13891 scope.spawn(|| {
13892 let mut cache = super::SymlinkDirCache::default();
13893 assert!(!cache.has_symlink_in_path_bytes(wt, b"top.txt", false, false));
13894 assert!(!cache.has_symlink_in_path_bytes(wt, b"linked", false, false));
13895 assert!(!cache.has_symlink_in_path_bytes(wt, b"real/deep/f", false, false));
13896 assert!(cache.has_symlink_in_path_bytes(wt, b"linked/f", false, false));
13897 assert!(cache.has_symlink_in_path_bytes(wt, b"linked/deep/f", false, false));
13898 assert!(!cache.has_symlink_in_path_bytes(wt, b"\xff\xfe", false, false));
13899 });
13900 }
13901 });
13902 }
13903
13904 #[cfg(unix)]
13905 #[test]
13906 fn symlink_dir_cache_accepts_a_work_tree_reached_through_a_symlink() {
13907 let td = tempfile::tempdir().expect("tempdir");
13908 let real = td.path().join("real");
13909 fs::create_dir_all(real.join("a/b/c")).expect("mkdir");
13910 fs::create_dir_all(real.join("t/b/c")).expect("mkdir");
13911 std::os::unix::fs::symlink(real.join("t"), real.join("l")).expect("symlink");
13912 let wt = td.path().join("alias");
13913 std::os::unix::fs::symlink(&real, &wt).expect("symlink");
13914 let mut cache = super::SymlinkDirCache::default();
13915 assert!(!cache.has_symlink_in_path_bytes(&wt, b"a/b/c/f", false, false));
13916 assert!(cache.has_symlink_in_path_bytes(&wt, b"l/b/c/f", false, false));
13917 assert!(!cache.has_symlink_in_path_bytes(&real, b"a/b/c/f", false, false));
13918 assert!(cache.has_symlink_in_path_bytes(&real, b"l/b/c/f", false, false));
13919 }
13920
13921 #[cfg(all(target_os = "linux", target_pointer_width = "64"))]
13922 #[test]
13923 fn real_dir_chain_confirms_only_symlink_free_directories() {
13924 let td = tempfile::tempdir().expect("tempdir");
13925 let wt = td.path();
13926 fs::create_dir_all(wt.join("a/b/c")).expect("mkdir");
13927 fs::create_dir_all(wt.join("t/c")).expect("mkdir");
13928 fs::write(wt.join("a/file"), b"f").expect("write");
13929 std::os::unix::fs::symlink(wt.join("t"), wt.join("a/l")).expect("symlink");
13930 let mut chain = crate::worktree_scan::RealDirChain::open(wt);
13931 assert_eq!(chain.work_tree(), wt);
13932 assert!(chain.confirms(b"a"));
13933 assert!(chain.confirms(b"a/b/c"));
13934 assert!(!chain.confirms(b"a/l"));
13935 assert!(!chain.confirms(b"a/l/c"));
13936 assert!(!chain.confirms(b"a/file"));
13937 assert!(!chain.confirms(b"a/missing/c"));
13938 assert!(!chain.confirms(b""));
13939 assert!(!chain.confirms(b"a/\0b"));
13940 assert!(chain.confirms(b"a/b"));
13941 }
13942
13943 #[test]
13944 fn symlink_dir_cache_probes_each_new_directory_once_in_index_order() {
13945 let td = tempfile::tempdir().expect("tempdir");
13946 let wt = td.path();
13947 let depth = 12;
13948 let chains = 20;
13949 let mut paths = Vec::new();
13950 for chain in 0..chains {
13951 let dir: Vec<String> = (0..depth)
13952 .map(|level| format!("c{chain:02}l{level:02}"))
13953 .collect();
13954 let dir = format!("shared/{}", dir.join("/"));
13955 fs::create_dir_all(wt.join(&dir)).expect("mkdir");
13956 paths.extend((0..3).map(|file| format!("{dir}/f{file}")));
13957 }
13958 paths.sort();
13959 let mut cache = super::SymlinkDirCache::default();
13960 for rel in &paths {
13961 assert!(!cache.has_symlink_in_path_bytes(wt, rel.as_bytes(), false, false));
13962 }
13963 let unique_dirs = 1 + chains * depth;
13965 assert!(
13966 cache.fs_queries <= unique_dirs,
13967 "{} filesystem queries for {unique_dirs} directories",
13968 cache.fs_queries
13969 );
13970 #[cfg(all(target_os = "linux", target_pointer_width = "64"))]
13971 assert!(
13972 cache.fs_queries <= chains + 1,
13973 "one chain walk per new directory: {} queries for {chains} chains",
13974 cache.fs_queries
13975 );
13976 }
13977
13978 #[test]
13979 fn common_dir_prefix_len_stops_at_whole_components() {
13980 use super::{common_dir_prefix_len, path_within_dir};
13981 assert_eq!(common_dir_prefix_len(b"a/b/c", b"a/b/c"), 5);
13982 assert_eq!(common_dir_prefix_len(b"a/b/c", b"a/b"), 3);
13983 assert_eq!(common_dir_prefix_len(b"a/b", b"a/b/c"), 3);
13984 assert_eq!(common_dir_prefix_len(b"a/bc/d", b"a/b"), 1);
13985 assert_eq!(common_dir_prefix_len(b"a/b", b"a/bc/d"), 1);
13986 assert_eq!(common_dir_prefix_len(b"ab", b"a"), 0);
13987 assert_eq!(common_dir_prefix_len(b"a/b", b""), 0);
13988 assert!(path_within_dir(b"a/b", b"a/b"));
13989 assert!(path_within_dir(b"a/b/c", b"a/b"));
13990 assert!(!path_within_dir(b"a/bc", b"a/b"));
13991 assert!(!path_within_dir(b"a", b"a/b"));
13992 }
13993
13994 #[cfg(unix)]
13996 fn naive_has_symlink_ancestor(wt: &Path, rel: &str) -> bool {
13997 let Some((dir, _)) = rel.rsplit_once('/') else {
13998 return false;
13999 };
14000 let mut prefix = wt.to_path_buf();
14001 dir.split('/').any(|component| {
14002 prefix.push(component);
14003 fs::symlink_metadata(&prefix).is_ok_and(|m| m.file_type().is_symlink())
14004 })
14005 }
14006
14007 #[cfg(unix)]
14008 #[test]
14009 fn symlink_dir_cache_matches_uncached_probe_in_any_query_order() {
14010 let td = tempfile::tempdir().expect("tempdir");
14011 let wt = td.path();
14012 fs::create_dir_all(wt.join("a/b/c/d")).expect("mkdir");
14013 fs::create_dir_all(wt.join("a/bb/c")).expect("mkdir");
14014 fs::create_dir_all(wt.join("a/b2")).expect("mkdir");
14015 fs::create_dir_all(wt.join("t/c/d")).expect("mkdir");
14016 fs::create_dir_all(wt.join("x/y")).expect("mkdir");
14017 fs::write(wt.join("a/file"), b"f").expect("write");
14018 fs::write(wt.join("x/y/plain"), b"f").expect("write");
14019 std::os::unix::fs::symlink(wt.join("t"), wt.join("a/b/link")).expect("symlink");
14020 std::os::unix::fs::symlink(wt.join("t"), wt.join("x/l")).expect("symlink");
14021 std::os::unix::fs::symlink(wt.join("t"), wt.join("top")).expect("symlink");
14022
14023 let paths = [
14024 "a/b/c/d/f",
14025 "a/b/c/f",
14026 "a/b/f",
14027 "a/b/link/f",
14028 "a/b/link/c/d/f",
14029 "a/b/linker/f",
14030 "a/b2/f",
14031 "a/bb/c/f",
14032 "a/file/under/f",
14033 "a/missing/deeper/f",
14034 "a/missing/f",
14035 "a/f",
14036 "f",
14037 "top/c/f",
14038 "top/f",
14039 "topper/f",
14040 "x/l/c/d/f",
14041 "x/y/f",
14042 "x/y/plain/f",
14043 ];
14044 let mut orders: Vec<Vec<&str>> = vec![paths.to_vec()];
14045 orders.push(paths.iter().rev().copied().collect());
14046 orders.push(
14047 paths
14048 .iter()
14049 .step_by(2)
14050 .chain(paths.iter().skip(1).step_by(2))
14051 .copied()
14052 .collect(),
14053 );
14054 orders.push(paths.iter().flat_map(|p| [*p, *p]).collect());
14055 for order in orders {
14056 let mut cache = super::SymlinkDirCache::default();
14057 for rel in order {
14058 assert_eq!(
14059 cache.has_symlink_in_path_bytes(wt, rel.as_bytes(), false, false),
14060 naive_has_symlink_ancestor(wt, rel),
14061 "ancestor symlink answer for {rel}"
14062 );
14063 }
14064 }
14065 }
14066
14067 #[test]
14068 fn content_hash_skip_trusts_empty_blob_but_not_smudged_size() {
14069 use crate::index::StatMatchPolicy;
14070 let td = tempfile::tempdir().expect("tempdir");
14071 let wt = td.path();
14072 let (_git_dir, odb) = minimal_repo(wt, "[core]\n\tfilemode = false\n");
14073 let file_mtime = (1_700_000_000, 0);
14074 let index_mtime = Some((file_mtime.0 + 1, 0));
14075 let policy = StatMatchPolicy::from_config(None);
14076
14077 let empty = staged_entry_for_file(&odb, wt, "empty.txt", b"", file_mtime);
14078 assert_eq!(empty.size, 0);
14079 assert!(
14080 super::index_entry_skips_worktree_content_hash_after_stat_match(
14081 &empty,
14082 index_mtime,
14083 policy,
14084 )
14085 );
14086
14087 let smudged_oid = odb.hash(ObjectKind::Blob, b"abc\n").expect("hash");
14088 let smudged = staged_entry_with_oid(wt, "smudged.txt", b"", smudged_oid, file_mtime);
14089 assert_eq!(smudged.size, 0);
14090 assert!(
14091 !super::index_entry_skips_worktree_content_hash_after_stat_match(
14092 &smudged,
14093 index_mtime,
14094 policy,
14095 )
14096 );
14097 }
14098
14099 #[test]
14100 fn status_diff_reports_smudged_entry_whose_file_became_empty() {
14101 let td = tempfile::tempdir().expect("tempdir");
14102 let wt = td.path();
14103 let (git_dir, odb) = minimal_repo(wt, "[core]\n\tfilemode = false\n");
14104 let oid = odb.write(ObjectKind::Blob, b"abc\n").expect("write blob");
14105 let file_mtime = (1_700_000_000, 0);
14106 let mut index = Index::new();
14107 index.entries.push(staged_entry_with_oid(
14108 wt,
14109 "smudged.txt",
14110 b"",
14111 oid,
14112 file_mtime,
14113 ));
14114 let (entries, _) = super::diff_index_to_worktree_with_options(
14115 &odb,
14116 &mut index,
14117 wt,
14118 super::DiffIndexToWorktreeOptions {
14119 repository_git_dir: Some(git_dir),
14120 index_mtime: Some((file_mtime.0 + 1, 0)),
14121 for_status: true,
14122 stat_parallel_threads: Some(1),
14123 ..Default::default()
14124 },
14125 )
14126 .expect("diff");
14127 assert_eq!(entries.len(), 1, "{entries:?}");
14128 assert_eq!(entries[0].status, DiffStatus::Modified);
14129 }
14130
14131 #[test]
14132 fn status_diff_verifies_content_when_index_stat_cache_unset() {
14133 let td = tempfile::tempdir().expect("tempdir");
14134 let wt = td.path();
14135 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
14136 let rel = "unset-stat.txt";
14137 let bytes = b"same bytes\n";
14138 let oid = odb.hash(ObjectKind::Blob, bytes).expect("hash");
14139 fs::write(wt.join(rel), bytes).expect("write");
14140 let mut index = Index::new();
14141 let mut entry = staged_entry_for_file(&odb, wt, rel, bytes, (100, 0));
14142 entry.size = 0;
14143 entry.mtime_sec = 0;
14144 entry.mtime_nsec = 0;
14145 entry.oid = oid;
14146 index.entries.push(entry);
14147 let (entries, _) = super::diff_index_to_worktree_with_options(
14148 &odb,
14149 &mut index,
14150 wt,
14151 super::DiffIndexToWorktreeOptions {
14152 repository_git_dir: Some(git_dir),
14153 for_status: true,
14154 ..Default::default()
14155 },
14156 )
14157 .expect("diff");
14158 assert!(
14159 entries.is_empty(),
14160 "unset stat must not size-mismatch a matching worktree: {entries:?}"
14161 );
14162 }
14163
14164 #[cfg_attr(
14165 not(feature = "full-tests"),
14166 ignore = "starts system git or is heavy; run with --features full-tests"
14167 )]
14168 #[test]
14169 fn status_reports_modified_when_ctime_differs_but_mtime_sec_and_size_match_non_racy() {
14170 let td = tempfile::tempdir().expect("tempdir");
14171 let wt = td.path();
14172 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
14173 let rel = "tracked.txt";
14174 let original = b"original\n";
14175 let modified = b"modified\n";
14176 assert_eq!(original.len(), modified.len());
14177
14178 let entry_mtime = (1_700_000_000u32, 0u32);
14179 let index_file_mtime = (1_700_000_100u32, 0u32);
14180
14181 std::process::Command::new("git")
14182 .args(["-C", &wt.to_string_lossy(), "init", "-q"])
14183 .status()
14184 .expect("git init");
14185 std::process::Command::new("git")
14186 .args([
14187 "-C",
14188 &wt.to_string_lossy(),
14189 "config",
14190 "user.email",
14191 "t@example.com",
14192 ])
14193 .status()
14194 .expect("git config email");
14195 std::process::Command::new("git")
14196 .args(["-C", &wt.to_string_lossy(), "config", "user.name", "T"])
14197 .status()
14198 .expect("git config name");
14199
14200 fs::write(wt.join(rel), original).expect("write original");
14201 pin_mtime_and_sample(&wt.join(rel), entry_mtime.0, entry_mtime.1);
14202 std::process::Command::new("git")
14203 .args(["-C", &wt.to_string_lossy(), "add", rel])
14204 .status()
14205 .expect("git add");
14206 std::process::Command::new("git")
14207 .args([
14208 "-C",
14209 &wt.to_string_lossy(),
14210 "commit",
14211 "-qm",
14212 "seed",
14213 "--no-gpg-sign",
14214 ])
14215 .status()
14216 .expect("git commit");
14217
14218 let mut index = Index::new();
14219 index.source_mtime = Some(index_file_mtime);
14220 let staged = staged_entry_for_file(&odb, wt, rel, original, entry_mtime);
14221 let oid_original = staged.oid;
14222 index.entries.push(staged);
14223
14224 fs::write(wt.join(rel), modified).expect("modify worktree");
14225 let git_dirty = std::process::Command::new("git")
14226 .args([
14227 "-C",
14228 &wt.to_string_lossy(),
14229 "--no-optional-locks",
14230 "status",
14231 "--short",
14232 ])
14233 .output()
14234 .expect("git status");
14235 assert!(
14236 git_dirty.status.success(),
14237 "git status failed: {}",
14238 String::from_utf8_lossy(&git_dirty.stderr)
14239 );
14240 assert!(
14241 String::from_utf8_lossy(&git_dirty.stdout).contains("tracked.txt"),
14242 "system git must see modification before mtime pin: {}",
14243 String::from_utf8_lossy(&git_dirty.stdout)
14244 );
14245
14246 pin_mtime_and_sample(&wt.join(rel), entry_mtime.0, entry_mtime.1);
14247
14248 let meta = fs::symlink_metadata(wt.join(rel)).expect("stat");
14249 let ie = &mut index.entries[0];
14250 assert_eq!(ie.oid, oid_original);
14251 if stat_matches(ie, &meta) {
14252 ie.ctime_sec = ie.ctime_sec.wrapping_sub(3600);
14253 }
14254 assert!(
14255 !stat_matches(ie, &meta),
14256 "ctime/inode change must fail full stat match"
14257 );
14258 let stat_policy = crate::index::StatMatchPolicy::default();
14259 assert!(
14260 !entry_is_racy_with_policy(ie, Some(index_file_mtime), stat_policy),
14261 "fixture must be non-racy"
14262 );
14263
14264 let (entries, index_changed) = super::diff_index_to_worktree_with_options(
14265 &odb,
14266 &mut index,
14267 wt,
14268 super::DiffIndexToWorktreeOptions {
14269 index_mtime: Some(index_file_mtime),
14270 repository_git_dir: Some(git_dir),
14271 refresh_index_stat_in_pass: true,
14272 for_status: true,
14273 ..Default::default()
14274 },
14275 )
14276 .expect("diff");
14277 assert_eq!(
14278 entries.len(),
14279 1,
14280 "must report modified, not refresh stat clean: {entries:?}, index_changed={index_changed}"
14281 );
14282 assert_eq!(entries[0].path(), rel);
14283 }
14284
14285 #[test]
14286 fn diff_name_status_skips_big_worktree_read_on_size_mismatch() {
14287 let td = tempfile::tempdir().expect("tempdir");
14288 let wt = td.path();
14289 let (git_dir, odb) = minimal_repo(wt, "[core]\n\tbigFileThreshold = 4k\n");
14290 let rel = "big.bin";
14291 let indexed = vec![b'x'; 100];
14292 let mut index = Index::new();
14293 index
14294 .entries
14295 .push(staged_entry_for_file(&odb, wt, rel, &indexed, (100, 0)));
14296 fs::write(wt.join(rel), vec![b'y'; 5000]).expect("rewrite bigger");
14297 let metrics = odb.hot_path_metrics();
14298 metrics.reset_blob_content_reads();
14299 metrics.set_blob_counting(true);
14300 let (entries, _) = super::diff_index_to_worktree_with_options(
14301 &odb,
14302 &mut index,
14303 wt,
14304 super::DiffIndexToWorktreeOptions {
14305 repository_git_dir: Some(git_dir),
14306 for_status: false,
14307 ..Default::default()
14308 },
14309 )
14310 .expect("diff");
14311 metrics.set_blob_counting(false);
14312 assert_eq!(entries.len(), 1);
14313 assert_eq!(entries[0].status, super::DiffStatus::Modified);
14314 assert_eq!(entries[0].new_oid, super::zero_oid());
14315 assert_eq!(
14316 metrics.blob_content_reads(),
14317 0,
14318 "large size-only drift must not hash or read the worktree"
14319 );
14320 }
14321
14322 #[test]
14323 fn status_diff_skips_hash_when_worktree_size_differs() {
14324 let td = tempfile::tempdir().expect("tempdir");
14325 let wt = td.path();
14326 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
14327 let rel = "size-change.txt";
14328 let original = b"short\n";
14329 let mut index = Index::new();
14330 index
14331 .entries
14332 .push(staged_entry_for_file(&odb, wt, rel, original, (100, 0)));
14333 fs::write(wt.join(rel), b"longer content\n").expect("rewrite");
14334 let metrics = odb.hot_path_metrics();
14335 metrics.reset_blob_content_reads();
14336 metrics.set_blob_counting(true);
14337 let (entries, _) = super::diff_index_to_worktree_with_options(
14338 &odb,
14339 &mut index,
14340 wt,
14341 super::DiffIndexToWorktreeOptions {
14342 repository_git_dir: Some(git_dir),
14343 for_status: true,
14344 ..Default::default()
14345 },
14346 )
14347 .expect("diff");
14348 metrics.set_blob_counting(false);
14349 assert_eq!(entries.len(), 1);
14350 assert_eq!(entries[0].status, super::DiffStatus::Modified);
14351 assert_eq!(entries[0].path(), rel);
14352 assert_eq!(entries[0].old_oid, index.entries[0].oid);
14353 assert_eq!(entries[0].new_oid, super::zero_oid());
14354 assert_eq!(
14355 metrics.blob_content_reads(),
14356 0,
14357 "identity conversion: a size change alone proves the file modified"
14358 );
14359 }
14360
14361 #[test]
14362 fn status_diff_hashes_size_change_when_eol_conversion_restores_blob() {
14363 for refresh_index_stat_in_pass in [false, true] {
14364 let td = tempfile::tempdir().expect("tempdir");
14365 let wt = td.path();
14366 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
14367 fs::write(wt.join(".gitattributes"), "*.txt text eol=crlf\n").expect("attrs");
14368 let rel = "n.txt";
14369 let blob = odb
14370 .write(ObjectKind::Blob, b"one\ntwo\n")
14371 .expect("write blob");
14372 let mut index = Index::new();
14373 index.entries.push(staged_entry_with_oid(
14374 wt,
14375 rel,
14376 b"one\r\ntwo\r\n",
14377 blob,
14378 (100, 0),
14379 ));
14380 fs::write(wt.join(rel), b"one\ntwo\n").expect("rewrite as LF");
14381 let (entries, index_changed) = super::diff_index_to_worktree_with_options(
14382 &odb,
14383 &mut index,
14384 wt,
14385 super::DiffIndexToWorktreeOptions {
14386 index_mtime: Some((200, 0)),
14387 repository_git_dir: Some(git_dir),
14388 refresh_index_stat_in_pass,
14389 for_status: true,
14390 ..Default::default()
14391 },
14392 )
14393 .expect("diff");
14394 assert!(
14395 entries.is_empty(),
14396 "refresh={refresh_index_stat_in_pass}: LF worktree cleans to the indexed blob: \
14397 {entries:?}"
14398 );
14399 if refresh_index_stat_in_pass {
14400 assert!(index_changed, "verified entry adopts the new stat");
14401 assert_eq!(index.entries[0].size, 8);
14402 }
14403 }
14404 }
14405
14406 #[test]
14407 fn status_diff_reads_size_changed_converted_file_once() {
14408 for refresh_index_stat_in_pass in [false, true] {
14409 let td = tempfile::tempdir().expect("tempdir");
14410 let wt = td.path();
14411 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
14412 fs::write(wt.join(".gitattributes"), "*.txt text eol=crlf\n").expect("attrs");
14413 let rel = "n.txt";
14414 let blob = odb
14415 .write(ObjectKind::Blob, b"one\ntwo\n")
14416 .expect("write blob");
14417 let mut index = Index::new();
14418 index.entries.push(staged_entry_with_oid(
14419 wt,
14420 rel,
14421 b"one\r\ntwo\r\n",
14422 blob,
14423 (100, 0),
14424 ));
14425 fs::write(wt.join(rel), b"one\ntwo\nthree\n").expect("real edit");
14426 let metrics = odb.hot_path_metrics();
14427 metrics.reset_blob_content_reads();
14428 metrics.set_blob_counting(true);
14429 let (entries, _) = super::diff_index_to_worktree_with_options(
14430 &odb,
14431 &mut index,
14432 wt,
14433 super::DiffIndexToWorktreeOptions {
14434 index_mtime: Some((200, 0)),
14435 repository_git_dir: Some(git_dir),
14436 refresh_index_stat_in_pass,
14437 for_status: true,
14438 ..Default::default()
14439 },
14440 )
14441 .expect("diff");
14442 metrics.set_blob_counting(false);
14443 assert_eq!(entries.len(), 1, "refresh={refresh_index_stat_in_pass}");
14444 assert_eq!(entries[0].status, super::DiffStatus::Modified);
14445 assert_eq!(entries[0].new_oid, zero_oid());
14446 assert_eq!(
14447 index.entries[0].size, 10,
14448 "a modified file keeps its stale stat"
14449 );
14450 assert_eq!(
14451 metrics.blob_content_reads(),
14452 1,
14453 "refresh={refresh_index_stat_in_pass}: read once, not again after the refresh"
14454 );
14455 }
14456 }
14457
14458 #[test]
14459 fn stat_matches_ignores_subsecond_nsec_by_default() {
14460 let td = tempfile::tempdir().expect("tempdir");
14461 let wt = td.path();
14462 fs::write(wt.join("f.txt"), b"x\n").expect("write");
14463 pin_mtime_and_sample(&wt.join("f.txt"), 100, 0);
14464 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
14465 let mut entry = entry_from_metadata(&meta, b"f.txt", zero_oid(), MODE_REGULAR);
14466 entry.mtime_nsec = entry.mtime_nsec.wrapping_add(1_000_000);
14467 assert!(stat_matches(&entry, &meta));
14468 }
14469
14470 #[test]
14471 fn classify_add_up_to_date_when_stat_matches() {
14472 let td = tempfile::tempdir().expect("tempdir");
14473 let wt = td.path();
14474 let (_git_dir, odb) = minimal_repo(wt, "[core]\n");
14475 let bytes = b"hello world\n";
14476 let entry = staged_entry_for_file(&odb, wt, "f.txt", bytes, (100, 0));
14477 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
14478 let conv = crlf::ConversionConfig::from_config(&ConfigSet::new());
14479 let attrs = FileAttrs::default();
14480 let params = WorktreeAddRefreshParams {
14481 odb: &odb,
14482 ie: &entry,
14483 meta: &meta,
14484 abs_path: &wt.join("f.txt"),
14485 rel_path: "f.txt",
14486 conv: &conv,
14487 file_attrs: &attrs,
14488 index_mtime: Some((200, 0)),
14489 staged_mode: MODE_REGULAR,
14490 filter_process: None,
14491 };
14492 let refresh = classify_worktree_entry_for_add(¶ms).expect("classify");
14493 assert_eq!(refresh, WorktreeAddRefresh::UpToDate);
14494 }
14495
14496 #[test]
14497 fn classify_add_rehashes_when_index_mtime_unknown_and_stat_matches() {
14498 let td = tempfile::tempdir().expect("tempdir");
14499 let wt = td.path();
14500 let (_git_dir, odb) = minimal_repo(wt, "[core]\n");
14501 let entry = staged_entry_for_file(&odb, wt, "f.txt", b"old content\n", (100, 0));
14502 fs::write(wt.join("f.txt"), b"new content\n").expect("rewrite");
14503 pin_mtime_and_sample(&wt.join("f.txt"), 100, 0);
14504 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
14505 let conv = crlf::ConversionConfig::from_config(&ConfigSet::new());
14506 let attrs = FileAttrs::default();
14507 let params = WorktreeAddRefreshParams {
14508 odb: &odb,
14509 ie: &entry,
14510 meta: &meta,
14511 abs_path: &wt.join("f.txt"),
14512 rel_path: "f.txt",
14513 conv: &conv,
14514 file_attrs: &attrs,
14515 index_mtime: None,
14516 staged_mode: MODE_REGULAR,
14517 filter_process: None,
14518 };
14519 let refresh = classify_worktree_entry_for_add(¶ms).expect("classify");
14520 assert_eq!(refresh, WorktreeAddRefresh::NeedsRestage);
14521 }
14522
14523 #[test]
14524 fn classify_add_needs_restaging_when_content_differs() {
14525 let td = tempfile::tempdir().expect("tempdir");
14526 let wt = td.path();
14527 let (_git_dir, odb) = minimal_repo(wt, "[core]\n");
14528 let entry = staged_entry_for_file(&odb, wt, "f.txt", b"old content\n", (100, 0));
14529 fs::write(wt.join("f.txt"), b"new content\n").expect("rewrite");
14530 pin_mtime_and_sample(&wt.join("f.txt"), 101, 0);
14531 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
14532 let conv = crlf::ConversionConfig::from_config(&ConfigSet::new());
14533 let attrs = FileAttrs::default();
14534 let params = WorktreeAddRefreshParams {
14535 odb: &odb,
14536 ie: &entry,
14537 meta: &meta,
14538 abs_path: &wt.join("f.txt"),
14539 rel_path: "f.txt",
14540 conv: &conv,
14541 file_attrs: &attrs,
14542 index_mtime: Some((200, 0)),
14543 staged_mode: MODE_REGULAR,
14544 filter_process: None,
14545 };
14546 let refresh = classify_worktree_entry_for_add(¶ms).expect("classify");
14547 assert_eq!(refresh, WorktreeAddRefresh::NeedsRestage);
14548 }
14549
14550 #[test]
14551 fn smudges_racily_clean_modified_entry() {
14552 let td = tempfile::tempdir().expect("tempdir");
14553 let wt = td.path();
14554 let index_mtime = setup_index_mtime(wt);
14555 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
14556 let content_a = b"same length!!";
14557 let content_b = b"same length??";
14558 assert_eq!(content_a.len(), content_b.len());
14559
14560 let mut index = Index::new();
14561 let entry = staged_entry_for_file(&odb, wt, "f.txt", content_a, index_mtime);
14562 let stale_oid = entry.oid;
14563
14564 fs::write(wt.join("f.txt"), content_b).expect("rewrite");
14565 pin_mtime_and_sample(&wt.join("f.txt"), index_mtime.0, index_mtime.1);
14566 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
14567 let entry = entry_from_metadata(&meta, b"f.txt", stale_oid, MODE_REGULAR);
14569 index.entries.push(entry.clone());
14570
14571 assert!(stat_matches(&entry, &meta));
14572 assert!(entry_is_racy(&entry, Some(index_mtime)));
14573
14574 let smudged = smudge_racily_clean_entries(
14575 &odb,
14576 &git_dir,
14577 &mut index,
14578 wt,
14579 Some(index_mtime),
14580 None,
14581 None,
14582 );
14583 assert!(smudged);
14584 assert_eq!(index.entries[0].size, 0);
14585
14586 let diff = diff_index_to_worktree(&odb, &mut index, wt, false, false).expect("diff");
14587 assert_eq!(diff.len(), 1);
14588 assert_eq!(diff[0].path(), "f.txt");
14589 }
14590
14591 fn racy_stale_entry(wt: &Path) -> (PathBuf, Odb, Index, (u32, u32), ObjectId) {
14593 let index_mtime = setup_index_mtime(wt);
14594 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
14595 let entry = staged_entry_for_file(&odb, wt, "f.txt", b"same length!!", index_mtime);
14596 let stale_oid = entry.oid;
14597 fs::write(wt.join("f.txt"), b"same length??").expect("rewrite");
14598 pin_mtime_and_sample(&wt.join("f.txt"), index_mtime.0, index_mtime.1);
14599 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
14600 let mut index = Index::new();
14601 index.entries.push(entry_from_metadata(
14602 &meta,
14603 b"f.txt",
14604 stale_oid,
14605 MODE_REGULAR,
14606 ));
14607 (git_dir, odb, index, index_mtime, stale_oid)
14608 }
14609
14610 #[test]
14611 fn content_verified_entry_is_not_reread_and_the_mark_is_consumed() {
14612 let td = tempfile::tempdir().expect("tempdir");
14613 let wt = td.path();
14614 let (git_dir, odb, mut index, index_mtime, stale_oid) = racy_stale_entry(wt);
14615 index.mark_content_verified(b"f.txt", stale_oid);
14616
14617 let smudge = |index: &mut Index| {
14618 smudge_racily_clean_entries(&odb, &git_dir, index, wt, Some(index_mtime), None, None)
14619 };
14620 assert!(!smudge(&mut index), "a verified entry is trusted");
14621 assert_ne!(index.entries[0].size, 0);
14622 assert!(index.content_verified.is_empty());
14623 assert!(smudge(&mut index), "without the mark the entry is checked");
14624 assert_eq!(index.entries[0].size, 0);
14625 }
14626
14627 #[test]
14628 fn content_verified_entry_bit_is_trusted_and_consumed() {
14629 let td = tempfile::tempdir().expect("tempdir");
14630 let wt = td.path();
14631 let (git_dir, odb, mut index, index_mtime, _) = racy_stale_entry(wt);
14632 index.entries[0].mark_content_verified();
14633
14634 let smudge = |index: &mut Index| {
14635 smudge_racily_clean_entries(&odb, &git_dir, index, wt, Some(index_mtime), None, None)
14636 };
14637 assert!(!smudge(&mut index), "a marked entry is trusted");
14638 assert_ne!(index.entries[0].size, 0);
14639 assert_eq!(
14640 index.entries[0].flags_extended, None,
14641 "the mark is consumed"
14642 );
14643 assert!(smudge(&mut index), "without the mark the entry is checked");
14644 assert_eq!(index.entries[0].size, 0);
14645 }
14646
14647 #[test]
14648 fn content_verified_mark_for_another_oid_does_not_skip_the_check() {
14649 let td = tempfile::tempdir().expect("tempdir");
14650 let wt = td.path();
14651 let (git_dir, odb, mut index, index_mtime, _) = racy_stale_entry(wt);
14652 let other = odb.hash(ObjectKind::Blob, b"other").expect("hash");
14653 index.mark_content_verified(b"f.txt", other);
14654 assert!(smudge_racily_clean_entries(
14655 &odb,
14656 &git_dir,
14657 &mut index,
14658 wt,
14659 Some(index_mtime),
14660 None,
14661 None,
14662 ));
14663 assert_eq!(index.entries[0].size, 0);
14664 }
14665
14666 #[test]
14667 fn leaves_non_racy_entry_alone() {
14668 let td = tempfile::tempdir().expect("tempdir");
14669 let wt = td.path();
14670 let index_mtime = setup_index_mtime(wt);
14671 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
14672 let content_a = b"same length!!";
14673 let content_b = b"same length??";
14674 let entry_mtime = (index_mtime.0 - 10, 0);
14675
14676 let mut index = Index::new();
14677 let entry = staged_entry_for_file(&odb, wt, "f.txt", content_a, entry_mtime);
14678 index.entries.push(entry.clone());
14679
14680 fs::write(wt.join("f.txt"), content_b).expect("rewrite");
14681 pin_mtime_and_sample(&wt.join("f.txt"), entry_mtime.0, entry_mtime.1);
14682
14683 assert!(!entry_is_racy(&entry, Some(index_mtime)));
14684 let smudged = smudge_racily_clean_entries(
14685 &odb,
14686 &git_dir,
14687 &mut index,
14688 wt,
14689 Some(index_mtime),
14690 None,
14691 None,
14692 );
14693 assert!(!smudged);
14694 assert_eq!(index.entries[0].size, entry.size);
14695 }
14696
14697 #[test]
14698 fn leaves_matching_content_alone() {
14699 let td = tempfile::tempdir().expect("tempdir");
14700 let wt = td.path();
14701 let index_mtime = setup_index_mtime(wt);
14702 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
14703 let bytes = b"unchanged!!!!";
14704
14705 let mut index = Index::new();
14706 let entry = staged_entry_for_file(&odb, wt, "f.txt", bytes, index_mtime);
14707 index.entries.push(entry.clone());
14708
14709 assert!(entry_is_racy(&entry, Some(index_mtime)));
14710 let smudged = smudge_racily_clean_entries(
14711 &odb,
14712 &git_dir,
14713 &mut index,
14714 wt,
14715 Some(index_mtime),
14716 None,
14717 None,
14718 );
14719 assert!(!smudged);
14720 assert_eq!(index.entries[0].size, entry.size);
14721 }
14722
14723 #[test]
14724 fn status_diff_skips_refresh_rewrite_for_racy_clean_matching_stat() {
14725 let td = tempfile::tempdir().expect("tempdir");
14726 let wt = td.path();
14727 let index_mtime = setup_index_mtime(wt);
14728 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
14729 let bytes = b"racy clean payload\n";
14730
14731 let mut index = Index::new();
14732 index.source_mtime = Some(index_mtime);
14733 let entry = staged_entry_for_file(&odb, wt, "f.txt", bytes, index_mtime);
14734 index.entries.push(entry.clone());
14735 assert!(entry_is_racy(&entry, Some(index_mtime)));
14736
14737 let (entries, index_changed) = super::diff_index_to_worktree_with_options(
14738 &odb,
14739 &mut index,
14740 wt,
14741 super::DiffIndexToWorktreeOptions {
14742 repository_git_dir: Some(git_dir),
14743 index_mtime: Some(index_mtime),
14744 refresh_index_stat_in_pass: true,
14745 for_status: true,
14746 ..Default::default()
14747 },
14748 )
14749 .expect("status diff");
14750 assert!(entries.is_empty(), "unexpected unstaged: {entries:?}");
14751 assert!(
14752 !index_changed,
14753 "matching stat on a racy entry must skip refresh without rewriting the index"
14754 );
14755 }
14756
14757 #[test]
14758 fn leaves_matching_sha256_racy_content_alone() {
14759 let td = tempfile::tempdir().expect("tempdir");
14760 let wt = td.path();
14761 let index_mtime = setup_index_mtime(wt);
14762 let (git_dir, odb) = minimal_repo(wt, "[extensions]\n\tobjectformat = sha256\n");
14763 assert_eq!(odb.hash_algo(), HashAlgo::Sha256);
14764
14765 let bytes = b"sha256 width!!";
14766 let oid = odb.hash(ObjectKind::Blob, bytes).expect("hash");
14767 let mut index = Index::new();
14768 let entry = staged_entry_with_oid(wt, "f.txt", bytes, oid, index_mtime);
14769 index.entries.push(entry.clone());
14770
14771 assert!(entry_is_racy(&entry, Some(index_mtime)));
14772 let smudged = smudge_racily_clean_entries(
14773 &odb,
14774 &git_dir,
14775 &mut index,
14776 wt,
14777 Some(index_mtime),
14778 None,
14779 None,
14780 );
14781 assert!(!smudged, "matching SHA-256 blob must not be smudged");
14782 assert_eq!(index.entries[0].size, entry.size);
14783 }
14784
14785 #[test]
14786 fn materialize_big_binary_under_autocrlf_streams_into_pack() {
14787 use std::io::Write;
14788
14789 let td = tempfile::tempdir().expect("tempdir");
14790 let wt = td.path();
14791 let (git_dir, odb) =
14792 minimal_repo(wt, "[core]\n\tautocrlf = true\n\tbigFileThreshold = 64k\n");
14793 let path = wt.join("big.bin");
14794 {
14795 let mut f = fs::File::create(&path).expect("create");
14796 f.write_all(&[0u8; 9000]).expect("nul prefix");
14797 let chunk = vec![1u8; 256 * 1024];
14798 for _ in 0..20 {
14799 f.write_all(&chunk).expect("chunk");
14800 }
14801 }
14802 let meta = fs::metadata(&path).expect("stat");
14803 let mut conv = crlf::ConversionConfig::from_config(&ConfigSet::new());
14804 conv.autocrlf = crlf::AutoCrlf::True;
14805 let attrs = FileAttrs::default();
14806 let oid =
14807 materialize_worktree_blob(&odb, &path, &meta, &conv, &attrs, "big.bin", None, None)
14808 .expect("materialize");
14809 assert!(
14810 !odb.test_loose_object_path(&oid).exists(),
14811 "big binary under autocrlf should pack, not write loose"
14812 );
14813 let pack_dir = git_dir.join("objects/pack");
14814 let names: Vec<_> = fs::read_dir(&pack_dir)
14815 .expect("pack dir")
14816 .map(|e| e.expect("entry").file_name())
14817 .collect();
14818 assert!(
14819 names.iter().any(|n| n.to_string_lossy().ends_with(".pack")),
14820 "{names:?}"
14821 );
14822 assert!(odb.read(&oid).expect("read blob").data.len() > 9000);
14823 }
14824
14825 #[test]
14826 fn indexed_binary_under_autocrlf_rehash_streams() {
14827 use std::io::Write;
14828
14829 let td = tempfile::tempdir().expect("tempdir");
14830 let wt = td.path();
14831 let (_git_dir, odb) =
14832 minimal_repo(wt, "[core]\n\tautocrlf = true\n\tbigFileThreshold = 1m\n");
14833 let path = wt.join("big.bin");
14834 {
14835 let mut f = fs::File::create(&path).expect("create");
14836 f.write_all(&[0u8; 9000]).expect("nul prefix");
14837 let chunk = vec![1u8; 512 * 1024];
14838 for _ in 0..3 {
14839 f.write_all(&chunk).expect("chunk");
14840 }
14841 }
14842 let meta = fs::symlink_metadata(&path).expect("stat");
14843 let oid = hash_worktree_file(
14844 &odb,
14845 &path,
14846 &meta,
14847 &crlf::ConversionConfig::from_config(&ConfigSet::new()),
14848 &FileAttrs::default(),
14849 "big.bin",
14850 None,
14851 None,
14852 )
14853 .expect("hash");
14854 let entry = entry_from_metadata(&meta, b"big.bin", oid, MODE_REGULAR);
14855 let mut conv = crlf::ConversionConfig::from_config(&ConfigSet::new());
14856 conv.autocrlf = crlf::AutoCrlf::True;
14857 let attrs = FileAttrs::default();
14858 let content = read_regular_worktree_blob(
14859 &odb,
14860 &path,
14861 &conv,
14862 &attrs,
14863 "big.bin",
14864 Some(&entry),
14865 None,
14866 1024 * 1024,
14867 )
14868 .expect("read blob");
14869 assert!(
14870 matches!(content, WorktreeBlobContent::Stream { .. }),
14871 "indexed binary re-hash must stream, not buffer"
14872 );
14873 assert_eq!(content.hash(&odb).expect("hash"), entry.oid);
14874 }
14875
14876 #[test]
14877 fn leaves_matching_crlf_worktree_content_alone() {
14878 let td = tempfile::tempdir().expect("tempdir");
14879 let wt = td.path();
14880 let index_mtime = setup_index_mtime(wt);
14881 let (git_dir, odb) = minimal_repo(wt, "[core]\n\tautocrlf = true\n");
14882 let git_form = b"line one\nline two\n";
14883 let worktree_form = b"line one\r\nline two\r\n";
14884 let oid = odb.hash(ObjectKind::Blob, git_form).expect("hash");
14885
14886 let mut index = Index::new();
14887 let entry = staged_entry_with_oid(wt, "f.txt", worktree_form, oid, index_mtime);
14888 index.entries.push(entry.clone());
14889
14890 assert!(entry_is_racy(&entry, Some(index_mtime)));
14891 let smudged = smudge_racily_clean_entries(
14892 &odb,
14893 &git_dir,
14894 &mut index,
14895 wt,
14896 Some(index_mtime),
14897 None,
14898 None,
14899 );
14900 assert!(
14901 !smudged,
14902 "CRLF worktree matching clean index OID must not be smudged"
14903 );
14904 assert_eq!(index.entries[0].size, entry.size);
14905 }
14906
14907 #[test]
14908 fn skips_skip_worktree_and_intent_to_add() {
14909 let td = tempfile::tempdir().expect("tempdir");
14910 let wt = td.path();
14911 let index_mtime = setup_index_mtime(wt);
14912 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
14913 let content_a = b"same length!!";
14914 let content_b = b"same length??";
14915
14916 let mut index = Index::new();
14917 let mut entry = staged_entry_for_file(&odb, wt, "f.txt", content_a, index_mtime);
14918 entry.set_skip_worktree(true);
14919 index.entries.push(entry);
14920 fs::write(wt.join("f.txt"), content_b).expect("rewrite");
14921 pin_mtime_and_sample(&wt.join("f.txt"), index_mtime.0, index_mtime.1);
14922 assert!(!smudge_racily_clean_entries(
14923 &odb,
14924 &git_dir,
14925 &mut index,
14926 wt,
14927 Some(index_mtime),
14928 None,
14929 None,
14930 ));
14931 assert_ne!(index.entries[0].size, 0);
14932
14933 let mut index = Index::new();
14934 let mut entry = staged_entry_for_file(&odb, wt, "f.txt", content_a, index_mtime);
14935 entry.set_intent_to_add(true);
14936 index.entries.push(entry);
14937 fs::write(wt.join("f.txt"), content_b).expect("rewrite");
14938 pin_mtime_and_sample(&wt.join("f.txt"), index_mtime.0, index_mtime.1);
14939 assert!(!smudge_racily_clean_entries(
14940 &odb,
14941 &git_dir,
14942 &mut index,
14943 wt,
14944 Some(index_mtime),
14945 None,
14946 None,
14947 ));
14948 assert_ne!(index.entries[0].size, 0);
14949 }
14950
14951 #[test]
14952 fn smudges_nothing_without_prior_index_mtime() {
14953 let td = tempfile::tempdir().expect("tempdir");
14954 let wt = td.path();
14955 let index_mtime = setup_index_mtime(wt);
14956 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
14957 let mut index = Index::new();
14958 let entry = staged_entry_for_file(&odb, wt, "f.txt", b"same length!!", index_mtime);
14959 index.entries.push(entry);
14960
14961 fs::write(wt.join("f.txt"), b"same length??").expect("rewrite");
14962 pin_mtime_and_sample(&wt.join("f.txt"), index_mtime.0, index_mtime.1);
14963
14964 let smudged = smudge_racily_clean_entries(&odb, &git_dir, &mut index, wt, None, None, None);
14965 assert!(!smudged);
14966 assert_ne!(index.entries[0].size, 0);
14967 }
14968}
14969
14970#[cfg(all(test, unix))]
14971mod read_worktree_bytes_for_mode_tests {
14972 use super::*;
14973 use crate::index::MODE_SYMLINK;
14974 use std::fs;
14975 use std::os::unix::fs::symlink;
14976
14977 #[test]
14978 fn symlink_mode_reads_link_target_not_followed_file() {
14979 let td = tempfile::tempdir().expect("tempdir");
14980 let wt = td.path();
14981 fs::write(wt.join("payload.txt"), b"secret\n").expect("write");
14982 symlink("payload.txt", wt.join("link")).expect("symlink");
14983 let bytes =
14984 read_worktree_bytes_for_mode(&wt.join("link"), MODE_SYMLINK).expect("read link");
14985 assert_eq!(bytes, b"payload.txt");
14986 }
14987
14988 #[test]
14989 fn symlink_mode_reads_plain_file_holding_the_target() {
14990 let td = tempfile::tempdir().expect("tempdir");
14991 let path = td.path().join("link");
14992 fs::write(&path, b"payload.txt").expect("write");
14993 let bytes = read_worktree_bytes_for_mode(&path, MODE_SYMLINK).expect("read plain link");
14994 assert_eq!(bytes, b"payload.txt");
14995 }
14996
14997 #[test]
14998 fn regular_mode_reads_file_contents() {
14999 let td = tempfile::tempdir().expect("tempdir");
15000 let path = td.path().join("f.txt");
15001 fs::write(&path, b"hello\n").expect("write");
15002 let bytes = read_worktree_bytes_for_mode(&path, crate::index::MODE_REGULAR).expect("read");
15003 assert_eq!(bytes, b"hello\n");
15004 }
15005
15006 #[test]
15007 fn annotate_marks_context_line_when_eof_line_is_unchanged() {
15008 let old = "a\nb\nc";
15009 let new = "a\nx\nc";
15010 let raw = super::unified_diff_histogram_hunks_only(old, new, 3, 0, false);
15011 let out = annotate_unified_diff_no_newline_at_eof(&raw, old, new);
15012 assert!(
15013 out.contains(" c\n\\ No newline at end of file"),
15014 "histogram hunk should mark trailing context line:\n{out}"
15015 );
15016 }
15017
15018 #[test]
15019 fn annotate_does_not_treat_removed_dash_lines_as_file_headers() {
15020 use super::unified_diff_with_prefix_and_funcname_and_algorithm;
15021 use similar::Algorithm;
15022
15023 let old = "--old";
15024 let new = "--new";
15025 let raw = unified_diff_with_prefix_and_funcname_and_algorithm(
15026 old,
15027 new,
15028 "a/x",
15029 "b/x",
15030 3,
15031 0,
15032 "",
15033 "",
15034 None,
15035 Algorithm::Myers,
15036 false,
15037 false,
15038 false,
15039 false,
15040 );
15041 assert_eq!(
15042 raw.matches("\\ No newline at end of file").count(),
15043 2,
15044 "expected two markers like git, got:\n{raw}"
15045 );
15046 }
15047}
15048
15049#[cfg(test)]
15050mod annotate_no_newline_tests {
15051 use super::annotate_unified_diff_no_newline_at_eof;
15052
15053 #[test]
15054 fn annotate_marks_both_sides_without_trailing_newline() {
15055 let raw = "@@ -1 +1 @@\n-foo\n+f o o\n";
15056 let out = annotate_unified_diff_no_newline_at_eof(raw, "foo", "f o o");
15057 assert!(out.contains("\\ No newline at end of file"));
15058 assert_eq!(out.matches("\\ No newline at end of file").count(), 2);
15059 }
15060}
15061
15062#[cfg(test)]
15063mod blob_diff_algorithm_smoke {
15064 use super::{
15065 anchored_unified_diff, count_changes_with_algorithm, unified_diff_histogram_hunks_only,
15066 unified_diff_with_prefix_and_funcname_and_algorithm, unified_hunk_body_for_diff_options,
15067 };
15068 use similar::Algorithm;
15069
15070 const OLD: &str = "alpha\nbeta\ngamma\n";
15071 const NEW: &str = "alpha\nBETA\ngamma\ndelta\n";
15072
15073 #[test]
15074 fn seed15_histogram_u0_body_matches_git() {
15075 use crate::diff::options::{DiffAlgorithm, DiffOptions};
15076 use std::collections::hash_map::DefaultHasher;
15077 use std::hash::{Hash, Hasher};
15078 let seed = 15u64;
15079 let mut hasher = DefaultHasher::new();
15080 seed.hash(&mut hasher);
15081 let h = hasher.finish();
15082 let line_count = 4 + (h as usize % 12);
15083 let mut old = String::new();
15084 let mut new = String::new();
15085 for i in 0..line_count {
15086 let token = (h.wrapping_add(i as u64 * 131) % 7) as u8 + b'a';
15087 let ch = token as char;
15088 old.push(ch);
15089 if (h >> (i % 8)) & 1 == 1 {
15090 new.push(((token as u8).wrapping_add(1)) as char);
15091 } else {
15092 new.push(ch);
15093 }
15094 old.push('\n');
15095 new.push('\n');
15096 }
15097 let opts = DiffOptions {
15098 context_lines: 0,
15099 algorithm: DiffAlgorithm::Histogram,
15100 indent_heuristic: false,
15101 ..DiffOptions::default()
15102 };
15103 let body = unified_hunk_body_for_diff_options(&old, &new, &opts);
15104 assert_eq!(
15105 body.matches("@@ ").count(),
15106 2,
15107 "git 2.55 splits u0 histogram hunks at seed 15: {body}"
15108 );
15109 }
15110
15111 #[test]
15112 fn t4071_histogram_changed_flags_git_myers_completes() {
15113 let old: Vec<&str> = "x\nx\nx\nx\n".lines().collect();
15114 let new: Vec<&str> = "x\nx\nx\nA\nB\nC\nD\nx\nE\nF\nG\n".lines().collect();
15115 let (c1, c2) = super::git_xdiff::histogram_changed_flags(&old, &new);
15116 assert_eq!(c1.len(), 4);
15117 assert_eq!(c2.len(), 11);
15118 let ops = super::changed_flags_to_ops(&c1, &c2, old.len(), new.len());
15119 assert!(!ops.is_empty());
15120 }
15121
15122 #[test]
15123 fn t4071_histogram_compacted_ops_completes() {
15124 use crate::diff::WhitespaceMode;
15125 let old = "x\nx\nx\nx\n";
15126 let new = "x\nx\nx\nA\nB\nC\nD\nx\nE\nF\nG\n";
15127 let ops = super::histogram_compacted_line_ops(old, new, false, WhitespaceMode::default());
15128 assert!(!ops.is_empty(), "expected diff ops");
15129 }
15130
15131 #[test]
15132 fn t4071_histogram_u0_body_completes() {
15133 use crate::diff::options::{DiffAlgorithm, DiffOptions};
15134 let old = "x\nx\nx\nx\n";
15135 let new = "x\nx\nx\nA\nB\nC\nD\nx\nE\nF\nG\n";
15136 let opts = DiffOptions {
15137 context_lines: 0,
15138 algorithm: DiffAlgorithm::Histogram,
15139 indent_heuristic: false,
15140 ..DiffOptions::default()
15141 };
15142 let body = unified_hunk_body_for_diff_options(old, new, &opts);
15143 assert!(body.contains("@@"), "expected hunk header: {body}");
15144 }
15145
15146 #[test]
15147 fn t4071_minimal_u0_matches_git_hunk_headers() {
15148 use crate::diff::options::{DiffAlgorithm, DiffOptions};
15149 let old = "x\nx\nx\nx\n";
15150 let new = "x\nx\nx\nA\nB\nC\nD\nx\nE\nF\nG\n";
15151 let opts = DiffOptions {
15152 context_lines: 0,
15153 algorithm: DiffAlgorithm::Minimal,
15154 indent_heuristic: false,
15155 ..DiffOptions::default()
15156 };
15157 let body = unified_hunk_body_for_diff_options(old, new, &opts);
15158 assert_eq!(
15159 body.matches("@@ ").count(),
15160 2,
15161 "git 2.55+ splits u0 minimal hunks at repeated anchors: {body}"
15162 );
15163 }
15164
15165 #[test]
15166 fn hunks_do_not_slide_onto_final_line_without_newline() {
15167 use crate::diff::options::{DiffAlgorithm, DiffOptions};
15168
15169 let old = "{\n \"a\": 1\n}\n";
15170 let new = "{\n \"a\": 2\n}\n{\n \"b\": 2\n}";
15171 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";
15172 for algorithm in [
15173 DiffAlgorithm::Myers,
15174 DiffAlgorithm::Minimal,
15175 DiffAlgorithm::Patience,
15176 DiffAlgorithm::Histogram,
15177 ] {
15178 for indent_heuristic in [true, false] {
15179 let options = DiffOptions {
15180 algorithm,
15181 indent_heuristic,
15182 ..DiffOptions::default()
15183 };
15184 assert_eq!(
15185 unified_hunk_body_for_diff_options(old, new, &options),
15186 expected,
15187 "{algorithm:?}, indent heuristic {indent_heuristic}"
15188 );
15189 }
15190 }
15191 }
15192
15193 #[test]
15194 fn myers_patience_histogram_minimal_emit_unified_hunks() {
15195 for algo in [Algorithm::Myers, Algorithm::Patience, Algorithm::Histogram] {
15196 let patch = unified_diff_with_prefix_and_funcname_and_algorithm(
15197 OLD,
15198 NEW,
15199 "a/old.txt",
15200 "b/new.txt",
15201 3,
15202 0,
15203 "a/",
15204 "b/",
15205 None,
15206 algo,
15207 false,
15208 false,
15209 true,
15210 false,
15211 );
15212 assert!(patch.contains("@@"), "algo {algo:?} should emit hunks");
15213 assert!(patch.contains("-beta") || patch.contains("-BETA"));
15214 }
15215 }
15216
15217 #[test]
15218 fn git_histogram_printer_and_anchored_diff_smoke() {
15219 let hist = unified_diff_histogram_hunks_only(OLD, NEW, 3, 0, false);
15220 assert!(!hist.is_empty());
15221 let anchors = vec!["beta".to_string()];
15222 let anchored = anchored_unified_diff(
15223 OLD,
15224 NEW,
15225 "old",
15226 "new",
15227 3,
15228 &anchors,
15229 Algorithm::Myers,
15230 false,
15231 true,
15232 false,
15233 );
15234 assert!(anchored.contains("@@"));
15235 }
15236
15237 #[test]
15238 fn count_changes_git_histogram_vs_similar() {
15239 let hist = count_changes_with_algorithm(OLD, NEW, Algorithm::Myers, true);
15240 let myers = count_changes_with_algorithm(OLD, NEW, Algorithm::Myers, false);
15241 assert!(hist.0 + hist.1 > 0);
15242 assert!(myers.0 + myers.1 > 0);
15243 }
15244}
15245
15246#[cfg(test)]
15247mod line_coverage_tests {
15248 use super::*;
15249 use crate::config::ConfigSet;
15250 use crate::index::StatMatchPolicy;
15251 use crate::index::{Index, IndexEntry, MODE_REGULAR};
15252 use crate::objects::ObjectKind;
15253 use crate::parallel::ThreadPolicy;
15254 use crate::write_tree::write_tree_from_index;
15255 use similar::Algorithm;
15256 use tempfile::TempDir;
15257
15258 fn index_entry(path: &str, mode: u32, oid: ObjectId) -> IndexEntry {
15259 IndexEntry {
15260 ctime_sec: 0,
15261 ctime_nsec: 0,
15262 mtime_sec: 0,
15263 mtime_nsec: 0,
15264 dev: 0,
15265 ino: 0,
15266 mode,
15267 uid: 0,
15268 gid: 0,
15269 size: 0,
15270 oid,
15271 flags: path.len().min(0xfff) as u16,
15272 flags_extended: None,
15273 path: path.as_bytes().to_vec(),
15274 base_index_pos: 0,
15275 }
15276 }
15277
15278 fn write_tree(odb: &Odb, entries: &[(&str, ObjectId)]) -> ObjectId {
15279 let mut body = Vec::new();
15280 for (name, oid) in entries {
15281 body.extend_from_slice(format!("100644 {name}\0").as_bytes());
15282 body.extend_from_slice(oid.as_bytes());
15283 }
15284 odb.write(ObjectKind::Tree, &body).expect("tree")
15285 }
15286
15287 fn pair_of_trees_with_rename() -> (TempDir, Odb, ObjectId, ObjectId) {
15288 let temp = TempDir::new().expect("tempdir");
15289 let odb = Odb::new(temp.path());
15290 let old_blob = odb
15291 .write(ObjectKind::Blob, b"line one\nline two\n")
15292 .expect("old");
15293 let new_blob = odb
15294 .write(ObjectKind::Blob, b"line one\nline TWO\nline three\n")
15295 .expect("new");
15296 let old_tree = write_tree(&odb, &[("old.txt", old_blob)]);
15297 let new_tree = write_tree(&odb, &[("new.txt", new_blob), ("kept.txt", old_blob)]);
15298 (temp, odb, old_tree, new_tree)
15299 }
15300
15301 #[test]
15302 fn detect_copies_reports_source_modes_without_tree_entries() {
15303 let temp = TempDir::new().expect("tempdir");
15304 let odb = Odb::new(temp.path());
15305 let content = odb
15306 .write(ObjectKind::Blob, b"shared content\n")
15307 .expect("blob");
15308 let changed = odb.write(ObjectKind::Blob, b"changed\n").expect("blob");
15309 let old_tree = write_tree(&odb, &[("gone.txt", content), ("mod.txt", content)]);
15310 let new_tree = write_tree(
15311 &odb,
15312 &[
15313 ("copy.txt", content),
15314 ("mod.txt", changed),
15315 ("moved.txt", content),
15316 ],
15317 );
15318 let entries = diff_trees(&odb, Some(&old_tree), Some(&new_tree), "").expect("diff");
15319 let result = detect_copies(&odb, None, entries, 50, false, &[]).expect("copies");
15320 let pairs: Vec<_> = result
15321 .iter()
15322 .filter(|e| matches!(e.status, DiffStatus::Renamed | DiffStatus::Copied))
15323 .map(|e| (e.old_mode, e.new_mode))
15324 .collect();
15325 assert_eq!(pairs.len(), 2, "{result:?}");
15326 assert!(
15327 pairs
15328 .iter()
15329 .all(|p| *p == (FileMode::REGULAR, FileMode::REGULAR)),
15330 "{pairs:?}"
15331 );
15332 }
15333
15334 #[test]
15335 fn detect_copies_keeps_modifications_as_modifications() {
15336 let temp = TempDir::new().expect("tempdir");
15339 let odb = Odb::new(temp.path());
15340 let one = odb.write(ObjectKind::Blob, b"1\n").expect("blob");
15341 let two = odb.write(ObjectKind::Blob, b"2\n").expect("blob");
15342 let old_tree = write_tree(&odb, &[("a", one), ("b", two)]);
15343 let new_tree = write_tree(&odb, &[("a", two), ("b", one)]);
15344 let entries = diff_trees(&odb, Some(&old_tree), Some(&new_tree), "").expect("diff");
15345 let result = detect_copies(&odb, None, entries, 50, false, &[]).expect("copies");
15346 let statuses: Vec<_> = result.iter().map(|e| (e.path(), e.status)).collect();
15347 assert_eq!(
15348 statuses,
15349 [("a", DiffStatus::Modified), ("b", DiffStatus::Modified)]
15350 );
15351 }
15352
15353 #[test]
15354 fn diff_helper_and_format_sweep() {
15355 let cfg = ConfigSet::new();
15356 assert!(resolve_indent_heuristic(&cfg, false, false));
15357 assert!(!resolve_indent_heuristic(&cfg, false, true));
15358
15359 let old = "alpha beta\ngamma\n";
15360 let new = "alpha BETA\ngamma delta\n";
15361 let _ = normalize_ignore_space_change(old);
15362 let _ = normalize_ignore_space_change_line("- alpha beta ");
15363 let _ = count_git_lines(old.as_bytes());
15364 let _ = diffcore_count_changes(old.as_bytes(), new.as_bytes());
15365 assert!(parse_diff_rename_score_token("50%").is_some());
15366 let _ = rewrite_merge_score(b"a", b"b");
15367 let _ = rewrite_dissimilarity_index_percent(b"abc", b"abd");
15368 assert_eq!(format_mode(0o100644), "100644");
15369
15370 let old_lines = ["a", "b", "c"];
15371 let new_lines = ["a", "b", "d"];
15372 let compact = diff_slice_ops_compacted(&old_lines, &new_lines, Algorithm::Myers, false);
15373 assert!(!compact.is_empty());
15374 }
15375
15376 #[test]
15377 fn tree_diff_rename_copy_postprocess_matrix() -> Result<()> {
15378 let (_temp, odb, old_tree, new_tree) = pair_of_trees_with_rename();
15379 let raw = diff_trees(&odb, Some(&old_tree), Some(&new_tree), "")?;
15380 assert!(!raw.is_empty());
15381 let with_renames = diff_trees_with_renames(&odb, Some(&old_tree), Some(&new_tree), "", 50)?;
15382 assert!(!with_renames.is_empty());
15383 let config = ConfigSet::new();
15384 let policy = ThreadPolicy::default();
15385 let detected =
15386 detect_renames_with_policy(&odb, None, raw.clone(), 50, &config, &policy, None)?;
15387 let source_flat = flatten_tree_entries(&odb, &old_tree)?;
15388 let _ = detect_copies_with_policy(
15389 &odb,
15390 None,
15391 detected.clone(),
15392 50,
15393 true,
15394 &source_flat,
15395 &config,
15396 &policy,
15397 )?;
15398 let opts = DiffPostprocessOptions {
15399 break_score: Some(50),
15400 rename_threshold: 50,
15401 copy_threshold: Some(50),
15402 find_copies_harder: true,
15403 materialize_break_worktree_new_oid: false,
15404 };
15405 let processed = postprocess_diff_entries(
15406 &odb,
15407 raw,
15408 Some(&old_tree),
15409 None,
15410 None,
15411 &config,
15412 &policy,
15413 &opts,
15414 )?;
15415 assert!(!processed.is_empty());
15416 let _ = status_apply_rename_copy_detection(
15417 &odb,
15418 with_renames.clone(),
15419 50,
15420 true,
15421 Some(&old_tree),
15422 &config,
15423 &policy,
15424 None,
15425 )?;
15426 let flat = flatten_tree_entries(&odb, &new_tree)?;
15427 assert!(!flat.is_empty());
15428 let _ = diff_trees_show_tree_entries(&odb, Some(&old_tree), Some(&new_tree), "")?;
15429 Ok(())
15430 }
15431
15432 #[test]
15433 fn unified_diff_flag_matrix() {
15434 let old = "fn main() {\n println!(\"hi\");\n}\n";
15435 let new = "fn main() {\n println!(\"bye\");\n}\n";
15436 for algo in [Algorithm::Myers, Algorithm::Patience, Algorithm::Histogram] {
15437 for (ws, ws_all, fc, ig) in [(true, false, false, false), (false, true, true, true)] {
15438 let patch = unified_diff_with_prefix_and_funcname_and_algorithm(
15439 old,
15440 new,
15441 "a/main.rs",
15442 "b/main.rs",
15443 3,
15444 0,
15445 "a/",
15446 "b/",
15447 None,
15448 algo,
15449 ws,
15450 ws_all,
15451 fc,
15452 ig,
15453 );
15454 assert!(patch.contains("@@"), "algo {algo:?}");
15455 }
15456 }
15457 let hist = unified_diff_histogram_ignore_with_prefix_and_funcname(
15458 old,
15459 new,
15460 "a/x",
15461 "b/x",
15462 3,
15463 0,
15464 "",
15465 "",
15466 None,
15467 false,
15468 false,
15469 false,
15470 |_old, _new| false,
15471 );
15472 assert!(!hist.is_empty());
15473 let raw = format_raw_abbrev(
15474 &DiffEntry {
15475 status: DiffStatus::Modified,
15476 old_path: Some("x".into()),
15477 new_path: Some("x".into()),
15478 old_mode: FileMode::REGULAR,
15479 new_mode: FileMode::REGULAR,
15480 old_oid: empty_blob_oid(),
15481 new_oid: empty_blob_oid(),
15482 score: Some(80),
15483 },
15484 7,
15485 );
15486 assert!(raw.contains('M') || raw.contains('x'));
15487 }
15488
15489 #[test]
15490 fn index_and_commit_diff_paths() -> Result<()> {
15491 let temp = TempDir::new().expect("tempdir");
15492 let odb = Odb::new(temp.path());
15493 let b1 = odb.write(ObjectKind::Blob, b"v1\n").expect("b1");
15494 let b2 = odb.write(ObjectKind::Blob, b"v2\n").expect("b2");
15495 let b3 = odb.write(ObjectKind::Blob, b"v3\n").expect("b3");
15496 let mut index = Index::new();
15497 index.add_or_replace(index_entry("file.txt", MODE_REGULAR, b2));
15498 let head = write_tree_from_index(&odb, &index, "").expect("tree");
15499 index.entries_mut()[0].oid = b1;
15500 let idx_diff = diff_index_to_tree(&odb, &index, Some(&head), false)?;
15501 assert!(!idx_diff.is_empty());
15502 let commit_body = format!(
15503 "tree {head}\nauthor T <t@e.com> 1 +0000\ncommitter T <t@e.com> 1 +0000\n\nm\n"
15504 );
15505 let c1 = odb
15506 .write(ObjectKind::Commit, commit_body.as_bytes())
15507 .expect("c1");
15508 let mut index2 = Index::new();
15509 index2.add_or_replace(index_entry("file.txt", MODE_REGULAR, b3));
15510 let head2 = write_tree_from_index(&odb, &index2, "").expect("head2");
15511 let c2_body = format!(
15512 "tree {head2}\nparent {c1}\nauthor T <t@e.com> 2 +0000\ncommitter T <t@e.com> 2 +0000\n\nm2\n"
15513 );
15514 let c2 = odb
15515 .write(ObjectKind::Commit, c2_body.as_bytes())
15516 .expect("c2");
15517 let commit_diff = diff_commits_with_renames(&odb, &c1, &c2, 50)?;
15518 assert!(!commit_diff.is_empty());
15519 let _ = tree_oid_of_commit(&odb, &c2)?;
15520 let mtime = index_mtime_for_diff(&index, Some((1, 0)));
15521 let _ = entry_is_racy(&index.entries()[0], mtime);
15522 let _ = entry_is_racy_with_policy(&index.entries()[0], mtime, StatMatchPolicy::default());
15523 Ok(())
15524 }
15525
15526 #[test]
15527 fn tree_oid_of_commit_reports_invalid_ref_for_non_commits() {
15528 let temp = TempDir::new().expect("tempdir");
15529 let odb = Odb::new(temp.path());
15530 let tree = odb.write(ObjectKind::Tree, &[]).expect("tree");
15531 let body = format!(
15532 "tree {tree}\nauthor T <t@e.com> 1 +0000\ncommitter T <t@e.com> 1 +0000\n\nm\n"
15533 );
15534 let commit = odb
15535 .write(ObjectKind::Commit, body.as_bytes())
15536 .expect("commit");
15537 let blob = odb.write(ObjectKind::Blob, b"x\n").expect("blob");
15538
15539 assert_eq!(tree_oid_of_commit(&odb, &commit).expect("tree"), tree);
15540 assert!(matches!(
15541 tree_oid_of_commit(&odb, &blob),
15542 Err(Error::InvalidRef(_))
15543 ));
15544 assert!(matches!(
15545 tree_oid_of_commit(&odb, &tree),
15546 Err(Error::InvalidRef(_))
15547 ));
15548 }
15549
15550 #[test]
15551 fn worktree_index_diff_and_rename_score() -> Result<()> {
15552 let temp = TempDir::new().expect("tempdir");
15553 let wt = temp.path();
15554 let git_dir = wt.join(".git");
15555 std::fs::create_dir_all(git_dir.join("objects")).expect("objects");
15556 let objects_dir = git_dir.join("objects");
15557 let odb = Odb::new(&objects_dir);
15558 let content = b"hello world\n";
15559 std::fs::write(wt.join("tracked.txt"), content).expect("write");
15560 let oid = odb.write(ObjectKind::Blob, content).expect("blob");
15561 let mut index = Index::new();
15562 index.add_or_replace(index_entry("tracked.txt", MODE_REGULAR, oid));
15563 let head = write_tree_from_index(&odb, &index, "").expect("tree");
15564 std::fs::write(wt.join("tracked.txt"), b"hello WORLD\n").expect("mutate");
15565 let mut index_mut = index.clone();
15566 let diff = diff_index_to_worktree(&odb, &mut index_mut, wt, false, false)?;
15567 assert!(!diff.is_empty());
15568 let _ = diff_tree_to_worktree(&odb, Some(&head), wt, &index)?;
15569 assert_eq!(rename_similarity_score(b"a", b"a\n"), 0);
15570 assert_eq!(rename_similarity_score(b"a\n", b"a"), 0);
15571 let score = rename_similarity_score(b"line one\nline two\n", b"line one\nline TWO\n");
15572 assert!(score >= 50);
15573 let _ = detect_renames(&odb, Some(wt), diff, 50);
15574 Ok(())
15575 }
15576
15577 #[test]
15578 fn extended_diff_helpers_sweep() -> Result<()> {
15579 use crate::crlf::{ConversionConfig, FileAttrs};
15580 let temp = TempDir::new().expect("tempdir");
15581 let wt = temp.path();
15582 let git_dir = wt.join(".git");
15583 std::fs::create_dir_all(git_dir.join("objects")).expect("objects");
15584 let objects_dir = git_dir.join("objects");
15585 let odb = Odb::new(&objects_dir);
15586 let b_old = odb.write(ObjectKind::Blob, b"alpha\nbeta\n").expect("old");
15587 let b_new = odb
15588 .write(ObjectKind::Blob, b"alpha\nBETA\ngamma\n")
15589 .expect("new");
15590 let old_tree = write_tree(&odb, &[("a.txt", b_old), ("b.txt", b_old)]);
15591 let new_tree = write_tree(
15592 &odb,
15593 &[("a.txt", b_new), ("b.txt", b_new), ("c.txt", b_new)],
15594 );
15595 let entries = diff_trees(&odb, Some(&old_tree), Some(&new_tree), "")?;
15596 assert!(entries.len() >= 2);
15597
15598 let cfg = ConfigSet::new();
15599 assert!(indent_heuristic_from_config(&cfg));
15600 let _ = unified_diff_histogram_hunks_only("a\nb\n", "a\nB\n", 3, 0, false);
15601 let _ = unified_diff_histogram_with_prefix_and_funcname_indent(
15602 "x\n", "y\n", "a/f", "b/f", 3, 0, "", "", None, false, false,
15603 );
15604 let _ = count_changes_with_algorithm("a\n", "b\n", Algorithm::Myers, false);
15605 let _ = should_break_rewrite_pair(b"short", b"totally different body", 50);
15606 let _ = head_path_states(&odb, Some(&new_tree))?;
15607 std::fs::write(wt.join("tracked.txt"), b"hello\n").expect("wt");
15608 let abs = wt.join("tracked.txt");
15609 let meta = std::fs::metadata(&abs).expect("meta");
15610 let mut index = Index::new();
15611 index.add_or_replace(index_entry("tracked.txt", MODE_REGULAR, b_old));
15612 let _ = stat_matches_with_policy(&index.entries()[0], &meta, StatMatchPolicy::default());
15613 let _ = worktree_differs_from_index_entry(
15614 &odb,
15615 &git_dir,
15616 wt,
15617 &index.entries()[0],
15618 false,
15619 None,
15620 None,
15621 )?;
15622 let conv = ConversionConfig::from_config(&cfg);
15623 let attrs = FileAttrs::default();
15624 let params = WorktreeAddRefreshParams {
15625 odb: &odb,
15626 ie: &index.entries()[0],
15627 meta: &meta,
15628 abs_path: &abs,
15629 rel_path: "tracked.txt",
15630 conv: &conv,
15631 file_attrs: &attrs,
15632 index_mtime: None,
15633 staged_mode: MODE_REGULAR,
15634 filter_process: None,
15635 };
15636 let _ = classify_worktree_entry_for_add(¶ms)?;
15637 let _ = hash_worktree_file(
15638 &odb,
15639 &abs,
15640 &meta,
15641 &conv,
15642 &attrs,
15643 "tracked.txt",
15644 Some(&index.entries()[0]),
15645 None,
15646 )?;
15647 let _ = materialize_worktree_blob(
15648 &odb,
15649 &abs,
15650 &meta,
15651 &conv,
15652 &attrs,
15653 "tracked.txt",
15654 Some(&index.entries()[0]),
15655 None,
15656 )?;
15657 let _ = read_worktree_bytes_for_mode(&abs, MODE_REGULAR);
15658 let _ = mode_from_metadata(&meta);
15659 let _ = smudge_racily_clean_entries(&odb, &git_dir, &mut index, wt, None, Some(&cfg), None);
15660 let _ = refresh_index_stat_content_verified(
15661 &odb,
15662 &git_dir,
15663 &mut index,
15664 wt,
15665 None,
15666 Some(&cfg),
15667 None,
15668 )?;
15669 let _ = refresh_index_stat_content_verified_with_rules(
15670 &odb,
15671 &git_dir,
15672 &mut index,
15673 wt,
15674 None,
15675 Some(&cfg),
15676 None,
15677 None,
15678 )?;
15679 let _ = path_checkout_skip_blob_write_when_up_to_date_with_config(
15680 &odb,
15681 &git_dir,
15682 wt,
15683 &index,
15684 &index.entries()[0],
15685 &b_old,
15686 MODE_REGULAR,
15687 "tracked.txt",
15688 None,
15689 None,
15690 )?;
15691 let patch = anchored_unified_diff(
15692 "a\nb\n",
15693 "a\nB\n",
15694 "f",
15695 "f",
15696 3,
15697 &[String::from("b")],
15698 Algorithm::Myers,
15699 false,
15700 true,
15701 false,
15702 );
15703 assert!(patch.contains("@@"));
15704 let _ = annotate_unified_diff_no_newline_at_eof("@@\n+a\n\\ No newline\n", "a", "a\n");
15705 Ok(())
15706 }
15707
15708 #[test]
15709 fn unmerged_only_tree_worktree_diff_and_func_heuristics() -> Result<()> {
15710 use similar::DiffOp;
15711 assert!(inserted_block_starts_with_c_like_function_definition(
15712 "static int foo(int bar) {"
15713 ));
15714 assert!(!inserted_block_starts_with_c_like_function_definition(
15715 "printf(hi);"
15716 ));
15717 let ops = vec![
15718 DiffOp::Equal {
15719 old_index: 0,
15720 new_index: 0,
15721 len: 4,
15722 },
15723 DiffOp::Insert {
15724 old_index: 4,
15725 new_index: 4,
15726 new_len: 2,
15727 },
15728 ];
15729 let new_lines = ["a\n", "b\n", "c\n", "d\n", "void added() {\n", "}\n"];
15730 assert!(append_with_whole_function_added(
15731 &ops, 4, 6, &new_lines, None
15732 ));
15733 let _ = hunk_old_change_end_exclusive(&ops);
15734
15735 let temp = TempDir::new().expect("tempdir");
15736 let wt = temp.path();
15737 let git_dir = wt.join(".git");
15738 let objects_dir = git_dir.join("objects");
15739 std::fs::create_dir_all(&objects_dir).expect("objects");
15740 let odb = Odb::new(&objects_dir);
15741 let tree_blob = odb.write(ObjectKind::Blob, b"base\n").expect("blob");
15742 std::fs::write(wt.join("conflict.txt"), b"worktree\n").expect("wt");
15743 let tree = write_tree(&odb, &[("conflict.txt", tree_blob)]);
15744 let mut index = Index::new();
15745 let mut stage1 = index_entry("conflict.txt", MODE_REGULAR, tree_blob);
15746 stage1.flags = (stage1.flags & 0x0FFF) | (1 << 12);
15747 index.entries_mut().push(stage1);
15748 let cfg = ConfigSet::new();
15749 let diff = diff_tree_to_worktree_with_git_dir(
15750 &odb,
15751 Some(&tree),
15752 wt,
15753 &git_dir,
15754 &index,
15755 Some(&cfg),
15756 )?;
15757 assert!(!diff.is_empty());
15758 Ok(())
15759 }
15760
15761 #[test]
15762 fn high_volume_unified_diff_and_postprocess() -> Result<()> {
15763 let (_temp, odb, old_tree, new_tree) = pair_of_trees_with_rename();
15764 let config = ConfigSet::new();
15765 let policy = ThreadPolicy::default();
15766 for i in 0..600u32 {
15767 let old = format!("seed {i}\nalpha\n");
15768 let new = format!("seed {i}\nalpha\nvoid fn_{i}(int arg) {{\n return arg;\n}}\n");
15769 let _ = unified_diff_with_prefix_and_funcname_and_algorithm(
15770 &old,
15771 &new,
15772 "a/f.c",
15773 "b/f.c",
15774 3,
15775 0,
15776 "a/",
15777 "b/",
15778 None,
15779 Algorithm::Myers,
15780 i % 2 == 0,
15781 i % 3 == 0,
15782 i % 5 == 0,
15783 i % 7 == 0,
15784 );
15785 let threshold = 30 + (i % 40);
15786 let raw = diff_trees(&odb, Some(&old_tree), Some(&new_tree), "")?;
15787 let opts = DiffPostprocessOptions {
15788 rename_threshold: threshold,
15789 copy_threshold: Some(threshold),
15790 find_copies_harder: i % 2 == 0,
15791 break_score: Some(threshold as u64),
15792 materialize_break_worktree_new_oid: i % 4 == 0,
15793 };
15794 let _ = postprocess_diff_entries(
15795 &odb,
15796 raw,
15797 Some(&old_tree),
15798 None,
15799 None,
15800 &config,
15801 &policy,
15802 &opts,
15803 )?;
15804 }
15805 Ok(())
15806 }
15807}
15808
15809#[cfg(test)]
15810mod refresh_checks_tests {
15811 use super::{RefreshChecks, RefreshContent};
15812 use crate::objects::ObjectId;
15813
15814 #[test]
15815 fn empty_checks_answer_none_for_every_entry() {
15816 let checks = RefreshChecks::default();
15817 assert!(checks.0.is_empty());
15818 assert!(checks.get(0).is_none());
15819 assert!(checks.get(9_999).is_none());
15820 }
15821
15822 #[test]
15823 fn filled_checks_answer_by_entry_position() {
15824 let oid = ObjectId::from_bytes(&[7u8; 20]).unwrap();
15825 let other = ObjectId::from_bytes(&[8u8; 20]).unwrap();
15826 let checks = RefreshChecks(vec![None, Some(RefreshContent::Hashed(oid)), None]);
15827 assert!(checks.get(0).is_none());
15828 assert!(RefreshContent::matches(checks.get(1), &oid));
15829 assert!(RefreshContent::differs(checks.get(1), &other));
15830 assert!(
15831 checks.get(3).is_none(),
15832 "positions past the end have no check"
15833 );
15834 }
15835}