1use crate::git_path::CanonicalizeExt;
25use std::borrow::Cow;
26use std::collections::HashSet;
27use std::fs;
28#[cfg(unix)]
29use std::os::unix::fs::MetadataExt;
30use std::path::{Path, PathBuf};
31
32use crate::config::ConfigSet;
33use crate::diff_indent_heuristic;
34use crate::diff_rename_parallel::{
35 diff_rename_parallel_options, inexact_matrix_exceeds_limit, parallel_load_optional_blobs,
36 parallel_map_indices, parse_rename_limit,
37};
38use crate::error::{Error, Result};
39use crate::index::{CacheTreeNode, Index, IndexEntry, MODE_SYMLINK};
40use crate::objects::{
41 parse_commit, CommitData, FileMode, ObjectId, ObjectKind, TreeEntry, TreeEntryRef, TreeRefIter,
42};
43use crate::odb::{Odb, SessionTree};
44use crate::parallel::ThreadPolicy;
45use crate::repo::Repository;
46use crate::userdiff::FuncnameMatcher;
47use crate::worktree_rules::WorktreeRules;
48
49use super::rename_lines::RenameLineTable;
50use super::unified_emit::{
51 emit_unified_body_from_edits, line_ending_only_edits, EditSpan, LineEnding, LineSide,
52 UnifiedEmitOptions,
53};
54
55fn line_compare_keys<'a>(
61 text: &'a str,
62 side: &LineSide<'a>,
63 ws: super::WhitespaceMode,
64 normalized: &'a mut Vec<Cow<'a, str>>,
65) -> Vec<&'a str> {
66 if ws.normalizes_line_compare() {
67 normalized.extend(
69 side.lines
70 .iter()
71 .map(|line| ws.normalize_line_for_compare(line.body)),
72 );
73 return normalized.iter().map(Cow::as_ref).collect();
74 }
75 let base = text.as_ptr() as usize;
78 side.lines
79 .iter()
80 .map(|line| {
81 let start = line.body.as_ptr() as usize - base;
82 let end = start + line.body.len() + usize::from(line.ending != LineEnding::Missing);
83 &text[start..end]
84 })
85 .collect()
86}
87
88#[path = "../structured_diff.rs"]
89pub mod structured;
90
91pub(crate) fn imara_unified_hunk_slices(body: &str) -> Vec<&str> {
92 let mut starts: Vec<usize> = Vec::new();
93 if body.starts_with("@@") {
94 starts.push(0);
95 }
96 for (idx, _) in body.match_indices("\n@@ ") {
97 starts.push(idx + 1);
98 }
99 starts.push(body.len());
100 starts.windows(2).map(|w| &body[w[0]..w[1]]).collect()
101}
102
103pub(crate) fn histogram_compacted_line_ops(
105 old_content: &str,
106 new_content: &str,
107 indent_heuristic: bool,
108 ws: super::WhitespaceMode,
109) -> Vec<similar::DiffOp> {
110 let before = LineSide::from_text(old_content);
111 let after = LineSide::from_text(new_content);
112 let old_lines: Vec<&str> = before.lines.iter().map(|l| l.body).collect();
113 let new_lines: Vec<&str> = after.lines.iter().map(|l| l.body).collect();
114 let old_len = old_lines.len();
115 let new_len = new_lines.len();
116 let (mut old_normalized, mut new_normalized) = (Vec::new(), Vec::new());
117 let old_compare = line_compare_keys(old_content, &before, ws, &mut old_normalized);
118 let new_compare = line_compare_keys(new_content, &after, ws, &mut new_normalized);
119
120 let (changed1, changed2) = git_xdiff::histogram_changed_flags(&old_compare, &new_compare);
121
122 if !changed1.iter().any(|c| *c) && !changed2.iter().any(|c| *c) {
123 return Vec::new();
124 }
125 let raw = changed_flags_to_ops(&changed1, &changed2, old_len, new_len);
126 diff_indent_heuristic::apply_change_compact_to_ops_with_ws_histogram(
127 &raw,
128 diff_indent_heuristic::CompactSide {
129 lines: &old_lines,
130 keys: &old_compare,
131 compare_ids: None,
132 },
133 diff_indent_heuristic::CompactSide {
134 lines: &new_lines,
135 keys: &new_compare,
136 compare_ids: None,
137 },
138 indent_heuristic,
139 ws,
140 true,
141 )
142}
143
144pub(crate) fn histogram_unified_body_raw(
145 old_content: &str,
146 new_content: &str,
147 context_lines: usize,
148 inter_hunk_context: usize,
149 indent_heuristic: bool,
150 ws: super::WhitespaceMode,
151) -> String {
152 let before_side = LineSide::from_text(old_content);
153 let after_side = LineSide::from_text(new_content);
154 let ops = histogram_compacted_line_ops(old_content, new_content, indent_heuristic, ws);
155 let mut edits = diff_ops_to_edit_spans(&ops);
156 if edits.is_empty() {
157 if old_content != new_content && !ws.normalizes_line_compare() {
158 edits = line_ending_only_edits(&before_side, &after_side);
159 }
160 if edits.is_empty() {
161 return String::new();
162 }
163 }
164 emit_unified_body_from_edits(
165 &before_side,
166 &after_side,
167 &edits,
168 UnifiedEmitOptions {
169 context_lines,
170 inter_hunk_context,
171 function_context: false,
172 },
173 None,
174 )
175}
176
177#[allow(clippy::too_many_arguments)]
193fn diff_ops_to_ignorable_edit_spans<F>(
194 ops: &[similar::DiffOp],
195 old_lines: &[&str],
196 new_lines: &[&str],
197 is_ignorable_change: F,
198) -> Vec<EditSpan>
199where
200 F: Fn(&[&str], &[&str]) -> bool,
201{
202 use similar::DiffOp;
203 let mut out = Vec::new();
204 for op in ops {
205 match *op {
206 DiffOp::Equal { .. } => {}
207 DiffOp::Delete {
208 old_index,
209 old_len,
210 new_index,
211 } => {
212 let removed = &old_lines[old_index..old_index + old_len];
213 out.push(EditSpan {
214 before_start: old_index as i64,
215 before_len: old_len as i64,
216 after_start: new_index as i64,
217 after_len: 0,
218 skip_in_output: is_ignorable_change(removed, &[]),
219 });
220 }
221 DiffOp::Insert {
222 old_index,
223 new_index,
224 new_len,
225 } => {
226 let added = &new_lines[new_index..new_index + new_len];
227 out.push(EditSpan {
228 before_start: old_index as i64,
229 before_len: 0,
230 after_start: new_index as i64,
231 after_len: new_len as i64,
232 skip_in_output: is_ignorable_change(&[], added),
233 });
234 }
235 DiffOp::Replace {
236 old_index,
237 old_len,
238 new_index,
239 new_len,
240 } => {
241 let removed = &old_lines[old_index..old_index + old_len];
242 let added = &new_lines[new_index..new_index + new_len];
243 out.push(EditSpan {
244 before_start: old_index as i64,
245 before_len: old_len as i64,
246 after_start: new_index as i64,
247 after_len: new_len as i64,
248 skip_in_output: is_ignorable_change(removed, added),
249 });
250 }
251 }
252 }
253 out
254}
255
256#[allow(clippy::too_many_arguments)]
257pub(crate) fn histogram_unified_body_ignore_fc<F>(
258 old_content: &str,
259 new_content: &str,
260 context_lines: usize,
261 inter_hunk_context: usize,
262 function_context: bool,
263 funcname_matcher: Option<&FuncnameMatcher>,
264 indent_heuristic: bool,
265 ws: super::WhitespaceMode,
266 is_ignorable_change: F,
267) -> String
268where
269 F: Fn(&[&str], &[&str]) -> bool,
270{
271 let before = LineSide::from_text(old_content);
272 let after = LineSide::from_text(new_content);
273 let old_lines: Vec<&str> = before.lines.iter().map(|l| l.body).collect();
274 let new_lines: Vec<&str> = after.lines.iter().map(|l| l.body).collect();
275 let ops = histogram_compacted_line_ops(old_content, new_content, indent_heuristic, ws);
276 if ops.is_empty() {
277 return String::new();
278 }
279 let edits = diff_ops_to_ignorable_edit_spans(&ops, &old_lines, &new_lines, is_ignorable_change);
280 emit_unified_body_from_edits(
281 &before,
282 &after,
283 &edits,
284 UnifiedEmitOptions {
285 context_lines,
286 inter_hunk_context,
287 function_context,
288 },
289 funcname_matcher,
290 )
291}
292
293fn line_bodies<'a>(side: &LineSide<'a>) -> Vec<&'a str> {
295 side.lines.iter().map(|l| l.body).collect()
296}
297
298#[must_use]
300fn count_diff_ops(ops: &[similar::DiffOp]) -> (usize, usize) {
301 use similar::DiffOp;
302 let mut insertions = 0usize;
303 let mut deletions = 0usize;
304 for op in ops {
305 match *op {
306 DiffOp::Equal { .. } => {}
307 DiffOp::Delete { old_len, .. } => deletions += old_len,
308 DiffOp::Insert { new_len, .. } => insertions += new_len,
309 DiffOp::Replace {
310 old_len, new_len, ..
311 } => {
312 deletions += old_len;
313 insertions += new_len;
314 }
315 }
316 }
317 (insertions, deletions)
318}
319
320pub(crate) fn compacted_line_ops_for_options(
322 old_content: &str,
323 new_content: &str,
324 options: &crate::diff::options::DiffOptions,
325) -> Vec<similar::DiffOp> {
326 use crate::diff::options::DiffAlgorithm;
327
328 let ws = options.whitespace;
329 match options.algorithm {
330 DiffAlgorithm::Histogram => {
331 histogram_compacted_line_ops(old_content, new_content, options.indent_heuristic, ws)
332 }
333 DiffAlgorithm::Patience => {
334 let before = LineSide::from_text(old_content);
335 let after = LineSide::from_text(new_content);
336 if before.is_empty() && after.is_empty() {
337 return Vec::new();
338 }
339 let old_refs: Vec<&str> = before.lines.iter().map(|l| l.body).collect();
340 let new_refs: Vec<&str> = after.lines.iter().map(|l| l.body).collect();
341 let (mut old_normalized, mut new_normalized) = (Vec::new(), Vec::new());
342 let old_compare = line_compare_keys(old_content, &before, ws, &mut old_normalized);
343 let new_compare = line_compare_keys(new_content, &after, ws, &mut new_normalized);
344 let (changed1, changed2) =
345 git_xdiff::patience_changed_flags(&old_compare, &new_compare);
346 let raw = changed_flags_to_ops(&changed1, &changed2, old_refs.len(), new_refs.len());
347 diff_indent_heuristic::apply_change_compact_to_ops_with_ws_histogram(
348 &raw,
349 diff_indent_heuristic::CompactSide {
350 lines: &old_refs,
351 keys: &old_compare,
352 compare_ids: None,
353 },
354 diff_indent_heuristic::CompactSide {
355 lines: &new_refs,
356 keys: &new_compare,
357 compare_ids: None,
358 },
359 options.indent_heuristic,
360 ws,
361 false,
362 )
363 }
364 DiffAlgorithm::Myers | DiffAlgorithm::Minimal => {
365 let before = LineSide::from_text(old_content);
366 let after = LineSide::from_text(new_content);
367 if before.is_empty() && after.is_empty() {
368 return Vec::new();
369 }
370 let (mut old_normalized, mut new_normalized) = (Vec::new(), Vec::new());
371 let old_compare = line_compare_keys(old_content, &before, ws, &mut old_normalized);
372 let new_compare = line_compare_keys(new_content, &after, ws, &mut new_normalized);
373 git_xdiff_compacted_line_ops(
374 &old_compare,
375 &new_compare,
376 &before,
377 &after,
378 options.indent_heuristic,
379 options.algorithm == DiffAlgorithm::Minimal,
380 )
381 }
382 }
383}
384
385#[must_use]
387pub(crate) fn count_line_changes_with_options(
388 old_content: &str,
389 new_content: &str,
390 options: &crate::diff::options::DiffOptions,
391) -> (usize, usize) {
392 let ws = options.whitespace;
393 let ops = compacted_line_ops_for_options(old_content, new_content, options);
394 let (mut insertions, mut deletions) = count_diff_ops(&ops);
395 if insertions == 0
396 && deletions == 0
397 && old_content != new_content
398 && !ws.normalizes_line_compare()
399 {
400 let before = LineSide::from_text(old_content);
401 let after = LineSide::from_text(new_content);
402 for edit in line_ending_only_edits(&before, &after) {
403 insertions += edit.after_len as usize;
404 deletions += edit.before_len as usize;
405 }
406 }
407 (insertions, deletions)
408}
409
410type XdiffTrimmedIds = ((Vec<bool>, Vec<bool>), Vec<u32>, Vec<u32>);
412
413fn xdiff_changed_flags_trimmed_ids(
414 old_compare: &[&str],
415 new_compare: &[&str],
416 need_min: bool,
417) -> XdiffTrimmedIds {
418 use crate::line_interner::LineInterner;
419
420 let nrec1 = old_compare.len();
421 let nrec2 = new_compare.len();
422 if nrec1 == 0 && nrec2 == 0 {
423 return ((Vec::new(), Vec::new()), Vec::new(), Vec::new());
424 }
425 let mut interner = LineInterner::with_capacity(nrec1 + nrec2);
426 let ids1_full = interner.intern_all(old_compare);
427 let ids2_full = interner.intern_all(new_compare);
428
429 let lim = nrec1.min(nrec2);
430 let mut head = 0usize;
431 while head < lim && ids1_full[head] == ids2_full[head] {
432 head += 1;
433 }
434 let mut tail = 0usize;
435 let rem = lim - head;
436 while tail < rem && ids1_full[nrec1 - 1 - tail] == ids2_full[nrec2 - 1 - tail] {
437 tail += 1;
438 }
439 let shared_records = head + tail;
440 let (full1, full2) = if shared_records == 0 {
441 git_xdiff::changed_flags(&ids1_full, &ids2_full, need_min)
442 } else {
443 let cut_ids1 = &ids1_full[head..nrec1 - tail];
444 let cut_ids2 = &ids2_full[head..nrec2 - tail];
445 let nclasses = ids1_full
446 .iter()
447 .chain(&ids2_full)
448 .max()
449 .map_or(0, |&max| max as usize + 1);
450 let mut shared_counts = vec![0u32; nclasses];
451 for &id in &ids1_full[..head] {
452 shared_counts[id as usize] += 1;
453 }
454 for &id in &ids1_full[nrec1 - tail..] {
455 shared_counts[id as usize] += 1;
456 }
457 let (mid1, mid2) = git_xdiff::changed_flags_with_shared(
458 cut_ids1,
459 cut_ids2,
460 &shared_counts,
461 shared_records,
462 need_min,
463 );
464 let mut full1 = vec![false; nrec1];
465 let mut full2 = vec![false; nrec2];
466 if !mid1.is_empty() {
467 full1[head..nrec1 - tail].copy_from_slice(&mid1);
468 }
469 if !mid2.is_empty() {
470 full2[head..nrec2 - tail].copy_from_slice(&mid2);
471 }
472 (full1, full2)
473 };
474 ((full1, full2), ids1_full, ids2_full)
475}
476
477pub(crate) fn git_xdiff_compacted_line_ops(
478 old_compare: &[&str],
479 new_compare: &[&str],
480 before: &LineSide<'_>,
481 after: &LineSide<'_>,
482 indent_heuristic: bool,
483 need_min: bool,
484) -> Vec<similar::DiffOp> {
485 let ((changed1, changed2), old_key_ids, new_key_ids) =
486 xdiff_changed_flags_trimmed_ids(old_compare, new_compare, need_min);
487 let old_lines = line_bodies(before);
488 let new_lines = line_bodies(after);
489 diff_indent_heuristic::apply_change_compact_to_masks(
490 changed1,
491 changed2,
492 diff_indent_heuristic::CompactSide {
493 lines: &old_lines,
494 keys: old_compare,
495 compare_ids: Some(&old_key_ids),
496 },
497 diff_indent_heuristic::CompactSide {
498 lines: &new_lines,
499 keys: new_compare,
500 compare_ids: Some(&new_key_ids),
501 },
502 indent_heuristic,
503 )
504}
505
506fn diff_ops_to_edit_spans(ops: &[similar::DiffOp]) -> Vec<EditSpan> {
507 use similar::DiffOp;
508 let mut out = Vec::new();
509 for op in ops {
510 match *op {
511 DiffOp::Equal { .. } => {}
512 DiffOp::Delete {
513 old_index,
514 old_len,
515 new_index,
516 } => out.push(EditSpan {
517 before_start: old_index as i64,
518 before_len: old_len as i64,
519 after_start: new_index as i64,
520 after_len: 0,
521 skip_in_output: false,
522 }),
523 DiffOp::Insert {
524 old_index,
525 new_index,
526 new_len,
527 } => out.push(EditSpan {
528 before_start: old_index as i64,
529 before_len: 0,
530 after_start: new_index as i64,
531 after_len: new_len as i64,
532 skip_in_output: false,
533 }),
534 DiffOp::Replace {
535 old_index,
536 old_len,
537 new_index,
538 new_len,
539 } => out.push(EditSpan {
540 before_start: old_index as i64,
541 before_len: old_len as i64,
542 after_start: new_index as i64,
543 after_len: new_len as i64,
544 skip_in_output: false,
545 }),
546 }
547 }
548 out
549}
550
551fn git_xdiff_unified_body_raw(
553 old_content: &str,
554 new_content: &str,
555 context_lines: usize,
556 inter_hunk_context: usize,
557 indent_heuristic: bool,
558 need_min: bool,
559 ws: super::WhitespaceMode,
560) -> String {
561 git_xdiff_unified_body_raw_with_ignorable(
562 old_content,
563 new_content,
564 context_lines,
565 inter_hunk_context,
566 indent_heuristic,
567 need_min,
568 ws,
569 None::<fn(&[&str], &[&str]) -> bool>,
570 )
571}
572
573#[allow(clippy::too_many_arguments)]
574fn git_xdiff_unified_body_raw_with_ignorable<F>(
575 old_content: &str,
576 new_content: &str,
577 context_lines: usize,
578 inter_hunk_context: usize,
579 indent_heuristic: bool,
580 need_min: bool,
581 ws: super::WhitespaceMode,
582 is_ignorable_change: Option<F>,
583) -> String
584where
585 F: Fn(&[&str], &[&str]) -> bool,
586{
587 let before = LineSide::from_text(old_content);
588 let after = LineSide::from_text(new_content);
589 if before.is_empty() && after.is_empty() {
590 return String::new();
591 }
592 let (mut old_normalized, mut new_normalized) = (Vec::new(), Vec::new());
593 let old_compare = line_compare_keys(old_content, &before, ws, &mut old_normalized);
594 let new_compare = line_compare_keys(new_content, &after, ws, &mut new_normalized);
595 let ops = git_xdiff_compacted_line_ops(
596 &old_compare,
597 &new_compare,
598 &before,
599 &after,
600 indent_heuristic,
601 need_min,
602 );
603 let mut edits = if let Some(is_ignorable) = is_ignorable_change {
604 diff_ops_to_ignorable_edit_spans(
605 &ops,
606 &line_bodies(&before),
607 &line_bodies(&after),
608 is_ignorable,
609 )
610 } else {
611 diff_ops_to_edit_spans(&ops)
612 };
613 if edits.is_empty() {
614 if old_content != new_content && !ws.normalizes_line_compare() {
615 edits = line_ending_only_edits(&before, &after);
616 }
617 if edits.is_empty() {
618 return String::new();
619 }
620 }
621 emit_unified_body_from_edits(
622 &before,
623 &after,
624 &edits,
625 UnifiedEmitOptions {
626 context_lines,
627 inter_hunk_context,
628 function_context: false,
629 },
630 None,
631 )
632}
633
634#[allow(clippy::too_many_arguments)]
635fn git_patience_unified_body_raw_with_ignorable<F>(
636 old_content: &str,
637 new_content: &str,
638 context_lines: usize,
639 inter_hunk_context: usize,
640 indent_heuristic: bool,
641 ws: super::WhitespaceMode,
642 is_ignorable_change: Option<F>,
643) -> String
644where
645 F: Fn(&[&str], &[&str]) -> bool,
646{
647 let before = LineSide::from_text(old_content);
648 let after = LineSide::from_text(new_content);
649 if before.is_empty() && after.is_empty() {
650 return String::new();
651 }
652 let old_refs: Vec<&str> = before.lines.iter().map(|l| l.body).collect();
653 let new_refs: Vec<&str> = after.lines.iter().map(|l| l.body).collect();
654 let (mut old_normalized, mut new_normalized) = (Vec::new(), Vec::new());
655 let old_compare = line_compare_keys(old_content, &before, ws, &mut old_normalized);
656 let new_compare = line_compare_keys(new_content, &after, ws, &mut new_normalized);
657 let (changed1, changed2) = git_xdiff::patience_changed_flags(&old_compare, &new_compare);
658 let raw = changed_flags_to_ops(&changed1, &changed2, old_refs.len(), new_refs.len());
659 let ops = diff_indent_heuristic::apply_change_compact_to_ops_with_ws_histogram(
660 &raw,
661 diff_indent_heuristic::CompactSide {
662 lines: &old_refs,
663 keys: &old_compare,
664 compare_ids: None,
665 },
666 diff_indent_heuristic::CompactSide {
667 lines: &new_refs,
668 keys: &new_compare,
669 compare_ids: None,
670 },
671 indent_heuristic,
672 ws,
673 false,
674 );
675 let mut edits = if let Some(is_ignorable) = is_ignorable_change {
676 diff_ops_to_ignorable_edit_spans(&ops, &old_refs, &new_refs, is_ignorable)
677 } else {
678 diff_ops_to_edit_spans(&ops)
679 };
680 if edits.is_empty() {
681 if old_content != new_content && !ws.normalizes_line_compare() {
682 edits = line_ending_only_edits(&before, &after);
683 }
684 if edits.is_empty() {
685 return String::new();
686 }
687 }
688 emit_unified_body_from_edits(
689 &before,
690 &after,
691 &edits,
692 UnifiedEmitOptions {
693 context_lines,
694 inter_hunk_context,
695 function_context: false,
696 },
697 None,
698 )
699}
700
701fn blank_line_ignorable(ws: super::WhitespaceMode) -> impl Fn(&[&str], &[&str]) -> bool + Copy {
702 move |removed: &[&str], added: &[&str]| {
703 ws.ignore_blank_lines
704 && removed.iter().all(|l| crate::ws::ws_blank_line(l))
705 && added.iter().all(|l| crate::ws::ws_blank_line(l))
706 }
707}
708
709pub(crate) fn unified_hunk_body_for_diff_options(
711 old_content: &str,
712 new_content: &str,
713 options: &crate::diff::options::DiffOptions,
714) -> String {
715 use crate::diff::options::DiffAlgorithm;
716
717 let ws = options.whitespace;
718 if ws.is_active() {
719 let blank_only = blank_line_ignorable(ws);
720 if ws.normalizes_line_compare() {
721 let ignorable = ws.ignore_blank_lines.then_some(blank_only);
722 return match options.algorithm {
723 DiffAlgorithm::Myers => git_xdiff_unified_body_raw_with_ignorable(
724 old_content,
725 new_content,
726 options.context_lines,
727 options.inter_hunk_context,
728 options.indent_heuristic,
729 false,
730 ws,
731 ignorable,
732 ),
733 DiffAlgorithm::Minimal => git_xdiff_unified_body_raw_with_ignorable(
734 old_content,
735 new_content,
736 options.context_lines,
737 options.inter_hunk_context,
738 options.indent_heuristic,
739 true,
740 ws,
741 ignorable,
742 ),
743 DiffAlgorithm::Patience => git_patience_unified_body_raw_with_ignorable(
744 old_content,
745 new_content,
746 options.context_lines,
747 options.inter_hunk_context,
748 options.indent_heuristic,
749 ws,
750 ignorable,
751 ),
752 DiffAlgorithm::Histogram => {
753 if ws.ignore_blank_lines {
754 histogram_unified_body_ignore_fc(
755 old_content,
756 new_content,
757 options.context_lines,
758 options.inter_hunk_context,
759 false,
760 None,
761 options.indent_heuristic,
762 ws,
763 blank_only,
764 )
765 } else {
766 histogram_unified_body_raw(
767 old_content,
768 new_content,
769 options.context_lines,
770 options.inter_hunk_context,
771 options.indent_heuristic,
772 ws,
773 )
774 }
775 }
776 };
777 }
778 if ws.ignore_blank_lines {
779 return match options.algorithm {
780 DiffAlgorithm::Myers => git_xdiff_unified_body_raw_with_ignorable(
781 old_content,
782 new_content,
783 options.context_lines,
784 options.inter_hunk_context,
785 options.indent_heuristic,
786 false,
787 ws,
788 Some(blank_only),
789 ),
790 DiffAlgorithm::Minimal => git_xdiff_unified_body_raw_with_ignorable(
791 old_content,
792 new_content,
793 options.context_lines,
794 options.inter_hunk_context,
795 options.indent_heuristic,
796 true,
797 ws,
798 Some(blank_only),
799 ),
800 DiffAlgorithm::Patience => git_patience_unified_body_raw_with_ignorable(
801 old_content,
802 new_content,
803 options.context_lines,
804 options.inter_hunk_context,
805 options.indent_heuristic,
806 ws,
807 Some(blank_only),
808 ),
809 DiffAlgorithm::Histogram => histogram_unified_body_ignore_fc(
810 old_content,
811 new_content,
812 options.context_lines,
813 options.inter_hunk_context,
814 false,
815 None,
816 options.indent_heuristic,
817 ws,
818 blank_only,
819 ),
820 };
821 }
822 }
823 match options.algorithm {
824 DiffAlgorithm::Myers => git_xdiff_unified_body_raw(
825 old_content,
826 new_content,
827 options.context_lines,
828 options.inter_hunk_context,
829 options.indent_heuristic,
830 false,
831 ws,
832 ),
833 DiffAlgorithm::Minimal => git_xdiff_unified_body_raw(
834 old_content,
835 new_content,
836 options.context_lines,
837 options.inter_hunk_context,
838 options.indent_heuristic,
839 true,
840 ws,
841 ),
842 DiffAlgorithm::Patience => git_patience_unified_body_raw(
843 old_content,
844 new_content,
845 options.context_lines,
846 options.inter_hunk_context,
847 options.indent_heuristic,
848 ),
849 DiffAlgorithm::Histogram => histogram_unified_body_raw(
850 old_content,
851 new_content,
852 options.context_lines,
853 options.inter_hunk_context,
854 options.indent_heuristic,
855 ws,
856 ),
857 }
858}
859
860fn git_patience_unified_body_raw(
862 old_content: &str,
863 new_content: &str,
864 context_lines: usize,
865 inter_hunk_context: usize,
866 indent_heuristic: bool,
867) -> String {
868 let before = LineSide::from_text(old_content);
869 let after = LineSide::from_text(new_content);
870 if before.is_empty() && after.is_empty() {
871 return String::new();
872 }
873 let old_refs: Vec<&str> = before.lines.iter().map(|l| l.body).collect();
874 let new_refs: Vec<&str> = after.lines.iter().map(|l| l.body).collect();
875 let ws = super::WhitespaceMode::default();
876 let (mut old_normalized, mut new_normalized) = (Vec::new(), Vec::new());
877 let old_compare = line_compare_keys(old_content, &before, ws, &mut old_normalized);
878 let new_compare = line_compare_keys(new_content, &after, ws, &mut new_normalized);
879 let (changed1, changed2) = git_xdiff::patience_changed_flags(&old_compare, &new_compare);
880 let raw = changed_flags_to_ops(&changed1, &changed2, old_refs.len(), new_refs.len());
881 let ops = diff_indent_heuristic::apply_change_compact_to_ops_with_ws_histogram(
882 &raw,
883 diff_indent_heuristic::CompactSide {
884 lines: &old_refs,
885 keys: &old_compare,
886 compare_ids: None,
887 },
888 diff_indent_heuristic::CompactSide {
889 lines: &new_refs,
890 keys: &new_compare,
891 compare_ids: None,
892 },
893 indent_heuristic,
894 ws,
895 false,
896 );
897 let mut edits = diff_ops_to_edit_spans(&ops);
898 if edits.is_empty() {
899 if old_content != new_content {
900 edits = line_ending_only_edits(&before, &after);
901 }
902 if edits.is_empty() {
903 return String::new();
904 }
905 }
906 emit_unified_body_from_edits(
907 &before,
908 &after,
909 &edits,
910 UnifiedEmitOptions {
911 context_lines,
912 inter_hunk_context,
913 function_context: false,
914 },
915 None,
916 )
917}
918
919pub(crate) fn unified_hunk_header_counts(
921 header_line: &str,
922) -> Option<(usize, usize, usize, usize)> {
923 let rest = header_line.strip_prefix("@@ ")?;
924 let (old_part, rest2) = rest.split_once(" +")?;
925 let old_tok = old_part.strip_prefix('-')?.split(' ').next()?;
926 let new_tok = rest2.split(" @@").next()?.split(' ').next()?;
927
928 fn parse_side(spec: &str) -> (usize, usize) {
929 if let Some((start, count)) = spec.split_once(',') {
930 (start.parse().unwrap_or(0), count.parse().unwrap_or(0))
931 } else {
932 (spec.parse().unwrap_or(0), 1)
933 }
934 }
935
936 let (os, ol) = parse_side(old_tok);
937 let (ns, nl) = parse_side(new_tok);
938 Some((os, ol, ns, nl))
939}
940
941#[allow(clippy::too_many_arguments)]
943pub(crate) fn write_unified_diff_from_diff_options<W: std::io::Write>(
944 out: &mut W,
945 old_content: &str,
946 new_content: &str,
947 old_path: &str,
948 new_path: &str,
949 src_prefix: &str,
950 dst_prefix: &str,
951 funcname_matcher: Option<&FuncnameMatcher>,
952 options: &crate::diff::options::DiffOptions,
953 quote_path_fully: bool,
954) -> Result<()> {
955 use crate::quote_path::format_diff_path_with_prefix;
956
957 if old_path == "/dev/null" {
958 out.write_all(b"--- /dev/null\n")?;
959 } else if src_prefix.is_empty() {
960 writeln!(out, "--- {old_path}")?;
961 } else {
962 out.write_all(b"--- ")?;
963 out.write_all(
964 format_diff_path_with_prefix(src_prefix, old_path, quote_path_fully).as_bytes(),
965 )?;
966 out.write_all(b"\n")?;
967 }
968 if new_path == "/dev/null" {
969 out.write_all(b"+++ /dev/null\n")?;
970 } else if dst_prefix.is_empty() {
971 writeln!(out, "+++ {new_path}")?;
972 } else {
973 out.write_all(b"+++ ")?;
974 out.write_all(
975 format_diff_path_with_prefix(dst_prefix, new_path, quote_path_fully).as_bytes(),
976 )?;
977 out.write_all(b"\n")?;
978 }
979
980 let body = unified_hunk_body_for_diff_options(old_content, new_content, options);
981 if body.is_empty() {
982 return Ok(());
983 }
984 let old_lines: Vec<&str> = old_content.lines().collect();
985 for hunk_str in imara_unified_hunk_slices(&body) {
986 if hunk_str.is_empty() {
987 continue;
988 }
989 if let Some(first_newline) = hunk_str.find('\n') {
990 let header_line = &hunk_str[..first_newline];
991 let rest = &hunk_str[first_newline..];
992 if let Some(func_ctx) =
993 extract_function_context(header_line, &old_lines, funcname_matcher, Some(old_path))
994 {
995 out.write_all(header_line.as_bytes())?;
996 out.write_all(b" ")?;
997 out.write_all(func_ctx.as_bytes())?;
998 out.write_all(rest.as_bytes())?;
999 } else {
1000 out.write_all(hunk_str.as_bytes())?;
1001 }
1002 } else {
1003 out.write_all(hunk_str.as_bytes())?;
1004 }
1005 }
1006 Ok(())
1007}
1008
1009#[expect(clippy::too_many_arguments)]
1010fn unified_diff_git_myers_with_prefix_and_funcname(
1011 old_content: &str,
1012 new_content: &str,
1013 old_path: &str,
1014 new_path: &str,
1015 context_lines: usize,
1016 inter_hunk_context: usize,
1017 src_prefix: &str,
1018 dst_prefix: &str,
1019 funcname_matcher: Option<&FuncnameMatcher>,
1020 indent_heuristic: bool,
1021 quote_path_fully: bool,
1022) -> String {
1023 use crate::quote_path::format_diff_path_with_prefix;
1024
1025 let body = git_xdiff_unified_body_raw(
1026 old_content,
1027 new_content,
1028 context_lines,
1029 inter_hunk_context,
1030 indent_heuristic,
1031 false,
1032 super::WhitespaceMode::default(),
1033 );
1034
1035 let mut output = String::new();
1036 if old_path == "/dev/null" {
1037 output.push_str("--- /dev/null\n");
1038 } else if src_prefix.is_empty() {
1039 output.push_str(&format!("--- {old_path}\n"));
1040 } else {
1041 output.push_str("--- ");
1042 output.push_str(&format_diff_path_with_prefix(
1043 src_prefix,
1044 old_path,
1045 quote_path_fully,
1046 ));
1047 output.push('\n');
1048 }
1049 if new_path == "/dev/null" {
1050 output.push_str("+++ /dev/null\n");
1051 } else if dst_prefix.is_empty() {
1052 output.push_str(&format!("+++ {new_path}\n"));
1053 } else {
1054 output.push_str("+++ ");
1055 output.push_str(&format_diff_path_with_prefix(
1056 dst_prefix,
1057 new_path,
1058 quote_path_fully,
1059 ));
1060 output.push('\n');
1061 }
1062
1063 let old_lines: Vec<&str> = old_content.lines().collect();
1064 for hunk_str in imara_unified_hunk_slices(&body) {
1065 if hunk_str.is_empty() {
1066 continue;
1067 }
1068 if let Some(first_newline) = hunk_str.find('\n') {
1069 let header_line = &hunk_str[..first_newline];
1070 let rest = &hunk_str[first_newline..];
1071 if let Some(func_ctx) =
1072 extract_function_context(header_line, &old_lines, funcname_matcher, Some(old_path))
1073 {
1074 output.push_str(header_line);
1075 output.push(' ');
1076 output.push_str(&func_ctx);
1077 output.push_str(rest);
1078 } else {
1079 output.push_str(hunk_str);
1080 }
1081 } else {
1082 output.push_str(hunk_str);
1083 }
1084 }
1085
1086 annotate_unified_diff_no_newline_at_eof(&output, old_content, new_content)
1087}
1088
1089#[expect(clippy::too_many_arguments)]
1090fn unified_diff_git_minimal_with_prefix_and_funcname(
1091 old_content: &str,
1092 new_content: &str,
1093 old_path: &str,
1094 new_path: &str,
1095 context_lines: usize,
1096 inter_hunk_context: usize,
1097 src_prefix: &str,
1098 dst_prefix: &str,
1099 funcname_matcher: Option<&FuncnameMatcher>,
1100 indent_heuristic: bool,
1101 quote_path_fully: bool,
1102) -> String {
1103 use crate::quote_path::format_diff_path_with_prefix;
1104
1105 let body = git_xdiff_unified_body_raw(
1106 old_content,
1107 new_content,
1108 context_lines,
1109 inter_hunk_context,
1110 indent_heuristic,
1111 true,
1112 super::WhitespaceMode::default(),
1113 );
1114
1115 let mut output = String::new();
1116 if old_path == "/dev/null" {
1117 output.push_str("--- /dev/null\n");
1118 } else if src_prefix.is_empty() {
1119 output.push_str(&format!("--- {old_path}\n"));
1120 } else {
1121 output.push_str("--- ");
1122 output.push_str(&format_diff_path_with_prefix(
1123 src_prefix,
1124 old_path,
1125 quote_path_fully,
1126 ));
1127 output.push('\n');
1128 }
1129 if new_path == "/dev/null" {
1130 output.push_str("+++ /dev/null\n");
1131 } else if dst_prefix.is_empty() {
1132 output.push_str(&format!("+++ {new_path}\n"));
1133 } else {
1134 output.push_str("+++ ");
1135 output.push_str(&format_diff_path_with_prefix(
1136 dst_prefix,
1137 new_path,
1138 quote_path_fully,
1139 ));
1140 output.push('\n');
1141 }
1142
1143 let old_lines: Vec<&str> = old_content.lines().collect();
1144 for hunk_str in imara_unified_hunk_slices(&body) {
1145 if hunk_str.is_empty() {
1146 continue;
1147 }
1148 if let Some(first_newline) = hunk_str.find('\n') {
1149 let header_line = &hunk_str[..first_newline];
1150 let rest = &hunk_str[first_newline..];
1151 if let Some(func_ctx) =
1152 extract_function_context(header_line, &old_lines, funcname_matcher, Some(old_path))
1153 {
1154 output.push_str(header_line);
1155 output.push(' ');
1156 output.push_str(&func_ctx);
1157 output.push_str(rest);
1158 } else {
1159 output.push_str(hunk_str);
1160 }
1161 } else {
1162 output.push_str(hunk_str);
1163 }
1164 }
1165
1166 annotate_unified_diff_no_newline_at_eof(&output, old_content, new_content)
1167}
1168
1169#[expect(clippy::too_many_arguments)]
1170fn unified_diff_git_patience_with_prefix_and_funcname(
1171 old_content: &str,
1172 new_content: &str,
1173 old_path: &str,
1174 new_path: &str,
1175 context_lines: usize,
1176 inter_hunk_context: usize,
1177 src_prefix: &str,
1178 dst_prefix: &str,
1179 funcname_matcher: Option<&FuncnameMatcher>,
1180 indent_heuristic: bool,
1181 quote_path_fully: bool,
1182) -> String {
1183 use crate::quote_path::format_diff_path_with_prefix;
1184
1185 let body = git_patience_unified_body_raw(
1186 old_content,
1187 new_content,
1188 context_lines,
1189 inter_hunk_context,
1190 indent_heuristic,
1191 );
1192
1193 let mut output = String::new();
1194 if old_path == "/dev/null" {
1195 output.push_str("--- /dev/null\n");
1196 } else if src_prefix.is_empty() {
1197 output.push_str(&format!("--- {old_path}\n"));
1198 } else {
1199 output.push_str("--- ");
1200 output.push_str(&format_diff_path_with_prefix(
1201 src_prefix,
1202 old_path,
1203 quote_path_fully,
1204 ));
1205 output.push('\n');
1206 }
1207 if new_path == "/dev/null" {
1208 output.push_str("+++ /dev/null\n");
1209 } else if dst_prefix.is_empty() {
1210 output.push_str(&format!("+++ {new_path}\n"));
1211 } else {
1212 output.push_str("+++ ");
1213 output.push_str(&format_diff_path_with_prefix(
1214 dst_prefix,
1215 new_path,
1216 quote_path_fully,
1217 ));
1218 output.push('\n');
1219 }
1220
1221 let old_lines: Vec<&str> = old_content.lines().collect();
1222 for hunk_str in imara_unified_hunk_slices(&body) {
1223 if hunk_str.is_empty() {
1224 continue;
1225 }
1226 if let Some(first_newline) = hunk_str.find('\n') {
1227 let header_line = &hunk_str[..first_newline];
1228 let rest = &hunk_str[first_newline..];
1229 if let Some(func_ctx) =
1230 extract_function_context(header_line, &old_lines, funcname_matcher, Some(old_path))
1231 {
1232 output.push_str(header_line);
1233 output.push(' ');
1234 output.push_str(&func_ctx);
1235 output.push_str(rest);
1236 } else {
1237 output.push_str(hunk_str);
1238 }
1239 } else {
1240 output.push_str(hunk_str);
1241 }
1242 }
1243
1244 annotate_unified_diff_no_newline_at_eof(&output, old_content, new_content)
1245}
1246
1247#[must_use]
1251pub fn unified_diff_histogram_hunks_only(
1252 old_content: &str,
1253 new_content: &str,
1254 context_lines: usize,
1255 inter_hunk_context: usize,
1256 indent_heuristic: bool,
1257) -> String {
1258 histogram_unified_body_raw(
1259 old_content,
1260 new_content,
1261 context_lines,
1262 inter_hunk_context,
1263 indent_heuristic,
1264 super::WhitespaceMode::default(),
1265 )
1266}
1267
1268#[expect(clippy::too_many_arguments)]
1269fn unified_diff_histogram_with_prefix_and_funcname_indent(
1270 old_content: &str,
1271 new_content: &str,
1272 old_path: &str,
1273 new_path: &str,
1274 context_lines: usize,
1275 inter_hunk_context: usize,
1276 src_prefix: &str,
1277 dst_prefix: &str,
1278 funcname_matcher: Option<&FuncnameMatcher>,
1279 indent_heuristic: bool,
1280 quote_path_fully: bool,
1281) -> String {
1282 use crate::quote_path::format_diff_path_with_prefix;
1283
1284 let body = histogram_unified_body_raw(
1285 old_content,
1286 new_content,
1287 context_lines,
1288 inter_hunk_context,
1289 indent_heuristic,
1290 super::WhitespaceMode::default(),
1291 );
1292
1293 let mut output = String::new();
1294 if old_path == "/dev/null" {
1295 output.push_str("--- /dev/null\n");
1296 } else if src_prefix.is_empty() {
1297 output.push_str(&format!("--- {old_path}\n"));
1298 } else {
1299 output.push_str("--- ");
1300 output.push_str(&format_diff_path_with_prefix(
1301 src_prefix,
1302 old_path,
1303 quote_path_fully,
1304 ));
1305 output.push('\n');
1306 }
1307 if new_path == "/dev/null" {
1308 output.push_str("+++ /dev/null\n");
1309 } else if dst_prefix.is_empty() {
1310 output.push_str(&format!("+++ {new_path}\n"));
1311 } else {
1312 output.push_str("+++ ");
1313 output.push_str(&format_diff_path_with_prefix(
1314 dst_prefix,
1315 new_path,
1316 quote_path_fully,
1317 ));
1318 output.push('\n');
1319 }
1320
1321 let old_lines: Vec<&str> = old_content.lines().collect();
1322 for hunk_str in imara_unified_hunk_slices(&body) {
1323 if hunk_str.is_empty() {
1324 continue;
1325 }
1326 if let Some(first_newline) = hunk_str.find('\n') {
1327 let header_line = &hunk_str[..first_newline];
1328 let rest = &hunk_str[first_newline..];
1329 if let Some(func_ctx) =
1330 extract_function_context(header_line, &old_lines, funcname_matcher, Some(old_path))
1331 {
1332 output.push_str(header_line);
1333 output.push(' ');
1334 output.push_str(&func_ctx);
1335 output.push_str(rest);
1336 } else {
1337 output.push_str(hunk_str);
1338 }
1339 } else {
1340 output.push_str(hunk_str);
1341 }
1342 }
1343
1344 annotate_unified_diff_no_newline_at_eof(&output, old_content, new_content)
1345}
1346
1347#[allow(clippy::too_many_arguments)]
1357pub fn unified_diff_histogram_ignore_with_prefix_and_funcname<F>(
1358 old_content: &str,
1359 new_content: &str,
1360 old_path: &str,
1361 new_path: &str,
1362 context_lines: usize,
1363 inter_hunk_context: usize,
1364 src_prefix: &str,
1365 dst_prefix: &str,
1366 funcname_matcher: Option<&FuncnameMatcher>,
1367 quote_path_fully: bool,
1368 function_context: bool,
1369 indent_heuristic: bool,
1370 is_ignorable_change: F,
1371) -> String
1372where
1373 F: Fn(&[&str], &[&str]) -> bool,
1374{
1375 use crate::quote_path::format_diff_path_with_prefix;
1376
1377 let body = histogram_unified_body_ignore_fc(
1378 old_content,
1379 new_content,
1380 context_lines,
1381 inter_hunk_context,
1382 function_context,
1383 funcname_matcher,
1384 indent_heuristic,
1385 super::WhitespaceMode::default(),
1386 is_ignorable_change,
1387 );
1388 if body.is_empty() {
1389 return String::new();
1390 }
1391
1392 let mut output = String::new();
1393 if old_path == "/dev/null" {
1394 output.push_str("--- /dev/null\n");
1395 } else if src_prefix.is_empty() {
1396 output.push_str(&format!("--- {old_path}\n"));
1397 } else {
1398 output.push_str("--- ");
1399 output.push_str(&format_diff_path_with_prefix(
1400 src_prefix,
1401 old_path,
1402 quote_path_fully,
1403 ));
1404 output.push('\n');
1405 }
1406 if new_path == "/dev/null" {
1407 output.push_str("+++ /dev/null\n");
1408 } else if dst_prefix.is_empty() {
1409 output.push_str(&format!("+++ {new_path}\n"));
1410 } else {
1411 output.push_str("+++ ");
1412 output.push_str(&format_diff_path_with_prefix(
1413 dst_prefix,
1414 new_path,
1415 quote_path_fully,
1416 ));
1417 output.push('\n');
1418 }
1419
1420 let old_lines: Vec<&str> = old_content.lines().collect();
1421 for hunk_str in imara_unified_hunk_slices(&body) {
1422 if hunk_str.is_empty() {
1423 continue;
1424 }
1425 if let Some(first_newline) = hunk_str.find('\n') {
1426 let header_line = &hunk_str[..first_newline];
1427 let rest = &hunk_str[first_newline..];
1428 if let Some(func_ctx) =
1429 extract_function_context(header_line, &old_lines, funcname_matcher, Some(old_path))
1430 {
1431 output.push_str(header_line);
1432 output.push(' ');
1433 output.push_str(&func_ctx);
1434 output.push_str(rest);
1435 } else {
1436 output.push_str(hunk_str);
1437 }
1438 } else {
1439 output.push_str(hunk_str);
1440 }
1441 }
1442
1443 output
1444}
1445
1446#[must_use]
1448pub fn indent_heuristic_from_config(config: &ConfigSet) -> bool {
1449 match config.get_bool("diff.indentHeuristic") {
1450 Some(Ok(b)) => b,
1451 Some(Err(_)) | None => true,
1452 }
1453}
1454
1455#[must_use]
1457pub fn resolve_indent_heuristic(
1458 config: &ConfigSet,
1459 cli_indent_heuristic: bool,
1460 cli_no_indent_heuristic: bool,
1461) -> bool {
1462 if cli_no_indent_heuristic {
1463 false
1464 } else if cli_indent_heuristic {
1465 true
1466 } else {
1467 indent_heuristic_from_config(config)
1468 }
1469}
1470
1471mod git_xdiff {
1481 use std::cell::RefCell;
1482
1483 const XDL_MAX_COST_MIN: i64 = 256;
1484 const XDL_HEUR_MIN_COST: i64 = 256;
1485 const XDL_SNAKE_CNT: i64 = 20;
1486 const XDL_K_HEUR: i64 = 4;
1487 const XDL_LINE_MAX: i64 = i64::MAX;
1488 const XDL_KPDIS_RUN: i64 = 4;
1489 const XDL_MAX_EQLIMIT: i64 = 1024;
1490 const XDL_SIMSCAN_WINDOW: i64 = 100;
1491
1492 const DISCARD: u8 = 0;
1494 const KEEP: u8 = 1;
1495 const INVESTIGATE: u8 = 2;
1496
1497 fn bogosqrt(mut n: i64) -> i64 {
1499 let mut i: i64 = 1;
1500 while n > 0 {
1501 i <<= 1;
1502 n >>= 2;
1503 }
1504 i
1505 }
1506
1507 struct XdAlgoEnv {
1508 mxcost: i64,
1509 snake_cnt: i64,
1510 heur_min: i64,
1511 }
1512
1513 struct XdpSplit {
1514 i1: i64,
1515 i2: i64,
1516 min_lo: bool,
1517 min_hi: bool,
1518 }
1519
1520 struct XdFile<'a> {
1523 #[allow(dead_code)]
1524 ids: &'a [u32],
1525 changed: Vec<u8>,
1526 reference_index: Vec<usize>,
1527 effective_ids: Vec<u32>,
1529 dstart: i64,
1530 dend: i64,
1531 nreff: i64,
1532 }
1533
1534 #[inline]
1535 fn effective_id(xdf: &XdFile<'_>, idx: i64) -> u32 {
1536 xdf.effective_ids[idx as usize]
1537 }
1538
1539 struct XdiffScratch {
1540 count1: Vec<i64>,
1541 count2: Vec<i64>,
1542 action1: Vec<u8>,
1543 action2: Vec<u8>,
1544 kvd: Vec<i64>,
1545 }
1546
1547 impl XdiffScratch {
1548 fn prepare_counts(&mut self, nclasses: usize) {
1549 if self.count1.len() < nclasses {
1550 self.count1.resize(nclasses, 0);
1551 self.count2.resize(nclasses, 0);
1552 }
1553 self.count1[..nclasses].fill(0);
1554 self.count2[..nclasses].fill(0);
1555 }
1556
1557 fn prepare_actions(&mut self, nrec1: usize, nrec2: usize) {
1558 let a1 = nrec1 + 1;
1559 let a2 = nrec2 + 1;
1560 if self.action1.len() < a1 {
1561 self.action1.resize(a1, 0);
1562 } else {
1563 self.action1[..a1].fill(0);
1564 }
1565 if self.action2.len() < a2 {
1566 self.action2.resize(a2, 0);
1567 } else {
1568 self.action2[..a2].fill(0);
1569 }
1570 }
1571
1572 fn prepare_kvd(&mut self, kvd_len: usize) {
1573 if self.kvd.len() < kvd_len {
1574 self.kvd.resize(kvd_len, 0);
1575 }
1576 }
1577 }
1578
1579 thread_local! { static XDIFF_SCRATCH: RefCell<XdiffScratch> = const {
1581 RefCell::new(XdiffScratch {
1582 count1: Vec::new(),
1583 count2: Vec::new(),
1584 action1: Vec::new(),
1585 action2: Vec::new(),
1586 kvd: Vec::new(),
1587 })
1588 };
1589 }
1590
1591 fn bool_changed(changed: Vec<u8>) -> Vec<bool> {
1592 changed.into_iter().map(|b| b != 0).collect()
1593 }
1594
1595 #[allow(clippy::too_many_arguments)]
1598 fn xdl_split(
1599 xdf1: &XdFile<'_>,
1600 off1: i64,
1601 lim1: i64,
1602 xdf2: &XdFile<'_>,
1603 off2: i64,
1604 lim2: i64,
1605 kvdf: &mut [i64],
1606 kvdb: &mut [i64],
1607 koff: i64,
1608 need_min: bool,
1609 spl: &mut XdpSplit,
1610 xenv: &XdAlgoEnv,
1611 ) -> i64 {
1612 let dmin = off1 - lim2;
1613 let dmax = lim1 - off2;
1614 let fmid = off1 - off2;
1615 let bmid = lim1 - lim2;
1616 let odd = ((fmid - bmid) & 1) != 0;
1617 let mut fmin = fmid;
1618 let mut fmax = fmid;
1619 let mut bmin = bmid;
1620 let mut bmax = bmid;
1621
1622 let kf = |k: i64| -> usize { (k + koff) as usize };
1623
1624 kvdf[kf(fmid)] = off1;
1625 kvdb[kf(bmid)] = lim1;
1626
1627 let mut ec: i64 = 1;
1628 loop {
1629 let mut got_snake = false;
1630
1631 if fmin > dmin {
1632 fmin -= 1;
1633 kvdf[kf(fmin - 1)] = -1;
1634 } else {
1635 fmin += 1;
1636 }
1637 if fmax < dmax {
1638 fmax += 1;
1639 kvdf[kf(fmax + 1)] = -1;
1640 } else {
1641 fmax -= 1;
1642 }
1643
1644 let mut d = fmax;
1645 while d >= fmin {
1646 let mut i1 = if kvdf[kf(d - 1)] >= kvdf[kf(d + 1)] {
1647 kvdf[kf(d - 1)] + 1
1648 } else {
1649 kvdf[kf(d + 1)]
1650 };
1651 let prev1 = i1;
1652 let mut i2 = i1 - d;
1653 while i1 < lim1 && i2 < lim2 && effective_id(xdf1, i1) == effective_id(xdf2, i2) {
1654 i1 += 1;
1655 i2 += 1;
1656 }
1657 if i1 - prev1 > xenv.snake_cnt {
1658 got_snake = true;
1659 }
1660 kvdf[kf(d)] = i1;
1661 if odd && bmin <= d && d <= bmax && kvdb[kf(d)] <= i1 {
1662 spl.i1 = i1;
1663 spl.i2 = i2;
1664 spl.min_lo = true;
1665 spl.min_hi = true;
1666 return ec;
1667 }
1668 d -= 2;
1669 }
1670
1671 if bmin > dmin {
1672 bmin -= 1;
1673 kvdb[kf(bmin - 1)] = XDL_LINE_MAX;
1674 } else {
1675 bmin += 1;
1676 }
1677 if bmax < dmax {
1678 bmax += 1;
1679 kvdb[kf(bmax + 1)] = XDL_LINE_MAX;
1680 } else {
1681 bmax -= 1;
1682 }
1683
1684 let mut d = bmax;
1685 while d >= bmin {
1686 let mut i1 = if kvdb[kf(d - 1)] < kvdb[kf(d + 1)] {
1687 kvdb[kf(d - 1)]
1688 } else {
1689 kvdb[kf(d + 1)] - 1
1690 };
1691 let prev1 = i1;
1692 let mut i2 = i1 - d;
1693 while i1 > off1
1694 && i2 > off2
1695 && effective_id(xdf1, i1 - 1) == effective_id(xdf2, i2 - 1)
1696 {
1697 i1 -= 1;
1698 i2 -= 1;
1699 }
1700 if prev1 - i1 > xenv.snake_cnt {
1701 got_snake = true;
1702 }
1703 kvdb[kf(d)] = i1;
1704 if !odd && fmin <= d && d <= fmax && i1 <= kvdf[kf(d)] {
1705 spl.i1 = i1;
1706 spl.i2 = i2;
1707 spl.min_lo = true;
1708 spl.min_hi = true;
1709 return ec;
1710 }
1711 d -= 2;
1712 }
1713
1714 if need_min {
1715 ec += 1;
1716 continue;
1717 }
1718
1719 if got_snake && ec > xenv.heur_min {
1720 let mut best = 0i64;
1721 let mut d = fmax;
1722 while d >= fmin {
1723 let dd = if d > fmid { d - fmid } else { fmid - d };
1724 let i1 = kvdf[kf(d)];
1725 let i2 = i1 - d;
1726 let v = (i1 - off1) + (i2 - off2) - dd;
1727 if v > XDL_K_HEUR * ec
1728 && v > best
1729 && off1 + xenv.snake_cnt <= i1
1730 && i1 < lim1
1731 && off2 + xenv.snake_cnt <= i2
1732 && i2 < lim2
1733 {
1734 let mut k = 1i64;
1735 while effective_id(xdf1, i1 - k) == effective_id(xdf2, i2 - k) {
1736 if k == xenv.snake_cnt {
1737 best = v;
1738 spl.i1 = i1;
1739 spl.i2 = i2;
1740 break;
1741 }
1742 k += 1;
1743 }
1744 }
1745 d -= 2;
1746 }
1747 if best > 0 {
1748 spl.min_lo = true;
1749 spl.min_hi = false;
1750 return ec;
1751 }
1752
1753 let mut best = 0i64;
1754 let mut d = bmax;
1755 while d >= bmin {
1756 let dd = if d > bmid { d - bmid } else { bmid - d };
1757 let i1 = kvdb[kf(d)];
1758 let i2 = i1 - d;
1759 let v = (lim1 - i1) + (lim2 - i2) - dd;
1760 if v > XDL_K_HEUR * ec
1761 && v > best
1762 && off1 < i1
1763 && i1 <= lim1 - xenv.snake_cnt
1764 && off2 < i2
1765 && i2 <= lim2 - xenv.snake_cnt
1766 {
1767 let mut k = 0i64;
1768 while effective_id(xdf1, i1 + k) == effective_id(xdf2, i2 + k) {
1769 if k == xenv.snake_cnt - 1 {
1770 best = v;
1771 spl.i1 = i1;
1772 spl.i2 = i2;
1773 break;
1774 }
1775 k += 1;
1776 }
1777 }
1778 d -= 2;
1779 }
1780 if best > 0 {
1781 spl.min_lo = false;
1782 spl.min_hi = true;
1783 return ec;
1784 }
1785 }
1786
1787 if ec >= xenv.mxcost {
1788 let mut fbest = -1i64;
1789 let mut fbest1 = -1i64;
1790 let mut d = fmax;
1791 while d >= fmin {
1792 let mut i1 = kvdf[kf(d)].min(lim1);
1793 let mut i2 = i1 - d;
1794 if lim2 < i2 {
1795 i1 = lim2 + d;
1796 i2 = lim2;
1797 }
1798 if fbest < i1 + i2 {
1799 fbest = i1 + i2;
1800 fbest1 = i1;
1801 }
1802 d -= 2;
1803 }
1804
1805 let mut bbest = XDL_LINE_MAX;
1806 let mut bbest1 = XDL_LINE_MAX;
1807 let mut d = bmax;
1808 while d >= bmin {
1809 let mut i1 = off1.max(kvdb[kf(d)]);
1810 let mut i2 = i1 - d;
1811 if i2 < off2 {
1812 i1 = off2 + d;
1813 i2 = off2;
1814 }
1815 if i1 + i2 < bbest {
1816 bbest = i1 + i2;
1817 bbest1 = i1;
1818 }
1819 d -= 2;
1820 }
1821
1822 if (lim1 + lim2) - bbest < fbest - (off1 + off2) {
1823 spl.i1 = fbest1;
1824 spl.i2 = fbest - fbest1;
1825 spl.min_lo = true;
1826 spl.min_hi = false;
1827 } else {
1828 spl.i1 = bbest1;
1829 spl.i2 = bbest - bbest1;
1830 spl.min_lo = false;
1831 spl.min_hi = true;
1832 }
1833 return ec;
1834 }
1835
1836 ec += 1;
1837 }
1838 }
1839
1840 #[allow(clippy::too_many_arguments)]
1842 fn xdl_recs_cmp(
1843 xdf1: &mut XdFile<'_>,
1844 mut off1: i64,
1845 mut lim1: i64,
1846 xdf2: &mut XdFile<'_>,
1847 mut off2: i64,
1848 mut lim2: i64,
1849 kvdf: &mut [i64],
1850 kvdb: &mut [i64],
1851 koff: i64,
1852 need_min: bool,
1853 xenv: &XdAlgoEnv,
1854 ) {
1855 while off1 < lim1 && off2 < lim2 && effective_id(xdf1, off1) == effective_id(xdf2, off2) {
1856 off1 += 1;
1857 off2 += 1;
1858 }
1859 while off1 < lim1
1860 && off2 < lim2
1861 && effective_id(xdf1, lim1 - 1) == effective_id(xdf2, lim2 - 1)
1862 {
1863 lim1 -= 1;
1864 lim2 -= 1;
1865 }
1866
1867 if off1 == lim1 {
1868 while off2 < lim2 {
1869 let r = xdf2.reference_index[off2 as usize];
1870 xdf2.changed[r] = 1;
1871 off2 += 1;
1872 }
1873 } else if off2 == lim2 {
1874 while off1 < lim1 {
1875 let r = xdf1.reference_index[off1 as usize];
1876 xdf1.changed[r] = 1;
1877 off1 += 1;
1878 }
1879 } else {
1880 let mut spl = XdpSplit {
1881 i1: 0,
1882 i2: 0,
1883 min_lo: false,
1884 min_hi: false,
1885 };
1886 xdl_split(
1887 xdf1, off1, lim1, xdf2, off2, lim2, kvdf, kvdb, koff, need_min, &mut spl, xenv,
1888 );
1889 xdl_recs_cmp(
1890 xdf1, off1, spl.i1, xdf2, off2, spl.i2, kvdf, kvdb, koff, spl.min_lo, xenv,
1891 );
1892 xdl_recs_cmp(
1893 xdf1, spl.i1, lim1, xdf2, spl.i2, lim2, kvdf, kvdb, koff, spl.min_hi, xenv,
1894 );
1895 }
1896 }
1897
1898 fn clean_mmatch(action: &[u8], i: i64, mut s: i64, mut e: i64) -> bool {
1900 if i - s > XDL_SIMSCAN_WINDOW {
1901 s = i - XDL_SIMSCAN_WINDOW;
1902 }
1903 if e - i > XDL_SIMSCAN_WINDOW {
1904 e = i + XDL_SIMSCAN_WINDOW;
1905 }
1906
1907 let mut rdis0 = 0i64;
1908 let mut rpdis0 = 1i64;
1909 let mut r = 1i64;
1910 while i - r >= s {
1911 match action[(i - r) as usize] {
1912 DISCARD => rdis0 += 1,
1913 INVESTIGATE => rpdis0 += 1,
1914 _ => break, }
1916 r += 1;
1917 }
1918 if rdis0 == 0 {
1919 return false;
1920 }
1921 let mut rdis1 = 0i64;
1922 let mut rpdis1 = 1i64;
1923 let mut r = 1i64;
1924 while i + r <= e {
1925 match action[(i + r) as usize] {
1926 DISCARD => rdis1 += 1,
1927 INVESTIGATE => rpdis1 += 1,
1928 _ => break, }
1930 r += 1;
1931 }
1932 if rdis1 == 0 {
1933 return false;
1934 }
1935 rdis1 += rdis0;
1936 rpdis1 += rpdis0;
1937 rpdis1 * XDL_KPDIS_RUN < (rpdis1 + rdis1)
1938 }
1939
1940 pub fn changed_flags(ids1: &[u32], ids2: &[u32], need_min: bool) -> (Vec<bool>, Vec<bool>) {
1946 changed_flags_with_shared(ids1, ids2, &[], 0, need_min)
1947 }
1948
1949 pub fn changed_flags_with_shared(
1956 ids1: &[u32],
1957 ids2: &[u32],
1958 shared_counts: &[u32],
1959 shared_records: usize,
1960 need_min: bool,
1961 ) -> (Vec<bool>, Vec<bool>) {
1962 XDIFF_SCRATCH.with(|cell| {
1963 changed_flags_with_shared_inner(
1964 cell,
1965 ids1,
1966 ids2,
1967 shared_counts,
1968 shared_records,
1969 need_min,
1970 )
1971 })
1972 }
1973
1974 fn changed_flags_with_shared_inner(
1975 scratch: &RefCell<XdiffScratch>,
1976 ids1: &[u32],
1977 ids2: &[u32],
1978 shared_counts: &[u32],
1979 shared_records: usize,
1980 need_min: bool,
1981 ) -> (Vec<bool>, Vec<bool>) {
1982 let nrec1 = ids1.len();
1983 let nrec2 = ids2.len();
1984
1985 let nclasses = ids1
1987 .iter()
1988 .chain(ids2)
1989 .max()
1990 .map_or(0, |&max| max as usize + 1);
1991 let mut work = scratch.borrow_mut();
1992 work.prepare_counts(nclasses);
1993 for &id in ids1 {
1994 work.count1[id as usize] += 1;
1995 }
1996 for &id in ids2 {
1997 work.count2[id as usize] += 1;
1998 }
1999 for (id, &shared) in shared_counts.iter().enumerate().take(nclasses) {
2000 work.count1[id] += i64::from(shared);
2001 work.count2[id] += i64::from(shared);
2002 }
2003
2004 let mut xdf1 = XdFile {
2005 ids: ids1,
2006 changed: vec![0u8; nrec1],
2007 reference_index: Vec::new(),
2008 effective_ids: Vec::new(),
2009 dstart: 0,
2010 dend: nrec1 as i64 - 1,
2011 nreff: 0,
2012 };
2013 let mut xdf2 = XdFile {
2014 ids: ids2,
2015 changed: vec![0u8; nrec2],
2016 reference_index: Vec::new(),
2017 effective_ids: Vec::new(),
2018 dstart: 0,
2019 dend: nrec2 as i64 - 1,
2020 nreff: 0,
2021 };
2022
2023 let lim = nrec1.min(nrec2) as i64;
2025 let mut i = 0i64;
2026 while i < lim && ids1[i as usize] == ids2[i as usize] {
2027 i += 1;
2028 }
2029 xdf1.dstart = i;
2030 xdf2.dstart = i;
2031 let mut j = 0i64;
2032 let rem = lim - i;
2033 while j < rem && ids1[nrec1 - 1 - j as usize] == ids2[nrec2 - 1 - j as usize] {
2034 j += 1;
2035 }
2036 xdf1.dend = nrec1 as i64 - j - 1;
2037 xdf2.dend = nrec2 as i64 - j - 1;
2038
2039 work.prepare_actions(nrec1, nrec2);
2041
2042 let mlim1 = if need_min {
2043 i64::MAX
2044 } else {
2045 let mut mlim = bogosqrt((nrec1 + shared_records) as i64);
2046 if mlim > XDL_MAX_EQLIMIT {
2047 mlim = XDL_MAX_EQLIMIT;
2048 }
2049 mlim
2050 };
2051 let mut idx = xdf1.dstart;
2052 while idx <= xdf1.dend {
2053 let id = ids1[idx as usize];
2054 let nm = work.count2[id as usize];
2055 work.action1[idx as usize] = if nm == 0 {
2056 DISCARD
2057 } else if nm >= mlim1 {
2058 INVESTIGATE
2059 } else {
2060 KEEP
2061 };
2062 idx += 1;
2063 }
2064
2065 let mlim2 = if need_min {
2066 i64::MAX
2067 } else {
2068 let mut mlim = bogosqrt((nrec2 + shared_records) as i64);
2069 if mlim > XDL_MAX_EQLIMIT {
2070 mlim = XDL_MAX_EQLIMIT;
2071 }
2072 mlim
2073 };
2074 let mut idx = xdf2.dstart;
2075 while idx <= xdf2.dend {
2076 let id = ids2[idx as usize];
2077 let nm = work.count1[id as usize];
2078 work.action2[idx as usize] = if nm == 0 {
2079 DISCARD
2080 } else if nm >= mlim2 {
2081 INVESTIGATE
2082 } else {
2083 KEEP
2084 };
2085 idx += 1;
2086 }
2087
2088 let mut idx = xdf1.dstart;
2089 while idx <= xdf1.dend {
2090 let id = ids1[idx as usize];
2091 let a = work.action1[idx as usize];
2092 if a == KEEP
2093 || (a == INVESTIGATE
2094 && !clean_mmatch(&work.action1[..nrec1 + 1], idx, xdf1.dstart, xdf1.dend))
2095 {
2096 xdf1.reference_index.push(idx as usize);
2097 xdf1.effective_ids.push(id);
2098 } else {
2099 xdf1.changed[idx as usize] = 1;
2100 }
2101 idx += 1;
2102 }
2103 xdf1.nreff = xdf1.reference_index.len() as i64;
2104
2105 let mut idx = xdf2.dstart;
2106 while idx <= xdf2.dend {
2107 let id = ids2[idx as usize];
2108 let a = work.action2[idx as usize];
2109 if a == KEEP
2110 || (a == INVESTIGATE
2111 && !clean_mmatch(&work.action2[..nrec2 + 1], idx, xdf2.dstart, xdf2.dend))
2112 {
2113 xdf2.reference_index.push(idx as usize);
2114 xdf2.effective_ids.push(id);
2115 } else {
2116 xdf2.changed[idx as usize] = 1;
2117 }
2118 idx += 1;
2119 }
2120 xdf2.nreff = xdf2.reference_index.len() as i64;
2121
2122 let ndiags = xdf1.nreff + xdf2.nreff + 3;
2124 let kvd_len = (2 * ndiags + 2) as usize;
2125 work.prepare_kvd(kvd_len);
2126 let kvd = &mut work.kvd[..kvd_len];
2127 let koff = xdf2.nreff + 1;
2129 let (kvdf_slice, kvdb_slice) = kvd.split_at_mut(ndiags as usize);
2130 let xenv = XdAlgoEnv {
2133 mxcost: bogosqrt(ndiags).max(XDL_MAX_COST_MIN),
2134 snake_cnt: XDL_SNAKE_CNT,
2135 heur_min: XDL_HEUR_MIN_COST,
2136 };
2137
2138 let nreff1 = xdf1.nreff;
2139 let nreff2 = xdf2.nreff;
2140 xdl_recs_cmp(
2141 &mut xdf1, 0, nreff1, &mut xdf2, 0, nreff2, kvdf_slice, kvdb_slice, koff, need_min,
2142 &xenv,
2143 );
2144
2145 (bool_changed(xdf1.changed), bool_changed(xdf2.changed))
2146 }
2147
2148 pub fn patience_changed_flags(lines1: &[&str], lines2: &[&str]) -> (Vec<bool>, Vec<bool>) {
2150 let mut changed1 = vec![false; lines1.len()];
2151 let mut changed2 = vec![false; lines2.len()];
2152 patience_recurse(
2153 lines1,
2154 lines2,
2155 &mut changed1,
2156 &mut changed2,
2157 0,
2158 lines1.len(),
2159 0,
2160 lines2.len(),
2161 );
2162 (changed1, changed2)
2163 }
2164
2165 #[allow(clippy::too_many_arguments)]
2166 fn patience_recurse(
2167 lines1: &[&str],
2168 lines2: &[&str],
2169 changed1: &mut [bool],
2170 changed2: &mut [bool],
2171 o_start: usize,
2172 o_end: usize,
2173 n_start: usize,
2174 n_end: usize,
2175 ) {
2176 if o_start >= o_end && n_start >= n_end {
2177 return;
2178 }
2179 if o_start >= o_end {
2180 changed2[n_start..n_end].fill(true);
2181 return;
2182 }
2183 if n_start >= n_end {
2184 changed1[o_start..o_end].fill(true);
2185 return;
2186 }
2187
2188 let anchors = patience_anchors(lines1, lines2, o_start, o_end, n_start, n_end);
2189 if anchors.is_empty() {
2190 merge_myers_region(
2191 lines1, lines2, changed1, changed2, o_start, o_end, n_start, n_end,
2192 );
2193 return;
2194 }
2195
2196 let mut prev_o = o_start;
2197 let mut prev_n = n_start;
2198 for (ao, an) in anchors {
2199 patience_recurse(lines1, lines2, changed1, changed2, prev_o, ao, prev_n, an);
2200 prev_o = ao + 1;
2201 prev_n = an + 1;
2202 }
2203 patience_recurse(
2204 lines1, lines2, changed1, changed2, prev_o, o_end, prev_n, n_end,
2205 );
2206 }
2207
2208 #[allow(clippy::too_many_arguments)]
2209 fn patience_anchors(
2210 lines1: &[&str],
2211 lines2: &[&str],
2212 o_start: usize,
2213 o_end: usize,
2214 n_start: usize,
2215 n_end: usize,
2216 ) -> Vec<(usize, usize)> {
2217 use std::collections::HashMap;
2218
2219 let old_slice = &lines1[o_start..o_end];
2220 let new_slice = &lines2[n_start..n_end];
2221 let mut old_pos: HashMap<&str, Vec<usize>> = HashMap::new();
2222 let mut new_pos: HashMap<&str, Vec<usize>> = HashMap::new();
2223 for (i, line) in old_slice.iter().enumerate() {
2224 old_pos.entry(*line).or_default().push(o_start + i);
2225 }
2226 for (i, line) in new_slice.iter().enumerate() {
2227 new_pos.entry(*line).or_default().push(n_start + i);
2228 }
2229
2230 let mut pairs: Vec<(usize, usize)> = Vec::new();
2231 for (line, o_indices) in &old_pos {
2232 if o_indices.len() != 1 {
2233 continue;
2234 }
2235 if let Some(n_indices) = new_pos.get(line) {
2236 if n_indices.len() == 1 {
2237 pairs.push((o_indices[0], n_indices[0]));
2238 }
2239 }
2240 }
2241 pairs.sort_by_key(|p| p.0);
2242 longest_increasing_by_second(&pairs)
2243 }
2244
2245 fn longest_increasing_by_second(pairs: &[(usize, usize)]) -> Vec<(usize, usize)> {
2246 if pairs.is_empty() {
2247 return Vec::new();
2248 }
2249 let n = pairs.len();
2250 let mut tails = vec![0usize; n];
2251 let mut prev = vec![usize::MAX; n];
2252 let mut len = 0usize;
2253 for i in 0..n {
2254 let v = pairs[i].1;
2255 let mut lo = 0usize;
2256 let mut hi = len;
2257 while lo < hi {
2258 let mid = (lo + hi) / 2;
2259 if pairs[tails[mid]].1 < v {
2260 lo = mid + 1;
2261 } else {
2262 hi = mid;
2263 }
2264 }
2265 if lo > 0 {
2266 prev[i] = tails[lo - 1];
2267 }
2268 tails[lo] = i;
2269 if lo == len {
2270 len += 1;
2271 }
2272 }
2273 let mut out = Vec::with_capacity(len);
2274 let mut k = tails[len.saturating_sub(1)];
2275 while k != usize::MAX {
2276 out.push(pairs[k]);
2277 k = prev[k];
2278 }
2279 out.reverse();
2280 out
2281 }
2282
2283 pub(crate) fn histogram_changed_flags(
2284 lines1: &[&str],
2285 lines2: &[&str],
2286 ) -> (Vec<bool>, Vec<bool>) {
2287 crate::diff::git_histogram::histogram_changed_flags(
2288 lines1,
2289 lines2,
2290 |changed1, changed2, o_start, o_end, n_start, n_end| {
2291 merge_myers_region(
2292 lines1, lines2, changed1, changed2, o_start, o_end, n_start, n_end,
2293 );
2294 },
2295 )
2296 }
2297
2298 #[allow(clippy::too_many_arguments)]
2299 fn merge_myers_region(
2300 lines1: &[&str],
2301 lines2: &[&str],
2302 changed1: &mut [bool],
2303 changed2: &mut [bool],
2304 o_start: usize,
2305 o_end: usize,
2306 n_start: usize,
2307 n_end: usize,
2308 ) {
2309 use crate::line_interner::LineInterner;
2310
2311 let old_slice = &lines1[o_start..o_end];
2312 let new_slice = &lines2[n_start..n_end];
2313 if old_slice.is_empty() && new_slice.is_empty() {
2314 return;
2315 }
2316 let mut interner = LineInterner::with_capacity(old_slice.len() + new_slice.len());
2317 let ids1 = interner.intern_all(old_slice);
2318 let ids2 = interner.intern_all(new_slice);
2319 let (sub1, sub2) = changed_flags(&ids1, &ids2, false);
2320 for (i, ch) in sub1.iter().enumerate() {
2321 if *ch {
2322 changed1[o_start + i] = true;
2323 }
2324 }
2325 for (j, ch) in sub2.iter().enumerate() {
2326 if *ch {
2327 changed2[n_start + j] = true;
2328 }
2329 }
2330 }
2331}
2332
2333pub(crate) fn myers_rediff_changed_masks_for_region<'a>(
2335 old_lines: &[&'a str],
2336 new_lines: &[&'a str],
2337 ws: super::WhitespaceMode,
2338) -> (Vec<bool>, Vec<bool>) {
2339 use crate::line_interner::LineInterner;
2340
2341 if old_lines.is_empty() && new_lines.is_empty() {
2342 return (Vec::new(), Vec::new());
2343 }
2344 let normalize = |lines: &[&'a str]| -> Vec<Cow<'a, str>> {
2345 lines
2346 .iter()
2347 .map(|line| ws.normalize_line_for_compare(line))
2348 .collect()
2349 };
2350 let (old_normalized, new_normalized) = (normalize(old_lines), normalize(new_lines));
2351 let old_keys: Vec<&str> = old_normalized.iter().map(Cow::as_ref).collect();
2352 let new_keys: Vec<&str> = new_normalized.iter().map(Cow::as_ref).collect();
2353 let mut interner = LineInterner::with_capacity(old_keys.len() + new_keys.len());
2354 let ids1 = interner.intern_all(&old_keys);
2355 let ids2 = interner.intern_all(&new_keys);
2356 git_xdiff::changed_flags(&ids1, &ids2, false)
2357}
2358
2359fn changed_flags_to_ops(
2361 changed1: &[bool],
2362 changed2: &[bool],
2363 old_len: usize,
2364 new_len: usize,
2365) -> Vec<similar::DiffOp> {
2366 use similar::DiffOp;
2367 let mut ops: Vec<DiffOp> = Vec::new();
2368 let mut i = 0usize;
2369 let mut j = 0usize;
2370 while i < old_len || j < new_len {
2371 let del = i < old_len && changed1[i];
2372 let ins = j < new_len && changed2[j];
2373 if !del && !ins {
2374 if i < old_len && j < new_len {
2375 let start_i = i;
2377 let start_j = j;
2378 while i < old_len && j < new_len && !changed1[i] && !changed2[j] {
2379 i += 1;
2380 j += 1;
2381 }
2382 ops.push(DiffOp::Equal {
2383 old_index: start_i,
2384 new_index: start_j,
2385 len: i - start_i,
2386 });
2387 } else if i < old_len {
2388 let start_i = i;
2389 while i < old_len {
2390 i += 1;
2391 }
2392 ops.push(DiffOp::Delete {
2393 old_index: start_i,
2394 old_len: i - start_i,
2395 new_index: j,
2396 });
2397 } else if j < new_len {
2398 let start_j = j;
2399 while j < new_len {
2400 j += 1;
2401 }
2402 ops.push(DiffOp::Insert {
2403 old_index: i,
2404 new_index: start_j,
2405 new_len: j - start_j,
2406 });
2407 }
2408 } else {
2409 let start_i = i;
2411 let start_j = j;
2412 while i < old_len && changed1[i] {
2413 i += 1;
2414 }
2415 while j < new_len && changed2[j] {
2416 j += 1;
2417 }
2418 let dlen = i - start_i;
2419 let ilen = j - start_j;
2420 if dlen > 0 && ilen > 0 {
2421 ops.push(DiffOp::Replace {
2422 old_index: start_i,
2423 old_len: dlen,
2424 new_index: start_j,
2425 new_len: ilen,
2426 });
2427 } else if dlen > 0 {
2428 ops.push(DiffOp::Delete {
2429 old_index: start_i,
2430 old_len: dlen,
2431 new_index: start_j,
2432 });
2433 } else if ilen > 0 {
2434 ops.push(DiffOp::Insert {
2435 old_index: start_i,
2436 new_index: start_j,
2437 new_len: ilen,
2438 });
2439 }
2440 }
2441 }
2442 ops
2443}
2444
2445#[must_use]
2447pub fn diff_slice_ops_compacted(
2448 old_lines: &[&str],
2449 new_lines: &[&str],
2450 algorithm: similar::Algorithm,
2451 indent_heuristic: bool,
2452) -> Vec<similar::DiffOp> {
2453 diff_indent_heuristic::diff_slice_ops_compacted(
2454 old_lines,
2455 new_lines,
2456 algorithm,
2457 indent_heuristic,
2458 )
2459}
2460
2461#[must_use]
2464#[allow(clippy::too_many_arguments)]
2468pub(crate) fn line_diff_ops_git_xdiff_compacted_ids(
2469 old_ids: &[u32],
2470 new_ids: &[u32],
2471 shared_counts: &[u32],
2472 shared_records: usize,
2473 old_lines: &[&str],
2474 new_lines: &[&str],
2475 indent_heuristic: bool,
2476 ws: super::WhitespaceMode,
2477) -> Vec<similar::DiffOp> {
2478 let (changed1, changed2) = git_xdiff::changed_flags_with_shared(
2479 old_ids,
2480 new_ids,
2481 shared_counts,
2482 shared_records,
2483 false,
2484 );
2485 let ops = changed_flags_to_ops(&changed1, &changed2, old_lines.len(), new_lines.len());
2486 diff_indent_heuristic::apply_change_compact_to_ops_with_ws(
2487 &ops,
2488 old_lines,
2489 new_lines,
2490 indent_heuristic,
2491 ws,
2492 )
2493}
2494
2495#[cfg(test)]
2497#[must_use]
2498pub(crate) fn xdiff_new_line_changed_flags(
2499 old_lines: &[&str],
2500 new_lines: &[&str],
2501 indent_heuristic: bool,
2502) -> Vec<bool> {
2503 let mut interner =
2504 crate::line_interner::LineInterner::with_capacity(old_lines.len() + new_lines.len());
2505 let ids1 = interner.intern_all(old_lines);
2506 let ids2 = interner.intern_all(new_lines);
2507 let (changed1, changed2) = git_xdiff::changed_flags(&ids1, &ids2, false);
2508 let ops = changed_flags_to_ops(&changed1, &changed2, old_lines.len(), new_lines.len());
2509 let compacted = diff_indent_heuristic::apply_change_compact_to_ops(
2510 &ops,
2511 old_lines,
2512 new_lines,
2513 indent_heuristic,
2514 );
2515 let mut flags = vec![false; new_lines.len()];
2516 for op in compacted {
2517 match op.tag() {
2518 similar::DiffTag::Insert | similar::DiffTag::Replace => {
2519 for idx in op.new_range() {
2520 if idx < flags.len() {
2521 flags[idx] = true;
2522 }
2523 }
2524 }
2525 similar::DiffTag::Equal | similar::DiffTag::Delete => {}
2526 }
2527 }
2528 flags
2529}
2530
2531#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2533#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
2534#[cfg_attr(feature = "serde-models", serde(rename_all = "snake_case"))]
2535pub enum DiffStatus {
2536 Added,
2538 Deleted,
2540 Modified,
2542 Renamed,
2544 Copied,
2546 TypeChanged,
2548 Unmerged,
2550}
2551
2552impl DiffStatus {
2553 #[must_use]
2555 pub fn letter(&self) -> char {
2556 match self {
2557 Self::Added => 'A',
2558 Self::Deleted => 'D',
2559 Self::Modified => 'M',
2560 Self::Renamed => 'R',
2561 Self::Copied => 'C',
2562 Self::TypeChanged => 'T',
2563 Self::Unmerged => 'U',
2564 }
2565 }
2566
2567 #[must_use]
2574 pub(crate) fn for_content_change(old_mode: u32, new_mode: u32) -> Self {
2575 if old_mode & 0o170000 != new_mode & 0o170000 {
2576 Self::TypeChanged
2577 } else {
2578 Self::Modified
2579 }
2580 }
2581}
2582
2583#[derive(Debug, Clone, PartialEq, Eq)]
2585#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
2586pub struct DiffEntry {
2587 pub status: DiffStatus,
2589 pub old_path: Option<String>,
2591 pub new_path: Option<String>,
2593 pub old_mode: FileMode,
2595 pub new_mode: FileMode,
2597 pub old_oid: ObjectId,
2599 pub new_oid: ObjectId,
2602 pub score: Option<u32>,
2604}
2605
2606impl DiffEntry {
2607 #[must_use]
2609 pub fn path(&self) -> &str {
2610 self.new_path
2611 .as_deref()
2612 .or(self.old_path.as_deref())
2613 .unwrap_or("")
2614 }
2615
2616 #[must_use]
2621 pub fn display_path(&self) -> String {
2622 match self.status {
2623 DiffStatus::Renamed | DiffStatus::Copied => {
2624 let old = self.old_path.as_deref().unwrap_or("");
2625 let new = self.new_path.as_deref().unwrap_or("");
2626 if old.is_empty() || new.is_empty() {
2627 self.path().to_owned()
2628 } else {
2629 format!("{old} -> {new}")
2630 }
2631 }
2632 _ => self.path().to_owned(),
2633 }
2634 }
2635}
2636
2637#[must_use]
2639pub fn zero_oid() -> ObjectId {
2640 ObjectId::from_bytes(&[0u8; 20]).unwrap_or_else(|_| {
2641 panic!("internal error: failed to create zero OID");
2643 })
2644}
2645
2646#[must_use]
2648pub fn empty_blob_oid() -> ObjectId {
2649 ObjectId::from_hex("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391").unwrap_or_else(|_| {
2650 panic!("internal error: failed to create empty blob OID");
2652 })
2653}
2654
2655pub fn diff_trees(
2670 odb: &Odb,
2671 old_tree_oid: Option<&ObjectId>,
2672 new_tree_oid: Option<&ObjectId>,
2673 prefix: &str,
2674) -> Result<Vec<DiffEntry>> {
2675 crate::trace::trace_span!("grit.diff.tree");
2676 diff_trees_opts(odb, old_tree_oid, new_tree_oid, prefix, false, None)
2677}
2678
2679pub(crate) fn diff_trees_with_index(
2682 odb: &Odb,
2683 index: &Index,
2684 old_tree_oid: Option<&ObjectId>,
2685 new_tree_oid: Option<&ObjectId>,
2686 prefix: &str,
2687 trust_index_for_old_tree: bool,
2688) -> Result<Vec<DiffEntry>> {
2689 let hint = (trust_index_for_old_tree
2690 && old_tree_oid.is_some()
2691 && index.entries_sorted
2692 && !index.entries.iter().any(|e| e.stage() != 0))
2693 .then_some(DiffTreesWalkHint {
2694 index: Some(index),
2695 old_tree_matches: true,
2696 ..DiffTreesWalkHint::default()
2697 });
2698 diff_trees_opts(
2699 odb,
2700 old_tree_oid,
2701 new_tree_oid,
2702 prefix,
2703 false,
2704 hint.as_ref(),
2705 )
2706}
2707
2708pub(crate) fn diff_trees_outside_sparse_directories(
2712 odb: &Odb,
2713 index: &Index,
2714 old_tree_oid: Option<&ObjectId>,
2715 new_tree_oid: Option<&ObjectId>,
2716) -> Result<Vec<DiffEntry>> {
2717 crate::trace::trace_span!("grit.diff.tree");
2718 let hint = index
2719 .has_sparse_directory_placeholders()
2720 .then_some(DiffTreesWalkHint {
2721 index: Some(index),
2722 skip_sparse_directories: true,
2723 ..DiffTreesWalkHint::default()
2724 });
2725 diff_trees_opts(odb, old_tree_oid, new_tree_oid, "", false, hint.as_ref())
2726}
2727
2728pub(crate) fn diff_trees_for_pathspecs(
2732 odb: &Odb,
2733 old_tree_oid: Option<&ObjectId>,
2734 new_tree_oid: Option<&ObjectId>,
2735 pathspecs: &[String],
2736) -> Result<Vec<DiffEntry>> {
2737 if pathspecs.is_empty() {
2738 return diff_trees(odb, old_tree_oid, new_tree_oid, "");
2739 }
2740 crate::trace::trace_span!("grit.diff.tree");
2741 let hint = DiffTreesWalkHint {
2742 pathspecs,
2743 ..DiffTreesWalkHint::default()
2744 };
2745 let mut entries = diff_trees_opts(odb, old_tree_oid, new_tree_oid, "", false, Some(&hint))?;
2746 entries.retain(|entry| {
2747 let path = entry.path();
2748 !path.is_empty() && crate::pathspec::matches_pathspec_list(path, pathspecs)
2749 });
2750 Ok(entries)
2751}
2752
2753#[derive(Clone, Copy, Default)]
2755struct DiffTreesWalkHint<'a> {
2756 index: Option<&'a Index>,
2757 old_tree_matches: bool,
2759 skip_sparse_directories: bool,
2761 pathspecs: &'a [String],
2763}
2764
2765impl DiffTreesWalkHint<'_> {
2766 fn skips(&self, dir: &str) -> bool {
2768 if dir.is_empty() {
2769 return false;
2770 }
2771 if !self.pathspecs.is_empty()
2772 && !crate::pathspec::pathspec_entry_is_relevant(dir, true, self.pathspecs)
2773 {
2774 return true;
2775 }
2776 let Some(index) = self.index.filter(|_| self.skip_sparse_directories) else {
2777 return false;
2778 };
2779 let mut row = crate::repo_relpath::storage_bytes(dir).into_owned();
2780 row.push(b'/');
2781 index
2782 .find_entry_pos(&row, 0)
2783 .is_some_and(|pos| index.entries[pos].is_sparse_directory_placeholder())
2784 }
2785}
2786
2787pub fn diff_tree_path_change_count(
2795 odb: &Odb,
2796 old_tree_oid: Option<&ObjectId>,
2797 new_tree_oid: Option<&ObjectId>,
2798 prefix: &str,
2799) -> Result<usize> {
2800 if old_tree_oid == new_tree_oid {
2801 return Ok(0);
2802 }
2803 let old_entries = match old_tree_oid {
2804 Some(oid) => read_tree(odb, oid)?,
2805 None => Vec::new(),
2806 };
2807 let new_entries = match new_tree_oid {
2808 Some(oid) => read_tree(odb, oid)?,
2809 None => Vec::new(),
2810 };
2811 let mut count = 0usize;
2812 diff_tree_entries_count(odb, &old_entries, &new_entries, prefix, false, &mut count)?;
2813 Ok(count)
2814}
2815
2816pub fn diff_trees_with_renames(
2825 odb: &Odb,
2826 old_tree_oid: Option<&ObjectId>,
2827 new_tree_oid: Option<&ObjectId>,
2828 prefix: &str,
2829 rename_threshold: u32,
2830) -> Result<Vec<DiffEntry>> {
2831 let config = ConfigSet::new();
2832 let policy = ThreadPolicy::default();
2833 diff_trees_with_renames_and_policy(
2834 odb,
2835 old_tree_oid,
2836 new_tree_oid,
2837 prefix,
2838 rename_threshold,
2839 &config,
2840 &policy,
2841 )
2842}
2843
2844pub fn diff_trees_with_renames_and_policy(
2850 odb: &Odb,
2851 old_tree_oid: Option<&ObjectId>,
2852 new_tree_oid: Option<&ObjectId>,
2853 prefix: &str,
2854 rename_threshold: u32,
2855 config: &ConfigSet,
2856 thread_policy: &ThreadPolicy,
2857) -> Result<Vec<DiffEntry>> {
2858 let mut entries = diff_trees(odb, old_tree_oid, new_tree_oid, prefix)?;
2859 if rename_threshold <= 100 {
2860 entries = detect_renames_with_policy(
2861 odb,
2862 None,
2863 entries,
2864 rename_threshold,
2865 config,
2866 thread_policy,
2867 None,
2868 )?;
2869 }
2870 Ok(entries)
2871}
2872
2873pub fn tree_oid_of_commit(odb: &Odb, commit_oid: &ObjectId) -> Result<ObjectId> {
2880 odb.commit_tree(commit_oid).map_err(|err| match err {
2881 Error::UnexpectedObjectKind { .. } => {
2882 Error::InvalidRef(format!("invalid ref: {commit_oid} is not a commit"))
2883 }
2884 other => other,
2885 })
2886}
2887
2888pub fn tree_oid_of_commit_in_repo(
2894 repo: &crate::repo::Repository,
2895 commit_oid: &ObjectId,
2896) -> Result<ObjectId> {
2897 if let Some(chain) = repo.odb().commit_graph_chain() {
2898 if let Some(tree) = chain.graph_commit_tree(commit_oid) {
2899 return Ok(tree);
2900 }
2901 }
2902 tree_oid_of_commit(repo.odb(), commit_oid)
2903}
2904
2905pub fn diff_commits(odb: &Odb, base: &ObjectId, tip: &ObjectId) -> Result<Vec<DiffEntry>> {
2911 let base_tree = tree_oid_of_commit(odb, base)?;
2912 let tip_tree = tree_oid_of_commit(odb, tip)?;
2913 diff_trees(odb, Some(&base_tree), Some(&tip_tree), "")
2914}
2915
2916pub fn diff_commits_with_renames(
2925 odb: &Odb,
2926 base: &ObjectId,
2927 tip: &ObjectId,
2928 rename_threshold: u32,
2929) -> Result<Vec<DiffEntry>> {
2930 let config = ConfigSet::new();
2931 let policy = ThreadPolicy::default();
2932 diff_commits_with_renames_and_policy(odb, base, tip, rename_threshold, &config, &policy)
2933}
2934
2935pub fn diff_commits_with_renames_and_policy(
2941 odb: &Odb,
2942 base: &ObjectId,
2943 tip: &ObjectId,
2944 rename_threshold: u32,
2945 config: &ConfigSet,
2946 thread_policy: &ThreadPolicy,
2947) -> Result<Vec<DiffEntry>> {
2948 let base_tree = tree_oid_of_commit(odb, base)?;
2949 let tip_tree = tree_oid_of_commit(odb, tip)?;
2950 diff_trees_with_renames_and_policy(
2951 odb,
2952 Some(&base_tree),
2953 Some(&tip_tree),
2954 "",
2955 rename_threshold,
2956 config,
2957 thread_policy,
2958 )
2959}
2960
2961pub fn diff_trees_show_tree_entries(
2968 odb: &Odb,
2969 old_tree_oid: Option<&ObjectId>,
2970 new_tree_oid: Option<&ObjectId>,
2971 prefix: &str,
2972) -> Result<Vec<DiffEntry>> {
2973 diff_trees_opts(odb, old_tree_oid, new_tree_oid, prefix, true, None)
2974}
2975
2976fn diff_subtrees_in_place(
2977 odb: &Odb,
2978 old_tree_oid: Option<&ObjectId>,
2979 new_tree_oid: Option<&ObjectId>,
2980 prefix: &str,
2981 show_trees: bool,
2982 index_hint: Option<&DiffTreesWalkHint<'_>>,
2983 result: &mut Vec<DiffEntry>,
2984) -> Result<()> {
2985 if index_hint.is_some_and(|hint| hint.skips(prefix)) {
2986 return Ok(());
2987 }
2988 if !show_trees {
2989 if let (Some(a), Some(b)) = (old_tree_oid, new_tree_oid) {
2990 if a == b {
2991 return Ok(());
2992 }
2993 }
2994 }
2995 let old_tree = old_tree_oid
2998 .map(|oid| odb.cached_tree_for_walk(oid))
2999 .transpose()?;
3000 let new_tree = new_tree_oid
3001 .map(|oid| odb.cached_tree_for_walk(oid))
3002 .transpose()?;
3003 let oid_len = odb.hash_algo().len();
3004 diff_tree_entries_opts(
3005 odb,
3006 WalkEntries::new(old_tree.as_ref(), oid_len),
3007 WalkEntries::new(new_tree.as_ref(), oid_len),
3008 prefix,
3009 show_trees,
3010 index_hint,
3011 result,
3012 )
3013}
3014
3015enum WalkEntries<'a> {
3018 Parsed(std::slice::Iter<'a, TreeEntry>),
3019 Raw(TreeRefIter<'a>),
3020}
3021
3022impl<'a> WalkEntries<'a> {
3023 fn new(tree: Option<&'a SessionTree>, oid_len: usize) -> Self {
3025 match tree {
3026 Some(SessionTree::Parsed(entries)) => Self::Parsed(entries.iter()),
3027 Some(SessionTree::Raw(data)) => Self::Raw(TreeRefIter::with_oid_len(data, oid_len)),
3028 None => Self::Parsed([].iter()),
3029 }
3030 }
3031
3032 fn next_entry(&mut self) -> Result<Option<TreeEntryRef<'a>>> {
3033 match self {
3034 Self::Parsed(entries) => Ok(entries.next().map(|entry| TreeEntryRef {
3035 mode: entry.mode,
3036 name: &entry.name,
3037 oid: entry.oid,
3038 })),
3039 Self::Raw(entries) => entries.next().transpose(),
3040 }
3041 }
3042}
3043
3044fn diff_trees_opts(
3045 odb: &Odb,
3046 old_tree_oid: Option<&ObjectId>,
3047 new_tree_oid: Option<&ObjectId>,
3048 prefix: &str,
3049 show_trees: bool,
3050 index_hint: Option<&DiffTreesWalkHint<'_>>,
3051) -> Result<Vec<DiffEntry>> {
3052 let mut result = Vec::new();
3053 diff_subtrees_in_place(
3054 odb,
3055 old_tree_oid,
3056 new_tree_oid,
3057 prefix,
3058 show_trees,
3059 index_hint,
3060 &mut result,
3061 )?;
3062 Ok(result)
3063}
3064
3065#[cfg(test)]
3066use std::cell::RefCell;
3067#[cfg(test)]
3068use std::rc::Rc;
3069#[cfg(test)]
3070use std::sync::atomic::{AtomicUsize, Ordering};
3071
3072#[cfg(test)]
3073thread_local! {
3074 static DIFF_TREE_READ_HOOK: RefCell<Option<Rc<AtomicUsize>>> = const { RefCell::new(None) };
3075}
3076
3077#[cfg(test)]
3079pub(crate) fn set_diff_tree_read_hook_for_tests(hook: Option<Rc<AtomicUsize>>) {
3080 DIFF_TREE_READ_HOOK.with(|cell| *cell.borrow_mut() = hook);
3081}
3082
3083fn read_tree_from_odb(odb: &Odb, oid: &ObjectId) -> Result<Vec<TreeEntry>> {
3085 #[cfg(test)]
3086 if let Some(counter) = DIFF_TREE_READ_HOOK.with(|cell| cell.borrow().clone()) {
3087 counter.fetch_add(1, Ordering::Relaxed);
3088 }
3089 Ok((*odb.cached_tree_entries(oid)?).clone())
3090}
3091
3092fn read_tree(odb: &Odb, oid: &ObjectId) -> Result<Vec<TreeEntry>> {
3094 read_tree_from_odb(odb, oid)
3095}
3096
3097fn diff_tree_entries_count(
3098 odb: &Odb,
3099 old: &[TreeEntry],
3100 new: &[TreeEntry],
3101 prefix: &str,
3102 show_trees: bool,
3103 count: &mut usize,
3104) -> Result<()> {
3105 let mut oi = 0;
3106 let mut ni = 0;
3107
3108 while oi < old.len() || ni < new.len() {
3109 match (old.get(oi), new.get(ni)) {
3110 (Some(o), Some(n)) => {
3111 let cmp = crate::objects::tree_entry_cmp(
3112 &o.name,
3113 is_tree_mode(o.mode),
3114 &n.name,
3115 is_tree_mode(n.mode),
3116 );
3117 match cmp {
3118 std::cmp::Ordering::Less => {
3119 emit_deleted_count(odb, o, prefix, show_trees, count)?;
3120 oi += 1;
3121 }
3122 std::cmp::Ordering::Greater => {
3123 emit_added_count(odb, n, prefix, show_trees, count)?;
3124 ni += 1;
3125 }
3126 std::cmp::Ordering::Equal => {
3127 if o.oid != n.oid
3128 || crate::index::canon_mode(o.mode) != crate::index::canon_mode(n.mode)
3129 {
3130 let path = crate::repo_relpath::format_path_stored(prefix, &o.name);
3131 if is_tree_mode(o.mode) && is_tree_mode(n.mode) {
3132 if show_trees {
3133 *count += 1;
3134 }
3135 *count += diff_tree_path_change_count(
3136 odb,
3137 Some(&o.oid),
3138 Some(&n.oid),
3139 &path,
3140 )?;
3141 } else if is_tree_mode(o.mode) != is_tree_mode(n.mode) {
3142 emit_deleted_count(odb, o, prefix, show_trees, count)?;
3143 emit_added_count(odb, n, prefix, show_trees, count)?;
3144 } else {
3145 *count += 1;
3146 }
3147 }
3148 oi += 1;
3149 ni += 1;
3150 }
3151 }
3152 }
3153 (Some(o), None) => {
3154 emit_deleted_count(odb, o, prefix, show_trees, count)?;
3155 oi += 1;
3156 }
3157 (None, Some(n)) => {
3158 emit_added_count(odb, n, prefix, show_trees, count)?;
3159 ni += 1;
3160 }
3161 (None, None) => break,
3162 }
3163 }
3164
3165 Ok(())
3166}
3167
3168fn emit_deleted_count(
3169 odb: &Odb,
3170 entry: &TreeEntry,
3171 prefix: &str,
3172 show_trees: bool,
3173 count: &mut usize,
3174) -> Result<()> {
3175 let path = crate::repo_relpath::format_path_stored(prefix, &entry.name);
3176 if is_tree_mode(entry.mode) {
3177 if show_trees {
3178 *count += 1;
3179 }
3180 *count += diff_tree_path_change_count(odb, Some(&entry.oid), None, &path)?;
3181 } else {
3182 *count += 1;
3183 }
3184 Ok(())
3185}
3186
3187fn emit_added_count(
3188 odb: &Odb,
3189 entry: &TreeEntry,
3190 prefix: &str,
3191 show_trees: bool,
3192 count: &mut usize,
3193) -> Result<()> {
3194 let path = crate::repo_relpath::format_path_stored(prefix, &entry.name);
3195 if is_tree_mode(entry.mode) {
3196 if show_trees {
3197 *count += 1;
3198 }
3199 *count += diff_tree_path_change_count(odb, None, Some(&entry.oid), &path)?;
3200 } else {
3201 *count += 1;
3202 }
3203 Ok(())
3204}
3205
3206fn diff_tree_entries_opts(
3208 odb: &Odb,
3209 mut old: WalkEntries<'_>,
3210 mut new: WalkEntries<'_>,
3211 prefix: &str,
3212 show_trees: bool,
3213 index_hint: Option<&DiffTreesWalkHint<'_>>,
3214 result: &mut Vec<DiffEntry>,
3215) -> Result<()> {
3216 let mut old_entry = old.next_entry()?;
3217 let mut new_entry = new.next_entry()?;
3218
3219 loop {
3220 match (&old_entry, &new_entry) {
3221 (Some(o), Some(n)) => {
3222 let cmp = crate::objects::tree_entry_cmp(
3223 o.name,
3224 is_tree_mode(o.mode),
3225 n.name,
3226 is_tree_mode(n.mode),
3227 );
3228 match cmp {
3229 std::cmp::Ordering::Less => {
3230 emit_deleted_opts(odb, o, prefix, show_trees, index_hint, result)?;
3232 old_entry = old.next_entry()?;
3233 }
3234 std::cmp::Ordering::Greater => {
3235 emit_added_opts(odb, n, prefix, show_trees, index_hint, result)?;
3237 new_entry = new.next_entry()?;
3238 }
3239 std::cmp::Ordering::Equal => {
3240 if o.oid != n.oid
3242 || crate::index::canon_mode(o.mode) != crate::index::canon_mode(n.mode)
3243 {
3244 let path = crate::repo_relpath::format_path_stored(prefix, o.name);
3245 if is_tree_mode(o.mode) && is_tree_mode(n.mode) {
3246 if show_trees {
3248 result.push(DiffEntry {
3249 status: DiffStatus::Modified,
3250 old_path: Some(path.clone()),
3251 new_path: Some(path.clone()),
3252 old_mode: FileMode::new(crate::index::canon_mode(o.mode)),
3253 new_mode: FileMode::new(crate::index::canon_mode(n.mode)),
3254 old_oid: o.oid,
3255 new_oid: n.oid,
3256 score: None,
3257 });
3258 }
3259 diff_subtrees_in_place(
3260 odb,
3261 Some(&o.oid),
3262 Some(&n.oid),
3263 &path,
3264 show_trees,
3265 index_hint,
3266 result,
3267 )?;
3268 } else if is_tree_mode(o.mode) && !is_tree_mode(n.mode) {
3269 emit_deleted_opts(odb, o, prefix, show_trees, index_hint, result)?;
3271 emit_added_opts(odb, n, prefix, show_trees, index_hint, result)?;
3272 } else if !is_tree_mode(o.mode) && is_tree_mode(n.mode) {
3273 emit_deleted_opts(odb, o, prefix, show_trees, index_hint, result)?;
3275 emit_added_opts(odb, n, prefix, show_trees, index_hint, result)?;
3276 } else {
3277 result.push(DiffEntry {
3278 status: DiffStatus::for_content_change(o.mode, n.mode),
3279 old_path: Some(path.clone()),
3280 new_path: Some(path),
3281 old_mode: FileMode::new(o.mode),
3282 new_mode: FileMode::new(n.mode),
3283 old_oid: o.oid,
3284 new_oid: n.oid,
3285 score: None,
3286 });
3287 }
3288 }
3289 old_entry = old.next_entry()?;
3290 new_entry = new.next_entry()?;
3291 }
3292 }
3293 }
3294 (Some(o), None) => {
3295 emit_deleted_opts(odb, o, prefix, show_trees, index_hint, result)?;
3296 old_entry = old.next_entry()?;
3297 }
3298 (None, Some(n)) => {
3299 emit_added_opts(odb, n, prefix, show_trees, index_hint, result)?;
3300 new_entry = new.next_entry()?;
3301 }
3302 (None, None) => break,
3303 }
3304 }
3305
3306 Ok(())
3307}
3308
3309fn emit_deleted_tree_from_index(index: &Index, dir_prefix: &str, result: &mut Vec<DiffEntry>) {
3310 let scan_prefix: Vec<u8> = if dir_prefix.is_empty() {
3311 Vec::new()
3312 } else {
3313 format!("{dir_prefix}/").into_bytes()
3314 };
3315 if index.entries_sorted {
3316 let start = if scan_prefix.is_empty() {
3317 0
3318 } else {
3319 index
3320 .entries
3321 .partition_point(|e| e.path.as_slice() < scan_prefix.as_slice())
3322 };
3323 for ie in index.entries[start..].iter() {
3324 if ie.stage() != 0 {
3325 continue;
3326 }
3327 if !scan_prefix.is_empty() && !ie.path.starts_with(&scan_prefix) {
3328 break;
3329 }
3330 if scan_prefix.is_empty() && ie.path.contains(&b'/') {
3331 continue;
3332 }
3333 let path = String::from_utf8_lossy(&ie.path).into_owned();
3334 result.push(DiffEntry {
3335 status: DiffStatus::Deleted,
3336 old_path: Some(path),
3337 new_path: None,
3338 old_mode: FileMode::new(ie.mode),
3339 new_mode: FileMode::ABSENT,
3340 old_oid: ie.oid,
3341 new_oid: zero_oid(),
3342 score: None,
3343 });
3344 }
3345 return;
3346 }
3347 for ie in &index.entries {
3348 if ie.stage() != 0 {
3349 continue;
3350 }
3351 let Ok(path) = std::str::from_utf8(&ie.path) else {
3352 continue;
3353 };
3354 let under = if dir_prefix.is_empty() {
3355 !path.contains('/')
3356 } else if path == dir_prefix {
3357 false
3358 } else {
3359 path.strip_prefix(dir_prefix)
3360 .is_some_and(|rest| rest.starts_with('/'))
3361 };
3362 if !under {
3363 continue;
3364 }
3365 result.push(DiffEntry {
3366 status: DiffStatus::Deleted,
3367 old_path: Some(path.to_owned()),
3368 new_path: None,
3369 old_mode: FileMode::new(ie.mode),
3370 new_mode: FileMode::ABSENT,
3371 old_oid: ie.oid,
3372 new_oid: zero_oid(),
3373 score: None,
3374 });
3375 }
3376}
3377
3378fn emit_deleted_opts(
3379 odb: &Odb,
3380 entry: &TreeEntryRef<'_>,
3381 prefix: &str,
3382 show_trees: bool,
3383 index_hint: Option<&DiffTreesWalkHint<'_>>,
3384 result: &mut Vec<DiffEntry>,
3385) -> Result<()> {
3386 let path = crate::repo_relpath::format_path_stored(prefix, entry.name);
3387 if is_tree_mode(entry.mode) {
3388 if show_trees {
3389 result.push(DiffEntry {
3390 status: DiffStatus::Deleted,
3391 old_path: Some(path.clone()),
3392 new_path: None,
3393 old_mode: FileMode::new(entry.mode),
3394 new_mode: FileMode::ABSENT,
3395 old_oid: entry.oid,
3396 new_oid: zero_oid(),
3397 score: None,
3398 });
3399 }
3400 if let Some(index) = index_hint
3401 .filter(|hint| hint.old_tree_matches)
3402 .and_then(|hint| hint.index)
3403 .filter(|index| !index.has_sparse_directory_placeholders())
3404 {
3405 emit_deleted_tree_from_index(index, &path, result);
3406 return Ok(());
3407 }
3408 diff_subtrees_in_place(
3409 odb,
3410 Some(&entry.oid),
3411 None,
3412 &path,
3413 show_trees,
3414 index_hint,
3415 result,
3416 )?;
3417 } else {
3418 result.push(DiffEntry {
3419 status: DiffStatus::Deleted,
3420 old_path: Some(path.clone()),
3421 new_path: None,
3422 old_mode: FileMode::new(entry.mode),
3423 new_mode: FileMode::ABSENT,
3424 old_oid: entry.oid,
3425 new_oid: zero_oid(),
3426 score: None,
3427 });
3428 }
3429 Ok(())
3430}
3431
3432fn emit_added_opts(
3433 odb: &Odb,
3434 entry: &TreeEntryRef<'_>,
3435 prefix: &str,
3436 show_trees: bool,
3437 index_hint: Option<&DiffTreesWalkHint<'_>>,
3438 result: &mut Vec<DiffEntry>,
3439) -> Result<()> {
3440 let path = crate::repo_relpath::format_path_stored(prefix, entry.name);
3441 if is_tree_mode(entry.mode) {
3442 if show_trees {
3443 result.push(DiffEntry {
3444 status: DiffStatus::Added,
3445 old_path: None,
3446 new_path: Some(path.clone()),
3447 old_mode: FileMode::ABSENT,
3448 new_mode: FileMode::new(entry.mode),
3449 old_oid: zero_oid(),
3450 new_oid: entry.oid,
3451 score: None,
3452 });
3453 }
3454 diff_subtrees_in_place(
3455 odb,
3456 None,
3457 Some(&entry.oid),
3458 &path,
3459 show_trees,
3460 index_hint,
3461 result,
3462 )?;
3463 } else {
3464 result.push(DiffEntry {
3465 status: DiffStatus::Added,
3466 old_path: None,
3467 new_path: Some(path),
3468 old_mode: FileMode::ABSENT,
3469 new_mode: FileMode::new(entry.mode),
3470 old_oid: zero_oid(),
3471 new_oid: entry.oid,
3472 score: None,
3473 });
3474 }
3475 Ok(())
3476}
3477
3478pub fn diff_index_to_tree(
3498 odb: &Odb,
3499 index: &Index,
3500 tree_oid: Option<&ObjectId>,
3501 ignore_submodules: bool,
3502) -> Result<Vec<DiffEntry>> {
3503 match index_cache_tree_walk(index, tree_oid) {
3504 CacheTreeWalk::Fast {
3505 head_tree,
3506 cache_root,
3507 } => {
3508 diff_index_to_tree_with_cache_tree(odb, index, head_tree, cache_root, ignore_submodules)
3509 }
3510 CacheTreeWalk::Flatten => {
3511 diff_index_to_tree_flatten(odb, index, tree_oid, ignore_submodules)
3512 }
3513 }
3514}
3515
3516pub fn diff_index_to_tree_with_renames(
3525 odb: &Odb,
3526 index: &Index,
3527 tree_oid: Option<&ObjectId>,
3528 ignore_submodules: bool,
3529 rename_threshold: u32,
3530 config: &ConfigSet,
3531 thread_policy: &ThreadPolicy,
3532) -> Result<Vec<DiffEntry>> {
3533 let entries = diff_index_to_tree(odb, index, tree_oid, ignore_submodules)?;
3534 apply_rename_detection_to_diff_entries(
3535 odb,
3536 entries,
3537 None,
3538 rename_threshold,
3539 config,
3540 thread_policy,
3541 )
3542}
3543
3544pub fn apply_rename_detection_to_diff_entries(
3553 odb: &Odb,
3554 entries: Vec<DiffEntry>,
3555 work_root: Option<&Path>,
3556 rename_threshold: u32,
3557 config: &ConfigSet,
3558 thread_policy: &ThreadPolicy,
3559) -> Result<Vec<DiffEntry>> {
3560 if entries.is_empty() || rename_threshold >= 100 {
3561 return Ok(entries);
3562 }
3563 if work_root.is_some() {
3564 let opts = DiffPostprocessOptions {
3565 rename_threshold,
3566 ..DiffPostprocessOptions::default()
3567 };
3568 postprocess_diff_entries(
3569 odb,
3570 entries,
3571 None,
3572 work_root,
3573 None,
3574 config,
3575 thread_policy,
3576 &opts,
3577 )
3578 } else {
3579 detect_renames_with_policy(
3580 odb,
3581 None,
3582 entries,
3583 rename_threshold,
3584 config,
3585 thread_policy,
3586 None,
3587 )
3588 }
3589}
3590
3591pub fn append_untracked_worktree_added_for_rename_detection(
3603 repo: &Repository,
3604 index: &Index,
3605 rules: &WorktreeRules,
3606 mut entries: Vec<DiffEntry>,
3607) -> Result<(Vec<DiffEntry>, HashSet<String>)> {
3608 let mut synthetic_new_paths = HashSet::new();
3609 if !entries.iter().any(|e| e.status == DiffStatus::Deleted) {
3610 return Ok((entries, synthetic_new_paths));
3611 }
3612 let mut existing_new: HashSet<String> =
3613 entries.iter().filter_map(|e| e.new_path.clone()).collect();
3614 let untracked = list_untracked_worktree_files_for_rename(repo, index, rules)?;
3615 for path in untracked {
3616 if existing_new.contains(&path) {
3617 continue;
3618 }
3619 existing_new.insert(path.clone());
3620 synthetic_new_paths.insert(path.clone());
3621 entries.push(DiffEntry {
3622 status: DiffStatus::Added,
3623 old_path: None,
3624 new_path: Some(path),
3625 old_mode: FileMode::ABSENT,
3626 new_mode: FileMode::REGULAR,
3627 old_oid: zero_oid(),
3628 new_oid: zero_oid(),
3629 score: None,
3630 });
3631 }
3632 Ok((entries, synthetic_new_paths))
3633}
3634
3635#[must_use]
3637pub fn drop_unused_synthetic_rename_candidates(
3638 entries: Vec<DiffEntry>,
3639 synthetic_new_paths: &HashSet<String>,
3640) -> Vec<DiffEntry> {
3641 if synthetic_new_paths.is_empty() {
3642 return entries;
3643 }
3644 entries
3645 .into_iter()
3646 .filter(|e| {
3647 !(e.status == DiffStatus::Added
3648 && e.new_path
3649 .as_ref()
3650 .is_some_and(|p| synthetic_new_paths.contains(p)))
3651 })
3652 .collect()
3653}
3654
3655fn list_untracked_worktree_files_for_rename(
3656 repo: &Repository,
3657 index: &Index,
3658 rules: &WorktreeRules,
3659) -> Result<Vec<String>> {
3660 let work_tree = repo.work_tree().ok_or(Error::RequiresWorkTree)?;
3661 let mut out = Vec::new();
3662 walk_untracked_worktree_files(repo, index, work_tree, "", rules, &mut out)?;
3663 out.sort();
3664 Ok(out)
3665}
3666
3667fn walk_untracked_worktree_files(
3668 repo: &Repository,
3669 index: &Index,
3670 dir: &Path,
3671 rel_prefix: &str,
3672 rules: &WorktreeRules,
3673 out: &mut Vec<String>,
3674) -> Result<()> {
3675 let read = match fs::read_dir(dir) {
3676 Ok(r) => r,
3677 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
3678 Err(e) => return Err(Error::Io(e)),
3679 };
3680 for entry in read {
3681 let entry = entry.map_err(Error::Io)?;
3682 let name = entry.file_name();
3683 let name = name.to_string_lossy();
3684 if rel_prefix.is_empty() && name == ".git" {
3685 continue;
3686 }
3687 let rel_path = if rel_prefix.is_empty() {
3688 name.to_string()
3689 } else {
3690 format!("{rel_prefix}/{name}")
3691 };
3692 let file_type = entry.file_type().map_err(Error::Io)?;
3693 if file_type.is_dir() {
3694 let (ignored, _) = rules
3695 .ignore_mut()
3696 .check_path(repo, Some(index), &rel_path, true)?;
3697 if ignored {
3698 continue;
3699 }
3700 walk_untracked_worktree_files(
3701 repo,
3702 index,
3703 &dir.join(name.as_ref()),
3704 &rel_path,
3705 rules,
3706 out,
3707 )?;
3708 } else if file_type.is_file() || file_type.is_symlink() {
3709 if index.get(rel_path.as_bytes(), 0).is_some() {
3710 continue;
3711 }
3712 let (ignored, _) =
3713 rules
3714 .ignore_mut()
3715 .check_path(repo, Some(index), &rel_path, false)?;
3716 if !ignored {
3717 out.push(rel_path);
3718 }
3719 }
3720 }
3721 Ok(())
3722}
3723
3724pub fn diff_index_to_worktree_with_renames(
3734 repo: &Repository,
3735 index: &mut Index,
3736 work_tree: &Path,
3737 options: DiffIndexToWorktreeOptions,
3738 rename_threshold: u32,
3739) -> Result<(Vec<DiffEntry>, bool)> {
3740 let config = repo.config()?.as_ref().clone();
3741 let thread_policy = repo.thread_policy();
3742 let odb = repo.odb();
3743 let rules_for_append = options.worktree_rules.clone();
3744 let (mut entries, changed) =
3745 diff_index_to_worktree_with_options(odb, index, work_tree, options)?;
3746 let mut synthetic_new_paths = HashSet::new();
3747 if rename_threshold < 100 {
3748 if let Some(rules_arc) = rules_for_append {
3749 let rules = rules_arc
3750 .lock()
3751 .map_err(|e| Error::Message(format!("worktree rules lock poisoned: {e}")))?;
3752 (entries, synthetic_new_paths) =
3753 append_untracked_worktree_added_for_rename_detection(repo, index, &rules, entries)?;
3754 } else {
3755 let rules = WorktreeRules::from_repository(repo, index)?;
3756 (entries, synthetic_new_paths) =
3757 append_untracked_worktree_added_for_rename_detection(repo, index, &rules, entries)?;
3758 }
3759 }
3760 let entries = apply_rename_detection_to_diff_entries(
3761 odb,
3762 entries,
3763 Some(work_tree),
3764 rename_threshold,
3765 &config,
3766 &thread_policy,
3767 )?;
3768 let entries = drop_unused_synthetic_rename_candidates(entries, &synthetic_new_paths);
3769 Ok((entries, changed))
3770}
3771
3772enum CacheTreeWalk<'a> {
3773 Fast {
3774 head_tree: &'a ObjectId,
3775 cache_root: &'a CacheTreeNode,
3776 },
3777 Flatten,
3778}
3779
3780fn index_cache_tree_walk<'a>(
3783 index: &'a Index,
3784 tree_oid: Option<&'a ObjectId>,
3785) -> CacheTreeWalk<'a> {
3786 let Some(head_tree) = tree_oid else {
3787 return CacheTreeWalk::Flatten;
3788 };
3789 let Some(cache_root) = index.cache_tree.as_ref() else {
3790 return CacheTreeWalk::Flatten;
3791 };
3792 if index.entries.iter().any(|e| e.stage() != 0) {
3793 return CacheTreeWalk::Flatten;
3794 }
3795 CacheTreeWalk::Fast {
3796 head_tree,
3797 cache_root,
3798 }
3799}
3800
3801fn diff_index_to_tree_flatten(
3803 odb: &Odb,
3804 index: &Index,
3805 tree_oid: Option<&ObjectId>,
3806 ignore_submodules: bool,
3807) -> Result<Vec<DiffEntry>> {
3808 let sparse_index = index.has_sparse_directory_placeholders();
3809 let tree_entries = match tree_oid {
3811 Some(oid) if sparse_index => flatten_tree_with_index(odb, oid, "", Some(index))?,
3812 Some(oid) => flatten_tree(odb, oid, "")?,
3813 None => Vec::new(),
3814 };
3815
3816 let mut tree_map: std::collections::BTreeMap<&str, &FlatEntry> =
3818 std::collections::BTreeMap::new();
3819 for entry in &tree_entries {
3820 tree_map.insert(&entry.path, entry);
3821 }
3822
3823 let mut result = Vec::new();
3824 let mut stage0_paths = std::collections::BTreeSet::new();
3825 let mut unmerged_modes: std::collections::BTreeMap<String, (u8, u32)> =
3826 std::collections::BTreeMap::new();
3827
3828 for ie in &index.entries {
3830 let path = String::from_utf8_lossy(&ie.path).to_string();
3831 if ie.stage() == 0 && ie.intent_to_add() {
3832 continue;
3835 }
3836 if ie.stage() == 0 && ie.is_sparse_directory_placeholder() {
3837 let dir = path.strip_suffix('/').unwrap_or(path.as_str());
3838 let index_flat = flatten_tree(odb, &ie.oid, dir)?;
3839 let head_flat = match tree_oid {
3840 None => Vec::new(),
3841 Some(head_root) => match subtree_oid_at_path(odb, head_root, dir)? {
3842 None => Vec::new(),
3843 Some(head_sub) if head_sub == ie.oid => {
3844 continue;
3845 }
3846 Some(head_sub) => flatten_tree(odb, &head_sub, dir)?,
3847 },
3848 };
3849 diff_flattened_subtrees(
3850 &head_flat,
3851 &index_flat,
3852 ignore_submodules,
3853 &mut result,
3854 &mut stage0_paths,
3855 &mut tree_map,
3856 );
3857 continue;
3858 }
3859 if ie.stage() != 0 {
3860 let rank = match ie.stage() {
3861 2 => 0u8,
3862 3 => 1u8,
3863 1 => 2u8,
3864 _ => 3u8,
3865 };
3866 match unmerged_modes.get(&path) {
3867 Some((existing_rank, _)) if *existing_rank <= rank => {}
3868 _ => {
3869 unmerged_modes.insert(path, (rank, ie.mode));
3870 }
3871 }
3872 continue;
3873 }
3874 if ignore_submodules && ie.mode == 0o160000 {
3875 let _ = tree_map.remove(path.as_str());
3876 stage0_paths.insert(path.clone());
3877 continue;
3878 }
3879 stage0_paths.insert(path.clone());
3880 match tree_map.remove(path.as_str()) {
3881 Some(te) => {
3882 if te.oid != ie.oid
3884 || crate::index::canon_mode(te.mode) != crate::index::canon_mode(ie.mode)
3885 {
3886 let tree_mode = crate::index::canon_mode(te.mode);
3887 let index_mode = crate::index::canon_mode(ie.mode);
3888 result.push(DiffEntry {
3889 status: DiffStatus::for_content_change(tree_mode, index_mode),
3890 old_path: Some(path.clone()),
3891 new_path: Some(path),
3892 old_mode: FileMode::new(tree_mode),
3893 new_mode: FileMode::new(index_mode),
3894 old_oid: te.oid,
3895 new_oid: ie.oid,
3896 score: None,
3897 });
3898 }
3899 }
3900 None => {
3901 result.push(DiffEntry {
3903 status: DiffStatus::Added,
3904 old_path: None,
3905 new_path: Some(path),
3906 old_mode: FileMode::ABSENT,
3907 new_mode: FileMode::new(ie.mode),
3908 old_oid: zero_oid(),
3909 new_oid: ie.oid,
3910 score: None,
3911 });
3912 }
3913 }
3914 }
3915
3916 diff_index_to_tree_finish_unmerged_and_deleted(
3917 ignore_submodules,
3918 &mut tree_map,
3919 &unmerged_modes,
3920 &stage0_paths,
3921 &mut result,
3922 );
3923
3924 sort_entries_by_path(&mut result);
3925 Ok(result)
3926}
3927
3928fn diff_index_to_tree_finish_unmerged_and_deleted(
3929 ignore_submodules: bool,
3930 tree_map: &mut std::collections::BTreeMap<&str, &FlatEntry>,
3931 unmerged_modes: &std::collections::BTreeMap<String, (u8, u32)>,
3932 stage0_paths: &std::collections::BTreeSet<String>,
3933 result: &mut Vec<DiffEntry>,
3934) {
3935 for (path, (_, mode)) in unmerged_modes {
3936 if stage0_paths.contains(path) {
3937 continue;
3938 }
3939 tree_map.remove(path.as_str());
3940 result.push(DiffEntry {
3941 status: DiffStatus::Unmerged,
3942 old_path: Some(path.clone()),
3943 new_path: Some(path.clone()),
3944 old_mode: FileMode::ABSENT,
3945 new_mode: FileMode::new(*mode),
3946 old_oid: zero_oid(),
3947 new_oid: zero_oid(),
3948 score: None,
3949 });
3950 }
3951
3952 for (path, te) in std::mem::take(tree_map) {
3954 if ignore_submodules && te.mode == 0o160000 {
3955 continue;
3956 }
3957 result.push(DiffEntry {
3958 status: DiffStatus::Deleted,
3959 old_path: Some(path.to_owned()),
3960 new_path: None,
3961 old_mode: FileMode::new(te.mode),
3962 new_mode: FileMode::ABSENT,
3963 old_oid: te.oid,
3964 new_oid: zero_oid(),
3965 score: None,
3966 });
3967 }
3968}
3969
3970fn diff_index_to_tree_with_cache_tree(
3973 odb: &Odb,
3974 index: &Index,
3975 head_tree: &ObjectId,
3976 cache_root: &CacheTreeNode,
3977 ignore_submodules: bool,
3978) -> Result<Vec<DiffEntry>> {
3979 let mut result = Vec::new();
3980 let mut idx_cursor = 0usize;
3981
3982 let mut walk = DiffIndexTreeWalk {
3983 odb,
3984 index,
3985 idx_cursor: &mut idx_cursor,
3986 ignore_submodules,
3987 result: &mut result,
3988 abort_to_flatten: false,
3989 };
3990 diff_index_tree_at(&mut walk, head_tree, Some(cache_root), b"")?;
3991 if walk.abort_to_flatten {
3992 return diff_index_to_tree_flatten(odb, index, Some(head_tree), ignore_submodules);
3993 }
3994
3995 while idx_cursor < index.entries.len() {
3997 let ie = &index.entries[idx_cursor];
3998 idx_cursor += 1;
3999 let path = String::from_utf8_lossy(&ie.path).to_string();
4000 if ie.stage() == 0 && ie.intent_to_add() {
4001 continue;
4002 }
4003 if ie.stage() != 0 {
4004 continue;
4005 }
4006 if ignore_submodules && ie.mode == 0o160000 {
4007 continue;
4008 }
4009 result.push(DiffEntry {
4010 status: DiffStatus::Added,
4011 old_path: None,
4012 new_path: Some(path),
4013 old_mode: FileMode::ABSENT,
4014 new_mode: FileMode::new(ie.mode),
4015 old_oid: zero_oid(),
4016 new_oid: ie.oid,
4017 score: None,
4018 });
4019 }
4020
4021 sort_entries_by_path(&mut result);
4022 Ok(result)
4023}
4024
4025enum IndexLevelKind<'a> {
4026 Blob {
4027 entry: &'a IndexEntry,
4028 name: &'a [u8],
4029 },
4030 Tree {
4031 name: &'a [u8],
4032 },
4033}
4034
4035fn index_relative_in_dir<'a>(path: &'a [u8], dir_prefix: &[u8]) -> Option<&'a [u8]> {
4038 if dir_prefix.is_empty() {
4039 return Some(path);
4040 }
4041 let rest = path.strip_prefix(dir_prefix)?;
4042 match rest.split_first() {
4043 None => Some(rest),
4044 Some((b'/', tail)) => Some(tail),
4045 Some(_) => None,
4046 }
4047}
4048
4049fn join_index_path(dir_prefix: &[u8], name: &[u8]) -> Vec<u8> {
4050 if dir_prefix.is_empty() {
4051 return name.to_vec();
4052 }
4053 let mut path = Vec::with_capacity(dir_prefix.len() + 1 + name.len());
4054 path.extend_from_slice(dir_prefix);
4055 path.push(b'/');
4056 path.extend_from_slice(name);
4057 path
4058}
4059
4060fn peek_index_level_entry<'a>(
4061 index: &'a Index,
4062 cursor: &mut usize,
4063 dir_prefix: &[u8],
4064 ignore_submodules: bool,
4065) -> Option<IndexLevelKind<'a>> {
4066 while let Some(ie) = index.entries.get(*cursor) {
4067 let rel = index_relative_in_dir(&ie.path, dir_prefix)?;
4068 if ie.stage() != 0 || ie.intent_to_add() || rel.is_empty() {
4069 *cursor += 1;
4070 continue;
4071 }
4072 if let Some(slash) = rel.iter().position(|&b| b == b'/') {
4073 return Some(IndexLevelKind::Tree {
4074 name: &rel[..slash],
4075 });
4076 }
4077 if ignore_submodules && ie.mode == 0o160000 {
4078 *cursor += 1;
4079 continue;
4080 }
4081 return Some(IndexLevelKind::Blob {
4082 entry: ie,
4083 name: rel,
4084 });
4085 }
4086 None
4087}
4088
4089fn skip_index_subtree(index: &Index, cursor: &mut usize, dir_prefix: &[u8]) {
4090 while index
4091 .entries
4092 .get(*cursor)
4093 .is_some_and(|ie| index_relative_in_dir(&ie.path, dir_prefix).is_some())
4094 {
4095 *cursor += 1;
4096 }
4097}
4098
4099const CACHE_CHILD_LINEAR_SCAN_MAX: usize = 16;
4101
4102struct CacheChildren<'a> {
4108 children: &'a [CacheTreeNode],
4109 sorted: Vec<&'a CacheTreeNode>,
4110}
4111
4112impl<'a> CacheChildren<'a> {
4113 fn new(node: Option<&'a CacheTreeNode>) -> Self {
4114 let children = node.map_or(&[][..], |n| n.children.as_slice());
4115 let sorted = if children.len() > CACHE_CHILD_LINEAR_SCAN_MAX {
4116 let mut sorted: Vec<&CacheTreeNode> = children.iter().collect();
4117 sorted.sort_by(|a, b| a.name.cmp(&b.name));
4118 sorted
4119 } else {
4120 Vec::new()
4121 };
4122 Self { children, sorted }
4123 }
4124
4125 fn get(&self, name: &[u8]) -> Option<&'a CacheTreeNode> {
4126 if self.sorted.is_empty() {
4127 return self.children.iter().find(|c| c.name == name);
4128 }
4129 let pos = self.sorted.partition_point(|c| c.name.as_slice() < name);
4130 self.sorted.get(pos).copied().filter(|c| c.name == name)
4131 }
4132}
4133
4134struct DiffIndexTreeWalk<'a, 'b> {
4135 odb: &'a Odb,
4136 index: &'a Index,
4137 idx_cursor: &'b mut usize,
4138 ignore_submodules: bool,
4139 result: &'b mut Vec<DiffEntry>,
4140 abort_to_flatten: bool,
4142}
4143
4144fn tree_entries_have_duplicate_names(entries: &[TreeEntry]) -> bool {
4145 let mut seen = std::collections::BTreeSet::<&[u8]>::new();
4146 for entry in entries {
4147 if !seen.insert(entry.name.as_slice()) {
4148 return true;
4149 }
4150 }
4151 false
4152}
4153
4154fn diff_index_tree_at(
4156 walk: &mut DiffIndexTreeWalk<'_, '_>,
4157 tree_oid: &ObjectId,
4158 cache_node: Option<&CacheTreeNode>,
4159 dir_prefix: &[u8],
4160) -> Result<()> {
4161 if !dir_prefix.is_empty() {
4162 let mut path = dir_prefix.to_vec();
4163 path.push(b'/');
4164 if let Some(entry) = walk
4165 .index
4166 .entries
4167 .get(*walk.idx_cursor)
4168 .filter(|entry| entry.path == path && entry.is_sparse_directory_placeholder())
4169 {
4170 if entry.oid == *tree_oid {
4171 *walk.idx_cursor += 1;
4172 return Ok(());
4173 }
4174 let dir = String::from_utf8_lossy(dir_prefix);
4175 let head_flat = flatten_tree(walk.odb, tree_oid, dir.as_ref())?;
4176 let index_flat = flatten_tree(walk.odb, &entry.oid, dir.as_ref())?;
4177 let mut stage0_paths = std::collections::BTreeSet::new();
4178 let mut tree_map = std::collections::BTreeMap::new();
4179 diff_flattened_subtrees(
4180 &head_flat,
4181 &index_flat,
4182 walk.ignore_submodules,
4183 walk.result,
4184 &mut stage0_paths,
4185 &mut tree_map,
4186 );
4187 *walk.idx_cursor += 1;
4188 return Ok(());
4189 }
4190 }
4191 let tree_entries = read_tree(walk.odb, tree_oid)?;
4192 if tree_entries_have_duplicate_names(&tree_entries) {
4193 walk.abort_to_flatten = true;
4194 return Ok(());
4195 }
4196 let cache_children = CacheChildren::new(cache_node);
4197 let mut ti = 0usize;
4198
4199 loop {
4200 let te = tree_entries.get(ti);
4201 let ie_kind = peek_index_level_entry(
4202 walk.index,
4203 walk.idx_cursor,
4204 dir_prefix,
4205 walk.ignore_submodules,
4206 );
4207
4208 match (te, ie_kind) {
4209 (Some(t_entry), Some(ie)) => {
4210 let t_name = &t_entry.name;
4211 let (i_name, i_is_tree) = match &ie {
4212 IndexLevelKind::Blob { name, .. } => (*name, false),
4213 IndexLevelKind::Tree { name } => (*name, true),
4214 };
4215 let t_is_tree = is_tree_mode(t_entry.mode);
4216 let cmp = crate::objects::tree_entry_cmp(t_name, t_is_tree, i_name, i_is_tree);
4217 match cmp {
4218 std::cmp::Ordering::Less => {
4219 emit_index_tree_deleted(
4220 walk.odb,
4221 t_entry,
4222 dir_prefix,
4223 walk.ignore_submodules,
4224 walk.result,
4225 )?;
4226 ti += 1;
4227 }
4228 std::cmp::Ordering::Greater => {
4229 emit_index_tree_added(walk, dir_prefix)?;
4230 }
4231 std::cmp::Ordering::Equal => {
4232 if t_is_tree && i_is_tree {
4233 let full_path = join_index_path(dir_prefix, t_name);
4234 if diff_index_tree_skip_sparse_placeholder(walk, &full_path, t_entry)? {
4235 ti += 1;
4236 continue;
4237 }
4238 let child_cache = cache_children.get(t_name);
4239 if child_cache
4240 .is_some_and(|c| c.is_valid() && c.oid == Some(t_entry.oid))
4241 {
4242 skip_index_subtree(walk.index, walk.idx_cursor, &full_path);
4243 ti += 1;
4244 continue;
4245 }
4246 diff_index_tree_at(walk, &t_entry.oid, child_cache, &full_path)?;
4247 } else if t_is_tree != i_is_tree {
4248 emit_index_tree_deleted(
4249 walk.odb,
4250 t_entry,
4251 dir_prefix,
4252 walk.ignore_submodules,
4253 walk.result,
4254 )?;
4255 emit_index_tree_added(walk, dir_prefix)?;
4256 } else {
4257 let IndexLevelKind::Blob { entry: ie, .. } = ie else {
4258 ti += 1;
4259 continue;
4260 };
4261 *walk.idx_cursor += 1;
4262 if t_entry.oid != ie.oid
4263 || crate::index::canon_mode(t_entry.mode)
4264 != crate::index::canon_mode(ie.mode)
4265 {
4266 let path = String::from_utf8_lossy(&ie.path).into_owned();
4267 let tree_mode = crate::index::canon_mode(t_entry.mode);
4268 let index_mode = crate::index::canon_mode(ie.mode);
4269 walk.result.push(DiffEntry {
4270 status: DiffStatus::for_content_change(tree_mode, index_mode),
4271 old_path: Some(path.clone()),
4272 new_path: Some(path),
4273 old_mode: FileMode::new(tree_mode),
4274 new_mode: FileMode::new(index_mode),
4275 old_oid: t_entry.oid,
4276 new_oid: ie.oid,
4277 score: None,
4278 });
4279 }
4280 }
4281 ti += 1;
4282 }
4283 }
4284 }
4285 (Some(t_entry), None) => {
4286 emit_index_tree_deleted(
4287 walk.odb,
4288 t_entry,
4289 dir_prefix,
4290 walk.ignore_submodules,
4291 walk.result,
4292 )?;
4293 ti += 1;
4294 }
4295 (None, Some(_)) => {
4296 emit_index_tree_added(walk, dir_prefix)?;
4297 }
4298 (None, None) => break,
4299 }
4300 }
4301
4302 Ok(())
4303}
4304
4305fn diff_index_tree_skip_sparse_placeholder(
4306 walk: &mut DiffIndexTreeWalk<'_, '_>,
4307 full_path: &[u8],
4308 t_entry: &TreeEntry,
4309) -> Result<bool> {
4310 let Some(entry) = walk.index.entries.get(*walk.idx_cursor) else {
4311 return Ok(false);
4312 };
4313 if !entry.is_sparse_directory_placeholder() {
4314 return Ok(false);
4315 }
4316 let mut dir = full_path.to_vec();
4317 if !dir.ends_with(b"/") {
4318 dir.push(b'/');
4319 }
4320 if entry.path != dir || entry.oid != t_entry.oid {
4321 return Ok(false);
4322 }
4323 *walk.idx_cursor += 1;
4324 Ok(true)
4325}
4326
4327fn emit_index_tree_deleted(
4328 odb: &Odb,
4329 entry: &TreeEntry,
4330 dir_prefix: &[u8],
4331 ignore_submodules: bool,
4332 result: &mut Vec<DiffEntry>,
4333) -> Result<()> {
4334 if ignore_submodules && entry.mode == 0o160000 {
4335 return Ok(());
4336 }
4337 let path = String::from_utf8_lossy(&join_index_path(dir_prefix, &entry.name)).into_owned();
4338 if is_tree_mode(entry.mode) {
4339 for fe in flatten_tree(odb, &entry.oid, &path)? {
4340 if ignore_submodules && fe.mode == 0o160000 {
4341 continue;
4342 }
4343 result.push(DiffEntry {
4344 status: DiffStatus::Deleted,
4345 old_path: Some(fe.path.clone()),
4346 new_path: None,
4347 old_mode: FileMode::new(fe.mode),
4348 new_mode: FileMode::ABSENT,
4349 old_oid: fe.oid,
4350 new_oid: zero_oid(),
4351 score: None,
4352 });
4353 }
4354 } else {
4355 result.push(DiffEntry {
4356 status: DiffStatus::Deleted,
4357 old_path: Some(path),
4358 new_path: None,
4359 old_mode: FileMode::new(entry.mode),
4360 new_mode: FileMode::ABSENT,
4361 old_oid: entry.oid,
4362 new_oid: zero_oid(),
4363 score: None,
4364 });
4365 }
4366 Ok(())
4367}
4368
4369fn emit_index_tree_added(walk: &mut DiffIndexTreeWalk<'_, '_>, dir_prefix: &[u8]) -> Result<()> {
4370 let ie_kind = peek_index_level_entry(
4371 walk.index,
4372 walk.idx_cursor,
4373 dir_prefix,
4374 walk.ignore_submodules,
4375 )
4376 .ok_or_else(|| Error::IndexError("missing index entry during tree walk".to_owned()))?;
4377 match ie_kind {
4378 IndexLevelKind::Blob { entry: ie, .. } => {
4379 let path = String::from_utf8_lossy(&ie.path).into_owned();
4380 *walk.idx_cursor += 1;
4381 walk.result.push(DiffEntry {
4382 status: DiffStatus::Added,
4383 old_path: None,
4384 new_path: Some(path),
4385 old_mode: FileMode::ABSENT,
4386 new_mode: FileMode::new(ie.mode),
4387 old_oid: zero_oid(),
4388 new_oid: ie.oid,
4389 score: None,
4390 });
4391 }
4392 IndexLevelKind::Tree { name } => {
4393 let full_path = join_index_path(dir_prefix, name);
4394 let mut path_slash = full_path.clone();
4395 path_slash.push(b'/');
4396 if let Some(entry) =
4397 walk.index.entries.get(*walk.idx_cursor).filter(|entry| {
4398 entry.path == path_slash && entry.is_sparse_directory_placeholder()
4399 })
4400 {
4401 let dir = String::from_utf8_lossy(if dir_prefix.is_empty() {
4402 name
4403 } else {
4404 &full_path
4405 });
4406 emit_flattened_subtree_as_added(
4407 walk.odb,
4408 &entry.oid,
4409 dir.as_ref(),
4410 walk.ignore_submodules,
4411 walk.result,
4412 )?;
4413 *walk.idx_cursor += 1;
4414 return Ok(());
4415 }
4416 let start = *walk.idx_cursor;
4417 skip_index_subtree(walk.index, walk.idx_cursor, &full_path);
4418 for ie in &walk.index.entries[start..*walk.idx_cursor] {
4419 if ie.stage() != 0 || ie.intent_to_add() {
4420 continue;
4421 }
4422 if walk.ignore_submodules && ie.mode == 0o160000 {
4423 continue;
4424 }
4425 let path = String::from_utf8_lossy(&ie.path).into_owned();
4426 walk.result.push(DiffEntry {
4427 status: DiffStatus::Added,
4428 old_path: None,
4429 new_path: Some(path),
4430 old_mode: FileMode::ABSENT,
4431 new_mode: FileMode::new(ie.mode),
4432 old_oid: zero_oid(),
4433 new_oid: ie.oid,
4434 score: None,
4435 });
4436 }
4437 }
4438 }
4439 Ok(())
4440}
4441
4442#[derive(Default)]
4444pub(crate) struct HardResetIndexPlan {
4445 pub remove_paths: Vec<Vec<u8>>,
4446 pub updates: Vec<(Vec<u8>, u32, ObjectId)>,
4447 pub insert_entries: Vec<IndexEntry>,
4448}
4449
4450pub(crate) fn plan_hard_reset_index_align(
4455 odb: &Odb,
4456 index: &Index,
4457 target_tree: &ObjectId,
4458) -> Result<HardResetIndexPlan> {
4459 let mut plan = HardResetIndexPlan::default();
4460 let mut idx_cursor = 0usize;
4461 let cache_root = index.cache_tree.as_ref();
4462 let use_cache = cache_root.is_some()
4463 && index
4464 .entries
4465 .iter()
4466 .all(|entry| entry.stage() == 0 && !entry.intent_to_add());
4467 let cache_node = if use_cache { cache_root } else { None };
4468 let retry = {
4469 let mut walk = HardResetAlignWalk {
4470 odb,
4471 index,
4472 idx_cursor: &mut idx_cursor,
4473 ignore_submodules: false,
4474 plan: &mut plan,
4475 retry_without_cache: false,
4476 };
4477 hard_reset_align_index_tree_at(&mut walk, target_tree, cache_node, b"")?;
4478 walk.retry_without_cache
4479 };
4480 if retry {
4481 idx_cursor = 0;
4482 plan = HardResetIndexPlan::default();
4483 let mut walk = HardResetAlignWalk {
4484 odb,
4485 index,
4486 idx_cursor: &mut idx_cursor,
4487 ignore_submodules: false,
4488 plan: &mut plan,
4489 retry_without_cache: false,
4490 };
4491 hard_reset_align_index_tree_at(&mut walk, target_tree, None, b"")?;
4492 }
4493 while idx_cursor < index.entries.len() {
4494 let ie = &index.entries[idx_cursor];
4495 idx_cursor += 1;
4496 if ie.stage() != 0 || ie.intent_to_add() {
4497 continue;
4498 }
4499 plan.remove_paths.push(ie.path.clone());
4500 }
4501 Ok(plan)
4502}
4503
4504pub(crate) fn apply_hard_reset_index_plan(index: &mut Index, plan: HardResetIndexPlan) {
4506 for path in &plan.remove_paths {
4507 index.remove_path_all_stages(path);
4508 index.invalidate_cache_tree_for_path(path);
4509 }
4510 for (path, mode, oid) in plan.updates {
4511 if let Some(pos) = index.find_entry_pos(&path, 0) {
4512 index.entries[pos].mode = mode;
4513 index.entries[pos].oid = oid;
4514 index.entries[pos].size = 0;
4515 index.invalidate_cache_tree_for_path(&path);
4516 }
4517 }
4518 for entry in plan.insert_entries {
4519 let path = entry.path.clone();
4520 index.add_or_replace(entry);
4521 index.invalidate_cache_tree_for_path(&path);
4522 }
4523 index.sort();
4524}
4525
4526pub(crate) fn rebuild_hard_reset_cache_tree(
4529 odb: &Odb,
4530 index: &mut Index,
4531 target_tree: &ObjectId,
4532) -> Result<()> {
4533 let stage0_count = index
4534 .entries
4535 .iter()
4536 .filter(|entry| entry.stage() == 0 && !entry.intent_to_add())
4537 .count();
4538 let seed_root = index.cache_tree.as_ref();
4539 let mut idx_cursor = 0usize;
4540 let root = rebuild_hard_reset_cache_tree_at(
4541 odb,
4542 index,
4543 &mut idx_cursor,
4544 target_tree,
4545 seed_root,
4546 b"",
4547 Vec::new(),
4548 )?;
4549 if usize::try_from(root.entry_count).ok() == Some(stage0_count) {
4550 index.set_cache_tree(root);
4551 Ok(())
4552 } else {
4553 Err(Error::IndexError(
4554 "hard reset cache-tree entry count does not match index".into(),
4555 ))
4556 }
4557}
4558
4559fn rebuild_hard_reset_cache_tree_at(
4560 odb: &Odb,
4561 index: &Index,
4562 idx_cursor: &mut usize,
4563 tree_oid: &ObjectId,
4564 seed_node: Option<&CacheTreeNode>,
4565 dir_prefix: &[u8],
4566 name: Vec<u8>,
4567) -> Result<CacheTreeNode> {
4568 if !dir_prefix.is_empty() {
4569 let mut path = dir_prefix.to_vec();
4570 path.push(b'/');
4571 if let Some(entry) = index.entries.get(*idx_cursor).filter(|entry| {
4572 entry.path == path && entry.is_sparse_directory_placeholder() && entry.oid == *tree_oid
4573 }) {
4574 let _ = entry;
4575 *idx_cursor += 1;
4576 return Ok(CacheTreeNode::valid(name, 1, *tree_oid, Vec::new()));
4577 }
4578 }
4579 let tree_entries = read_tree(odb, tree_oid)?;
4580 let cache_children = CacheChildren::new(seed_node);
4581 let mut entry_count: i32 = 0;
4582 let mut children: Vec<CacheTreeNode> = Vec::new();
4583 let mut ti = 0usize;
4584
4585 loop {
4586 let te = tree_entries.get(ti);
4587 let ie_kind = peek_index_level_entry(index, idx_cursor, dir_prefix, false);
4588 match (te, ie_kind) {
4589 (Some(t_entry), Some(ie)) => {
4590 let t_name = &t_entry.name;
4591 let (i_name, i_is_tree) = match &ie {
4592 IndexLevelKind::Blob { name, .. } => (*name, false),
4593 IndexLevelKind::Tree { name } => (*name, true),
4594 };
4595 let t_is_tree = is_tree_mode(t_entry.mode);
4596 let cmp = crate::objects::tree_entry_cmp(t_name, t_is_tree, i_name, i_is_tree);
4597 if cmp != std::cmp::Ordering::Equal {
4598 return Err(Error::IndexError(
4599 "hard reset cache-tree rebuild: index out of sync with target tree".into(),
4600 ));
4601 }
4602 if t_is_tree && i_is_tree {
4603 let full_path = join_index_path(dir_prefix, t_name);
4604 if hard_reset_skip_sparse_cache_rebuild(index, idx_cursor, &full_path, t_entry)?
4605 {
4606 entry_count = entry_count.saturating_add(1);
4607 ti += 1;
4608 continue;
4609 }
4610 let child = if let Some(child_cache) = cache_children
4611 .get(t_name)
4612 .filter(|node| node.is_valid() && node.oid == Some(t_entry.oid))
4613 {
4614 skip_index_subtree(index, idx_cursor, &full_path);
4615 let mut reused = (*child_cache).clone();
4616 reused.name = t_name.clone();
4617 reused
4618 } else {
4619 rebuild_hard_reset_cache_tree_at(
4620 odb,
4621 index,
4622 idx_cursor,
4623 &t_entry.oid,
4624 cache_children.get(t_name),
4625 &full_path,
4626 t_name.clone(),
4627 )?
4628 };
4629 entry_count = entry_count.saturating_add(child.entry_count.max(0));
4630 children.push(child);
4631 } else if t_is_tree != i_is_tree {
4632 return Err(Error::IndexError(
4633 "hard reset cache-tree rebuild: tree/index type mismatch".into(),
4634 ));
4635 } else {
4636 *idx_cursor += 1;
4637 entry_count = entry_count.saturating_add(1);
4638 }
4639 ti += 1;
4640 }
4641 (None, None) => break,
4642 _ => {
4643 return Err(Error::IndexError(
4644 "hard reset cache-tree rebuild: index out of sync with target tree".into(),
4645 ));
4646 }
4647 }
4648 }
4649 children.sort_by(|a, b| a.name.cmp(&b.name));
4650 Ok(CacheTreeNode::valid(name, entry_count, *tree_oid, children))
4651}
4652
4653fn hard_reset_skip_sparse_cache_rebuild(
4654 index: &Index,
4655 idx_cursor: &mut usize,
4656 full_path: &[u8],
4657 t_entry: &TreeEntry,
4658) -> Result<bool> {
4659 let Some(entry) = index.entries.get(*idx_cursor) else {
4660 return Ok(false);
4661 };
4662 if !entry.is_sparse_directory_placeholder() {
4663 return Ok(false);
4664 }
4665 let mut dir = full_path.to_vec();
4666 if !dir.ends_with(b"/") {
4667 dir.push(b'/');
4668 }
4669 if entry.path != dir || entry.oid != t_entry.oid {
4670 return Ok(false);
4671 }
4672 *idx_cursor += 1;
4673 Ok(true)
4674}
4675
4676struct HardResetAlignWalk<'a, 'b> {
4677 odb: &'a Odb,
4678 index: &'a Index,
4679 idx_cursor: &'b mut usize,
4680 ignore_submodules: bool,
4681 plan: &'b mut HardResetIndexPlan,
4682 retry_without_cache: bool,
4683}
4684
4685fn hard_reset_align_index_tree_at(
4686 walk: &mut HardResetAlignWalk<'_, '_>,
4687 tree_oid: &ObjectId,
4688 cache_node: Option<&CacheTreeNode>,
4689 dir_prefix: &[u8],
4690) -> Result<()> {
4691 let tree_entries = read_tree(walk.odb, tree_oid)?;
4692 if tree_entries_have_duplicate_names(&tree_entries) {
4693 walk.retry_without_cache = true;
4694 return Ok(());
4695 }
4696 let cache_children = CacheChildren::new(cache_node);
4697 let mut ti = 0usize;
4698
4699 loop {
4700 let te = tree_entries.get(ti);
4701 let ie_kind = peek_index_level_entry(
4702 walk.index,
4703 walk.idx_cursor,
4704 dir_prefix,
4705 walk.ignore_submodules,
4706 );
4707
4708 match (te, ie_kind) {
4709 (Some(t_entry), Some(ie)) => {
4710 let t_name = &t_entry.name;
4711 let (i_name, i_is_tree) = match &ie {
4712 IndexLevelKind::Blob { name, .. } => (*name, false),
4713 IndexLevelKind::Tree { name } => (*name, true),
4714 };
4715 let t_is_tree = is_tree_mode(t_entry.mode);
4716 let cmp = crate::objects::tree_entry_cmp(t_name, t_is_tree, i_name, i_is_tree);
4717 match cmp {
4718 std::cmp::Ordering::Less => {
4719 hard_reset_plan_tree_only(walk, t_entry, dir_prefix)?;
4720 ti += 1;
4721 }
4722 std::cmp::Ordering::Greater => {
4723 hard_reset_plan_index_only(walk, dir_prefix)?;
4724 }
4725 std::cmp::Ordering::Equal => {
4726 if t_is_tree && i_is_tree {
4727 let full_path = join_index_path(dir_prefix, t_name);
4728 if hard_reset_skip_sparse_placeholder(walk, &full_path, t_entry)? {
4729 ti += 1;
4730 continue;
4731 }
4732 let child_cache = cache_children.get(t_name);
4733 if child_cache
4734 .is_some_and(|c| c.is_valid() && c.oid == Some(t_entry.oid))
4735 {
4736 skip_index_subtree(walk.index, walk.idx_cursor, &full_path);
4737 ti += 1;
4738 continue;
4739 }
4740 hard_reset_align_index_tree_at(
4741 walk,
4742 &t_entry.oid,
4743 child_cache,
4744 &full_path,
4745 )?;
4746 } else if t_is_tree != i_is_tree {
4747 hard_reset_plan_tree_only(walk, t_entry, dir_prefix)?;
4748 hard_reset_plan_index_only(walk, dir_prefix)?;
4749 } else {
4750 let IndexLevelKind::Blob { entry: ie, .. } = ie else {
4751 ti += 1;
4752 continue;
4753 };
4754 *walk.idx_cursor += 1;
4755 if t_entry.oid != ie.oid
4756 || crate::index::canon_mode(t_entry.mode)
4757 != crate::index::canon_mode(ie.mode)
4758 {
4759 walk.plan.updates.push((
4760 ie.path.clone(),
4761 crate::index::canon_mode(t_entry.mode),
4762 t_entry.oid,
4763 ));
4764 }
4765 }
4766 ti += 1;
4767 }
4768 }
4769 }
4770 (Some(t_entry), None) => {
4771 hard_reset_plan_tree_only(walk, t_entry, dir_prefix)?;
4772 ti += 1;
4773 }
4774 (None, Some(_)) => {
4775 hard_reset_plan_index_only(walk, dir_prefix)?;
4776 }
4777 (None, None) => break,
4778 }
4779 }
4780 Ok(())
4781}
4782
4783fn hard_reset_skip_sparse_placeholder(
4784 walk: &mut HardResetAlignWalk<'_, '_>,
4785 full_path: &[u8],
4786 t_entry: &TreeEntry,
4787) -> Result<bool> {
4788 let Some(ie) = walk.index.entries.get(*walk.idx_cursor) else {
4789 return Ok(false);
4790 };
4791 if !ie.is_sparse_directory_placeholder() {
4792 return Ok(false);
4793 }
4794 let mut dir = full_path.to_vec();
4795 if !dir.ends_with(b"/") {
4796 dir.push(b'/');
4797 }
4798 if ie.path != dir || ie.oid != t_entry.oid {
4799 return Ok(false);
4800 }
4801 *walk.idx_cursor += 1;
4802 Ok(true)
4803}
4804
4805fn hard_reset_plan_index_only(
4806 walk: &mut HardResetAlignWalk<'_, '_>,
4807 dir_prefix: &[u8],
4808) -> Result<()> {
4809 let ie_kind = peek_index_level_entry(
4810 walk.index,
4811 walk.idx_cursor,
4812 dir_prefix,
4813 walk.ignore_submodules,
4814 )
4815 .ok_or_else(|| Error::IndexError("missing index entry during hard reset align".into()))?;
4816 match ie_kind {
4817 IndexLevelKind::Blob { entry: ie, .. } => {
4818 walk.plan.remove_paths.push(ie.path.clone());
4819 *walk.idx_cursor += 1;
4820 }
4821 IndexLevelKind::Tree { name } => {
4822 let full_path = join_index_path(dir_prefix, name);
4823 let start = *walk.idx_cursor;
4824 skip_index_subtree(walk.index, walk.idx_cursor, &full_path);
4825 for ie in &walk.index.entries[start..*walk.idx_cursor] {
4826 if ie.stage() != 0 || ie.intent_to_add() {
4827 continue;
4828 }
4829 walk.plan.remove_paths.push(ie.path.clone());
4830 }
4831 }
4832 }
4833 Ok(())
4834}
4835
4836fn hard_reset_plan_tree_only(
4837 walk: &mut HardResetAlignWalk<'_, '_>,
4838 entry: &TreeEntry,
4839 dir_prefix: &[u8],
4840) -> Result<()> {
4841 if walk.ignore_submodules && entry.mode == 0o160000 {
4842 return Ok(());
4843 }
4844 let path = join_index_path(dir_prefix, &entry.name);
4845 if is_tree_mode(entry.mode) {
4846 hard_reset_append_entries_from_tree(
4847 walk.odb,
4848 &entry.oid,
4849 &path,
4850 &mut walk.plan.insert_entries,
4851 )?;
4852 } else {
4853 walk.plan
4854 .insert_entries
4855 .push(hard_reset_index_tree_row(path, entry.mode, entry.oid));
4856 }
4857 Ok(())
4858}
4859
4860fn hard_reset_index_tree_row(path: Vec<u8>, mode: u32, oid: ObjectId) -> IndexEntry {
4861 IndexEntry {
4862 ctime_sec: 0,
4863 ctime_nsec: 0,
4864 mtime_sec: 0,
4865 mtime_nsec: 0,
4866 dev: 0,
4867 ino: 0,
4868 mode: crate::index::canon_mode(mode),
4869 uid: 0,
4870 gid: 0,
4871 size: 0,
4872 oid,
4873 flags: path.len().min(0xFFF) as u16,
4874 flags_extended: None,
4875 path,
4876 base_index_pos: 0,
4877 }
4878}
4879
4880fn hard_reset_append_entries_from_tree(
4881 odb: &Odb,
4882 tree_oid: &ObjectId,
4883 prefix: &[u8],
4884 out: &mut Vec<IndexEntry>,
4885) -> Result<()> {
4886 for te in read_tree(odb, tree_oid)? {
4887 let mut path = prefix.to_vec();
4888 if !path.is_empty() {
4889 path.push(b'/');
4890 }
4891 path.extend_from_slice(&te.name);
4892 if is_tree_mode(te.mode) {
4893 hard_reset_append_entries_from_tree(odb, &te.oid, &path, out)?;
4894 } else {
4895 out.push(hard_reset_index_tree_row(path, te.mode, te.oid));
4896 }
4897 }
4898 Ok(())
4899}
4900
4901pub fn diff_index_to_worktree(
4926 odb: &Odb,
4927 index: &mut Index,
4928 work_tree: &Path,
4929 ignore_submodule_untracked: bool,
4930 simplify_gitlinks: bool,
4931) -> Result<Vec<DiffEntry>> {
4932 diff_index_to_worktree_with_options(
4933 odb,
4934 index,
4935 work_tree,
4936 DiffIndexToWorktreeOptions {
4937 ignore_submodule_untracked,
4938 simplify_gitlinks,
4939 ..DiffIndexToWorktreeOptions::default()
4940 },
4941 )
4942 .map(|(entries, _)| entries)
4943}
4944
4945#[derive(Clone)]
4947pub struct DiffIndexToWorktreeOptions {
4948 pub index_mtime: Option<(u32, u32)>,
4954 pub ignore_submodule_untracked: bool,
4956 pub simplify_gitlinks: bool,
4958 pub error_on_broken_gitlinks: bool,
4961 pub repository_git_dir: Option<std::path::PathBuf>,
4965 pub refresh_index_stat_in_pass: bool,
4968 pub config: Option<std::sync::Arc<crate::config::ConfigSet>>,
4970 pub stat_parallel_threads: Option<usize>,
4974 pub thread_policy: Option<crate::parallel::ThreadPolicy>,
4976 pub worktree_rules:
4978 Option<std::sync::Arc<std::sync::Mutex<crate::worktree_rules::WorktreeRules>>>,
4979 pub for_status: bool,
4986 pub refresh_fsmonitor: bool,
4988 pub command_runner: Option<std::sync::Arc<dyn crate::command_runner::CommandRunner>>,
4990 pub fsmonitor_query_boundary: Option<crate::fsmonitor::FsmonitorQueryBoundary>,
4992 pub restrict_to_paths: Option<std::sync::Arc<std::collections::BTreeSet<String>>>,
4999 pub hash_blobs_without_write: bool,
5001 pub cancel_watch: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
5004}
5005
5006impl Default for DiffIndexToWorktreeOptions {
5007 fn default() -> Self {
5008 Self {
5009 index_mtime: None,
5010 ignore_submodule_untracked: false,
5011 simplify_gitlinks: false,
5012 error_on_broken_gitlinks: false,
5013 repository_git_dir: None,
5014 refresh_index_stat_in_pass: false,
5015 config: None,
5016 stat_parallel_threads: None,
5017 thread_policy: None,
5018 worktree_rules: None,
5019 for_status: false,
5020 refresh_fsmonitor: true,
5021 command_runner: None,
5022 fsmonitor_query_boundary: None,
5023 restrict_to_paths: None,
5024 hash_blobs_without_write: false,
5025 cancel_watch: None,
5026 }
5027 }
5028}
5029
5030#[inline]
5031fn poll_diff_cancel_watch(
5032 watch: Option<&std::sync::Arc<std::sync::atomic::AtomicBool>>,
5033) -> crate::error::Result<()> {
5034 if let Some(flag) = watch {
5035 crate::progress::poll_watch(flag)?;
5036 }
5037 Ok(())
5038}
5039
5040#[inline]
5041fn poll_diff_cancel(
5042 options: &DiffIndexToWorktreeOptions,
5043 owner_poll: Option<&std::sync::Arc<crate::progress::StackCancelPoll>>,
5044) -> crate::error::Result<()> {
5045 if let Some(poll) = owner_poll {
5046 poll.poll_owner()?;
5047 } else {
5048 poll_diff_cancel_watch(options.cancel_watch.as_ref())?;
5049 }
5050 Ok(())
5051}
5052
5053struct StagingHashJob {
5054 entry_index: usize,
5055 rel_path: String,
5056 file_path: PathBuf,
5057 entry: IndexEntry,
5058 worktree_mode: u32,
5060 file_attrs: crate::crlf::FileAttrs,
5061}
5062
5063#[allow(clippy::too_many_arguments)]
5064fn parallel_process_blob_worktree_jobs(
5065 odb: &Odb,
5066 jobs: &[StagingHashJob],
5067 thread_policy: &crate::parallel::ThreadPolicy,
5068 config: &crate::config::ConfigSet,
5069 materialize_dirty_blobs: bool,
5070 hash_blobs_without_write: bool,
5071 conv: &crate::crlf::ConversionConfig,
5072 filter_process: Option<&crate::filter_process::FilterProcessState>,
5073 _icase_collisions: Option<&crate::path_icase::IcaseCheckoutCollisionMap>,
5074 result: &mut Vec<DiffEntry>,
5075) -> Result<()> {
5076 use crate::parallel::{try_par_map, ParallelHashError, ParallelOp};
5077
5078 if jobs.is_empty() {
5079 return Ok(());
5080 }
5081 let mut par_opts = thread_policy.resolve(ParallelOp::IndexRefresh, config);
5082 par_opts.min_items = 0;
5083 par_opts.min_total_bytes = None;
5084 let est_bytes = jobs.len().saturating_mul(8192);
5085 let store_dirty_blobs = (materialize_dirty_blobs && !hash_blobs_without_write)
5086 .then(|| odb.bulk_stage_write_options());
5087 let stream_from = if store_dirty_blobs.is_some() {
5088 odb.big_file_threshold()?
5089 } else {
5090 hash_stream_threshold(odb)?
5091 };
5092
5093 let rows = try_par_map::<StagingHashJob, Option<(usize, DiffEntry)>, Error, _>(
5094 jobs,
5095 par_opts,
5096 est_bytes,
5097 |job| {
5098 let modified = |new_oid| {
5099 Ok(Some((
5100 job.entry_index,
5101 worktree_content_change_entry(
5102 &job.entry,
5103 &job.rel_path,
5104 job.worktree_mode,
5105 new_oid,
5106 ),
5107 )))
5108 };
5109 let content = match read_regular_worktree_blob(
5110 odb,
5111 &job.file_path,
5112 conv,
5113 &job.file_attrs,
5114 &job.rel_path,
5115 Some(&job.entry),
5116 filter_process,
5117 stream_from,
5118 ) {
5119 Ok(content) => content,
5120 Err(err) if unreadable_worktree_blob_is_modified(&err, materialize_dirty_blobs) => {
5121 return modified(zero_oid());
5122 }
5123 Err(err) => return Err(err),
5124 };
5125 let eff_oid = content.object_id(odb, store_dirty_blobs)?;
5126 if eff_oid != job.entry.oid || job.worktree_mode != job.entry.mode {
5127 let new_oid = if eff_oid != job.entry.oid && !materialize_dirty_blobs {
5128 zero_oid()
5129 } else {
5130 eff_oid
5131 };
5132 modified(new_oid)
5133 } else {
5134 Ok(None)
5135 }
5136 },
5137 )
5138 .map_err(|e| match e {
5139 ParallelHashError::Task(err) => err,
5140 })?;
5141
5142 let mut rows: Vec<(usize, DiffEntry)> = rows.into_iter().flatten().collect();
5143 rows.sort_by_key(|(idx, _)| *idx);
5144 result.extend(rows.into_iter().map(|(_, d)| d));
5145 Ok(())
5146}
5147
5148fn worktree_content_change_entry(
5150 ie: &IndexEntry,
5151 path: &str,
5152 worktree_mode: u32,
5153 new_oid: ObjectId,
5154) -> DiffEntry {
5155 DiffEntry {
5156 status: DiffStatus::for_content_change(ie.mode, worktree_mode),
5157 old_path: Some(path.to_owned()),
5158 new_path: Some(path.to_owned()),
5159 old_mode: FileMode::new(ie.mode),
5160 new_mode: FileMode::new(worktree_mode),
5161 old_oid: ie.oid,
5162 new_oid,
5163 score: None,
5164 }
5165}
5166
5167fn unreadable_worktree_blob_is_modified(err: &Error, materialize_dirty_blobs: bool) -> bool {
5174 !materialize_dirty_blobs
5175 && matches!(
5176 err,
5177 Error::WorktreePathIo {
5178 operation: crate::worktree::WorktreePathOperation::Read,
5179 ..
5180 }
5181 )
5182}
5183
5184fn status_size_change_proves_modified(
5193 ie: &IndexEntry,
5194 worktree_size: u32,
5195 for_status: bool,
5196 conversion_is_identity: impl FnOnce() -> bool,
5197) -> bool {
5198 for_status && !ie.stat_cache_unset() && worktree_size != ie.size && conversion_is_identity()
5199}
5200
5201#[allow(clippy::too_many_arguments)]
5204fn try_push_status_modified_on_size_mismatch(
5205 odb: &Odb,
5206 ie: &IndexEntry,
5207 path_str_ref: &str,
5208 worktree_size: u32,
5209 worktree_mode: u32,
5210 raw_worktree_len: u64,
5211 for_status: bool,
5212 conversion_is_identity: impl FnOnce() -> bool,
5213 result: &mut Vec<DiffEntry>,
5214) -> Result<bool> {
5215 let identity = conversion_is_identity();
5216 let size_differs = worktree_size != ie.size;
5217 if !size_differs || !identity {
5218 return Ok(false);
5219 }
5220 let threshold = odb.big_file_threshold()?;
5221 let big_file = raw_worktree_len > threshold;
5222 let proves_modified = if big_file {
5223 true
5224 } else {
5225 status_size_change_proves_modified(ie, worktree_size, for_status, || identity)
5226 };
5227 if !proves_modified {
5228 return Ok(false);
5229 }
5230 result.push(worktree_content_change_entry(
5231 ie,
5232 path_str_ref,
5233 worktree_mode,
5234 zero_oid(),
5235 ));
5236 Ok(true)
5237}
5238
5239#[allow(clippy::too_many_arguments)]
5240fn push_index_blob_worktree_diff(
5241 odb: &Odb,
5242 ie: &IndexEntry,
5243 path_str_ref: &str,
5244 file_path: &Path,
5245 meta: fs::Metadata,
5246 index_mtime: Option<(u32, u32)>,
5247 mode_policy: crate::index::WorktreeModePolicy,
5248 stat_policy: crate::index::StatMatchPolicy,
5249 file_attrs_for: &dyn Fn(&str) -> crate::crlf::FileAttrs,
5250 conv: &crate::crlf::ConversionConfig,
5251 materialize_dirty_blobs: bool,
5252 hash_blobs_without_write: bool,
5253 for_status: bool,
5254 filter_process: Option<&crate::filter_process::FilterProcessState>,
5255 icase_collisions: Option<&crate::path_icase::IcaseCheckoutCollisionMap>,
5256 result: &mut Vec<DiffEntry>,
5257) -> Result<()> {
5258 if meta.is_dir() {
5259 if file_path.join(".git").exists() {
5260 let head = read_submodule_head_oid(file_path).unwrap_or_else(zero_oid);
5261 let path_owned = path_str_ref.to_owned();
5262 result.push(DiffEntry {
5263 status: DiffStatus::TypeChanged,
5264 old_path: Some(path_owned.clone()),
5265 new_path: Some(path_owned),
5266 old_mode: FileMode::new(ie.mode),
5267 new_mode: FileMode::new(0o160000),
5268 old_oid: ie.oid,
5269 new_oid: head,
5270 score: None,
5271 });
5272 return Ok(());
5273 }
5274 result.push(DiffEntry {
5275 status: DiffStatus::Deleted,
5276 old_path: Some(path_str_ref.to_owned()),
5277 new_path: None,
5278 old_mode: FileMode::new(ie.mode),
5279 new_mode: FileMode::ABSENT,
5280 old_oid: ie.oid,
5281 new_oid: zero_oid(),
5282 score: None,
5283 });
5284 return Ok(());
5285 }
5286
5287 let worktree_mode =
5288 mode_policy.index_mode_for_worktree(mode_from_metadata(&meta), Some(ie.mode));
5289 let stat_same = stat_matches_with_policy(ie, &meta, stat_policy);
5290 if stat_same && worktree_mode != ie.mode {
5291 result.push(worktree_content_change_entry(
5292 ie,
5293 path_str_ref,
5294 worktree_mode,
5295 ie.oid,
5296 ));
5297 return Ok(());
5298 }
5299
5300 if stat_same
5301 && index_entry_skips_worktree_content_hash_after_stat_match(ie, index_mtime, stat_policy)
5302 {
5303 return Ok(());
5304 }
5305
5306 let file_attrs = file_attrs_for(path_str_ref);
5307 if !icase_collisions.is_some_and(|map| map.diff_requires_worktree_hash(ie.path.as_slice()))
5308 && try_push_status_modified_on_size_mismatch(
5309 odb,
5310 ie,
5311 path_str_ref,
5312 crate::index::munge_index_size(meta.len()),
5313 worktree_mode,
5314 meta.len(),
5315 for_status,
5316 || crate::crlf::git_conversion_is_identity(conv, &file_attrs),
5317 result,
5318 )?
5319 {
5320 return Ok(());
5321 }
5322
5323 let store = (materialize_dirty_blobs && !hash_blobs_without_write)
5324 .then(|| odb.bulk_stage_write_options());
5325 let stream_from = if store.is_some() {
5326 odb.big_file_threshold()?
5327 } else {
5328 hash_stream_threshold(odb)?
5329 };
5330 let content = match worktree_blob_content_from(
5331 odb,
5332 file_path,
5333 &meta,
5334 conv,
5335 &file_attrs,
5336 path_str_ref,
5337 Some(ie),
5338 filter_process,
5339 stream_from,
5340 ) {
5341 Ok(content) => content,
5342 Err(err) if unreadable_worktree_blob_is_modified(&err, materialize_dirty_blobs) => {
5343 result.push(worktree_content_change_entry(
5344 ie,
5345 path_str_ref,
5346 worktree_mode,
5347 zero_oid(),
5348 ));
5349 return Ok(());
5350 }
5351 Err(err) => return Err(err),
5352 };
5353 let eff_oid = content.object_id(odb, store)?;
5354
5355 if eff_oid != ie.oid || worktree_mode != ie.mode {
5356 let new_oid = if eff_oid != ie.oid && !materialize_dirty_blobs {
5359 zero_oid()
5360 } else {
5361 eff_oid
5362 };
5363 result.push(worktree_content_change_entry(
5364 ie,
5365 path_str_ref,
5366 worktree_mode,
5367 new_oid,
5368 ));
5369 }
5370 Ok(())
5371}
5372
5373pub fn diff_index_to_worktree_with_options(
5383 odb: &Odb,
5384 index: &mut Index,
5385 work_tree: &Path,
5386 options: DiffIndexToWorktreeOptions,
5387) -> Result<(Vec<DiffEntry>, bool)> {
5388 let default_git_dir = work_tree.join(".git");
5389 let git_dir = options
5390 .repository_git_dir
5391 .clone()
5392 .unwrap_or(default_git_dir);
5393 diff_index_to_worktree_inner(odb, index, work_tree, &git_dir, options, false, None)
5394 .map(|(entries, changed, _)| (entries, changed))
5395}
5396
5397pub(crate) fn diff_index_to_worktree_with_owner_poll(
5400 odb: &Odb,
5401 index: &mut Index,
5402 work_tree: &Path,
5403 options: DiffIndexToWorktreeOptions,
5404 owner_poll: Option<std::sync::Arc<crate::progress::StackCancelPoll>>,
5405) -> Result<(Vec<DiffEntry>, bool)> {
5406 let default_git_dir = work_tree.join(".git");
5407 let git_dir = options
5408 .repository_git_dir
5409 .clone()
5410 .unwrap_or(default_git_dir);
5411 diff_index_to_worktree_inner(odb, index, work_tree, &git_dir, options, false, owner_poll)
5412 .map(|(entries, changed, _)| (entries, changed))
5413}
5414
5415pub fn diff_index_to_worktree_for_staging(
5421 odb: &Odb,
5422 repository_git_dir: &Path,
5423 index: &mut Index,
5424 work_tree: &Path,
5425 options: DiffIndexToWorktreeOptions,
5426) -> Result<(
5427 Vec<DiffEntry>,
5428 bool,
5429 std::collections::BTreeMap<String, crate::index::IndexStatFields>,
5430)> {
5431 diff_index_to_worktree_inner(
5432 odb,
5433 index,
5434 work_tree,
5435 repository_git_dir,
5436 options,
5437 true,
5438 None,
5439 )
5440}
5441
5442#[allow(clippy::too_many_arguments)]
5444fn worktree_diff_pass1_index_entry(
5445 odb: &Odb,
5446 work_tree: &Path,
5447 ie: &IndexEntry,
5448 path_str_ref: &str,
5449 simplify_gitlinks: bool,
5450 ignore_submodule_untracked: bool,
5451 error_on_broken_gitlinks: bool,
5452 materialize_dirty_blobs: bool,
5453 filter_fp: Option<&crate::filter_process::FilterProcessState>,
5454 conv: &crate::crlf::ConversionConfig,
5455 file_attrs_for: &dyn Fn(&str) -> crate::crlf::FileAttrs,
5456 precompose_unicode: bool,
5457 ignorecase: bool,
5458 result: &mut Vec<DiffEntry>,
5459) -> Result<()> {
5460 if ie.skip_worktree() || ie.assume_unchanged() {
5461 return Ok(());
5462 }
5463 let is_intent_to_add = ie.intent_to_add();
5464
5465 if ie.mode == 0o160000 {
5466 let sub_dir =
5467 index_entry_worktree_abs_bytes(work_tree, &ie.path, precompose_unicode, ignorecase);
5468 match fs::symlink_metadata(&sub_dir) {
5469 Err(e)
5470 if e.kind() == std::io::ErrorKind::NotFound
5471 || e.raw_os_error() == Some(20) =>
5472 {
5473 if !simplify_gitlinks {
5474 let path_owned = path_str_ref.to_owned();
5475 result.push(DiffEntry {
5476 status: DiffStatus::Deleted,
5477 old_path: Some(path_owned.clone()),
5478 new_path: Some(path_owned),
5479 old_mode: FileMode::new(ie.mode),
5480 new_mode: FileMode::ABSENT,
5481 old_oid: ie.oid,
5482 new_oid: zero_oid(),
5483 score: None,
5484 });
5485 }
5486 return Ok(());
5487 }
5488 Err(e) => return Err(Error::Io(e)),
5489 Ok(meta) if !meta.is_dir() => {
5490 let worktree_mode = mode_from_metadata(&meta);
5491 let file_attrs = file_attrs_for(path_str_ref);
5492 let worktree_oid = worktree_file_oid(
5493 odb,
5494 &sub_dir,
5495 &meta,
5496 conv,
5497 &file_attrs,
5498 path_str_ref,
5499 None,
5500 materialize_dirty_blobs,
5501 filter_fp,
5502 )?;
5503 let path_owned = path_str_ref.to_owned();
5504 result.push(DiffEntry {
5505 status: DiffStatus::TypeChanged,
5506 old_path: Some(path_owned.clone()),
5507 new_path: Some(path_owned),
5508 old_mode: FileMode::new(ie.mode),
5509 new_mode: FileMode::new(worktree_mode),
5510 old_oid: ie.oid,
5511 new_oid: worktree_oid,
5512 score: None,
5513 });
5514 return Ok(());
5515 }
5516 Ok(_) => {}
5517 }
5518 let sub_head_oid = read_submodule_head_oid(&sub_dir);
5519 let ref_matches = if let Some(oid) = sub_head_oid {
5520 oid == ie.oid
5521 } else {
5522 if error_on_broken_gitlinks && submodule_embedded_git_dir(&sub_dir).is_some() {
5523 return Err(Error::Config(
5524 format!("could not read submodule HEAD for '{path_str_ref}'").into(),
5525 ));
5526 }
5527 uninitialized_gitlink_worktree_matches_index(&sub_dir)
5528 };
5529 if simplify_gitlinks {
5530 if !ref_matches {
5531 let path_owned = path_str_ref.to_owned();
5532 let new_oid = sub_head_oid.unwrap_or_else(zero_oid);
5533 result.push(DiffEntry {
5534 status: DiffStatus::Modified,
5535 old_path: Some(path_owned.clone()),
5536 new_path: Some(path_owned),
5537 old_mode: FileMode::new(ie.mode),
5538 new_mode: FileMode::new(ie.mode),
5539 old_oid: ie.oid,
5540 new_oid,
5541 score: None,
5542 });
5543 }
5544 return Ok(());
5545 }
5546 if sub_head_oid.is_some() && submodule_head_object_broken(&sub_dir) {
5547 return Err(Error::Config(
5548 format!("'git status --porcelain=2' failed in submodule {path_str_ref}").into(),
5549 ));
5550 }
5551 let mut flags = submodule_porcelain_flags(work_tree, path_str_ref, ie.oid);
5552 if ignore_submodule_untracked {
5553 flags.untracked = false;
5554 }
5555 let inner_dirty = flags.modified || flags.untracked;
5556 if !ref_matches || inner_dirty {
5557 let path_owned = path_str_ref.to_owned();
5558 let new_oid = if !ref_matches {
5559 sub_head_oid.unwrap_or_else(zero_oid)
5560 } else {
5561 zero_oid()
5562 };
5563 result.push(DiffEntry {
5564 status: DiffStatus::Modified,
5565 old_path: Some(path_owned.clone()),
5566 new_path: Some(path_owned),
5567 old_mode: FileMode::new(ie.mode),
5568 new_mode: FileMode::new(ie.mode),
5569 old_oid: ie.oid,
5570 new_oid,
5571 score: None,
5572 });
5573 }
5574 return Ok(());
5575 }
5576
5577 let file_path =
5578 index_entry_worktree_abs_bytes(work_tree, &ie.path, precompose_unicode, ignorecase);
5579 let path_match = crate::repo_relpath::as_utf8_for_matching(path_str_ref);
5580
5581 if is_intent_to_add {
5582 match fs::symlink_metadata(&file_path) {
5583 Ok(meta) => {
5584 let file_attrs = file_attrs_for(path_match.as_ref());
5585 let worktree_oid = worktree_file_oid(
5586 odb,
5587 &file_path,
5588 &meta,
5589 conv,
5590 &file_attrs,
5591 path_match.as_ref(),
5592 None,
5593 materialize_dirty_blobs,
5594 filter_fp,
5595 )?;
5596 let worktree_mode = mode_from_metadata(&meta);
5597 result.push(DiffEntry {
5598 status: DiffStatus::Added,
5599 old_path: None,
5600 new_path: Some(path_str_ref.to_owned()),
5601 old_mode: FileMode::ABSENT,
5602 new_mode: FileMode::new(worktree_mode),
5603 old_oid: zero_oid(),
5604 new_oid: worktree_oid,
5605 score: None,
5606 });
5607 }
5608 Err(e)
5609 if e.kind() == std::io::ErrorKind::NotFound
5610 || e.raw_os_error() == Some(20) =>
5611 {
5612 result.push(DiffEntry {
5613 status: DiffStatus::Deleted,
5614 old_path: Some(path_str_ref.to_owned()),
5615 new_path: None,
5616 old_mode: FileMode::new(ie.mode),
5617 new_mode: FileMode::ABSENT,
5618 old_oid: ie.oid,
5619 new_oid: zero_oid(),
5620 score: None,
5621 });
5622 }
5623 Err(e) => return Err(Error::Io(e)),
5624 }
5625 }
5626 Ok(())
5627}
5628
5629#[inline]
5630fn index_entry_needs_worktree_diff_pass1(ie: &IndexEntry) -> bool {
5631 ie.mode == crate::index::MODE_GITLINK || ie.intent_to_add()
5632}
5633
5634fn diff_index_to_worktree_inner(
5635 odb: &Odb,
5636 index: &mut Index,
5637 work_tree: &Path,
5638 repository_git_dir: &Path,
5639 options: DiffIndexToWorktreeOptions,
5640 materialize_dirty_blobs: bool,
5641 owner_poll: Option<std::sync::Arc<crate::progress::StackCancelPoll>>,
5642) -> Result<(
5643 Vec<DiffEntry>,
5644 bool,
5645 std::collections::BTreeMap<String, crate::index::IndexStatFields>,
5646)> {
5647 use crate::config::ConfigSet;
5648 use crate::crlf;
5649 use std::collections::BTreeMap;
5650
5651 let mut staging_stat_cache = BTreeMap::new();
5652 poll_diff_cancel(&options, owner_poll.as_ref())?;
5653 let index_mtime = index_mtime_for_diff(index, options.index_mtime);
5654
5655 let ignore_submodule_untracked = options.ignore_submodule_untracked;
5656 let simplify_gitlinks = options.simplify_gitlinks;
5657
5658 let rules_lock = options
5659 .worktree_rules
5660 .as_ref()
5661 .map(|shared| {
5662 shared
5663 .lock()
5664 .map_err(|e| Error::Message(format!("worktree rules lock poisoned: {e}")))
5665 })
5666 .transpose()?;
5667 let config = if let Some(rules) = rules_lock.as_ref() {
5668 rules.config().clone()
5669 } else if let Some(c) = options.config.as_ref() {
5670 c.as_ref().clone()
5671 } else {
5672 ConfigSet::load(
5673 &crate::environment::Environment::empty(),
5674 Some(repository_git_dir),
5675 true,
5676 )
5677 .unwrap_or_else(|_| ConfigSet::new())
5678 };
5679 let conv = rules_lock
5680 .as_ref()
5681 .map(|r| r.conversion().clone())
5682 .unwrap_or_else(|| crlf::ConversionConfig::from_config(&config));
5683 let legacy_attrs = if rules_lock.is_none() {
5684 Some(crlf::load_gitattributes_parsed(work_tree))
5685 } else {
5686 None
5687 };
5688 let file_attrs_for = |path: &str| -> crlf::FileAttrs {
5689 if let Some(rules) = rules_lock.as_ref() {
5690 rules.file_attrs(path, false)
5691 } else if let Some(parsed) = legacy_attrs.as_ref() {
5692 crlf::file_attrs_from_gitattributes(&parsed.rules, &parsed.macros, path, false, &config)
5693 } else {
5694 crlf::FileAttrs::default()
5695 }
5696 };
5697 let filter_fp = rules_lock.as_ref().map(|r| r.filter_process());
5698 let precompose_unicode = config
5699 .get_bool("core.precomposeunicode")
5700 .and_then(|r| r.ok())
5701 .unwrap_or(false);
5702 let mode_policy = crate::index::WorktreeModePolicy::from_config(&config);
5703 let stat_policy = crate::index::StatMatchPolicy::from_config(Some(&config));
5704 let ignorecase = config
5705 .get_bool("core.ignorecase")
5706 .and_then(|r| r.ok())
5707 .unwrap_or(false);
5708 let icase_collisions = ignorecase
5709 .then(|| crate::path_icase::IcaseCheckoutCollisionMap::from_index(work_tree, index, true));
5710 let hash_blobs_without_write = options.hash_blobs_without_write;
5711
5712 let mut result = Vec::new();
5713 let refresh_index_stat_in_pass = options.refresh_index_stat_in_pass;
5714 let mut index_stat_refresh_changed = false;
5715 let mut honor_fsmonitor_valid = false;
5716 if options.refresh_fsmonitor {
5717 let default_runner = crate::command_runner::system_command_runner();
5718 let runner = options
5719 .command_runner
5720 .as_deref()
5721 .unwrap_or(default_runner.as_ref());
5722 let boundary = options.fsmonitor_query_boundary.unwrap_or_else(|| {
5723 crate::fsmonitor::FsmonitorQueryBoundary(
5724 crate::environment::wall_clock_unix_timestamp_nanos(),
5725 )
5726 });
5727 let refresh_opts = crate::fsmonitor::FsmonitorRefreshOptions::with_query_boundary(boundary);
5728 let refresh = crate::fsmonitor::refresh_index(
5729 index,
5730 repository_git_dir,
5731 work_tree,
5732 &config,
5733 Some(runner),
5734 &refresh_opts,
5735 )?;
5736 if refresh.index_changed {
5737 index_stat_refresh_changed = true;
5738 }
5739 honor_fsmonitor_valid = refresh.honor_valid_bits;
5740 }
5741 let use_fsmonitor_stat_skip = honor_fsmonitor_valid;
5742 let mut unmerged_base: std::collections::BTreeMap<String, (u8, IndexEntry)> =
5743 std::collections::BTreeMap::new();
5744
5745 let error_on_broken_gitlinks = options.error_on_broken_gitlinks;
5746 if options.restrict_to_paths.is_none() {
5747 for ie in &index.entries {
5748 poll_diff_cancel(&options, owner_poll.as_ref())?;
5749 if ie.stage() != 0 {
5750 let path = String::from_utf8_lossy(&ie.path).to_string();
5751 let rank = match ie.stage() {
5752 2 => 0u8,
5753 3 => 1u8,
5754 1 => 2u8,
5755 _ => 3u8,
5756 };
5757 match unmerged_base.get(&path) {
5758 Some((existing_rank, _)) if *existing_rank <= rank => {}
5759 _ => {
5760 unmerged_base.insert(path, (rank, ie.clone()));
5761 }
5762 }
5763 }
5764 }
5765 }
5766
5767 if let Some(limit) = options.restrict_to_paths.as_ref() {
5768 for path in limit.iter() {
5769 let Some(ie) = index.get(path.as_bytes(), 0) else {
5770 continue;
5771 };
5772 if !index_entry_needs_worktree_diff_pass1(ie) {
5773 continue;
5774 }
5775 worktree_diff_pass1_index_entry(
5776 odb,
5777 work_tree,
5778 ie,
5779 path.as_str(),
5780 simplify_gitlinks,
5781 ignore_submodule_untracked,
5782 error_on_broken_gitlinks,
5783 materialize_dirty_blobs,
5784 filter_fp,
5785 &conv,
5786 &file_attrs_for,
5787 precompose_unicode,
5788 ignorecase,
5789 &mut result,
5790 )?;
5791 }
5792 } else {
5793 for ie in &index.entries {
5794 poll_diff_cancel(&options, owner_poll.as_ref())?;
5795 if ie.stage() != 0 {
5796 continue;
5797 }
5798 if !index_entry_needs_worktree_diff_pass1(ie) {
5799 continue;
5800 }
5801 let path_stored = crate::repo_relpath::storage_string(&ie.path);
5802 worktree_diff_pass1_index_entry(
5803 odb,
5804 work_tree,
5805 ie,
5806 &path_stored,
5807 simplify_gitlinks,
5808 ignore_submodule_untracked,
5809 error_on_broken_gitlinks,
5810 materialize_dirty_blobs,
5811 filter_fp,
5812 &conv,
5813 &file_attrs_for,
5814 precompose_unicode,
5815 ignorecase,
5816 &mut result,
5817 )?;
5818 }
5819 }
5820
5821 use crate::worktree_scan::{
5822 collect_blob_lookup_batches_with_literal_paths, group_blob_entries_by_dir,
5823 stat_scan_thread_count, visit_blob_lookup_batches, BlobDiskLookup, BlobScanSparseMode,
5824 WorktreeBlobScanOptions, PARALLEL_STAT_MIN_ENTRIES,
5825 };
5826 let preload_index = config
5827 .get_bool("core.preloadIndex")
5828 .and_then(|r| r.ok())
5829 .unwrap_or(true);
5830 let blob_scan = WorktreeBlobScanOptions {
5831 preload_index,
5832 stat_parallel_threads: options.stat_parallel_threads,
5833 };
5834 let blob_dirs = if let Some(limit) = options.restrict_to_paths.as_ref() {
5835 crate::worktree_scan::group_blob_entries_by_dir_for_paths(index, limit)
5836 } else {
5837 group_blob_entries_by_dir(index, honor_fsmonitor_valid)
5838 };
5839 let work_tree_owned = work_tree.to_path_buf();
5840 let dir_abs = move |dir: &[u8]| {
5841 if dir.is_empty() {
5842 work_tree_owned.clone()
5843 } else {
5844 index_entry_worktree_abs_bytes(&work_tree_owned, dir, precompose_unicode, ignorecase)
5845 }
5846 };
5847 let work_tree_for_files = work_tree.to_path_buf();
5848 let file_abs = move |rel: &[u8]| {
5849 index_entry_worktree_abs_bytes(&work_tree_for_files, rel, precompose_unicode, ignorecase)
5850 };
5851 let blob_count = blob_dirs.len();
5852 let thread_policy = options.thread_policy.unwrap_or_default();
5853 let hash_threads = stat_scan_thread_count(&thread_policy, &config, blob_scan, blob_count);
5854 let parallel_blob_work = blob_count >= PARALLEL_STAT_MIN_ENTRIES && hash_threads > 1;
5855 let mut staging_hash_jobs: Vec<StagingHashJob> = Vec::new();
5856 let mut refresh_content_verified = vec![false; index.entries.len()];
5858 let sparse = if parallel_blob_work {
5863 BlobScanSparseMode::StatusDiff {
5864 stat_policy,
5865 index_mtime,
5866 mode_policy,
5867 for_status: options.for_status,
5868 refresh_index_stat_in_pass,
5869 parallel_blob_work,
5870 honor_fsmonitor_stat_skip: use_fsmonitor_stat_skip,
5871 }
5872 } else {
5873 BlobScanSparseMode::Dense
5874 };
5875 let blob_stat_batches = collect_blob_lookup_batches_with_literal_paths(
5876 index,
5877 &blob_dirs,
5878 blob_scan,
5879 &thread_policy,
5880 &config,
5881 dir_abs,
5882 file_abs,
5883 !precompose_unicode && !ignorecase,
5884 || {
5885 let mut dir_symlinks = SymlinkDirCache::default();
5886 move |rel_path: &[u8]| {
5887 dir_symlinks.has_symlink_in_path_bytes(
5888 work_tree,
5889 rel_path,
5890 precompose_unicode,
5891 ignorecase,
5892 )
5893 }
5894 },
5895 sparse,
5896 options.cancel_watch.clone(),
5897 owner_poll.clone(),
5898 )?;
5899 let identity_by_entry = std::cell::RefCell::new(std::collections::HashMap::new());
5902 let conversion_is_identity = |entry_index: usize, path: &str| -> bool {
5903 *identity_by_entry
5904 .borrow_mut()
5905 .entry(entry_index)
5906 .or_insert_with(|| crlf::git_conversion_is_identity(&conv, &file_attrs_for(path)))
5907 };
5908 let refresh_checks = if refresh_index_stat_in_pass {
5909 let par_opts =
5910 refresh_hash_parallel_options(&thread_policy, &config, options.stat_parallel_threads);
5911 refresh_content_checks(
5912 odb,
5913 index,
5914 &blob_stat_batches,
5915 index_mtime,
5916 stat_policy,
5917 &|entry_index, ie, stat| {
5918 !(use_fsmonitor_stat_skip && crate::fsmonitor::entry_skips_worktree_stat(ie, index))
5919 && worktree_diff_refreshes_entry(
5920 ie,
5921 stat,
5922 index_mtime,
5923 stat_policy,
5924 options.for_status,
5925 || {
5926 conversion_is_identity(
5927 entry_index,
5928 &crate::repo_relpath::storage_string(&ie.path),
5929 )
5930 },
5931 )
5932 },
5933 &WorktreeHashContext {
5934 work_tree,
5935 precompose_unicode,
5936 ignorecase,
5937 conv: &conv,
5938 file_attrs_for: &file_attrs_for,
5939 filter_process: filter_fp,
5940 },
5941 par_opts,
5942 options.cancel_watch.as_ref(),
5943 )?
5944 } else {
5945 RefreshChecks::default()
5946 };
5947 poll_diff_cancel(&options, owner_poll.as_ref())?;
5948 visit_blob_lookup_batches(blob_stat_batches, |entry_index, lookup| {
5949 poll_diff_cancel(&options, owner_poll.as_ref())?;
5950 let path_owned = crate::repo_relpath::storage_string(&index.entries[entry_index].path);
5951 let path_str_ref = path_owned.as_str();
5952 if use_fsmonitor_stat_skip {
5953 let ie_skip = &index.entries[entry_index];
5954 if crate::fsmonitor::entry_skips_worktree_stat(ie_skip, index) {
5955 return Ok(());
5956 }
5957 }
5958 match lookup {
5959 BlobDiskLookup::Missing => {
5960 if icase_collisions.as_ref().is_some_and(|map| {
5961 map.preserve_missing_collision_loser(&index.entries[entry_index].path)
5962 }) {
5963 return Ok(());
5964 }
5965 let ie = &index.entries[entry_index];
5966 result.push(DiffEntry {
5967 status: DiffStatus::Deleted,
5968 old_path: Some(path_str_ref.to_owned()),
5969 new_path: None,
5970 old_mode: FileMode::new(ie.mode),
5971 new_mode: FileMode::ABSENT,
5972 old_oid: ie.oid,
5973 new_oid: zero_oid(),
5974 score: None,
5975 });
5976 }
5977 BlobDiskLookup::Io(e) => return Err(Error::Io(e)),
5978 BlobDiskLookup::Present(stat) => {
5979 if materialize_dirty_blobs {
5980 staging_stat_cache.insert(path_str_ref.to_owned(), stat.fields);
5981 }
5982 if refresh_index_stat_in_pass
5983 && worktree_diff_refreshes_entry(
5984 &index.entries[entry_index],
5985 &stat,
5986 index_mtime,
5987 stat_policy,
5988 options.for_status,
5989 || conversion_is_identity(entry_index, path_str_ref),
5990 )
5991 {
5992 let content_matches = RefreshContent::matches(
5993 refresh_checks.get(entry_index),
5994 &index.entries[entry_index].oid,
5995 );
5996 let refresh = refresh_index_blob_stat_from_scan(
5997 &mut index.entries[entry_index],
5998 &stat,
5999 index_mtime,
6000 stat_policy,
6001 content_matches,
6002 );
6003 if refresh.index_changed {
6004 index_stat_refresh_changed = true;
6005 }
6006 if refresh.content_verified {
6007 refresh_content_verified[entry_index] = true;
6008 let path = index.entries[entry_index].path.clone();
6009 let oid = index.entries[entry_index].oid;
6010 index.mark_content_verified(&path, oid);
6011 }
6012 if index.fsmonitor_last_update.is_some()
6013 && stat_matches_blob_stat(&index.entries[entry_index], &stat, stat_policy)
6014 && !refresh.index_changed
6015 {
6016 index.entries[entry_index].set_fsmonitor_valid(true);
6017 }
6018 }
6019 let ie = &index.entries[entry_index];
6020 let worktree_mode =
6021 mode_policy.index_mode_for_worktree(stat.git_mode, Some(ie.mode));
6022 if !materialize_dirty_blobs
6025 && !stat.is_dir
6026 && stat.git_mode != crate::index::MODE_SYMLINK
6027 && RefreshContent::differs(refresh_checks.get(entry_index), &ie.oid)
6028 {
6029 result.push(worktree_content_change_entry(
6030 ie,
6031 path_str_ref,
6032 worktree_mode,
6033 zero_oid(),
6034 ));
6035 return Ok(());
6036 }
6037 if parallel_blob_work && !stat.is_dir && stat.git_mode != crate::index::MODE_SYMLINK
6041 {
6042 let stat_same = stat_matches_blob_stat(ie, &stat, stat_policy);
6043 if stat_same
6044 && worktree_mode == ie.mode
6045 && index_entry_skips_worktree_content_hash_after_stat_match(
6046 ie,
6047 index_mtime,
6048 stat_policy,
6049 )
6050 {
6051 return Ok(());
6052 }
6053 if stat_same && worktree_mode != ie.mode {
6054 result.push(worktree_content_change_entry(
6055 ie,
6056 path_str_ref,
6057 worktree_mode,
6058 ie.oid,
6059 ));
6060 } else if !icase_collisions.as_ref().is_some_and(|map| {
6061 map.diff_requires_worktree_hash(&index.entries[entry_index].path)
6062 }) && try_push_status_modified_on_size_mismatch(
6063 odb,
6064 ie,
6065 path_str_ref,
6066 stat.worktree_size(),
6067 worktree_mode,
6068 stat.raw_size,
6069 options.for_status,
6070 || conversion_is_identity(entry_index, path_str_ref),
6071 &mut result,
6072 )? {
6073 } else if !refresh_content_verified[entry_index] {
6074 let file_path = index_entry_worktree_abs_bytes(
6075 work_tree,
6076 &index.entries[entry_index].path,
6077 precompose_unicode,
6078 ignorecase,
6079 );
6080 staging_hash_jobs.push(StagingHashJob {
6081 entry_index,
6082 rel_path: path_str_ref.to_owned(),
6083 file_path,
6084 entry: ie.clone(),
6085 worktree_mode,
6086 file_attrs: file_attrs_for(path_str_ref),
6087 });
6088 }
6089 } else {
6090 let same_file_type = (worktree_mode == crate::index::MODE_SYMLINK)
6094 == (ie.mode == crate::index::MODE_SYMLINK);
6095 if !stat.is_dir
6096 && worktree_mode == ie.mode
6097 && ((refresh_content_verified[entry_index] && same_file_type)
6098 || (stat_matches_blob_stat(ie, &stat, stat_policy)
6099 && index_entry_skips_worktree_content_hash_after_stat_match(
6100 ie,
6101 index_mtime,
6102 stat_policy,
6103 )))
6104 {
6105 return Ok(());
6106 }
6107 let file_path = index_entry_worktree_abs_bytes(
6108 work_tree,
6109 &index.entries[entry_index].path,
6110 precompose_unicode,
6111 ignorecase,
6112 );
6113 let meta = fs::symlink_metadata(&file_path).map_err(Error::Io)?;
6114 push_index_blob_worktree_diff(
6115 odb,
6116 ie,
6117 path_str_ref,
6118 &file_path,
6119 meta,
6120 index_mtime,
6121 mode_policy,
6122 stat_policy,
6123 &file_attrs_for,
6124 &conv,
6125 materialize_dirty_blobs,
6126 hash_blobs_without_write,
6127 options.for_status,
6128 filter_fp,
6129 icase_collisions.as_ref(),
6130 &mut result,
6131 )?;
6132 }
6133 }
6134 }
6135 Ok(())
6136 })?;
6137 if parallel_blob_work && !staging_hash_jobs.is_empty() {
6138 parallel_process_blob_worktree_jobs(
6139 odb,
6140 &staging_hash_jobs,
6141 &thread_policy,
6142 &config,
6143 materialize_dirty_blobs,
6144 hash_blobs_without_write,
6145 &conv,
6146 filter_fp,
6147 icase_collisions.as_ref(),
6148 &mut result,
6149 )?;
6150 }
6151
6152 for (path, (_, base_entry)) in unmerged_base {
6153 let file_path = index_entry_worktree_abs(work_tree, &path, precompose_unicode, ignorecase);
6154 let wt_meta = match fs::symlink_metadata(&file_path) {
6155 Ok(meta) => Some(meta),
6156 Err(e)
6157 if e.kind() == std::io::ErrorKind::NotFound
6158 || e.raw_os_error() == Some(20) =>
6159 {
6160 None
6161 }
6162 Err(e) => return Err(Error::Io(e)),
6163 };
6164
6165 let new_mode = wt_meta.as_ref().map_or_else(
6166 || FileMode::ABSENT,
6167 |meta| FileMode::new(mode_from_metadata(meta)),
6168 );
6169 result.push(DiffEntry {
6170 status: DiffStatus::Unmerged,
6171 old_path: Some(path.clone()),
6172 new_path: Some(path.clone()),
6173 old_mode: FileMode::ABSENT,
6174 new_mode,
6175 old_oid: zero_oid(),
6176 new_oid: zero_oid(),
6177 score: None,
6178 });
6179
6180 if !options.for_status {
6181 if let Some(meta) = wt_meta {
6182 let file_attrs = file_attrs_for(&path);
6183 let wt_oid = worktree_file_oid(
6184 odb,
6185 &file_path,
6186 &meta,
6187 &conv,
6188 &file_attrs,
6189 &path,
6190 Some(&base_entry),
6191 materialize_dirty_blobs,
6192 filter_fp,
6193 )?;
6194 let wt_mode = mode_from_metadata(&meta);
6195 if wt_oid != base_entry.oid || wt_mode != base_entry.mode {
6196 result.push(DiffEntry {
6197 status: DiffStatus::for_content_change(base_entry.mode, wt_mode),
6198 old_path: Some(path.clone()),
6199 new_path: Some(path),
6200 old_mode: FileMode::new(base_entry.mode),
6201 new_mode: FileMode::new(wt_mode),
6202 old_oid: base_entry.oid,
6203 new_oid: wt_oid,
6204 score: None,
6205 });
6206 }
6207 }
6208 }
6209 }
6210
6211 Ok((result, index_stat_refresh_changed, staging_stat_cache))
6212}
6213
6214#[derive(Default)]
6221pub(crate) struct SymlinkDirCache {
6222 real_dirs: Vec<u8>,
6225 blocked: Option<(Vec<u8>, bool)>,
6228 #[cfg(all(target_os = "linux", target_pointer_width = "64"))]
6230 chain: Option<crate::worktree_scan::RealDirChain>,
6231 #[cfg(test)]
6233 fs_queries: usize,
6234}
6235
6236fn path_within_dir(path: &[u8], dir: &[u8]) -> bool {
6238 path.starts_with(dir) && path.get(dir.len()).is_none_or(|&b| b == b'/')
6239}
6240
6241fn common_dir_prefix_len(a: &[u8], b: &[u8]) -> usize {
6243 let same = a.iter().zip(b).take_while(|(x, y)| x == y).count();
6244 let ends_component = |p: &[u8]| p.get(same).is_none_or(|&c| c == b'/');
6245 if ends_component(a) && ends_component(b) {
6246 return same;
6247 }
6248 a[..same].iter().rposition(|&c| c == b'/').unwrap_or(0)
6249}
6250
6251pub(crate) fn index_entry_worktree_abs(
6252 work_tree: &Path,
6253 index_relpath: &str,
6254 precompose_unicode: bool,
6255 ignorecase: bool,
6256) -> PathBuf {
6257 index_entry_worktree_abs_bytes(
6258 work_tree,
6259 index_relpath.as_bytes(),
6260 precompose_unicode,
6261 ignorecase,
6262 )
6263}
6264
6265pub(crate) fn index_entry_worktree_abs_bytes(
6266 work_tree: &Path,
6267 index_relpath: &[u8],
6268 precompose_unicode: bool,
6269 ignorecase: bool,
6270) -> PathBuf {
6271 if precompose_unicode || ignorecase {
6272 if let Ok(s) = std::str::from_utf8(index_relpath) {
6273 if precompose_unicode {
6274 return crate::unicode_normalization::resolve_worktree_path_for_staging(
6275 work_tree, s, true,
6276 )
6277 .abs;
6278 }
6279 return crate::path_icase::worktree_path_for_index_entry(work_tree, s, ignorecase);
6280 }
6281 }
6282 crate::index::worktree_path_from_index_rel(work_tree, index_relpath)
6283}
6284
6285#[must_use]
6290pub fn index_mtime_for_diff(index: &Index, explicit: Option<(u32, u32)>) -> Option<(u32, u32)> {
6291 explicit.or(index.source_mtime)
6292}
6293
6294impl SymlinkDirCache {
6295 pub(crate) fn has_symlink_in_path_bytes(
6300 &mut self,
6301 work_tree: &Path,
6302 rel_path: &[u8],
6303 precompose_unicode: bool,
6304 ignorecase: bool,
6305 ) -> bool {
6306 use crate::worktree_scan::{ancestor_kind, AncestorKind};
6307
6308 let Some(dir_len) = rel_path.iter().rposition(|&b| b == b'/') else {
6309 return false;
6310 };
6311 let dir = &rel_path[..dir_len];
6312 if let Some((blocked, is_symlink)) = &self.blocked {
6313 if path_within_dir(dir, blocked) {
6314 return *is_symlink;
6315 }
6316 }
6317 let verified = common_dir_prefix_len(dir, &self.real_dirs);
6318 self.real_dirs.truncate(verified);
6319 #[cfg(all(target_os = "linux", target_pointer_width = "64"))]
6320 if !precompose_unicode
6321 && !ignorecase
6322 && dir[verified..]
6323 .split(|&b| b == b'/')
6324 .filter(|component| !component.is_empty())
6325 .nth(1)
6326 .is_some()
6327 {
6328 let chain = match &mut self.chain {
6329 Some(chain) if chain.work_tree() == work_tree => chain,
6330 slot => slot.insert(crate::worktree_scan::RealDirChain::open(work_tree)),
6331 };
6332 #[cfg(test)]
6333 {
6334 self.fs_queries += 1;
6335 }
6336 if chain.confirms(dir) {
6337 self.real_dirs.clear();
6338 self.real_dirs.extend_from_slice(dir);
6339 return false;
6340 }
6341 }
6342 let mut start = verified;
6343 while start < dir.len() {
6344 if dir[start] == b'/' {
6345 start += 1;
6346 continue;
6347 }
6348 let end = dir[start..]
6349 .iter()
6350 .position(|&b| b == b'/')
6351 .map_or(dir.len(), |i| start + i);
6352 let prefix = &dir[..end];
6353 let abs =
6354 index_entry_worktree_abs_bytes(work_tree, prefix, precompose_unicode, ignorecase);
6355 #[cfg(test)]
6356 {
6357 self.fs_queries += 1;
6358 }
6359 match ancestor_kind(&abs) {
6360 AncestorKind::Directory => {
6361 let known = self.real_dirs.len();
6362 self.real_dirs.extend_from_slice(&prefix[known..]);
6363 }
6364 AncestorKind::Symlink => {
6365 self.blocked = Some((prefix.to_vec(), true));
6366 return true;
6367 }
6368 AncestorKind::Other => {
6369 self.blocked = Some((prefix.to_vec(), false));
6370 return false;
6371 }
6372 }
6373 start = end;
6374 }
6375 false
6376 }
6377}
6378
6379#[must_use]
6385pub fn entry_is_racy(ie: &IndexEntry, index_mtime: Option<(u32, u32)>) -> bool {
6386 entry_is_racy_with_policy(ie, index_mtime, crate::index::StatMatchPolicy::default())
6387}
6388
6389#[must_use]
6395pub fn entry_is_racy_with_policy(
6396 ie: &IndexEntry,
6397 index_mtime: Option<(u32, u32)>,
6398 policy: crate::index::StatMatchPolicy,
6399) -> bool {
6400 let Some((index_mtime_sec, index_mtime_nsec)) = index_mtime else {
6401 return false;
6402 };
6403 if index_mtime_sec == 0 {
6404 return false;
6405 }
6406 if policy.use_nanosec && policy.check_stat {
6407 index_mtime_sec < ie.mtime_sec
6408 || (index_mtime_sec == ie.mtime_sec && index_mtime_nsec <= ie.mtime_nsec)
6409 } else {
6410 index_mtime_sec <= ie.mtime_sec
6411 }
6412}
6413
6414pub fn smudge_racily_clean_entries(
6431 odb: &Odb,
6432 git_dir: &Path,
6433 index: &mut Index,
6434 work_tree: &Path,
6435 index_mtime: Option<(u32, u32)>,
6436 config: Option<&ConfigSet>,
6437 filter_process: Option<&crate::filter_process::FilterProcessState>,
6438) -> bool {
6439 use crate::crlf;
6440 use crate::index::{MODE_EXECUTABLE, MODE_REGULAR, MODE_SYMLINK};
6441
6442 let Some(index_mtime) = index_mtime else {
6443 return false;
6444 };
6445
6446 let config = config.cloned().unwrap_or_else(|| {
6447 ConfigSet::load(
6448 &crate::environment::Environment::empty(),
6449 Some(git_dir),
6450 true,
6451 )
6452 .unwrap_or_default()
6453 });
6454 let conv = crlf::ConversionConfig::from_config(&config);
6455 let attrs = crlf::load_gitattributes(work_tree);
6456 let verified = std::mem::take(&mut index.content_verified);
6457
6458 let mut changed = false;
6459 for ie in &mut index.entries {
6460 let marked_verified = ie.take_content_verified();
6461 if ie.stage() != 0 || ie.skip_worktree() || ie.assume_unchanged() || ie.intent_to_add() {
6462 continue;
6463 }
6464 if ie.mode != MODE_REGULAR && ie.mode != MODE_EXECUTABLE && ie.mode != MODE_SYMLINK {
6465 continue;
6466 }
6467 if ie.size == 0 {
6468 continue;
6469 }
6470 if !entry_is_racy(ie, Some(index_mtime)) {
6471 continue;
6472 }
6473 if marked_verified || verified.get(&ie.path) == Some(&ie.oid) {
6474 continue;
6475 }
6476 let Ok(path) = std::str::from_utf8(&ie.path) else {
6477 continue;
6478 };
6479 let abs = work_tree.join(path);
6480 let Ok(meta) = fs::symlink_metadata(&abs) else {
6481 continue;
6482 };
6483 if !stat_matches(ie, &meta) {
6484 continue;
6485 }
6486 let file_attrs = crlf::get_file_attrs(&attrs, path, false, &config);
6487 let content_matches = hash_worktree_file(
6488 odb,
6489 &abs,
6490 &meta,
6491 &conv,
6492 &file_attrs,
6493 path,
6494 Some(ie),
6495 filter_process,
6496 )
6497 .map(|oid| oid == ie.oid)
6498 .unwrap_or(false);
6499 if content_matches {
6500 continue;
6501 }
6502 ie.size = 0;
6503 changed = true;
6504 }
6505 changed
6506}
6507pub fn worktree_differs_from_index_entry(
6515 odb: &Odb,
6516 repository_git_dir: &Path,
6517 work_tree: &Path,
6518 ie: &IndexEntry,
6519 ignore_submodule_untracked: bool,
6520 config: Option<&ConfigSet>,
6521 filter_process: Option<&crate::filter_process::FilterProcessState>,
6522) -> Result<bool> {
6523 WorktreeEntryComparer::new(repository_git_dir, work_tree, config).differs(
6524 odb,
6525 ie,
6526 ignore_submodule_untracked,
6527 filter_process,
6528 )
6529}
6530
6531pub(crate) struct WorktreeEntryComparer<'a> {
6535 work_tree: &'a Path,
6536 config: ConfigSet,
6537 mode_policy: crate::index::WorktreeModePolicy,
6538 precompose_unicode: bool,
6539 ignorecase: bool,
6540 conv: std::cell::OnceCell<crate::crlf::ConversionConfig>,
6541 attrs: std::cell::OnceCell<Vec<crate::crlf::AttrRule>>,
6542}
6543
6544impl<'a> WorktreeEntryComparer<'a> {
6545 pub(crate) fn new(
6547 repository_git_dir: &Path,
6548 work_tree: &'a Path,
6549 config: Option<&ConfigSet>,
6550 ) -> Self {
6551 let config = config.cloned().unwrap_or_else(|| {
6552 ConfigSet::load(
6553 &crate::environment::Environment::empty(),
6554 Some(repository_git_dir),
6555 true,
6556 )
6557 .unwrap_or_else(|_| ConfigSet::new())
6558 });
6559 let flag =
6560 |key: &str, default: bool| config.get_bool(key).and_then(|r| r.ok()).unwrap_or(default);
6561 Self {
6562 work_tree,
6563 mode_policy: crate::index::WorktreeModePolicy::from_config(&config),
6564 precompose_unicode: flag("core.precomposeunicode", false),
6565 ignorecase: flag("core.ignorecase", false),
6566 config,
6567 conv: std::cell::OnceCell::new(),
6568 attrs: std::cell::OnceCell::new(),
6569 }
6570 }
6571
6572 pub(crate) fn differs(
6574 &self,
6575 odb: &Odb,
6576 ie: &IndexEntry,
6577 ignore_submodule_untracked: bool,
6578 filter_process: Option<&crate::filter_process::FilterProcessState>,
6579 ) -> Result<bool> {
6580 use crate::crlf;
6581
6582 let work_tree = self.work_tree;
6583 let path_stored = crate::repo_relpath::storage_string(&ie.path);
6584 let path_str_ref = crate::repo_relpath::as_utf8_for_matching(&path_stored);
6585 let file_path = index_entry_worktree_abs_bytes(
6586 work_tree,
6587 &ie.path,
6588 self.precompose_unicode,
6589 self.ignorecase,
6590 );
6591
6592 if ie.mode == 0o160000 {
6593 let sub_head_oid = read_submodule_head(&file_path);
6594 let ref_matches = match sub_head_oid {
6595 Some(oid) => oid == ie.oid,
6596 None => uninitialized_gitlink_worktree_matches_index(&file_path),
6597 };
6598 let mut flags = submodule_porcelain_flags(work_tree, path_str_ref.as_ref(), ie.oid);
6599 if ignore_submodule_untracked {
6600 flags.untracked = false;
6601 }
6602 return Ok(!ref_matches || flags.modified || flags.untracked);
6603 }
6604
6605 let meta = match fs::symlink_metadata(&file_path) {
6606 Ok(m) => m,
6607 Err(e)
6608 if e.kind() == std::io::ErrorKind::NotFound
6609 || e.raw_os_error() == Some(20) =>
6610 {
6611 return Ok(true);
6612 }
6613 Err(e) => return Err(Error::Io(e)),
6614 };
6615
6616 if meta.is_dir() {
6617 return Ok(true);
6618 }
6619
6620 if !self
6621 .mode_policy
6622 .worktree_mode_matches(mode_from_metadata(&meta), ie.mode)
6623 {
6624 return Ok(true);
6625 }
6626
6627 let conv = self
6628 .conv
6629 .get_or_init(|| crlf::ConversionConfig::from_config(&self.config));
6630 let attrs = self
6631 .attrs
6632 .get_or_init(|| crlf::load_gitattributes(work_tree));
6633 let file_attrs = crlf::get_file_attrs(attrs, path_str_ref.as_ref(), false, &self.config);
6634 let worktree_oid = hash_worktree_file(
6635 odb,
6636 &file_path,
6637 &meta,
6638 conv,
6639 &file_attrs,
6640 path_str_ref.as_ref(),
6641 Some(ie),
6642 filter_process,
6643 )?;
6644
6645 let mut eff_oid = worktree_oid;
6646 if eff_oid != ie.oid {
6647 if let Ok(raw) = fs::read(&file_path) {
6648 let raw_oid = odb.hash(ObjectKind::Blob, &raw)?;
6649 if raw_oid == ie.oid {
6650 eff_oid = ie.oid;
6651 }
6652 }
6653 }
6654
6655 Ok(eff_oid != ie.oid)
6656 }
6657}
6658
6659pub struct WorktreeAddRefreshParams<'a> {
6661 pub odb: &'a Odb,
6663 pub ie: &'a IndexEntry,
6665 pub meta: &'a fs::Metadata,
6667 pub abs_path: &'a Path,
6669 pub rel_path: &'a str,
6671 pub conv: &'a crate::crlf::ConversionConfig,
6673 pub file_attrs: &'a crate::crlf::FileAttrs,
6675 pub index_mtime: Option<(u32, u32)>,
6677 pub staged_mode: u32,
6680 pub filter_process: Option<&'a crate::filter_process::FilterProcessState>,
6682}
6683
6684#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6686pub enum WorktreeAddRefresh {
6687 UpToDate,
6689 ModeOnly {
6691 mode: u32,
6693 },
6694 StatOnly,
6696 NeedsRestage,
6698}
6699
6700#[must_use]
6702pub(crate) fn index_stat_cache_trustworthy(index_mtime: Option<(u32, u32)>) -> bool {
6703 match index_mtime {
6704 None => false,
6705 Some((0, _)) => false,
6706 Some(_) => true,
6707 }
6708}
6709
6710#[must_use]
6719pub(crate) fn index_entry_skips_worktree_content_hash_after_stat_match(
6720 ie: &IndexEntry,
6721 index_mtime: Option<(u32, u32)>,
6722 stat_policy: crate::index::StatMatchPolicy,
6723) -> bool {
6724 if ie.stat_cache_unset() || (ie.size == 0 && !ie.oid.is_empty_blob()) {
6725 return false;
6726 }
6727 if !index_stat_cache_trustworthy(index_mtime) {
6728 return false;
6729 }
6730 !entry_is_racy_with_policy(ie, index_mtime, stat_policy)
6731}
6732
6733pub fn classify_worktree_entry_for_add(
6747 params: &WorktreeAddRefreshParams<'_>,
6748) -> Result<WorktreeAddRefresh> {
6749 use crate::index::{MODE_EXECUTABLE, MODE_REGULAR, MODE_SYMLINK};
6750
6751 let ie = params.ie;
6752 let meta = params.meta;
6753 let staged_mode = params.staged_mode;
6754
6755 if ie.intent_to_add() {
6756 return Ok(WorktreeAddRefresh::NeedsRestage);
6757 }
6758
6759 let racy = entry_is_racy(ie, params.index_mtime);
6760 let stat_same = stat_matches(ie, meta);
6761 let trust_stat = index_stat_cache_trustworthy(params.index_mtime);
6762
6763 if ie.mode == MODE_SYMLINK {
6764 if !meta.file_type().is_symlink() && staged_mode != MODE_SYMLINK {
6767 return Ok(WorktreeAddRefresh::NeedsRestage);
6768 }
6769 if stat_same && !racy && trust_stat && staged_mode == ie.mode {
6770 return Ok(WorktreeAddRefresh::UpToDate);
6771 }
6772 if stat_same && !racy && trust_stat && staged_mode != ie.mode {
6773 return Ok(WorktreeAddRefresh::ModeOnly { mode: staged_mode });
6774 }
6775 return worktree_effective_oid_matches_index(params);
6776 }
6777
6778 if ie.mode != MODE_REGULAR && ie.mode != MODE_EXECUTABLE {
6779 return Ok(WorktreeAddRefresh::NeedsRestage);
6780 }
6781
6782 if meta.file_type().is_symlink() || meta.is_dir() {
6783 return Ok(WorktreeAddRefresh::NeedsRestage);
6784 }
6785
6786 if stat_same && !racy && trust_stat {
6787 if staged_mode == ie.mode {
6788 return Ok(WorktreeAddRefresh::UpToDate);
6789 }
6790 return Ok(WorktreeAddRefresh::ModeOnly { mode: staged_mode });
6791 }
6792
6793 worktree_effective_oid_matches_index(params)
6794}
6795
6796fn worktree_effective_oid_matches_index(
6797 params: &WorktreeAddRefreshParams<'_>,
6798) -> Result<WorktreeAddRefresh> {
6799 let WorktreeAddRefreshParams {
6800 odb,
6801 ie,
6802 meta,
6803 abs_path,
6804 rel_path,
6805 conv,
6806 file_attrs,
6807 staged_mode,
6808 ..
6809 } = params;
6810 let worktree_oid = hash_worktree_file(
6811 odb,
6812 abs_path,
6813 meta,
6814 conv,
6815 file_attrs,
6816 rel_path,
6817 Some(ie),
6818 params.filter_process,
6819 )?;
6820 let mut eff_oid = worktree_oid;
6821 if eff_oid != ie.oid {
6822 if let Ok(raw) = fs::read(abs_path) {
6823 let raw_oid = odb.hash(ObjectKind::Blob, &raw)?;
6824 if raw_oid == ie.oid {
6825 eff_oid = ie.oid;
6826 }
6827 }
6828 }
6829 if eff_oid != ie.oid {
6830 return Ok(WorktreeAddRefresh::NeedsRestage);
6831 }
6832 if *staged_mode != ie.mode {
6833 return Ok(WorktreeAddRefresh::ModeOnly { mode: *staged_mode });
6834 }
6835 if stat_matches(ie, meta) {
6836 Ok(WorktreeAddRefresh::UpToDate)
6837 } else {
6838 Ok(WorktreeAddRefresh::StatOnly)
6839 }
6840}
6841
6842#[must_use]
6844pub fn stat_matches(ie: &IndexEntry, meta: &fs::Metadata) -> bool {
6845 stat_matches_with_policy(ie, meta, crate::index::StatMatchPolicy::default())
6846}
6847
6848pub(crate) fn stat_matches_blob_stat(
6850 ie: &IndexEntry,
6851 stat: &crate::worktree_scan::BlobDiskStat,
6852 policy: crate::index::StatMatchPolicy,
6853) -> bool {
6854 stat.fields.matches_index_entry(ie, policy)
6855}
6856
6857#[must_use]
6859pub fn stat_matches_with_policy(
6860 ie: &IndexEntry,
6861 meta: &fs::Metadata,
6862 policy: crate::index::StatMatchPolicy,
6863) -> bool {
6864 if crate::index::munge_index_size(meta.len()) != ie.size {
6865 return false;
6866 }
6867 #[cfg(unix)]
6868 {
6869 use std::os::unix::fs::MetadataExt;
6870 if meta.mtime() as u32 != ie.mtime_sec {
6871 return false;
6872 }
6873 if policy.use_nanosec && policy.check_stat && meta.mtime_nsec() as u32 != ie.mtime_nsec {
6874 return false;
6875 }
6876 if policy.trust_ctime && policy.check_stat {
6877 if meta.ctime() as u32 != ie.ctime_sec {
6878 return false;
6879 }
6880 if policy.use_nanosec && meta.ctime_nsec() as u32 != ie.ctime_nsec {
6881 return false;
6882 }
6883 }
6884 if policy.check_stat && meta.ino() as u32 != ie.ino {
6885 return false;
6886 }
6887 }
6888 #[cfg(not(unix))]
6889 {
6890 let ((ctime_sec, ctime_nsec), (mtime_sec, mtime_nsec)) = crate::index::metadata_times(meta);
6891 if mtime_sec != ie.mtime_sec {
6892 return false;
6893 }
6894 if policy.use_nanosec && policy.check_stat && mtime_nsec != ie.mtime_nsec {
6895 return false;
6896 }
6897 if policy.trust_ctime && policy.check_stat {
6898 if ctime_sec != ie.ctime_sec {
6899 return false;
6900 }
6901 if policy.use_nanosec && ctime_nsec != ie.ctime_nsec {
6902 return false;
6903 }
6904 }
6905 }
6906 true
6907}
6908
6909pub fn refresh_index_stat_content_verified(
6932 odb: &Odb,
6933 git_dir: &Path,
6934 index: &mut Index,
6935 work_tree: &Path,
6936 index_mtime: Option<(u32, u32)>,
6937 config: Option<&ConfigSet>,
6938 _stat_parallel_threads: Option<usize>,
6939) -> Result<bool> {
6940 let _ = _stat_parallel_threads;
6941 refresh_index_stat_content_verified_with_rules(
6942 odb,
6943 git_dir,
6944 index,
6945 work_tree,
6946 index_mtime,
6947 config,
6948 _stat_parallel_threads,
6949 None,
6950 )
6951}
6952
6953#[allow(clippy::too_many_arguments)]
6961pub fn refresh_index_stat_content_verified_with_rules(
6962 odb: &Odb,
6963 git_dir: &Path,
6964 index: &mut Index,
6965 work_tree: &Path,
6966 index_mtime: Option<(u32, u32)>,
6967 config: Option<&ConfigSet>,
6968 stat_parallel_threads: Option<usize>,
6969 worktree_rules: Option<&std::sync::Arc<std::sync::Mutex<crate::worktree_rules::WorktreeRules>>>,
6970) -> Result<bool> {
6971 use crate::config::ConfigSet;
6972 use crate::crlf;
6973 use crate::worktree_scan::{
6974 collect_blob_lookup_batches, group_blob_entries_by_dir, visit_blob_lookup_batches,
6975 BlobDiskLookup, BlobScanSparseMode, WorktreeBlobScanOptions,
6976 };
6977
6978 let index_mtime = index_mtime_for_diff(index, index_mtime);
6979 let rules_lock = worktree_rules
6980 .map(|shared| {
6981 shared
6982 .lock()
6983 .map_err(|e| Error::Message(format!("worktree rules lock poisoned: {e}")))
6984 })
6985 .transpose()?;
6986 let config = rules_lock
6987 .as_ref()
6988 .map(|r| r.config().clone())
6989 .unwrap_or_else(|| {
6990 config.cloned().unwrap_or_else(|| {
6991 ConfigSet::load(
6992 &crate::environment::Environment::empty(),
6993 Some(git_dir),
6994 true,
6995 )
6996 .unwrap_or_default()
6997 })
6998 });
6999 let preload_index = config
7000 .get_bool("core.preloadIndex")
7001 .and_then(|r| r.ok())
7002 .unwrap_or(true);
7003 let blob_scan = WorktreeBlobScanOptions {
7004 preload_index,
7005 stat_parallel_threads,
7006 };
7007 let conv = rules_lock
7008 .as_ref()
7009 .map(|r| r.conversion().clone())
7010 .unwrap_or_else(|| crlf::ConversionConfig::from_config(&config));
7011 let legacy_attrs = if rules_lock.is_none() {
7012 Some(crlf::load_gitattributes_parsed(work_tree))
7013 } else {
7014 None
7015 };
7016 let file_attrs_for = |path: &str| -> crlf::FileAttrs {
7017 if let Some(rules) = rules_lock.as_ref() {
7018 rules.file_attrs(path, false)
7019 } else if let Some(parsed) = legacy_attrs.as_ref() {
7020 crlf::file_attrs_from_gitattributes(&parsed.rules, &parsed.macros, path, false, &config)
7021 } else {
7022 crlf::FileAttrs::default()
7023 }
7024 };
7025 let precompose_unicode = config
7026 .get_bool("core.precomposeunicode")
7027 .and_then(|r| r.ok())
7028 .unwrap_or(false);
7029 let ignorecase = config
7030 .get_bool("core.ignorecase")
7031 .and_then(|r| r.ok())
7032 .unwrap_or(false);
7033 let stat_policy = crate::index::StatMatchPolicy::from_config(Some(&config));
7034
7035 let blob_dirs = group_blob_entries_by_dir(index, false);
7036 let work_tree_owned = work_tree.to_path_buf();
7037 let dir_abs = move |dir: &[u8]| {
7038 if dir.is_empty() {
7039 work_tree_owned.clone()
7040 } else {
7041 index_entry_worktree_abs_bytes(&work_tree_owned, dir, precompose_unicode, ignorecase)
7042 }
7043 };
7044 let work_tree_for_files = work_tree.to_path_buf();
7045 let file_abs = move |rel: &[u8]| {
7046 index_entry_worktree_abs_bytes(&work_tree_for_files, rel, precompose_unicode, ignorecase)
7047 };
7048 let mut changed = false;
7049 let filter_fp = rules_lock.as_ref().map(|r| r.filter_process());
7050 let thread_policy = crate::parallel::ThreadPolicy::default();
7051 let refresh_batches = collect_blob_lookup_batches(
7053 index,
7054 &blob_dirs,
7055 blob_scan,
7056 &thread_policy,
7057 &config,
7058 dir_abs,
7059 file_abs,
7060 || {
7061 let mut dir_symlinks = SymlinkDirCache::default();
7062 move |rel_path: &[u8]| {
7063 dir_symlinks.has_symlink_in_path_bytes(
7064 work_tree,
7065 rel_path,
7066 precompose_unicode,
7067 ignorecase,
7068 )
7069 }
7070 },
7071 BlobScanSparseMode::StatRefresh {
7072 stat_policy,
7073 index_mtime,
7074 },
7075 )?;
7076 let par_opts = refresh_hash_parallel_options(&thread_policy, &config, stat_parallel_threads);
7077 let refresh_checks = refresh_content_checks(
7078 odb,
7079 index,
7080 &refresh_batches,
7081 index_mtime,
7082 stat_policy,
7083 &|_, _, _| true,
7084 &WorktreeHashContext {
7085 work_tree,
7086 precompose_unicode,
7087 ignorecase,
7088 conv: &conv,
7089 file_attrs_for: &file_attrs_for,
7090 filter_process: filter_fp,
7091 },
7092 par_opts,
7093 None,
7094 )?;
7095 visit_blob_lookup_batches(refresh_batches, |entry_index, lookup| {
7096 let BlobDiskLookup::Present(stat) = lookup else {
7097 return Ok(());
7098 };
7099 let content_matches = RefreshContent::matches(
7100 refresh_checks.get(entry_index),
7101 &index.entries[entry_index].oid,
7102 );
7103 let refresh = refresh_index_blob_stat_from_scan(
7104 &mut index.entries[entry_index],
7105 &stat,
7106 index_mtime,
7107 stat_policy,
7108 content_matches,
7109 );
7110 if refresh.index_changed {
7111 changed = true;
7112 }
7113 if refresh.content_verified {
7114 let path = index.entries[entry_index].path.clone();
7115 let oid = index.entries[entry_index].oid;
7116 index.mark_content_verified(&path, oid);
7117 }
7118 Ok(())
7119 })?;
7120 Ok(changed)
7121}
7122
7123struct BlobStatRefreshOutcome {
7125 index_changed: bool,
7127 content_verified: bool,
7129}
7130
7131fn refresh_index_entry_eligible(ie: &IndexEntry) -> bool {
7133 use crate::index::{MODE_EXECUTABLE, MODE_REGULAR, MODE_SYMLINK};
7134 ie.stage() == 0
7135 && !ie.skip_worktree()
7136 && !ie.assume_unchanged()
7137 && !ie.intent_to_add()
7138 && matches!(ie.mode, MODE_REGULAR | MODE_EXECUTABLE | MODE_SYMLINK)
7139}
7140
7141fn refresh_needs_content_check(
7144 ie: &IndexEntry,
7145 stat: &crate::worktree_scan::BlobDiskStat,
7146 index_mtime: Option<(u32, u32)>,
7147 stat_policy: crate::index::StatMatchPolicy,
7148) -> bool {
7149 refresh_index_entry_eligible(ie)
7150 && !stat.is_dir
7151 && (!stat_matches_blob_stat(ie, stat, stat_policy)
7152 || entry_is_racy_with_policy(ie, index_mtime, stat_policy))
7153}
7154
7155pub(crate) fn worktree_diff_refreshes_entry(
7162 ie: &IndexEntry,
7163 stat: &crate::worktree_scan::BlobDiskStat,
7164 index_mtime: Option<(u32, u32)>,
7165 stat_policy: crate::index::StatMatchPolicy,
7166 for_status: bool,
7167 conversion_is_identity: impl FnOnce() -> bool,
7168) -> bool {
7169 if !for_status {
7170 return true;
7171 }
7172 if stat_matches_blob_stat(ie, stat, stat_policy) {
7173 return entry_is_racy_with_policy(ie, index_mtime, stat_policy);
7174 }
7175 !status_size_change_proves_modified(
7176 ie,
7177 stat.worktree_size(),
7178 for_status,
7179 conversion_is_identity,
7180 )
7181}
7182
7183fn refresh_hash_parallel_options(
7190 thread_policy: &crate::parallel::ThreadPolicy,
7191 config: &ConfigSet,
7192 stat_parallel_threads: Option<usize>,
7193) -> crate::parallel::ParallelOptions {
7194 let policy = crate::parallel::ThreadPolicy {
7195 min_items_override: thread_policy.min_items_override.or(Some(0)),
7196 min_total_bytes_override: thread_policy.min_total_bytes_override.or(Some(0)),
7197 ..*thread_policy
7198 };
7199 let mut par_opts = policy.resolve(crate::parallel::ParallelOp::IndexRefresh, config);
7200 if let Some(cap) = stat_parallel_threads {
7201 par_opts.threads = par_opts
7202 .threads
7203 .min(std::num::NonZeroUsize::new(cap).unwrap_or(std::num::NonZeroUsize::MIN));
7204 }
7205 par_opts.min_items = 0;
7206 par_opts.min_total_bytes = None;
7207 par_opts
7208}
7209
7210struct WorktreeHashContext<'a> {
7212 work_tree: &'a Path,
7213 precompose_unicode: bool,
7214 ignorecase: bool,
7215 conv: &'a crate::crlf::ConversionConfig,
7216 file_attrs_for: &'a dyn Fn(&str) -> crate::crlf::FileAttrs,
7217 filter_process: Option<&'a crate::filter_process::FilterProcessState>,
7218}
7219
7220#[allow(clippy::too_many_arguments)]
7230fn refresh_content_checks(
7231 odb: &Odb,
7232 index: &Index,
7233 batches: &[Vec<(usize, crate::worktree_scan::BlobDiskLookup)>],
7234 index_mtime: Option<(u32, u32)>,
7235 stat_policy: crate::index::StatMatchPolicy,
7236 include: &dyn Fn(usize, &IndexEntry, &crate::worktree_scan::BlobDiskStat) -> bool,
7237 ctx: &WorktreeHashContext<'_>,
7238 par_opts: crate::parallel::ParallelOptions,
7239 cancel_watch: Option<&std::sync::Arc<std::sync::atomic::AtomicBool>>,
7240) -> Result<RefreshChecks> {
7241 use crate::progress::{poll_watch, CANCEL_POLL_STRIDE};
7242 use crate::worktree_scan::BlobDiskLookup;
7243
7244 struct ContentCheck {
7246 entry_index: usize,
7247 worktree_mode: u32,
7248 file_attrs: crate::crlf::FileAttrs,
7249 }
7250
7251 let mut jobs = Vec::new();
7252 let mut total_bytes = 0usize;
7253 let mut cancel_stride = 0usize;
7254 for (entry_index, lookup) in batches.iter().flatten() {
7255 cancel_stride = cancel_stride.wrapping_add(1);
7256 if cancel_stride.is_multiple_of(CANCEL_POLL_STRIDE) {
7257 if let Some(w) = cancel_watch {
7258 poll_watch(w)?;
7259 }
7260 }
7261 let BlobDiskLookup::Present(stat) = lookup else {
7262 continue;
7263 };
7264 let ie = &index.entries[*entry_index];
7265 if !refresh_needs_content_check(ie, stat, index_mtime, stat_policy)
7266 || !include(*entry_index, ie, stat)
7267 {
7268 continue;
7269 }
7270 total_bytes = total_bytes.saturating_add(ie.size as usize);
7271 jobs.push(ContentCheck {
7272 entry_index: *entry_index,
7273 worktree_mode: stat.git_mode,
7274 file_attrs: (ctx.file_attrs_for)(&crate::repo_relpath::storage_string(&ie.path)),
7275 });
7276 }
7277 if jobs.is_empty() {
7278 return Ok(RefreshChecks::default());
7279 }
7280 let mut checks = vec![None; index.entries.len()];
7281 let (work_tree, precompose_unicode, ignorecase) =
7282 (ctx.work_tree, ctx.precompose_unicode, ctx.ignorecase);
7283 let (conv, filter_process) = (ctx.conv, ctx.filter_process);
7284 let entries = index.entries.as_slice();
7285 let watch_for_hash = cancel_watch.cloned();
7286 let hash_one = |job: &ContentCheck| -> Result<RefreshContent> {
7287 if job.entry_index.is_multiple_of(CANCEL_POLL_STRIDE) {
7288 if let Some(w) = watch_for_hash.as_ref() {
7289 poll_watch(w)?;
7290 }
7291 }
7292 let ie = &entries[job.entry_index];
7293 let file_path =
7294 index_entry_worktree_abs_bytes(work_tree, &ie.path, precompose_unicode, ignorecase);
7295 Ok(RefreshContent::from(hash_worktree_file_with_mode(
7296 odb,
7297 &file_path,
7298 job.worktree_mode,
7299 conv,
7300 &job.file_attrs,
7301 &crate::repo_relpath::storage_string(&ie.path),
7302 Some(ie),
7303 filter_process,
7304 )))
7305 };
7306 let matches = if par_opts.threads.get() > 1 && jobs.len() > 1 {
7307 crate::parallel::try_par_map_force(&jobs, par_opts, hash_one).map_err(|e| match e {
7308 crate::parallel::ParallelHashError::Task(err) => err,
7309 })?
7310 } else {
7311 let mut out = Vec::with_capacity(jobs.len());
7312 let mut serial_stride = 0usize;
7313 for job in &jobs {
7314 serial_stride = serial_stride.wrapping_add(1);
7315 if serial_stride.is_multiple_of(CANCEL_POLL_STRIDE) {
7316 if let Some(w) = cancel_watch {
7317 poll_watch(w)?;
7318 }
7319 }
7320 out.push(hash_one(job)?);
7321 }
7322 out
7323 };
7324 for (job, content) in jobs.iter().zip(matches) {
7325 checks[job.entry_index] = Some(content);
7326 }
7327 Ok(RefreshChecks(checks))
7328}
7329
7330#[derive(Default)]
7335struct RefreshChecks(Vec<Option<RefreshContent>>);
7336
7337impl RefreshChecks {
7338 fn get(&self, entry_index: usize) -> Option<RefreshContent> {
7340 self.0.get(entry_index).copied().flatten()
7341 }
7342}
7343
7344#[derive(Clone, Copy)]
7346enum RefreshContent {
7347 Failed,
7349 Hashed(ObjectId),
7351}
7352
7353impl From<Result<ObjectId>> for RefreshContent {
7354 fn from(hashed: Result<ObjectId>) -> Self {
7355 hashed.map_or(Self::Failed, Self::Hashed)
7356 }
7357}
7358
7359impl RefreshContent {
7360 fn matches(check: Option<Self>, oid: &ObjectId) -> bool {
7362 matches!(check, Some(Self::Hashed(hashed)) if hashed == *oid)
7363 }
7364
7365 fn differs(check: Option<Self>, oid: &ObjectId) -> bool {
7367 matches!(check, Some(Self::Hashed(hashed)) if hashed != *oid)
7368 }
7369}
7370
7371fn refresh_index_blob_stat_from_scan(
7377 ie: &mut IndexEntry,
7378 stat: &crate::worktree_scan::BlobDiskStat,
7379 index_mtime: Option<(u32, u32)>,
7380 stat_policy: crate::index::StatMatchPolicy,
7381 content_matches: bool,
7382) -> BlobStatRefreshOutcome {
7383 const UNCHANGED: BlobStatRefreshOutcome = BlobStatRefreshOutcome {
7384 index_changed: false,
7385 content_verified: false,
7386 };
7387 if !refresh_index_entry_eligible(ie) || stat.is_dir {
7388 return UNCHANGED;
7389 }
7390 let cached = crate::index::IndexStatFields::of_entry(ie);
7391 let stat_same = cached.equivalent_to(&stat.fields, stat_policy);
7392 let stat_matches = stat_matches_blob_stat(ie, stat, stat_policy);
7393 if !stat_matches || entry_is_racy_with_policy(ie, index_mtime, stat_policy) {
7394 if !content_matches {
7395 if stat_matches {
7396 invalidate_index_stat_cache(ie);
7397 return BlobStatRefreshOutcome {
7398 index_changed: true,
7399 content_verified: false,
7400 };
7401 }
7402 return UNCHANGED;
7403 }
7404 if !stat_same {
7405 stat.fields.store_in(ie);
7406 }
7407 return BlobStatRefreshOutcome {
7408 index_changed: !stat_same,
7409 content_verified: true,
7410 };
7411 }
7412 if stat_same {
7413 return BlobStatRefreshOutcome {
7414 index_changed: false,
7415 content_verified: true,
7416 };
7417 }
7418 stat.fields.store_in(ie);
7419 BlobStatRefreshOutcome {
7420 index_changed: true,
7421 content_verified: false,
7422 }
7423}
7424
7425#[allow(clippy::too_many_arguments)]
7437pub(crate) fn path_checkout_skip_blob_write_when_up_to_date_with_config(
7438 odb: &Odb,
7439 git_dir: &Path,
7440 work_tree: &Path,
7441 index: &Index,
7442 entry: &IndexEntry,
7443 expected_oid: &ObjectId,
7444 expected_mode: u32,
7445 rel_path: &str,
7446 filter_process: Option<&crate::filter_process::FilterProcessState>,
7447 preloaded_config: Option<&crate::config::ConfigSet>,
7448) -> Result<bool> {
7449 use crate::config::ConfigSet;
7450 use crate::crlf;
7451 use crate::index::{MODE_EXECUTABLE, MODE_REGULAR};
7452
7453 if entry.oid != *expected_oid || entry.mode != expected_mode {
7454 return Ok(false);
7455 }
7456 if expected_mode != MODE_REGULAR && expected_mode != MODE_EXECUTABLE {
7457 return Ok(false);
7458 }
7459 let abs_path = crate::repo_relpath::worktree_abs(work_tree, rel_path);
7460 let meta = match fs::symlink_metadata(&abs_path) {
7461 Ok(m) => m,
7462 Err(_) => return Ok(false),
7463 };
7464 if !meta.is_file() || !stat_matches(entry, &meta) {
7465 return Ok(false);
7466 }
7467 let owned_config;
7468 let config = match preloaded_config {
7469 Some(cfg) => cfg,
7470 None => {
7471 owned_config = ConfigSet::load(
7472 &crate::environment::Environment::empty(),
7473 Some(git_dir),
7474 true,
7475 )
7476 .unwrap_or_default();
7477 &owned_config
7478 }
7479 };
7480 if !crate::index::WorktreeModePolicy::from_config(config)
7481 .worktree_mode_matches(mode_from_metadata(&meta), expected_mode)
7482 {
7483 return Ok(false);
7484 }
7485 let index_mtime = index_mtime_for_diff(index, None);
7486 if !entry_is_racy(entry, index_mtime) {
7487 return Ok(true);
7488 }
7489 let conv = crlf::ConversionConfig::from_config(config);
7490 let attrs = crlf::load_gitattributes(work_tree);
7491 let file_attrs = crlf::get_file_attrs(&attrs, rel_path, false, config);
7492 let wt_oid = hash_worktree_file(
7493 odb,
7494 &abs_path,
7495 &meta,
7496 &conv,
7497 &file_attrs,
7498 rel_path,
7499 Some(entry),
7500 filter_process,
7501 )?;
7502 Ok(wt_oid == *expected_oid)
7503}
7504
7505fn invalidate_index_stat_cache(ie: &mut IndexEntry) {
7507 ie.ctime_sec = 0;
7508 ie.ctime_nsec = 0;
7509 ie.mtime_sec = 0;
7510 ie.mtime_nsec = 0;
7511 ie.dev = 0;
7512 ie.ino = 0;
7513 ie.size = 0;
7514}
7515
7516pub fn read_worktree_bytes_for_mode(path: &Path, mode: u32) -> std::io::Result<Vec<u8>> {
7527 if mode == MODE_SYMLINK && fs::symlink_metadata(path)?.file_type().is_symlink() {
7528 let target = fs::read_link(path)?;
7529 return Ok(symlink_target_bytes(&target));
7530 }
7531 fs::read(path)
7532}
7533
7534pub fn read_worktree_side_bytes_for_diff(
7544 odb: &Odb,
7545 work_tree: &Path,
7546 rel_path: &str,
7547 mode: u32,
7548 config: &crate::config::ConfigSet,
7549 index_entry: Option<&IndexEntry>,
7550 worktree_rules: Option<&crate::worktree_rules::WorktreeRules>,
7551) -> Result<Vec<u8>> {
7552 Ok(load_worktree_side_for_diff(
7553 odb,
7554 work_tree,
7555 rel_path,
7556 mode,
7557 config,
7558 index_entry,
7559 worktree_rules,
7560 false,
7561 )?
7562 .0
7563 .data)
7564}
7565
7566#[allow(clippy::too_many_arguments)]
7576pub fn load_worktree_side_for_diff(
7577 odb: &Odb,
7578 work_tree: &Path,
7579 rel_path: &str,
7580 mode: u32,
7581 config: &crate::config::ConfigSet,
7582 index_entry: Option<&IndexEntry>,
7583 worktree_rules: Option<&crate::worktree_rules::WorktreeRules>,
7584 force_read_content: bool,
7585) -> Result<(super::structured::SideBytes, bool)> {
7586 use super::tree_content::{side_bytes_after_blob_read, treat_side_as_binary_without_reading};
7587 let file_path = crate::repo_relpath::worktree_abs(work_tree, rel_path);
7588 let rel_match = crate::repo_relpath::as_utf8_for_matching(rel_path);
7589 let meta = fs::symlink_metadata(&file_path).map_err(|source| {
7590 crate::worktree::map_worktree_io(
7591 rel_match.as_ref(),
7592 crate::worktree::WorktreePathOperation::Metadata,
7593 source,
7594 )
7595 })?;
7596 if mode == MODE_SYMLINK && meta.file_type().is_symlink() {
7597 let target = fs::read_link(&file_path).map_err(|source| {
7598 crate::worktree::map_worktree_io(
7599 rel_match.as_ref(),
7600 crate::worktree::WorktreePathOperation::Read,
7601 source,
7602 )
7603 })?;
7604 return Ok((
7605 super::structured::SideBytes::from_bytes(symlink_target_bytes(&target)),
7606 false,
7607 ));
7608 }
7609 if !force_read_content
7610 && treat_side_as_binary_without_reading(odb, meta.len(), Some(rel_match.as_ref()), false)?
7611 {
7612 return Ok((super::structured::SideBytes::from_bytes(Vec::new()), true));
7613 }
7614 let data = if let Some(rules) = worktree_rules {
7615 worktree_blob_bytes(
7616 odb,
7617 &file_path,
7618 &meta,
7619 rules.conversion(),
7620 &rules.file_attrs(rel_match.as_ref(), meta.is_dir()),
7621 rel_match.as_ref(),
7622 index_entry,
7623 Some(rules.filter_process()),
7624 )?
7625 } else {
7626 let conv = crate::crlf::ConversionConfig::from_config(config);
7627 let attrs = crate::crlf::load_gitattributes(work_tree);
7628 let file_attrs =
7629 crate::crlf::get_file_attrs(&attrs, rel_match.as_ref(), meta.is_dir(), config);
7630 worktree_blob_bytes(
7631 odb,
7632 &file_path,
7633 &meta,
7634 &conv,
7635 &file_attrs,
7636 rel_match.as_ref(),
7637 index_entry,
7638 None,
7639 )?
7640 };
7641 Ok(side_bytes_after_blob_read(data, force_read_content))
7642}
7643
7644pub(crate) fn symlink_target_bytes(target: &Path) -> Vec<u8> {
7650 #[cfg(unix)]
7651 {
7652 use std::os::unix::ffi::OsStrExt as _;
7653 target.as_os_str().as_bytes().to_vec()
7654 }
7655 #[cfg(not(unix))]
7656 {
7657 target.as_os_str().to_string_lossy().as_bytes().to_vec()
7658 }
7659}
7660
7661pub(crate) enum WorktreeBlobContent {
7668 Bytes(Vec<u8>),
7670 Stream {
7672 file: fs::File,
7674 size: u64,
7676 },
7677}
7678
7679impl WorktreeBlobContent {
7680 pub(crate) fn hash(self, odb: &Odb) -> Result<ObjectId> {
7682 match self {
7683 Self::Bytes(data) => odb.hash(ObjectKind::Blob, &data),
7684 Self::Stream { mut file, size } => odb.hash_stream(ObjectKind::Blob, size, &mut file),
7685 }
7686 }
7687
7688 pub(crate) fn store(self, odb: &Odb, options: crate::odb::WriteOptions) -> Result<ObjectId> {
7690 match self {
7691 Self::Bytes(data) => odb.write_with_options(ObjectKind::Blob, &data, options),
7692 Self::Stream { mut file, size } => {
7693 odb.write_stream(ObjectKind::Blob, size, &mut file, options)
7694 }
7695 }
7696 }
7697
7698 fn object_id(self, odb: &Odb, store: Option<crate::odb::WriteOptions>) -> Result<ObjectId> {
7700 match store {
7701 Some(options) => self.store(odb, options),
7702 None => self.hash(odb),
7703 }
7704 }
7705}
7706
7707#[allow(clippy::too_many_arguments)]
7709fn read_regular_worktree_bytes(
7710 odb: &Odb,
7711 mut file: fs::File,
7712 size: u64,
7713 conv: &crate::crlf::ConversionConfig,
7714 file_attrs: &crate::crlf::FileAttrs,
7715 rel_path: &str,
7716 index_entry: Option<&IndexEntry>,
7717 filter_process: Option<&crate::filter_process::FilterProcessState>,
7718) -> Result<Vec<u8>> {
7719 let needs_index_blob =
7720 index_entry.is_some_and(|_| crate::crlf::clean_uses_autocrlf_index_guard(file_attrs, conv));
7721 let prior_blob: Option<Vec<u8>> = if needs_index_blob {
7722 index_entry
7723 .filter(|e| e.oid != zero_oid())
7724 .and_then(|e| odb.read(&e.oid).ok().map(|o| o.data))
7725 } else {
7726 None
7727 };
7728 let mut raw = Vec::with_capacity(usize::try_from(size).unwrap_or_default());
7729 std::io::Read::read_to_end(&mut file, &mut raw).map_err(|source| {
7730 crate::worktree::map_worktree_io(
7731 rel_path,
7732 crate::worktree::WorktreePathOperation::Read,
7733 source,
7734 )
7735 })?;
7736 #[cfg(test)]
7737 odb.hot_path_metrics().record_blob_content_read();
7738 let opts = crate::crlf::ConvertToGitOpts {
7739 index_blob: prior_blob.as_deref(),
7740 renormalize: false,
7741 check_safecrlf: false,
7742 filter_process,
7743 command_runner: None,
7744 };
7745 Ok(crate::crlf::convert_to_git_owned(
7746 raw, rel_path, conv, file_attrs, opts,
7747 )?)
7748}
7749
7750fn open_with_size(path: &Path, rel_path: &str) -> Result<(fs::File, u64)> {
7751 let file = fs::File::open(path).map_err(|source| {
7752 crate::worktree::map_worktree_io(
7753 rel_path,
7754 crate::worktree::WorktreePathOperation::Read,
7755 source,
7756 )
7757 })?;
7758 let size = file
7759 .metadata()
7760 .map_err(|source| {
7761 crate::worktree::map_worktree_io(
7762 rel_path,
7763 crate::worktree::WorktreePathOperation::Metadata,
7764 source,
7765 )
7766 })?
7767 .len();
7768 Ok((file, size))
7769}
7770
7771#[allow(clippy::too_many_arguments)]
7774fn read_regular_worktree_blob(
7775 odb: &Odb,
7776 path: &Path,
7777 conv: &crate::crlf::ConversionConfig,
7778 file_attrs: &crate::crlf::FileAttrs,
7779 rel_path: &str,
7780 index_entry: Option<&IndexEntry>,
7781 filter_process: Option<&crate::filter_process::FilterProcessState>,
7782 stream_from: u64,
7783) -> Result<WorktreeBlobContent> {
7784 let (mut file, size) = open_with_size(path, rel_path)?;
7785 if size >= stream_from
7786 && crate::crlf::worktree_allows_raw_byte_stream(
7787 conv,
7788 file_attrs,
7789 index_entry,
7790 &mut file,
7791 size,
7792 )?
7793 {
7794 #[cfg(test)]
7795 odb.hot_path_metrics().record_blob_content_read();
7796 return Ok(WorktreeBlobContent::Stream { file, size });
7797 }
7798 read_regular_worktree_bytes(
7799 odb,
7800 file,
7801 size,
7802 conv,
7803 file_attrs,
7804 rel_path,
7805 index_entry,
7806 filter_process,
7807 )
7808 .map(WorktreeBlobContent::Bytes)
7809}
7810
7811#[allow(clippy::too_many_arguments)]
7814pub(crate) fn worktree_blob_content(
7815 odb: &Odb,
7816 path: &Path,
7817 meta: &fs::Metadata,
7818 conv: &crate::crlf::ConversionConfig,
7819 file_attrs: &crate::crlf::FileAttrs,
7820 rel_path: &str,
7821 index_entry: Option<&IndexEntry>,
7822 filter_process: Option<&crate::filter_process::FilterProcessState>,
7823) -> Result<WorktreeBlobContent> {
7824 worktree_blob_content_from(
7825 odb,
7826 path,
7827 meta,
7828 conv,
7829 file_attrs,
7830 rel_path,
7831 index_entry,
7832 filter_process,
7833 hash_stream_threshold(odb)?,
7834 )
7835}
7836
7837#[allow(clippy::too_many_arguments)]
7839fn worktree_blob_content_from(
7840 odb: &Odb,
7841 path: &Path,
7842 meta: &fs::Metadata,
7843 conv: &crate::crlf::ConversionConfig,
7844 file_attrs: &crate::crlf::FileAttrs,
7845 rel_path: &str,
7846 index_entry: Option<&IndexEntry>,
7847 filter_process: Option<&crate::filter_process::FilterProcessState>,
7848 stream_from: u64,
7849) -> Result<WorktreeBlobContent> {
7850 if meta.file_type().is_symlink() {
7851 let target = fs::read_link(path).map_err(|source| {
7852 crate::worktree::map_worktree_io(
7853 rel_path,
7854 crate::worktree::WorktreePathOperation::Read,
7855 source,
7856 )
7857 })?;
7858 return Ok(WorktreeBlobContent::Bytes(symlink_target_bytes(&target)));
7859 }
7860 if meta.is_dir() {
7861 return Ok(WorktreeBlobContent::Bytes(Vec::new()));
7862 }
7863 read_regular_worktree_blob(
7864 odb,
7865 path,
7866 conv,
7867 file_attrs,
7868 rel_path,
7869 index_entry,
7870 filter_process,
7871 stream_from,
7872 )
7873}
7874
7875#[allow(clippy::too_many_arguments)]
7878pub(crate) fn worktree_blob_bytes(
7879 odb: &Odb,
7880 path: &Path,
7881 meta: &fs::Metadata,
7882 conv: &crate::crlf::ConversionConfig,
7883 file_attrs: &crate::crlf::FileAttrs,
7884 rel_path: &str,
7885 index_entry: Option<&IndexEntry>,
7886 filter_process: Option<&crate::filter_process::FilterProcessState>,
7887) -> Result<Vec<u8>> {
7888 if meta.file_type().is_symlink() {
7889 let target = fs::read_link(path).map_err(|source| {
7890 crate::worktree::map_worktree_io(
7891 rel_path,
7892 crate::worktree::WorktreePathOperation::Read,
7893 source,
7894 )
7895 })?;
7896 return Ok(symlink_target_bytes(&target));
7897 }
7898 if meta.is_dir() {
7899 return Ok(Vec::new());
7900 }
7901 let (file, size) = open_with_size(path, rel_path)?;
7902 read_regular_worktree_bytes(
7903 odb,
7904 file,
7905 size,
7906 conv,
7907 file_attrs,
7908 rel_path,
7909 index_entry,
7910 filter_process,
7911 )
7912}
7913
7914#[allow(clippy::too_many_arguments)]
7915fn worktree_file_oid(
7916 odb: &Odb,
7917 path: &Path,
7918 meta: &fs::Metadata,
7919 conv: &crate::crlf::ConversionConfig,
7920 file_attrs: &crate::crlf::FileAttrs,
7921 rel_path: &str,
7922 index_entry: Option<&IndexEntry>,
7923 materialize: bool,
7924 filter_process: Option<&crate::filter_process::FilterProcessState>,
7925) -> Result<ObjectId> {
7926 let stream_from = if materialize {
7927 odb.big_file_threshold()?
7928 } else {
7929 hash_stream_threshold(odb)?
7930 };
7931 worktree_blob_content_from(
7932 odb,
7933 path,
7934 meta,
7935 conv,
7936 file_attrs,
7937 rel_path,
7938 index_entry,
7939 filter_process,
7940 stream_from,
7941 )?
7942 .object_id(odb, materialize.then(|| odb.bulk_stage_write_options()))
7943}
7944
7945#[allow(clippy::too_many_arguments)]
7956pub fn hash_worktree_file(
7957 odb: &Odb,
7958 path: &Path,
7959 meta: &fs::Metadata,
7960 conv: &crate::crlf::ConversionConfig,
7961 file_attrs: &crate::crlf::FileAttrs,
7962 rel_path: &str,
7963 index_entry: Option<&IndexEntry>,
7964 filter_process: Option<&crate::filter_process::FilterProcessState>,
7965) -> Result<ObjectId> {
7966 worktree_file_oid(
7967 odb,
7968 path,
7969 meta,
7970 conv,
7971 file_attrs,
7972 rel_path,
7973 index_entry,
7974 false,
7975 filter_process,
7976 )
7977}
7978
7979const STREAMED_HASH_MIN_SIZE: u64 = 1024 * 1024;
7982
7983fn hash_stream_threshold(odb: &Odb) -> Result<u64> {
7985 Ok(odb.big_file_threshold()?.min(STREAMED_HASH_MIN_SIZE))
7986}
7987
7988#[allow(clippy::too_many_arguments)]
7992pub(crate) fn hash_worktree_file_with_mode(
7993 odb: &Odb,
7994 path: &Path,
7995 worktree_mode: u32,
7996 conv: &crate::crlf::ConversionConfig,
7997 file_attrs: &crate::crlf::FileAttrs,
7998 rel_path: &str,
7999 index_entry: Option<&IndexEntry>,
8000 filter_process: Option<&crate::filter_process::FilterProcessState>,
8001) -> Result<ObjectId> {
8002 if worktree_mode == MODE_SYMLINK {
8003 let data = read_worktree_bytes_for_mode(path, worktree_mode)?;
8004 return odb.hash(ObjectKind::Blob, &data);
8005 }
8006 read_regular_worktree_blob(
8007 odb,
8008 path,
8009 conv,
8010 file_attrs,
8011 rel_path,
8012 index_entry,
8013 filter_process,
8014 hash_stream_threshold(odb)?,
8015 )?
8016 .hash(odb)
8017}
8018
8019#[allow(clippy::too_many_arguments)]
8027pub fn materialize_worktree_blob(
8028 odb: &Odb,
8029 path: &Path,
8030 meta: &fs::Metadata,
8031 conv: &crate::crlf::ConversionConfig,
8032 file_attrs: &crate::crlf::FileAttrs,
8033 rel_path: &str,
8034 index_entry: Option<&IndexEntry>,
8035 filter_process: Option<&crate::filter_process::FilterProcessState>,
8036) -> Result<ObjectId> {
8037 worktree_file_oid(
8038 odb,
8039 path,
8040 meta,
8041 conv,
8042 file_attrs,
8043 rel_path,
8044 index_entry,
8045 true,
8046 filter_process,
8047 )
8048}
8049
8050pub fn mode_from_metadata(meta: &fs::Metadata) -> u32 {
8052 if meta.file_type().is_symlink() {
8053 0o120000
8054 } else {
8055 #[cfg(unix)]
8056 {
8057 if meta.mode() & 0o111 != 0 {
8058 return 0o100755;
8059 }
8060 }
8061 0o100644
8062 }
8063}
8064
8065pub fn diff_tree_to_worktree(
8082 odb: &Odb,
8083 tree_oid: Option<&ObjectId>,
8084 work_tree: &Path,
8085 index: &Index,
8086) -> Result<Vec<DiffEntry>> {
8087 diff_tree_to_worktree_with_git_dir(
8088 odb,
8089 tree_oid,
8090 work_tree,
8091 &work_tree.join(".git"),
8092 index,
8093 None,
8094 )
8095}
8096
8097pub fn diff_tree_to_worktree_with_git_dir(
8101 odb: &Odb,
8102 tree_oid: Option<&ObjectId>,
8103 work_tree: &Path,
8104 repository_git_dir: &Path,
8105 index: &Index,
8106 config: Option<&ConfigSet>,
8107) -> Result<Vec<DiffEntry>> {
8108 diff_tree_to_worktree_with_git_dir_and_rules(
8109 odb,
8110 tree_oid,
8111 work_tree,
8112 repository_git_dir,
8113 index,
8114 config,
8115 None,
8116 )
8117}
8118
8119pub fn diff_tree_to_worktree_with_git_dir_and_rules(
8121 odb: &Odb,
8122 tree_oid: Option<&ObjectId>,
8123 work_tree: &Path,
8124 repository_git_dir: &Path,
8125 index: &Index,
8126 config: Option<&ConfigSet>,
8127 worktree_rules: Option<&crate::worktree_rules::WorktreeRules>,
8128) -> Result<Vec<DiffEntry>> {
8129 use crate::config::ConfigSet;
8130 use crate::crlf;
8131
8132 if worktree_rules.is_none() {
8133 if let Some(tree_oid) = tree_oid {
8134 if index.cache_tree_root.as_ref() == Some(tree_oid)
8135 && crate::write_tree::cache_tree_fully_valid(odb, index.cache_tree.as_ref())
8136 {
8137 let mut idx = index.clone();
8138 let options = DiffIndexToWorktreeOptions {
8139 repository_git_dir: Some(repository_git_dir.to_path_buf()),
8140 refresh_index_stat_in_pass: false,
8141 for_status: false,
8145 config: config.cloned().map(std::sync::Arc::new),
8146 ..DiffIndexToWorktreeOptions::default()
8147 };
8148 let (entries, _, _) = diff_index_to_worktree_inner(
8149 odb,
8150 &mut idx,
8151 work_tree,
8152 repository_git_dir,
8153 options,
8154 false,
8155 None,
8156 )?;
8157 return Ok(entries);
8158 }
8159 }
8160 }
8161
8162 let config = worktree_rules
8163 .map(|r| r.config().clone())
8164 .unwrap_or_else(|| {
8165 config.cloned().unwrap_or_else(|| {
8166 ConfigSet::load(
8167 &crate::environment::Environment::empty(),
8168 Some(repository_git_dir),
8169 true,
8170 )
8171 .unwrap_or_else(|_| ConfigSet::new())
8172 })
8173 });
8174 let conv = worktree_rules
8175 .map(|r| r.conversion().clone())
8176 .unwrap_or_else(|| crlf::ConversionConfig::from_config(&config));
8177 let legacy_attrs = if worktree_rules.is_none() {
8178 Some(crlf::load_gitattributes_parsed(work_tree))
8179 } else {
8180 None
8181 };
8182 let file_attrs_for = |path: &str| -> crlf::FileAttrs {
8183 if let Some(rules) = worktree_rules {
8184 rules.file_attrs(path, false)
8185 } else if let Some(parsed) = legacy_attrs.as_ref() {
8186 crlf::file_attrs_from_gitattributes(&parsed.rules, &parsed.macros, path, false, &config)
8187 } else {
8188 crlf::FileAttrs::default()
8189 }
8190 };
8191 let filter_fp = worktree_rules.map(|r| r.filter_process());
8192
8193 let sparse_index = index.has_sparse_directory_placeholders();
8194 let tree_flat = match tree_oid {
8196 Some(oid) if sparse_index => flatten_tree_with_index(odb, oid, "", Some(index))?,
8197 Some(oid) => flatten_tree(odb, oid, "")?,
8198 None => Vec::new(),
8199 };
8200 let tree_map: std::collections::BTreeMap<String, &FlatEntry> =
8201 tree_flat.iter().map(|e| (e.path.clone(), e)).collect();
8202
8203 let mut index_entries: std::collections::BTreeMap<&[u8], &IndexEntry> =
8205 std::collections::BTreeMap::new();
8206 let mut index_paths: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
8207 let mut stage0_paths: std::collections::BTreeSet<Vec<u8>> = std::collections::BTreeSet::new();
8208 for ie in &index.entries {
8209 if ie.stage() != 0 {
8210 continue;
8211 }
8212 let path = String::from_utf8_lossy(&ie.path).to_string();
8213 index_entries.insert(&ie.path, ie);
8214 index_paths.insert(path);
8215 stage0_paths.insert(ie.path.clone());
8216 }
8217
8218 let mut unmerged_only_paths: std::collections::BTreeSet<String> =
8221 std::collections::BTreeSet::new();
8222 for ie in &index.entries {
8223 if !(1..=3).contains(&ie.stage()) {
8224 continue;
8225 }
8226 if stage0_paths.contains(&ie.path) {
8227 continue;
8228 }
8229 unmerged_only_paths.insert(String::from_utf8_lossy(&ie.path).into_owned());
8230 }
8231
8232 let mut all_paths: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
8234 all_paths.extend(tree_map.keys().cloned());
8235 all_paths.extend(index_paths.iter().cloned());
8236 all_paths.extend(unmerged_only_paths.iter().cloned());
8237
8238 let index_mtime = index_mtime_for_diff(index, None);
8239 let stat_policy = crate::index::StatMatchPolicy::from_config(Some(&config));
8240 let mode_policy = crate::index::WorktreeModePolicy::from_config(&config);
8241 let mut result = Vec::new();
8242
8243 for path in &all_paths {
8244 if index_entries
8245 .get(path.as_bytes())
8246 .is_some_and(|ie| ie.skip_worktree())
8247 {
8248 continue;
8251 }
8252 if index_entries
8253 .get(path.as_bytes())
8254 .is_some_and(|ie| ie.assume_unchanged())
8255 {
8256 continue;
8257 }
8258
8259 let tree_entry = tree_map.get(path.as_str());
8260
8261 let is_gitlink = tree_entry.is_some_and(|te| te.mode == 0o160000)
8263 || index_entries
8264 .get(path.as_bytes())
8265 .is_some_and(|ie| ie.mode == 0o160000);
8266 if is_gitlink {
8267 let sub_dir = work_tree.join(path);
8268 let index_gitlink_oid = index_entries
8269 .get(path.as_bytes())
8270 .filter(|ie| ie.mode == 0o160000)
8271 .map(|ie| ie.oid);
8272 match (tree_entry, index_gitlink_oid) {
8273 (Some(te), _) => {
8274 let sub_head = read_submodule_head_oid(&sub_dir);
8275 if sub_head.is_none() && !sub_dir.exists() {
8279 result.push(DiffEntry {
8280 status: DiffStatus::Deleted,
8281 old_path: Some(path.clone()),
8282 new_path: Some(path.clone()),
8283 old_mode: FileMode::new(te.mode),
8284 new_mode: FileMode::ABSENT,
8285 old_oid: te.oid,
8286 new_oid: zero_oid(),
8287 score: None,
8288 });
8289 continue;
8290 }
8291 let ref_matches = if let Some(head) = sub_head {
8292 head == te.oid
8293 } else {
8294 uninitialized_gitlink_worktree_matches_index(&sub_dir)
8295 };
8296 let index_matches_tree = index_gitlink_oid.is_some_and(|oid| oid == te.oid);
8297 let sub_flags = submodule_porcelain_flags(work_tree, path, te.oid);
8300 let dirty_while_aligned =
8301 index_matches_tree && ref_matches && sub_flags.modified;
8302 if !ref_matches || dirty_while_aligned {
8303 let new_oid = if !ref_matches {
8307 sub_head.unwrap_or_else(zero_oid)
8308 } else {
8309 zero_oid()
8310 };
8311 result.push(DiffEntry {
8312 status: DiffStatus::Modified,
8313 old_path: Some(path.clone()),
8314 new_path: Some(path.clone()),
8315 old_mode: FileMode::new(te.mode),
8316 new_mode: FileMode::new(te.mode),
8317 old_oid: te.oid,
8318 new_oid,
8319 score: None,
8320 });
8321 }
8322 }
8323 (None, Some(idx_oid)) => {
8324 let new_oid = read_submodule_head_oid(&sub_dir).unwrap_or(idx_oid);
8329 result.push(DiffEntry {
8330 status: DiffStatus::Added,
8331 old_path: Some(path.clone()),
8332 new_path: Some(path.clone()),
8333 old_mode: FileMode::ABSENT,
8334 new_mode: FileMode::new(0o160000),
8335 old_oid: zero_oid(),
8336 new_oid,
8337 score: None,
8338 });
8339 }
8340 (None, None) => {}
8341 }
8342 continue;
8343 }
8344
8345 let file_path = work_tree.join(path);
8346
8347 let wt_meta = match fs::symlink_metadata(&file_path) {
8348 Ok(m) => Some(m),
8349 Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
8350 Err(e) => return Err(Error::Io(e)),
8351 };
8352
8353 if unmerged_only_paths.contains(path) {
8354 if let (Some(te), Some(meta)) = (tree_entry, wt_meta.as_ref()) {
8355 let tree_mode = crate::index::canon_mode(te.mode);
8356 let file_attrs = file_attrs_for(path);
8357 let wt_oid = hash_worktree_file(
8358 odb,
8359 &file_path,
8360 meta,
8361 &conv,
8362 &file_attrs,
8363 path,
8364 None,
8365 filter_fp,
8366 )?;
8367 let wt_mode =
8368 mode_policy.index_mode_for_worktree(mode_from_metadata(meta), Some(tree_mode));
8369 if wt_oid != te.oid || wt_mode != tree_mode {
8370 result.push(DiffEntry {
8371 status: DiffStatus::for_content_change(tree_mode, wt_mode),
8372 old_path: Some(path.clone()),
8373 new_path: Some(path.clone()),
8374 old_mode: FileMode::new(tree_mode),
8375 new_mode: FileMode::new(wt_mode),
8376 old_oid: te.oid,
8377 new_oid: wt_oid,
8378 score: None,
8379 });
8380 }
8381 }
8382 continue;
8383 }
8384
8385 match (tree_entry, wt_meta) {
8386 (Some(te), Some(ref meta)) => {
8387 let tree_mode = crate::index::canon_mode(te.mode);
8388 let Some(ie) = index_entries.get(path.as_bytes()) else {
8389 continue;
8390 };
8391 let wt_mode =
8392 mode_policy.index_mode_for_worktree(mode_from_metadata(meta), Some(ie.mode));
8393
8394 let index_matches_tree = ie.oid == te.oid && ie.mode == tree_mode;
8395
8396 let stat_trusted = stat_matches_with_policy(ie, meta, stat_policy)
8398 && !entry_is_racy_with_policy(ie, index_mtime, stat_policy);
8399 if index_matches_tree && stat_trusted && wt_mode == tree_mode {
8400 continue;
8401 }
8402
8403 let file_attrs = file_attrs_for(path);
8404 let idx_ent = index_entries.get(path.as_bytes()).copied();
8405
8406 if ie.oid == te.oid && ie.mode != tree_mode && stat_trusted && wt_mode == ie.mode {
8411 result.push(DiffEntry {
8412 status: DiffStatus::for_content_change(tree_mode, ie.mode),
8413 old_path: Some(path.clone()),
8414 new_path: Some(path.clone()),
8415 old_mode: FileMode::new(tree_mode),
8416 new_mode: FileMode::new(ie.mode),
8417 old_oid: te.oid,
8418 new_oid: te.oid,
8419 score: None,
8420 });
8421 continue;
8422 }
8423
8424 if index_matches_tree {
8427 let wt_oid = hash_worktree_file(
8428 odb,
8429 &file_path,
8430 meta,
8431 &conv,
8432 &file_attrs,
8433 path,
8434 None,
8435 filter_fp,
8436 )?;
8437 if wt_oid != te.oid {
8438 result.push(DiffEntry {
8439 status: DiffStatus::for_content_change(tree_mode, wt_mode),
8440 old_path: Some(path.clone()),
8441 new_path: Some(path.clone()),
8442 old_mode: FileMode::new(tree_mode),
8443 new_mode: FileMode::new(wt_mode),
8444 old_oid: te.oid,
8445 new_oid: zero_oid(),
8448 score: None,
8449 });
8450 } else if wt_mode != tree_mode {
8451 result.push(DiffEntry {
8452 status: DiffStatus::for_content_change(tree_mode, wt_mode),
8453 old_path: Some(path.clone()),
8454 new_path: Some(path.clone()),
8455 old_mode: FileMode::new(tree_mode),
8456 new_mode: FileMode::new(wt_mode),
8457 old_oid: te.oid,
8458 new_oid: te.oid,
8459 score: None,
8460 });
8461 }
8462 continue;
8463 }
8464
8465 let wt_oid = hash_worktree_file(
8467 odb,
8468 &file_path,
8469 meta,
8470 &conv,
8471 &file_attrs,
8472 path,
8473 idx_ent,
8474 filter_fp,
8475 )?;
8476 if wt_oid != te.oid || wt_mode != tree_mode {
8477 let new_oid = if wt_oid != te.oid { zero_oid() } else { te.oid };
8478 result.push(DiffEntry {
8479 status: DiffStatus::for_content_change(tree_mode, wt_mode),
8480 old_path: Some(path.clone()),
8481 new_path: Some(path.clone()),
8482 old_mode: FileMode::new(tree_mode),
8483 new_mode: FileMode::new(wt_mode),
8484 old_oid: te.oid,
8485 new_oid,
8486 score: None,
8487 });
8488 }
8489 }
8490 (Some(te), None) => {
8491 let tree_mode = crate::index::canon_mode(te.mode);
8492 result.push(DiffEntry {
8494 status: DiffStatus::Deleted,
8495 old_path: Some(path.clone()),
8496 new_path: None,
8497 old_mode: FileMode::new(tree_mode),
8498 new_mode: FileMode::ABSENT,
8499 old_oid: te.oid,
8500 new_oid: zero_oid(),
8501 score: None,
8502 });
8503 }
8504 (None, Some(ref meta)) => {
8505 let file_attrs = file_attrs_for(path);
8507 let idx_ent = index_entries.get(path.as_bytes()).copied();
8508 let wt_oid = materialize_worktree_blob(
8509 odb,
8510 &file_path,
8511 meta,
8512 &conv,
8513 &file_attrs,
8514 path,
8515 idx_ent,
8516 filter_fp,
8517 )?;
8518 let wt_mode = mode_from_metadata(meta);
8519 result.push(DiffEntry {
8520 status: DiffStatus::Added,
8521 old_path: None,
8522 new_path: Some(path.clone()),
8523 old_mode: FileMode::ABSENT,
8524 new_mode: FileMode::new(wt_mode),
8525 old_oid: zero_oid(),
8526 new_oid: wt_oid,
8527 score: None,
8528 });
8529 }
8530 (None, None) => {
8531 }
8533 }
8534 }
8535
8536 sort_entries_by_path(&mut result);
8537 Ok(result)
8538}
8539
8540fn read_added_entry_bytes(
8543 odb: &Odb,
8544 entry: &DiffEntry,
8545 work_root: Option<&Path>,
8546) -> Option<Vec<u8>> {
8547 if entry.new_oid != zero_oid() {
8548 if let Ok(obj) = odb.read(&entry.new_oid) {
8549 return Some(obj.data);
8550 }
8551 }
8552 let path = entry.new_path.as_deref()?;
8553 let root = work_root?;
8554 fs::read(root.join(path)).ok()
8555}
8556
8557#[allow(clippy::expect_used)]
8569pub fn detect_renames(
8570 odb: &Odb,
8571 work_root: Option<&Path>,
8572 entries: Vec<DiffEntry>,
8573 threshold: u32,
8574) -> Vec<DiffEntry> {
8575 let config = ConfigSet::new();
8576 let policy = ThreadPolicy::default();
8577 detect_renames_with_policy(odb, work_root, entries, threshold, &config, &policy, None)
8578 .expect("rename detection")
8579}
8580
8581#[inline]
8582fn poll_rename_cancel(
8583 cancel_watch: Option<&std::sync::atomic::AtomicBool>,
8584) -> crate::error::Result<()> {
8585 if let Some(watch) = cancel_watch {
8586 crate::progress::poll_watch(watch)?;
8587 }
8588 Ok(())
8589}
8590
8591pub fn detect_renames_with_policy(
8602 odb: &Odb,
8603 work_root: Option<&Path>,
8604 entries: Vec<DiffEntry>,
8605 threshold: u32,
8606 config: &ConfigSet,
8607 thread_policy: &ThreadPolicy,
8608 cancel_watch: Option<&std::sync::atomic::AtomicBool>,
8609) -> Result<Vec<DiffEntry>> {
8610 poll_rename_cancel(cancel_watch)?;
8611 let has_deleted = entries.iter().any(|e| e.status == DiffStatus::Deleted);
8612 let has_added = entries.iter().any(|e| e.status == DiffStatus::Added);
8613 if (!has_deleted || !has_added) && entries_strictly_sorted_by_path(&entries) {
8614 return Ok(entries);
8616 }
8617 let mut deleted: Vec<DiffEntry> = Vec::new();
8619 let mut added: Vec<DiffEntry> = Vec::new();
8620 let mut others: Vec<DiffEntry> = Vec::new();
8621
8622 for entry in entries {
8623 match entry.status {
8624 DiffStatus::Deleted => deleted.push(entry),
8625 DiffStatus::Added => added.push(entry),
8626 _ => others.push(entry),
8627 }
8628 }
8629
8630 if deleted.is_empty() || added.is_empty() {
8631 let mut result = others;
8633 result.extend(deleted);
8634 result.extend(added);
8635 sort_entries_by_path(&mut result);
8636 return Ok(result);
8637 }
8638
8639 fn is_regularish_mode(mode: FileMode) -> bool {
8640 mode == FileMode::REGULAR || mode == FileMode::EXECUTABLE
8641 }
8642
8643 fn same_path_same_blob(del: &DiffEntry, add: &DiffEntry) -> bool {
8644 del.old_path == add.new_path && del.old_oid == add.new_oid && del.old_mode == add.new_mode
8645 }
8646
8647 fn sort_rename_scores(
8648 scores: &mut [(u32, usize, usize)],
8649 deleted: &[DiffEntry],
8650 added: &[DiffEntry],
8651 ) {
8652 scores.sort_by(|a, b| {
8653 let a_noop = same_path_same_blob(&deleted[a.1], &added[a.2]);
8654 let b_noop = same_path_same_blob(&deleted[b.1], &added[b.2]);
8655 let a_same = same_basename(&deleted[a.1], &added[a.2]);
8656 let b_same = same_basename(&deleted[b.1], &added[b.2]);
8657 a_noop
8658 .cmp(&b_noop)
8659 .then_with(|| b_same.cmp(&a_same))
8660 .then_with(|| b.0.cmp(&a.0))
8661 });
8662 }
8663
8664 fn apply_rename_scores(
8665 scores: &[(u32, usize, usize)],
8666 deleted: &[DiffEntry],
8667 added: &[DiffEntry],
8668 used_deleted: &mut [bool],
8669 used_added: &mut [bool],
8670 renames: &mut Vec<DiffEntry>,
8671 ) {
8672 for (score, di, ai) in scores {
8673 if used_deleted[*di] || used_added[*ai] {
8674 continue;
8675 }
8676 used_deleted[*di] = true;
8677 used_added[*ai] = true;
8678
8679 let del = &deleted[*di];
8680 let add = &added[*ai];
8681 if same_path_same_blob(del, add) {
8682 continue;
8683 }
8684
8685 renames.push(DiffEntry {
8686 status: DiffStatus::Renamed,
8687 old_path: del.old_path.clone(),
8688 new_path: add.new_path.clone(),
8689 old_mode: del.old_mode,
8690 new_mode: add.new_mode,
8691 old_oid: del.old_oid,
8692 new_oid: add.new_oid,
8693 score: Some(*score),
8694 });
8695 }
8696 }
8697
8698 let mut used_deleted = vec![false; deleted.len()];
8699 let mut used_added = vec![false; added.len()];
8700 let mut renames: Vec<DiffEntry> = Vec::new();
8701
8702 let mut exact_scores: Vec<(u32, usize, usize)> = Vec::new();
8704 for (di, del) in deleted.iter().enumerate() {
8705 poll_rename_cancel(cancel_watch)?;
8706 for (ai, add) in added.iter().enumerate() {
8707 if del.old_oid == add.new_oid {
8708 exact_scores.push((100, di, ai));
8709 }
8710 }
8711 }
8712 sort_rename_scores(&mut exact_scores, &deleted, &added);
8713 apply_rename_scores(
8714 &exact_scores,
8715 &deleted,
8716 &added,
8717 &mut used_deleted,
8718 &mut used_added,
8719 &mut renames,
8720 );
8721
8722 let remaining_deleted: Vec<usize> = used_deleted
8723 .iter()
8724 .enumerate()
8725 .filter_map(|(i, used)| (!*used).then_some(i))
8726 .collect();
8727 let remaining_added: Vec<usize> = used_added
8728 .iter()
8729 .enumerate()
8730 .filter_map(|(i, used)| (!*used).then_some(i))
8731 .collect();
8732
8733 let rename_limit = parse_rename_limit(config);
8734 if !remaining_deleted.is_empty()
8735 && !remaining_added.is_empty()
8736 && !inexact_matrix_exceeds_limit(
8737 remaining_added.len(),
8738 remaining_deleted.len(),
8739 rename_limit,
8740 )
8741 {
8742 let par_opts = diff_rename_parallel_options(config, thread_policy);
8743
8744 let mut prefetch_entries: Vec<DiffEntry> =
8745 Vec::with_capacity(remaining_deleted.len() + remaining_added.len());
8746 for &di in &remaining_deleted {
8747 prefetch_entries.push(deleted[di].clone());
8748 }
8749 for &ai in &remaining_added {
8750 prefetch_entries.push(added[ai].clone());
8751 }
8752 super::tree_content::prefetch_tree_diff_blobs(odb, &prefetch_entries)?;
8753
8754 let deleted_sizes: Vec<Option<usize>> = remaining_deleted
8755 .iter()
8756 .map(|&di| blob_payload_size(odb, &deleted[di].old_oid))
8757 .collect();
8758 let added_sizes: Vec<Option<usize>> = remaining_added
8759 .iter()
8760 .map(|&ai| added_entry_payload_size(odb, &added[ai], work_root))
8761 .collect();
8762
8763 let mut load_deleted = vec![false; remaining_deleted.len()];
8764 let mut load_added = vec![false; remaining_added.len()];
8765 let mut candidate_pairs: Vec<(usize, usize)> = Vec::new();
8766 for (row, &di) in remaining_deleted.iter().enumerate() {
8767 poll_rename_cancel(cancel_watch)?;
8768 let del = &deleted[di];
8769 if !is_regularish_mode(del.old_mode) {
8770 continue;
8771 }
8772 let Some(ds) = deleted_sizes[row] else {
8773 continue;
8774 };
8775 for (col, &ai) in remaining_added.iter().enumerate() {
8776 let add = &added[ai];
8777 if !is_regularish_mode(add.new_mode) {
8778 continue;
8779 }
8780 let Some(asz) = added_sizes[col] else {
8781 continue;
8782 };
8783 if rename_pair_size_reachable(ds, asz, threshold) {
8784 load_deleted[row] = true;
8785 load_added[col] = true;
8786 candidate_pairs.push((row, col));
8787 }
8788 }
8789 }
8790
8791 if !candidate_pairs.is_empty() {
8792 let loads_needed = load_deleted.iter().filter(|&&b| b).count()
8793 + load_added.iter().filter(|&&b| b).count();
8794 let load_deleted_blob = |i: usize| -> Option<Vec<u8>> {
8795 if !load_deleted[i] {
8796 return None;
8797 }
8798 odb.read(&deleted[remaining_deleted[i]].old_oid)
8799 .ok()
8800 .map(|obj| obj.data)
8801 };
8802 let load_added_blob = |i: usize| -> Option<Vec<u8>> {
8803 if !load_added[i] {
8804 return None;
8805 }
8806 read_added_entry_bytes(odb, &added[remaining_added[i]], work_root)
8807 };
8808 let (deleted_contents, added_contents) = if loads_needed <= 32 {
8809 (
8810 (0..remaining_deleted.len())
8811 .map(load_deleted_blob)
8812 .collect(),
8813 (0..remaining_added.len()).map(load_added_blob).collect(),
8814 )
8815 } else {
8816 (
8817 parallel_load_optional_blobs(
8818 odb,
8819 remaining_deleted.len(),
8820 par_opts,
8821 0,
8822 load_deleted_blob,
8823 ),
8824 parallel_load_optional_blobs(
8825 odb,
8826 remaining_added.len(),
8827 par_opts,
8828 0,
8829 load_added_blob,
8830 ),
8831 )
8832 };
8833
8834 let est_table_bytes: usize = deleted_contents
8835 .iter()
8836 .chain(added_contents.iter())
8837 .filter_map(|opt| opt.as_ref().map(|b| b.len()))
8838 .sum();
8839 let build_deleted_table = |row: usize| -> std::result::Result<
8840 Option<RenameLineTable>,
8841 std::convert::Infallible,
8842 > {
8843 Ok(deleted_contents[row]
8844 .as_ref()
8845 .map(|d| RenameLineTable::from_blob(d)))
8846 };
8847 let build_added_table = |col: usize| -> std::result::Result<
8848 Option<RenameLineTable>,
8849 std::convert::Infallible,
8850 > {
8851 Ok(added_contents[col]
8852 .as_ref()
8853 .map(|d| RenameLineTable::from_blob(d)))
8854 };
8855 let deleted_tables: Vec<Option<RenameLineTable>> = if loads_needed <= 32 {
8856 (0..remaining_deleted.len())
8857 .map(|row| build_deleted_table(row).unwrap())
8858 .collect()
8859 } else {
8860 parallel_map_indices(
8861 odb,
8862 remaining_deleted.len(),
8863 par_opts,
8864 est_table_bytes,
8865 build_deleted_table,
8866 )
8867 .expect("deleted rename line tables")
8868 };
8869 let added_tables: Vec<Option<RenameLineTable>> = if loads_needed <= 32 {
8870 (0..remaining_added.len())
8871 .map(|col| build_added_table(col).unwrap())
8872 .collect()
8873 } else {
8874 parallel_map_indices(
8875 odb,
8876 remaining_added.len(),
8877 par_opts,
8878 est_table_bytes,
8879 build_added_table,
8880 )
8881 .expect("added rename line tables")
8882 };
8883
8884 let mut inexact_scores: Vec<(u32, usize, usize)> = Vec::new();
8885 for (row, col) in candidate_pairs {
8886 poll_rename_cancel(cancel_watch)?;
8887 let di = remaining_deleted[row];
8888 let ai = remaining_added[col];
8889 let (Some(old_table), Some(new_table)) = (&deleted_tables[row], &added_tables[col])
8890 else {
8891 continue;
8892 };
8893 let score = RenameLineTable::similarity_score(old_table, new_table);
8894 if score >= threshold {
8895 inexact_scores.push((score, di, ai));
8896 }
8897 }
8898 sort_rename_scores(&mut inexact_scores, &deleted, &added);
8899 apply_rename_scores(
8900 &inexact_scores,
8901 &deleted,
8902 &added,
8903 &mut used_deleted,
8904 &mut used_added,
8905 &mut renames,
8906 );
8907 }
8908 }
8909
8910 let mut result = others;
8912 result.extend(renames);
8913 for (i, entry) in deleted.into_iter().enumerate() {
8914 if !used_deleted[i] {
8915 result.push(entry);
8916 }
8917 }
8918 for (i, entry) in added.into_iter().enumerate() {
8919 if !used_added[i] {
8920 result.push(entry);
8921 }
8922 }
8923
8924 result = prune_break_deletions_obviated_by_renames(result);
8925 sort_entries_by_path(&mut result);
8926 Ok(result)
8927}
8928
8929pub fn detect_copies(
8944 odb: &Odb,
8945 work_root: Option<&Path>,
8946 entries: Vec<DiffEntry>,
8947 threshold: u32,
8948 find_copies_harder: bool,
8949 source_tree_entries: &[(String, FileMode, ObjectId)],
8950) -> Result<Vec<DiffEntry>> {
8951 let config = ConfigSet::new();
8952 let policy = ThreadPolicy::default();
8953 detect_copies_with_policy(
8954 odb,
8955 work_root,
8956 entries,
8957 threshold,
8958 find_copies_harder,
8959 source_tree_entries,
8960 &config,
8961 &policy,
8962 )
8963}
8964
8965#[expect(clippy::too_many_arguments)]
8971pub fn detect_copies_with_policy(
8972 odb: &Odb,
8973 work_root: Option<&Path>,
8974 entries: Vec<DiffEntry>,
8975 threshold: u32,
8976 find_copies_harder: bool,
8977 source_tree_entries: &[(String, FileMode, ObjectId)],
8978 config: &ConfigSet,
8979 thread_policy: &ThreadPolicy,
8980) -> Result<Vec<DiffEntry>> {
8981 use std::collections::{HashMap, HashSet};
8982
8983 let mut deleted: Vec<DiffEntry> = Vec::new();
8985 let mut added: Vec<DiffEntry> = Vec::new();
8986 let mut others: Vec<DiffEntry> = Vec::new();
8987
8988 for entry in entries {
8989 match entry.status {
8990 DiffStatus::Deleted => deleted.push(entry),
8991 DiffStatus::Added => added.push(entry),
8992 _ => others.push(entry),
8993 }
8994 }
8995
8996 let mut sources: Vec<(String, ObjectId, bool)> = Vec::new(); let mut deleted_source_idx: HashMap<String, usize> = HashMap::new();
9000
9001 for entry in &deleted {
9002 if let Some(ref path) = entry.old_path {
9003 deleted_source_idx.insert(path.clone(), sources.len());
9004 sources.push((path.clone(), entry.old_oid, true));
9005 }
9006 }
9007
9008 for entry in &others {
9011 if matches!(entry.status, DiffStatus::Modified | DiffStatus::TypeChanged) {
9012 if let Some(ref old_path) = entry.old_path {
9013 if !sources.iter().any(|(p, _, _)| p == old_path) {
9014 sources.push((old_path.clone(), entry.old_oid, false));
9015 }
9016 }
9017 }
9018 }
9019
9020 if find_copies_harder {
9022 for (path, _mode, oid) in source_tree_entries {
9023 if !sources.iter().any(|(p, _, _)| p == path) {
9024 sources.push((path.clone(), *oid, false));
9025 }
9026 }
9027 }
9028
9029 let mut source_modes: Vec<(String, FileMode, ObjectId)> = source_tree_entries.to_vec();
9032 for entry in deleted.iter().chain(&others) {
9033 if let Some(ref path) = entry.old_path {
9034 if !source_modes.iter().any(|(p, _, _)| p == path) {
9035 source_modes.push((path.clone(), entry.old_mode, entry.old_oid));
9036 }
9037 }
9038 }
9039
9040 if sources.is_empty() {
9041 let mut result = others;
9042 result.extend(deleted);
9043 result.extend(added);
9044 sort_entries_by_path(&mut result);
9045 return Ok(result);
9046 }
9047
9048 let par_opts = diff_rename_parallel_options(config, thread_policy);
9049 let rename_limit = parse_rename_limit(config);
9050
9051 fn apply_copy_score_assignments(
9052 scores: &[(u32, usize, usize)],
9053 sources: &[(String, ObjectId, bool)],
9054 added: &[DiffEntry],
9055 source_tree_entries: &[(String, FileMode, ObjectId)],
9056 used_added2: &mut [bool],
9057 renamed_deleted: &mut HashSet<usize>,
9058 result_entries: &mut Vec<DiffEntry>,
9059 ) {
9060 let mut used_added = vec![false; added.len()];
9061 let mut source_to_added: HashMap<usize, Vec<(usize, u32)>> = HashMap::new();
9062 for &(score, si, ai) in scores {
9063 if used_added[ai] {
9064 continue;
9065 }
9066 used_added[ai] = true;
9067 source_to_added.entry(si).or_default().push((ai, score));
9068 }
9069
9070 for (&si, assignments_for_src) in &source_to_added {
9071 let (_, _, is_deleted) = &sources[si];
9072 if *is_deleted && !assignments_for_src.is_empty() {
9073 let rename_ai = assignments_for_src
9074 .iter()
9075 .max_by_key(|(ai, _score)| added[*ai].path().to_string())
9076 .map(|(ai, _)| *ai);
9077
9078 for &(ai, score) in assignments_for_src {
9079 let (ref src_path, _, _) = sources[si];
9080 let add = &added[ai];
9081 let src_mode = source_tree_entries
9082 .iter()
9083 .find(|(p, _, _)| p == src_path)
9084 .map_or(add.old_mode, |(_, m, _)| *m);
9085
9086 let is_rename = Some(ai) == rename_ai;
9087 result_entries.push(DiffEntry {
9088 status: if is_rename {
9089 DiffStatus::Renamed
9090 } else {
9091 DiffStatus::Copied
9092 },
9093 old_path: Some(src_path.clone()),
9094 new_path: add.new_path.clone(),
9095 old_mode: src_mode,
9096 new_mode: add.new_mode,
9097 old_oid: sources[si].1,
9098 new_oid: add.new_oid,
9099 score: Some(score),
9100 });
9101 used_added2[ai] = true;
9102 }
9103 renamed_deleted.insert(si);
9104 } else {
9105 for &(ai, score) in assignments_for_src {
9106 let (ref src_path, _, _) = sources[si];
9107 let add = &added[ai];
9108 let src_mode = source_tree_entries
9109 .iter()
9110 .find(|(p, _, _)| p == src_path)
9111 .map_or(add.old_mode, |(_, m, _)| *m);
9112
9113 result_entries.push(DiffEntry {
9114 status: DiffStatus::Copied,
9115 old_path: Some(src_path.clone()),
9116 new_path: add.new_path.clone(),
9117 old_mode: src_mode,
9118 new_mode: add.new_mode,
9119 old_oid: sources[si].1,
9120 new_oid: add.new_oid,
9121 score: Some(score),
9122 });
9123 used_added2[ai] = true;
9124 }
9125 }
9126 }
9127 }
9128
9129 fn added_entry_oid(
9130 odb: &Odb,
9131 add: &DiffEntry,
9132 added_contents: &[Option<Vec<u8>>],
9133 ai: usize,
9134 ) -> Result<ObjectId> {
9135 if add.new_oid != zero_oid() {
9136 Ok(add.new_oid)
9137 } else if let Some(ref data) = added_contents[ai] {
9138 odb.hash(ObjectKind::Blob, data)
9139 } else {
9140 Ok(zero_oid())
9141 }
9142 }
9143
9144 let mut result_entries: Vec<DiffEntry> = Vec::new();
9145 let mut renamed_deleted: HashSet<usize> = HashSet::new();
9146 let mut used_added2 = vec![false; added.len()];
9147 let mut source_contents = vec![None; sources.len()];
9148
9149 if !added.is_empty() {
9150 let added_contents = parallel_load_optional_blobs(odb, added.len(), par_opts, 0, |ai| {
9151 read_added_entry_bytes(odb, &added[ai], work_root)
9152 });
9153
9154 let mut exact_scores: Vec<(u32, usize, usize)> = Vec::new();
9156 for (si, (src_path, src_oid, _)) in sources.iter().enumerate() {
9157 for (ai, add) in added.iter().enumerate() {
9158 if add.new_path.as_deref() == Some(src_path.as_str()) {
9159 continue;
9160 }
9161 let add_oid = added_entry_oid(odb, add, &added_contents, ai)?;
9162 if *src_oid == add_oid && add_oid != zero_oid() {
9163 exact_scores.push((100, si, ai));
9164 }
9165 }
9166 }
9167 exact_scores.sort_by_key(|a| std::cmp::Reverse(a.0));
9168 apply_copy_score_assignments(
9169 &exact_scores,
9170 &sources,
9171 &added,
9172 &source_modes,
9173 &mut used_added2,
9174 &mut renamed_deleted,
9175 &mut result_entries,
9176 );
9177
9178 let remaining_added: Vec<usize> = used_added2
9179 .iter()
9180 .enumerate()
9181 .filter_map(|(i, used)| (!*used).then_some(i))
9182 .collect();
9183 let remaining_source_count = sources
9184 .iter()
9185 .enumerate()
9186 .filter(|(si, (_, _, is_deleted))| !*is_deleted || !renamed_deleted.contains(si))
9187 .count();
9188
9189 let skip_inexact = remaining_added.is_empty()
9190 || inexact_matrix_exceeds_limit(
9191 remaining_added.len(),
9192 remaining_source_count,
9193 rename_limit,
9194 );
9195
9196 if !skip_inexact {
9197 source_contents = parallel_load_optional_blobs(odb, sources.len(), par_opts, 0, |si| {
9198 odb.read(&sources[si].1).ok().map(|obj| obj.data)
9199 });
9200
9201 let est_bytes: usize = source_contents
9202 .iter()
9203 .chain(added_contents.iter())
9204 .filter_map(|opt| opt.as_ref().map(|b| b.len()))
9205 .sum();
9206
9207 let source_tables: Vec<Option<RenameLineTable>> = parallel_map_indices(
9208 odb,
9209 sources.len(),
9210 par_opts,
9211 est_bytes,
9212 |si| -> std::result::Result<Option<RenameLineTable>, std::convert::Infallible> {
9213 Ok(source_contents[si]
9214 .as_ref()
9215 .map(|d| RenameLineTable::from_blob(d)))
9216 },
9217 )
9218 .expect("copy source line tables");
9219 let added_tables: Vec<Option<RenameLineTable>> = parallel_map_indices(
9220 odb,
9221 added.len(),
9222 par_opts,
9223 est_bytes,
9224 |ai| -> std::result::Result<Option<RenameLineTable>, std::convert::Infallible> {
9225 Ok(added_contents[ai]
9226 .as_ref()
9227 .map(|d| RenameLineTable::from_blob(d)))
9228 },
9229 )
9230 .expect("copy added line tables");
9231
9232 let score_rows: Vec<Vec<(u32, usize, usize)>> = parallel_map_indices(
9233 odb,
9234 sources.len(),
9235 par_opts,
9236 est_bytes,
9237 |si| -> std::result::Result<Vec<(u32, usize, usize)>, std::convert::Infallible> {
9238 if renamed_deleted.contains(&si) {
9239 return Ok(Vec::new());
9240 }
9241 let (src_path, _, _) = &sources[si];
9242 let mut row: Vec<(u32, usize, usize)> = Vec::new();
9243 for &ai in &remaining_added {
9244 let add = &added[ai];
9245 if add.new_path.as_deref() == Some(src_path.as_str()) {
9246 continue;
9247 }
9248 let score = match (&source_tables[si], &added_tables[ai]) {
9249 (Some(old_table), Some(new_table)) => {
9250 RenameLineTable::similarity_score(old_table, new_table)
9251 }
9252 _ => 0,
9253 };
9254 if score >= threshold {
9255 row.push((score, si, ai));
9256 }
9257 }
9258 Ok(row)
9259 },
9260 )
9261 .expect("copy score rows");
9262 let mut inexact_scores: Vec<(u32, usize, usize)> =
9263 score_rows.into_iter().flatten().collect();
9264 inexact_scores.sort_by_key(|a| std::cmp::Reverse(a.0));
9265 apply_copy_score_assignments(
9266 &inexact_scores,
9267 &sources,
9268 &added,
9269 &source_modes,
9270 &mut used_added2,
9271 &mut renamed_deleted,
9272 &mut result_entries,
9273 );
9274 }
9275 }
9276
9277 for entry in deleted.into_iter() {
9279 if let Some(ref path) = entry.old_path {
9280 if let Some(&si) = deleted_source_idx.get(path) {
9281 if renamed_deleted.contains(&si) {
9282 continue;
9284 }
9285 }
9286 }
9287 result_entries.push(entry);
9288 }
9289
9290 let mut result = others;
9291 result.extend(result_entries);
9292 for (i, entry) in added.into_iter().enumerate() {
9294 if !used_added2[i] {
9295 result.push(entry);
9296 }
9297 }
9298
9299 let mut final_result = result;
9303 sort_entries_by_path(&mut final_result);
9304 Ok(final_result)
9305}
9306
9307#[derive(Debug, Clone, Copy)]
9309pub struct DiffPostprocessOptions {
9310 pub break_score: Option<u64>,
9313 pub rename_threshold: u32,
9315 pub copy_threshold: Option<u32>,
9317 pub find_copies_harder: bool,
9319 pub materialize_break_worktree_new_oid: bool,
9322}
9323
9324impl Default for DiffPostprocessOptions {
9325 fn default() -> Self {
9326 Self {
9327 break_score: None,
9328 rename_threshold: 100,
9329 copy_threshold: None,
9330 find_copies_harder: false,
9331 materialize_break_worktree_new_oid: false,
9332 }
9333 }
9334}
9335
9336#[derive(Debug, Clone, Copy)]
9339pub struct DiffPostprocessWorktree<'a> {
9340 pub repository_git_dir: &'a Path,
9342 pub index: &'a Index,
9344}
9345
9346fn blob_bytes(odb: &Odb, oid: &ObjectId) -> Result<Vec<u8>> {
9347 Ok(odb.read(oid)?.data)
9348}
9349
9350fn read_new_side_bytes(
9355 odb: &Odb,
9356 entry: &DiffEntry,
9357 work_root: Option<&Path>,
9358 worktree: Option<&DiffPostprocessWorktree<'_>>,
9359 config: &ConfigSet,
9360) -> Result<Vec<u8>> {
9361 if entry.new_oid != zero_oid() {
9362 return blob_bytes(odb, &entry.new_oid);
9363 }
9364 let path = entry.new_path.as_deref().ok_or_else(|| {
9365 Error::ObjectNotFound("modified entry missing new_path for worktree read".into())
9366 })?;
9367 let root = work_root.ok_or_else(|| {
9368 Error::ObjectNotFound(format!(
9369 "worktree bytes required for {} but no work_root was provided",
9370 path
9371 ))
9372 })?;
9373 if let Some(wctx) = worktree {
9374 let mode = entry.new_mode.bits();
9375 let index_entry = wctx.index.get(path.as_bytes(), 0);
9376 return read_worktree_side_bytes_for_diff(odb, root, path, mode, config, index_entry, None);
9377 }
9378 fs::read(root.join(path)).map_err(Error::Io)
9379}
9380
9381fn worktree_side_differs_after_normalize(
9383 odb: &Odb,
9384 entry: &DiffEntry,
9385 work_root: &Path,
9386 worktree: &DiffPostprocessWorktree<'_>,
9387 config: &ConfigSet,
9388) -> Result<bool> {
9389 let path = entry.new_path.as_deref().ok_or_else(|| {
9390 Error::ObjectNotFound("modified entry missing new_path for worktree read".into())
9391 })?;
9392 let raw = fs::read(work_root.join(path)).map_err(Error::Io)?;
9393 let canonical = read_new_side_bytes(odb, entry, Some(work_root), Some(worktree), config)?;
9394 Ok(raw != canonical)
9395}
9396
9397fn split_broken_modifications(
9398 odb: &Odb,
9399 work_root: Option<&Path>,
9400 entries: Vec<DiffEntry>,
9401 break_score: u64,
9402 worktree: Option<&DiffPostprocessWorktree<'_>>,
9403 config: &ConfigSet,
9404) -> Result<Vec<DiffEntry>> {
9405 let mut out = Vec::with_capacity(entries.len());
9406 for e in entries {
9407 if e.status != DiffStatus::Modified {
9408 out.push(e);
9409 continue;
9410 }
9411 let old = blob_bytes(odb, &e.old_oid)?;
9412 let new = read_new_side_bytes(odb, &e, work_root, worktree, config)?;
9413 if should_break_rewrite_pair(&old, &new, break_score) {
9414 let path = e.path().to_owned();
9415 out.push(DiffEntry {
9416 status: DiffStatus::Deleted,
9417 old_path: Some(path.clone()),
9418 new_path: None,
9419 old_mode: e.old_mode,
9420 new_mode: FileMode::ABSENT,
9421 old_oid: e.old_oid,
9422 new_oid: zero_oid(),
9423 score: None,
9424 });
9425 out.push(DiffEntry {
9426 status: DiffStatus::Added,
9427 old_path: None,
9428 new_path: Some(path),
9429 old_mode: FileMode::ABSENT,
9430 new_mode: e.new_mode,
9431 old_oid: zero_oid(),
9432 new_oid: e.new_oid,
9433 score: None,
9434 });
9435 } else {
9436 out.push(e);
9437 }
9438 }
9439 Ok(out)
9440}
9441
9442fn annotate_broken_modifications(
9443 odb: &Odb,
9444 work_root: Option<&Path>,
9445 entries: Vec<DiffEntry>,
9446 break_score: u64,
9447 worktree: Option<&DiffPostprocessWorktree<'_>>,
9448 config: &ConfigSet,
9449 materialize_worktree_oid: bool,
9450) -> Result<Vec<DiffEntry>> {
9451 let mut out = Vec::with_capacity(entries.len());
9452 for mut e in entries {
9453 if e.status == DiffStatus::Modified {
9454 let old = blob_bytes(odb, &e.old_oid)?;
9455 let new = read_new_side_bytes(odb, &e, work_root, worktree, config)?;
9456 if should_break_rewrite_pair(&old, &new, break_score) {
9457 let show_score = if e.new_oid != zero_oid() {
9461 true
9462 } else if materialize_worktree_oid {
9463 match (work_root, worktree) {
9464 (Some(root), Some(wctx)) => {
9465 worktree_side_differs_after_normalize(odb, &e, root, wctx, config)?
9466 }
9467 _ => false,
9468 }
9469 } else {
9470 false
9471 };
9472 if show_score {
9473 e.score = rewrite_dissimilarity_index_percent(&old, &new);
9474 }
9475 }
9476 }
9477 out.push(e);
9478 }
9479 Ok(out)
9480}
9481
9482fn materialize_break_worktree_new_oids(
9483 odb: &Odb,
9484 work_root: Option<&Path>,
9485 entries: &mut [DiffEntry],
9486 worktree: Option<&DiffPostprocessWorktree<'_>>,
9487 config: &ConfigSet,
9488) -> Result<()> {
9489 for e in entries {
9490 if e.new_oid != zero_oid() {
9491 continue;
9492 }
9493 if !matches!(e.status, DiffStatus::Modified | DiffStatus::Added) {
9494 continue;
9495 }
9496 let bytes = read_new_side_bytes(odb, e, work_root, worktree, config)?;
9497 e.new_oid = odb.hash(ObjectKind::Blob, &bytes)?;
9498 }
9499 Ok(())
9500}
9501
9502fn prune_break_deletions_obviated_by_renames(entries: Vec<DiffEntry>) -> Vec<DiffEntry> {
9505 use std::collections::HashSet;
9506 let rename_targets: HashSet<String> = entries
9507 .iter()
9508 .filter(|e| e.status == DiffStatus::Renamed)
9509 .filter_map(|e| e.new_path.clone())
9510 .collect();
9511 if rename_targets.is_empty() {
9512 return entries;
9513 }
9514 entries
9515 .into_iter()
9516 .filter(|e| {
9517 if e.status != DiffStatus::Deleted {
9518 return true;
9519 }
9520 e.old_path
9521 .as_ref()
9522 .is_none_or(|p| !rename_targets.contains(p))
9523 })
9524 .collect()
9525}
9526
9527#[allow(clippy::too_many_arguments)]
9537pub fn postprocess_diff_entries(
9538 odb: &Odb,
9539 entries: Vec<DiffEntry>,
9540 source_tree_oid: Option<&ObjectId>,
9541 work_root: Option<&Path>,
9542 worktree: Option<&DiffPostprocessWorktree<'_>>,
9543 config: &ConfigSet,
9544 thread_policy: &ThreadPolicy,
9545 opts: &DiffPostprocessOptions,
9546) -> Result<Vec<DiffEntry>> {
9547 let mut entries = entries;
9548 if let Some(break_score) = opts.break_score {
9549 let rename_or_copy = opts.rename_threshold < 100 || opts.copy_threshold.is_some();
9550 entries = if rename_or_copy {
9551 split_broken_modifications(odb, work_root, entries, break_score, worktree, config)?
9552 } else {
9553 annotate_broken_modifications(
9554 odb,
9555 work_root,
9556 entries,
9557 break_score,
9558 worktree,
9559 config,
9560 opts.materialize_break_worktree_new_oid,
9561 )?
9562 };
9563 if opts.materialize_break_worktree_new_oid {
9564 materialize_break_worktree_new_oids(odb, work_root, &mut entries, worktree, config)?;
9565 }
9566 }
9567
9568 let source_tree_entries = source_tree_oid
9569 .map(|t| flatten_tree_entries(odb, t))
9570 .transpose()?
9571 .unwrap_or_default();
9572
9573 if opts.copy_threshold.is_some() || opts.find_copies_harder {
9574 let threshold = opts.copy_threshold.unwrap_or(opts.rename_threshold.min(99));
9575 detect_copies_with_policy(
9576 odb,
9577 work_root,
9578 entries,
9579 threshold,
9580 opts.find_copies_harder,
9581 &source_tree_entries,
9582 config,
9583 thread_policy,
9584 )
9585 } else if opts.rename_threshold < 100 {
9586 detect_renames_with_policy(
9587 odb,
9588 work_root,
9589 entries,
9590 opts.rename_threshold,
9591 config,
9592 thread_policy,
9593 None,
9594 )
9595 } else {
9596 Ok(entries)
9597 }
9598}
9599
9600pub fn flatten_tree_entries(
9602 odb: &Odb,
9603 tree_oid: &ObjectId,
9604) -> Result<Vec<(String, FileMode, ObjectId)>> {
9605 fn walk(
9606 odb: &Odb,
9607 tree_oid: &ObjectId,
9608 prefix: &str,
9609 out: &mut Vec<(String, FileMode, ObjectId)>,
9610 ) -> Result<()> {
9611 let obj = odb.read(tree_oid)?;
9612 let oid_len = odb.hash_algo().len();
9613 for e in TreeRefIter::with_oid_len(&obj.data, oid_len) {
9614 let e = e?;
9615 let name = String::from_utf8_lossy(e.name);
9616 let path = if prefix.is_empty() {
9617 name.to_string()
9618 } else {
9619 format!("{prefix}/{name}")
9620 };
9621 if e.mode == 0o040000 {
9622 walk(odb, &e.oid, &path, out)?;
9623 } else {
9624 out.push((path, FileMode::new(e.mode), e.oid));
9625 }
9626 }
9627 Ok(())
9628 }
9629 let mut out = Vec::new();
9630 walk(odb, tree_oid, "", &mut out)?;
9631 Ok(out)
9632}
9633
9634#[allow(clippy::too_many_arguments)]
9644pub fn status_apply_rename_copy_detection(
9645 odb: &Odb,
9646 unstaged_raw: Vec<DiffEntry>,
9647 threshold: u32,
9648 copies: bool,
9649 head_tree: Option<&ObjectId>,
9650 config: &ConfigSet,
9651 thread_policy: &ThreadPolicy,
9652 cancel_watch: Option<&std::sync::atomic::AtomicBool>,
9653) -> Result<Vec<DiffEntry>> {
9654 if !copies {
9655 return detect_renames_with_policy(
9656 odb,
9657 None,
9658 unstaged_raw,
9659 threshold,
9660 config,
9661 thread_policy,
9662 cancel_watch,
9663 );
9664 }
9665 let source_tree_entries: Vec<(String, FileMode, ObjectId)> = match head_tree {
9666 Some(oid) => flatten_tree(odb, oid, "")?
9667 .into_iter()
9668 .map(|e| (e.path, FileMode::new(e.mode), e.oid))
9669 .collect(),
9670 None => Vec::new(),
9671 };
9672 detect_copies_with_policy(
9673 odb,
9674 None,
9675 unstaged_raw,
9676 threshold,
9677 false,
9678 &source_tree_entries,
9679 config,
9680 thread_policy,
9681 )
9682}
9683
9684fn same_basename(del: &DiffEntry, add: &DiffEntry) -> bool {
9686 let old = del.old_path.as_deref().unwrap_or("");
9687 let new = add.new_path.as_deref().unwrap_or("");
9688 let old_base = old.rsplit('/').next().unwrap_or(old);
9689 let new_base = new.rsplit('/').next().unwrap_or(new);
9690 old_base == new_base && !old_base.is_empty()
9691}
9692
9693const RENAME_SCORE_MAX: u32 = 100;
9694
9695fn blob_payload_size(odb: &Odb, oid: &ObjectId) -> Option<usize> {
9697 if oid.is_zero() {
9698 return None;
9699 }
9700 odb.read_info(oid).ok().map(|info| info.size as usize)
9701}
9702
9703fn added_entry_payload_size(odb: &Odb, add: &DiffEntry, work_root: Option<&Path>) -> Option<usize> {
9705 if !add.new_oid.is_zero() {
9706 if let Some(size) = blob_payload_size(odb, &add.new_oid) {
9707 return Some(size);
9708 }
9709 }
9710 read_added_entry_bytes(odb, add, work_root).map(|data| data.len())
9711}
9712
9713fn rename_pair_size_reachable(src_size: usize, dst_size: usize, minimum_score: u32) -> bool {
9715 let max_size = src_size.max(dst_size);
9716 let min_size = src_size.min(dst_size);
9717 if max_size == 0 {
9718 return minimum_score <= RENAME_SCORE_MAX;
9719 }
9720 (max_size - min_size).saturating_mul(RENAME_SCORE_MAX as usize)
9721 <= max_size.saturating_mul((RENAME_SCORE_MAX - minimum_score) as usize)
9722}
9723
9724fn compute_similarity(old: &[u8], new: &[u8]) -> u32 {
9729 RenameLineTable::similarity_score(
9730 &RenameLineTable::from_blob(old),
9731 &RenameLineTable::from_blob(new),
9732 )
9733}
9734
9735#[must_use]
9740pub fn rename_similarity_score(old: &[u8], new: &[u8]) -> u32 {
9741 compute_similarity(old, new)
9742}
9743
9744pub fn format_raw(entry: &DiffEntry) -> String {
9750 let path = match entry.status {
9751 DiffStatus::Renamed | DiffStatus::Copied => {
9752 format!(
9753 "{}\t{}",
9754 entry.old_path.as_deref().unwrap_or(""),
9755 entry.new_path.as_deref().unwrap_or("")
9756 )
9757 }
9758 _ => entry.path().to_owned(),
9759 };
9760
9761 let status_str = match (entry.status, entry.score) {
9762 (DiffStatus::Renamed, Some(s)) => format!("R{:03}", s),
9763 (DiffStatus::Copied, Some(s)) => format!("C{:03}", s),
9764 (DiffStatus::Modified, Some(s)) => format!("M{s:03}"),
9765 (DiffStatus::TypeChanged, Some(s)) => format!("T{s:03}"),
9766 _ => entry.status.letter().to_string(),
9767 };
9768
9769 let (old_hex, new_hex) = raw_oid_hex_pair(&entry.old_oid, &entry.new_oid);
9770 format!(
9771 ":{} {} {} {} {}\t{}",
9772 entry.old_mode, entry.new_mode, old_hex, new_hex, status_str, path
9773 )
9774}
9775
9776fn raw_oid_hex_pair(old: &ObjectId, new: &ObjectId) -> (String, String) {
9784 let width = if !old.is_zero() {
9785 old.algo().hex_len()
9786 } else if !new.is_zero() {
9787 new.algo().hex_len()
9788 } else {
9789 old.algo().hex_len()
9790 };
9791 let render = |oid: &ObjectId| {
9792 if oid.is_zero() {
9793 "0".repeat(width)
9794 } else {
9795 oid.to_hex()
9796 }
9797 };
9798 (render(old), render(new))
9799}
9800
9801pub fn format_raw_abbrev(entry: &DiffEntry, abbrev_len: usize) -> String {
9803 let ellipsis = if crate::environment::Environment::empty()
9804 .var("GIT_PRINT_SHA1_ELLIPSIS")
9805 .as_deref()
9806 == Some("yes")
9807 {
9808 "..."
9809 } else {
9810 ""
9811 };
9812 let old_hex = format!("{}", entry.old_oid);
9813 let new_hex = format!("{}", entry.new_oid);
9814 let old_abbrev = &old_hex[..abbrev_len.min(old_hex.len())];
9815 let new_abbrev = &new_hex[..abbrev_len.min(new_hex.len())];
9816
9817 let path = match entry.status {
9819 DiffStatus::Renamed | DiffStatus::Copied => format!(
9820 "{}\t{}",
9821 entry.old_path.as_deref().unwrap_or(""),
9822 entry.new_path.as_deref().unwrap_or("")
9823 ),
9824 _ => entry.path().to_owned(),
9825 };
9826 let status_str = match (entry.status, entry.score) {
9827 (DiffStatus::Renamed, Some(s)) => format!("R{s:03}"),
9828 (DiffStatus::Copied, Some(s)) => format!("C{s:03}"),
9829 (DiffStatus::Modified, Some(s)) => format!("M{s:03}"),
9830 (DiffStatus::TypeChanged, Some(s)) => format!("T{s:03}"),
9831 _ => entry.status.letter().to_string(),
9832 };
9833
9834 format!(
9835 ":{} {} {}{} {}{} {}\t{}",
9836 entry.old_mode,
9837 entry.new_mode,
9838 old_abbrev,
9839 ellipsis,
9840 new_abbrev,
9841 ellipsis,
9842 status_str,
9843 path
9844 )
9845}
9846
9847pub fn unified_diff(
9862 old_content: &str,
9863 new_content: &str,
9864 old_path: &str,
9865 new_path: &str,
9866 context_lines: usize,
9867 indent_heuristic: bool,
9868 quote_path_fully: bool,
9869) -> String {
9870 unified_diff_with_prefix(
9871 old_content,
9872 new_content,
9873 old_path,
9874 new_path,
9875 context_lines,
9876 0,
9877 "a/",
9878 "b/",
9879 indent_heuristic,
9880 quote_path_fully,
9881 )
9882}
9883
9884#[allow(clippy::too_many_arguments)] pub fn unified_diff_with_prefix(
9890 old_content: &str,
9891 new_content: &str,
9892 old_path: &str,
9893 new_path: &str,
9894 context_lines: usize,
9895 inter_hunk_context: usize,
9896 src_prefix: &str,
9897 dst_prefix: &str,
9898 indent_heuristic: bool,
9899 quote_path_fully: bool,
9900) -> String {
9901 unified_diff_with_prefix_and_funcname(
9902 old_content,
9903 new_content,
9904 old_path,
9905 new_path,
9906 context_lines,
9907 inter_hunk_context,
9908 src_prefix,
9909 dst_prefix,
9910 None,
9911 indent_heuristic,
9912 quote_path_fully,
9913 )
9914}
9915
9916#[allow(clippy::too_many_arguments)]
9919pub fn unified_diff_with_prefix_and_funcname(
9920 old_content: &str,
9921 new_content: &str,
9922 old_path: &str,
9923 new_path: &str,
9924 context_lines: usize,
9925 inter_hunk_context: usize,
9926 src_prefix: &str,
9927 dst_prefix: &str,
9928 funcname_matcher: Option<&FuncnameMatcher>,
9929 indent_heuristic: bool,
9930 quote_path_fully: bool,
9931) -> String {
9932 unified_diff_with_prefix_and_funcname_and_algorithm(
9933 old_content,
9934 new_content,
9935 old_path,
9936 new_path,
9937 context_lines,
9938 inter_hunk_context,
9939 src_prefix,
9940 dst_prefix,
9941 funcname_matcher,
9942 similar::Algorithm::Myers,
9943 false,
9944 false,
9945 indent_heuristic,
9946 quote_path_fully,
9947 )
9948}
9949
9950#[allow(clippy::too_many_arguments)]
9956pub fn write_unified_diff_with_prefix_and_funcname_and_algorithm<W: std::io::Write>(
9957 out: &mut W,
9958 old_content: &str,
9959 new_content: &str,
9960 old_path: &str,
9961 new_path: &str,
9962 context_lines: usize,
9963 inter_hunk_context: usize,
9964 src_prefix: &str,
9965 dst_prefix: &str,
9966 funcname_matcher: Option<&FuncnameMatcher>,
9967 algorithm: similar::Algorithm,
9968 function_context: bool,
9969 use_git_histogram: bool,
9970 indent_heuristic: bool,
9971 quote_path_fully: bool,
9972) -> Result<()> {
9973 if function_context {
9974 let patch = unified_diff_with_function_context(
9975 old_content,
9976 new_content,
9977 old_path,
9978 new_path,
9979 context_lines,
9980 inter_hunk_context,
9981 src_prefix,
9982 dst_prefix,
9983 funcname_matcher,
9984 algorithm,
9985 indent_heuristic,
9986 quote_path_fully,
9987 );
9988 out.write_all(patch.as_bytes())?;
9989 return Ok(());
9990 }
9991
9992 if use_git_histogram {
9993 let patch = unified_diff_histogram_with_prefix_and_funcname_indent(
9994 old_content,
9995 new_content,
9996 old_path,
9997 new_path,
9998 context_lines,
9999 inter_hunk_context,
10000 src_prefix,
10001 dst_prefix,
10002 funcname_matcher,
10003 indent_heuristic,
10004 quote_path_fully,
10005 );
10006 out.write_all(patch.as_bytes())?;
10007 return Ok(());
10008 }
10009
10010 if algorithm == similar::Algorithm::Myers && !function_context {
10011 let patch = unified_diff_git_myers_with_prefix_and_funcname(
10012 old_content,
10013 new_content,
10014 old_path,
10015 new_path,
10016 context_lines,
10017 inter_hunk_context,
10018 src_prefix,
10019 dst_prefix,
10020 funcname_matcher,
10021 indent_heuristic,
10022 quote_path_fully,
10023 );
10024 out.write_all(patch.as_bytes())?;
10025 return Ok(());
10026 }
10027
10028 if algorithm == similar::Algorithm::Lcs && !function_context {
10029 let patch = unified_diff_git_minimal_with_prefix_and_funcname(
10030 old_content,
10031 new_content,
10032 old_path,
10033 new_path,
10034 context_lines,
10035 inter_hunk_context,
10036 src_prefix,
10037 dst_prefix,
10038 funcname_matcher,
10039 indent_heuristic,
10040 quote_path_fully,
10041 );
10042 out.write_all(patch.as_bytes())?;
10043 return Ok(());
10044 }
10045
10046 if algorithm == similar::Algorithm::Patience && !function_context {
10047 let patch = unified_diff_git_patience_with_prefix_and_funcname(
10048 old_content,
10049 new_content,
10050 old_path,
10051 new_path,
10052 context_lines,
10053 inter_hunk_context,
10054 src_prefix,
10055 dst_prefix,
10056 funcname_matcher,
10057 indent_heuristic,
10058 quote_path_fully,
10059 );
10060 out.write_all(patch.as_bytes())?;
10061 return Ok(());
10062 }
10063
10064 use crate::quote_path::format_diff_path_with_prefix;
10065 use similar::{udiff::UnifiedDiffHunk, TextDiff};
10066
10067 let diff = TextDiff::configure()
10068 .algorithm(algorithm)
10069 .diff_lines(old_content, new_content);
10070 let compacted_ops = diff_indent_heuristic::diff_lines_ops_compacted(
10071 old_content,
10072 new_content,
10073 algorithm,
10074 indent_heuristic,
10075 );
10076
10077 if old_path == "/dev/null" {
10078 out.write_all(b"--- /dev/null\n")?;
10079 } else if src_prefix.is_empty() {
10080 writeln!(out, "--- {old_path}")?;
10081 } else {
10082 out.write_all(b"--- ")?;
10083 out.write_all(
10084 format_diff_path_with_prefix(src_prefix, old_path, quote_path_fully).as_bytes(),
10085 )?;
10086 out.write_all(b"\n")?;
10087 }
10088 if new_path == "/dev/null" {
10089 out.write_all(b"+++ /dev/null\n")?;
10090 } else if dst_prefix.is_empty() {
10091 writeln!(out, "+++ {new_path}")?;
10092 } else {
10093 out.write_all(b"+++ ")?;
10094 out.write_all(
10095 format_diff_path_with_prefix(dst_prefix, new_path, quote_path_fully).as_bytes(),
10096 )?;
10097 out.write_all(b"\n")?;
10098 }
10099
10100 let old_lines: Vec<&str> = old_content.lines().collect();
10101 let max_common_gap = context_lines
10102 .saturating_mul(2)
10103 .saturating_add(inter_hunk_context);
10104 let op_groups = group_diff_ops_gap(compacted_ops, context_lines, max_common_gap);
10105
10106 let mut hunk_buf = String::new();
10107 let mut eof_annot = UnifiedDiffNoNewlineAnnotator::new(old_content, new_content);
10108 for ops in op_groups {
10109 if ops.is_empty() {
10110 continue;
10111 }
10112 let hunk = UnifiedDiffHunk::new(ops, &diff, true);
10113 hunk_buf.clear();
10114 use std::fmt::Write as FmtWrite;
10115 let _ = FmtWrite::write_fmt(&mut hunk_buf, format_args!("{hunk}"));
10116 let mut fragment = hunk_buf.clone();
10117 if let Some(first_newline) = hunk_buf.find('\n') {
10118 let header_line = &hunk_buf[..first_newline];
10119 let rest = &hunk_buf[first_newline..];
10120 if let Some(func_ctx) =
10121 extract_function_context(header_line, &old_lines, funcname_matcher, Some(old_path))
10122 {
10123 fragment.clear();
10124 fragment.push_str(header_line);
10125 fragment.push(' ');
10126 fragment.push_str(&func_ctx);
10127 fragment.push_str(rest);
10128 }
10129 }
10130 eof_annot.write_hunk(out, &fragment)?;
10131 }
10132 Ok(())
10133}
10134
10135#[allow(clippy::too_many_arguments)]
10141pub fn unified_diff_with_prefix_and_funcname_and_algorithm(
10142 old_content: &str,
10143 new_content: &str,
10144 old_path: &str,
10145 new_path: &str,
10146 context_lines: usize,
10147 inter_hunk_context: usize,
10148 src_prefix: &str,
10149 dst_prefix: &str,
10150 funcname_matcher: Option<&FuncnameMatcher>,
10151 algorithm: similar::Algorithm,
10152 function_context: bool,
10153 use_git_histogram: bool,
10154 indent_heuristic: bool,
10155 quote_path_fully: bool,
10156) -> String {
10157 let mut output = Vec::new();
10158 if write_unified_diff_with_prefix_and_funcname_and_algorithm(
10159 &mut output,
10160 old_content,
10161 new_content,
10162 old_path,
10163 new_path,
10164 context_lines,
10165 inter_hunk_context,
10166 src_prefix,
10167 dst_prefix,
10168 funcname_matcher,
10169 algorithm,
10170 function_context,
10171 use_git_histogram,
10172 indent_heuristic,
10173 quote_path_fully,
10174 )
10175 .is_err()
10176 {
10177 output.clear();
10178 }
10179 String::from_utf8_lossy(&output).into_owned()
10180}
10181
10182pub(crate) fn group_diff_ops_gap(
10188 mut ops: Vec<similar::DiffOp>,
10189 context: usize,
10190 max_common_gap: usize,
10191) -> Vec<Vec<similar::DiffOp>> {
10192 use similar::DiffOp;
10193 if ops.is_empty() {
10194 return vec![];
10195 }
10196
10197 let mut pending_group = Vec::new();
10198 let mut rv = Vec::new();
10199
10200 if let Some(DiffOp::Equal {
10201 old_index,
10202 new_index,
10203 len,
10204 }) = ops.first_mut()
10205 {
10206 let offset = (*len).saturating_sub(context);
10207 *old_index += offset;
10208 *new_index += offset;
10209 *len -= offset;
10210 }
10211
10212 if let Some(DiffOp::Equal { len, .. }) = ops.last_mut() {
10213 *len -= (*len).saturating_sub(context);
10214 }
10215
10216 for op in ops.into_iter() {
10217 if let DiffOp::Equal {
10218 old_index,
10219 new_index,
10220 len,
10221 } = op
10222 {
10223 if len > max_common_gap {
10226 pending_group.push(DiffOp::Equal {
10227 old_index,
10228 new_index,
10229 len: context,
10230 });
10231 rv.push(pending_group);
10232 let offset = len.saturating_sub(context);
10233 pending_group = vec![DiffOp::Equal {
10234 old_index: old_index + offset,
10235 new_index: new_index + offset,
10236 len: len - offset,
10237 }];
10238 continue;
10239 }
10240 }
10241 pending_group.push(op);
10242 }
10243
10244 match &pending_group[..] {
10245 &[] | &[similar::DiffOp::Equal { .. }] => {}
10246 _ => rv.push(pending_group),
10247 }
10248
10249 rv
10250}
10251#[expect(clippy::too_many_arguments)]
10252fn unified_diff_with_function_context(
10254 old_content: &str,
10255 new_content: &str,
10256 old_path: &str,
10257 new_path: &str,
10258 context_lines: usize,
10259 inter_hunk_context: usize,
10260 src_prefix: &str,
10261 dst_prefix: &str,
10262 funcname_matcher: Option<&FuncnameMatcher>,
10263 algorithm: similar::Algorithm,
10264 indent_heuristic: bool,
10265 quote_path_fully: bool,
10266) -> String {
10267 use crate::quote_path::format_diff_path_with_prefix;
10268 use similar::{udiff::UnifiedDiffHunk, TextDiff};
10269
10270 let diff = TextDiff::configure()
10271 .algorithm(algorithm)
10272 .diff_lines(old_content, new_content);
10273
10274 let old_lines: Vec<&str> = old_content.lines().collect();
10275 let new_lines: Vec<&str> = new_content.lines().collect();
10276 let n_old = old_lines.len();
10277 let n_new = new_lines.len();
10278
10279 let max_common_gap = context_lines
10286 .saturating_mul(2)
10287 .saturating_add(inter_hunk_context);
10288 let all_ops = diff.ops().to_vec();
10289 let op_groups = group_diff_ops_gap(all_ops.clone(), context_lines, max_common_gap);
10290
10291 let mut ranges: Vec<(usize, usize, usize, usize)> = Vec::new();
10292
10293 for ops in op_groups {
10294 if ops.is_empty() {
10295 continue;
10296 }
10297 let i1_anchor = func_context_old_anchor(&ops, n_old);
10298 let i1_end = hunk_old_change_end_exclusive(&ops);
10299 let skip_preimage_pull =
10300 append_with_whole_function_added(&ops, n_old, n_new, &new_lines, funcname_matcher);
10301 let hunk = UnifiedDiffHunk::new(ops, &diff, true);
10302 let hunk_str = format!("{hunk}");
10303 let header_line = hunk_str
10304 .lines()
10305 .next()
10306 .unwrap_or("")
10307 .trim_end_matches(['\r', '\n']);
10308 let Some((base_s1, _base_e1, _base_s2, _base_e2)) =
10309 parse_unified_hunk_header_ranges(header_line)
10310 else {
10311 continue;
10312 };
10313
10314 let ctx = context_lines;
10315 let (s1, e1, s2, e2) = if skip_preimage_pull {
10316 let s = n_old.saturating_sub(ctx);
10317 let s2 = map_old_line_to_new(&all_ops, s, n_new).min(n_new);
10318 (s, n_old, s2, n_new)
10319 } else {
10320 let mut s1 = base_s1.saturating_sub(ctx);
10321 let mut s2 = map_old_line_to_new(&all_ops, s1, n_new).min(n_new);
10322
10323 let base_pre_s1 = i1_anchor.saturating_sub(ctx);
10324 if base_pre_s1 < s1 {
10325 s1 = base_pre_s1;
10326 s2 = map_old_line_to_new(&all_ops, s1, n_new).min(n_new);
10327 }
10328
10329 let fs1 = expand_func_pre_start(s1, i1_anchor, n_old, &old_lines, funcname_matcher);
10330 if fs1 < s1 {
10331 s1 = fs1;
10332 s2 = map_old_line_to_new(&all_ops, s1, n_new).min(n_new);
10333 }
10334
10335 let mut e1 = (i1_end + ctx).min(n_old);
10341 let mut e2 = map_old_line_to_new(&all_ops, e1, n_new).min(n_new);
10342 let fe1 = expand_func_post_end(e1, i1_end, n_old, &old_lines, funcname_matcher);
10343 if fe1 > e1 {
10344 e1 = fe1;
10345 e2 = map_old_line_to_new(&all_ops, e1, n_new).min(n_new);
10346 }
10347 (s1, e1, s2, e2)
10348 };
10349
10350 ranges.push((s1, e1, s2, e2));
10351 }
10352
10353 ranges.sort_by_key(|r| (r.0, r.2));
10356 let mut merged: Vec<(usize, usize, usize, usize)> = Vec::with_capacity(ranges.len());
10357 for (s1, e1, s2, e2) in ranges {
10358 if let Some(last) = merged.last_mut() {
10359 if s1 < last.1 {
10360 last.1 = last.1.max(e1);
10361 last.3 = last.3.max(e2);
10362 continue;
10363 }
10364 }
10365 merged.push((s1, e1, s2, e2));
10366 }
10367 let ranges = merged;
10368
10369 let mut output = String::new();
10370 if old_path == "/dev/null" {
10371 output.push_str("--- /dev/null\n");
10372 } else if src_prefix.is_empty() {
10373 output.push_str(&format!("--- {old_path}\n"));
10374 } else {
10375 output.push_str("--- ");
10376 output.push_str(&format_diff_path_with_prefix(
10377 src_prefix,
10378 old_path,
10379 quote_path_fully,
10380 ));
10381 output.push('\n');
10382 }
10383 if new_path == "/dev/null" {
10384 output.push_str("+++ /dev/null\n");
10385 } else if dst_prefix.is_empty() {
10386 output.push_str(&format!("+++ {new_path}\n"));
10387 } else {
10388 output.push_str("+++ ");
10389 output.push_str(&format_diff_path_with_prefix(
10390 dst_prefix,
10391 new_path,
10392 quote_path_fully,
10393 ));
10394 output.push('\n');
10395 }
10396
10397 for (s1, e1, s2, e2) in ranges {
10398 if s1 >= e1 && s2 >= e2 {
10399 continue;
10400 }
10401 let old_seg =
10402 line_slice_for_diff_with_eof_nl(&old_lines, s1, e1, old_content.ends_with('\n'));
10403 let new_seg =
10404 line_slice_for_diff_with_eof_nl(&new_lines, s2, e2, new_content.ends_with('\n'));
10405 let inner_ctx = old_seg.lines().count().max(new_seg.lines().count()).max(1);
10406 let piece = unified_diff_with_prefix_and_funcname_and_algorithm(
10407 &old_seg,
10408 &new_seg,
10409 old_path,
10410 new_path,
10411 inner_ctx,
10412 0,
10413 src_prefix,
10414 dst_prefix,
10415 funcname_matcher,
10416 algorithm,
10417 false,
10418 false,
10419 indent_heuristic,
10420 quote_path_fully,
10421 );
10422 let shifted = shift_unified_hunk_headers_to_full_file(&piece, s1, s2);
10423 let with_func =
10424 enrich_unified_hunk_headers_funcname(&shifted, &old_lines, funcname_matcher);
10425 for line in with_func.lines() {
10426 if line.starts_with("--- ") || line.starts_with("+++ ") {
10427 continue;
10428 }
10429 output.push_str(line);
10430 output.push('\n');
10431 }
10432 }
10433
10434 output
10435}
10436
10437fn shift_unified_hunk_headers_to_full_file(
10442 patch: &str,
10443 delta_old: usize,
10444 delta_new: usize,
10445) -> String {
10446 if delta_old == 0 && delta_new == 0 {
10447 return patch.to_owned();
10448 }
10449 let mut out = String::with_capacity(patch.len());
10450 for line in patch.lines() {
10451 if let Some(shifted) = shift_one_unified_hunk_header(line, delta_old, delta_new) {
10452 out.push_str(&shifted);
10453 } else {
10454 out.push_str(line);
10455 }
10456 out.push('\n');
10457 }
10458 out
10459}
10460
10461fn shift_one_unified_hunk_header(line: &str, delta_old: usize, delta_new: usize) -> Option<String> {
10462 let rest = line.strip_prefix("@@ ")?;
10463 let (old_chunk, after_plus) = rest.split_once(" +")?;
10464 let old_spec = old_chunk.strip_prefix('-')?;
10465 let (new_spec, suffix) = after_plus.split_once(" @@")?;
10466 let shifted_old = shift_unified_range_spec(old_spec, delta_old)?;
10467 let shifted_new = shift_unified_range_spec(new_spec, delta_new)?;
10468 Some(format!("@@ -{shifted_old} +{shifted_new} @@{suffix}"))
10469}
10470
10471fn shift_unified_range_spec(spec: &str, delta: usize) -> Option<String> {
10472 let spec = spec.trim();
10473 if let Some((start_s, count_s)) = spec.split_once(',') {
10474 let start: usize = start_s.parse().ok()?;
10475 let count: usize = count_s.parse().ok()?;
10476 Some(format!("{},{}", start.saturating_add(delta), count))
10477 } else {
10478 let start: usize = spec.parse().ok()?;
10479 Some(format!("{}", start.saturating_add(delta)))
10480 }
10481}
10482
10483fn enrich_unified_hunk_headers_funcname(
10485 patch: &str,
10486 full_old_lines: &[&str],
10487 funcname_matcher: Option<&FuncnameMatcher>,
10488) -> String {
10489 let mut out = String::with_capacity(patch.len());
10490 for line in patch.lines() {
10491 if let Some(fixed) = enrich_one_hunk_header_funcname(line, full_old_lines, funcname_matcher)
10492 {
10493 out.push_str(&fixed);
10494 } else {
10495 out.push_str(line);
10496 }
10497 out.push('\n');
10498 }
10499 out
10500}
10501
10502fn enrich_one_hunk_header_funcname(
10503 line: &str,
10504 full_old_lines: &[&str],
10505 funcname_matcher: Option<&FuncnameMatcher>,
10506) -> Option<String> {
10507 let after_at = line.strip_prefix("@@ ")?;
10508 let idx = after_at.find(" @@")?;
10509 let mid = after_at[..idx].trim();
10510 let tail = after_at[idx + 3..].trim_start();
10511 let header_for_parse = format!("@@ {mid} @@");
10512 let func = extract_function_context(&header_for_parse, full_old_lines, funcname_matcher, None);
10513 Some(if let Some(f) = func {
10514 format!("@@ {mid} @@ {f}")
10515 } else if !tail.is_empty() {
10516 format!("@@ {mid} @@ {tail}")
10517 } else {
10518 format!("@@ {mid} @@")
10519 })
10520}
10521
10522fn line_slice_for_diff_with_eof_nl(
10523 lines: &[&str],
10524 start: usize,
10525 end: usize,
10526 full_file_ends_with_newline: bool,
10527) -> String {
10528 if start >= end {
10529 return String::new();
10530 }
10531 let mut s = lines[start..end].join("\n");
10532 let slice_is_suffix_of_file = end == lines.len();
10533 let need_trailing_nl = if slice_is_suffix_of_file {
10534 full_file_ends_with_newline
10535 } else {
10536 true
10537 };
10538 if need_trailing_nl && !s.ends_with('\n') {
10539 s.push('\n');
10540 }
10541 s
10542}
10543
10544fn map_old_line_to_new(ops: &[similar::DiffOp], old_line: usize, n_new: usize) -> usize {
10547 use similar::DiffOp;
10548 let mut n = 0usize;
10549 for op in ops {
10550 match *op {
10551 DiffOp::Equal {
10552 old_index,
10553 new_index,
10554 len,
10555 } => {
10556 if old_index + len <= old_line {
10557 n = new_index + len;
10558 continue;
10559 }
10560 if old_index < old_line {
10561 let take = old_line - old_index;
10562 return (new_index + take).min(n_new);
10563 }
10564 return new_index.min(n_new);
10565 }
10566 DiffOp::Delete {
10567 old_index,
10568 old_len,
10569 new_index,
10570 } => {
10571 if old_index + old_len <= old_line {
10572 n = new_index;
10573 continue;
10574 }
10575 if old_index < old_line {
10576 return new_index.min(n_new);
10577 }
10578 }
10579 DiffOp::Insert {
10580 old_index,
10581 new_index,
10582 new_len,
10583 } => {
10584 if old_index < old_line {
10585 n = new_index + new_len;
10586 continue;
10587 }
10588 if old_index == old_line {
10589 return (new_index + new_len).min(n_new);
10592 }
10593 return new_index.min(n_new);
10594 }
10595 DiffOp::Replace {
10596 old_index,
10597 old_len,
10598 new_index,
10599 new_len,
10600 } => {
10601 if old_index + old_len <= old_line {
10602 n = new_index + new_len;
10603 continue;
10604 }
10605 if old_index < old_line {
10606 let into_old = old_line - old_index;
10607 let mapped = new_index + into_old.min(new_len);
10608 return mapped.min(n_new);
10609 }
10610 return new_index.min(n_new);
10611 }
10612 }
10613 }
10614 n.min(n_new)
10615}
10616
10617fn parse_unified_hunk_header_ranges(header: &str) -> Option<(usize, usize, usize, usize)> {
10619 let rest = header.strip_prefix("@@ ")?;
10620 let (old_tok, rest2) = rest.split_once(" +")?;
10621 let old_tok = old_tok.strip_prefix('-')?;
10622 let new_tok = rest2.split_once(" @@").map(|(a, _)| a)?;
10623
10624 fn parse_side(spec: &str) -> Option<(usize, usize)> {
10625 let spec = spec.trim();
10626 let (start_one_based, count) = if let Some((a, b)) = spec.split_once(',') {
10627 (a.parse::<usize>().ok()?, b.parse::<usize>().ok()?)
10628 } else {
10629 let s = spec.parse::<usize>().ok()?;
10630 (s, 1usize)
10631 };
10632 let s0 = start_one_based.saturating_sub(1);
10633 let e0 = s0.saturating_add(count);
10634 Some((s0, e0))
10635 }
10636
10637 let (os, oe) = parse_side(old_tok)?;
10638 let (ns, ne) = parse_side(new_tok)?;
10639 Some((os, oe, ns, ne))
10640}
10641
10642fn append_with_whole_function_added(
10645 ops: &[similar::DiffOp],
10646 n_old: usize,
10647 n_new: usize,
10648 new_lines: &[&str],
10649 matcher: Option<&FuncnameMatcher>,
10650) -> bool {
10651 use similar::DiffOp;
10652 if n_old == 0 {
10653 return false;
10654 }
10655 let mut only_ins_or_eq = true;
10656 let mut min_new_ins = usize::MAX;
10657 for op in ops {
10658 match *op {
10659 DiffOp::Equal { .. } => {}
10660 DiffOp::Insert {
10661 new_index, new_len, ..
10662 } => {
10663 min_new_ins = min_new_ins.min(new_index);
10664 if new_len == 0 {
10665 only_ins_or_eq = false;
10666 }
10667 }
10668 DiffOp::Delete { .. } | DiffOp::Replace { .. } => {
10669 only_ins_or_eq = false;
10670 }
10671 }
10672 }
10673 let mut insert_at_eof = false;
10674 for op in ops {
10675 if let DiffOp::Insert { old_index, .. } = *op {
10676 if old_index == n_old {
10677 insert_at_eof = true;
10678 break;
10679 }
10680 }
10681 }
10682 let append_at_eof = min_new_ins == n_old || insert_at_eof;
10683 if !only_ins_or_eq || !append_at_eof || min_new_ins == usize::MAX {
10684 return false;
10685 }
10686 let mut j = min_new_ins;
10691 while j < n_new {
10692 let line = new_lines[j];
10693 if line.trim().is_empty() {
10694 j += 1;
10695 continue;
10696 }
10697 if let Some(m) = matcher {
10698 if m.match_line(line).is_some() {
10699 return true;
10700 }
10701 } else if inserted_block_starts_with_c_like_function_definition(line) {
10702 return true;
10703 }
10704 j += 1;
10705 }
10706 false
10707}
10708
10709fn inserted_block_starts_with_c_like_function_definition(line: &str) -> bool {
10710 let t = line.trim_start();
10711 let Some(open_paren) = t.find('(') else {
10712 return false;
10713 };
10714 let head = &t[..open_paren];
10715 let tokens: Vec<&str> = head.split_whitespace().collect();
10716 if tokens.len() < 2 {
10717 return false;
10719 }
10720 let nameish = tokens.last().copied().unwrap_or("");
10721 let name = nameish.trim_end_matches(['*', '&']);
10722 if name.is_empty() || !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
10723 return false;
10724 }
10725 let type_or_modifier = |tok: &str| {
10726 matches!(
10727 tok,
10728 "static"
10729 | "extern"
10730 | "inline"
10731 | "void"
10732 | "int"
10733 | "char"
10734 | "short"
10735 | "long"
10736 | "float"
10737 | "double"
10738 | "unsigned"
10739 | "signed"
10740 | "struct"
10741 | "enum"
10742 | "union"
10743 | "const"
10744 | "volatile"
10745 | "typedef"
10746 )
10747 };
10748 tokens[..tokens.len() - 1]
10749 .iter()
10750 .any(|tok| type_or_modifier(tok))
10751}
10752
10753fn hunk_old_change_end_exclusive(ops: &[similar::DiffOp]) -> usize {
10754 use similar::DiffOp;
10755 let mut max_o = 0usize;
10756 for op in ops {
10757 match *op {
10758 DiffOp::Delete {
10759 old_index, old_len, ..
10760 } => {
10761 max_o = max_o.max(old_index + old_len);
10762 }
10763 DiffOp::Replace {
10764 old_index, old_len, ..
10765 } => {
10766 max_o = max_o.max(old_index + old_len);
10767 }
10768 DiffOp::Insert { old_index, .. } => {
10769 max_o = max_o.max(old_index);
10772 }
10773 DiffOp::Equal { .. } => {}
10774 }
10775 }
10776 max_o
10777}
10778
10779fn func_context_old_anchor(ops: &[similar::DiffOp], n_old: usize) -> usize {
10780 use similar::DiffOp;
10781 let mut has_delete_or_replace = false;
10782 let mut min_del = usize::MAX;
10783 let mut min_ins_old = usize::MAX;
10784
10785 for op in ops {
10786 match *op {
10787 DiffOp::Delete {
10788 old_index, old_len, ..
10789 } => {
10790 has_delete_or_replace = true;
10791 min_del = min_del.min(old_index);
10792 min_del = min_del.min(old_index + old_len.saturating_sub(1));
10793 }
10794 DiffOp::Replace {
10795 old_index, old_len, ..
10796 } => {
10797 has_delete_or_replace = true;
10798 min_del = min_del.min(old_index);
10799 min_del = min_del.min(old_index + old_len.saturating_sub(1));
10800 }
10801 DiffOp::Insert { old_index, .. } => {
10802 min_ins_old = min_ins_old.min(old_index);
10803 }
10804 DiffOp::Equal { .. } => {}
10805 }
10806 }
10807
10808 let mut i1 = if has_delete_or_replace {
10809 min_del
10810 } else if min_ins_old != usize::MAX {
10811 min_ins_old
10812 } else {
10813 0
10814 };
10815
10816 let pure_insert = ops
10817 .iter()
10818 .all(|op| matches!(op, DiffOp::Insert { .. } | DiffOp::Equal { .. }))
10819 && ops.iter().any(|op| matches!(op, DiffOp::Insert { .. }));
10820
10821 if pure_insert && i1 >= n_old && n_old > 0 {
10822 i1 = n_old - 1;
10823 }
10824
10825 i1.min(n_old.saturating_sub(1))
10826}
10827
10828fn expand_func_pre_start(
10829 s1: usize,
10830 i1: usize,
10831 n_old: usize,
10832 old_lines: &[&str],
10833 matcher: Option<&FuncnameMatcher>,
10834) -> usize {
10835 if n_old == 0 {
10836 return s1;
10837 }
10838 let i1 = i1.min(n_old.saturating_sub(1));
10839 let mut fs1 = get_func_line_backward(old_lines, i1, matcher).unwrap_or(i1);
10840 while fs1 > 0
10841 && !is_line_empty_for_func_context(old_lines[fs1 - 1])
10842 && !is_func_line(old_lines[fs1 - 1], matcher)
10843 {
10844 fs1 -= 1;
10845 }
10846 s1.min(fs1)
10847}
10848
10849fn expand_func_post_end(
10850 e1: usize,
10851 i1_end: usize,
10852 n_old: usize,
10853 old_lines: &[&str],
10854 matcher: Option<&FuncnameMatcher>,
10855) -> usize {
10856 let from = i1_end.min(n_old);
10857 let fe1 = get_func_line_forward(old_lines, from, matcher).unwrap_or(n_old);
10858 let mut fe1_adj = fe1;
10859 while fe1_adj > 0 && is_line_empty_for_func_context(old_lines[fe1_adj - 1]) {
10860 fe1_adj -= 1;
10861 }
10862 e1.max(fe1_adj).min(n_old)
10863}
10864
10865fn is_line_empty_for_func_context(line: &str) -> bool {
10866 line.chars().all(|c| c.is_whitespace())
10867}
10868
10869pub(crate) fn is_func_line(line: &str, matcher: Option<&FuncnameMatcher>) -> bool {
10870 if let Some(m) = matcher {
10871 return m.match_line(line).is_some();
10872 }
10873 let t = line.trim_end_matches(['\n', '\r']);
10874 if t.is_empty() {
10875 return false;
10876 }
10877 let b = t.as_bytes()[0];
10878 if !(b.is_ascii_alphabetic() || b == b'_' || b == b'$') {
10879 return false;
10880 }
10881 true
10882}
10883
10884fn get_func_line_backward(
10885 old_lines: &[&str],
10886 start: usize,
10887 matcher: Option<&FuncnameMatcher>,
10888) -> Option<usize> {
10889 let mut l = start.min(old_lines.len().saturating_sub(1));
10890 if old_lines.is_empty() {
10891 return None;
10892 }
10893 loop {
10894 if is_func_line(old_lines[l], matcher) {
10895 return Some(l);
10896 }
10897 if l == 0 {
10898 break;
10899 }
10900 l -= 1;
10901 }
10902 None
10903}
10904
10905fn get_func_line_forward(
10906 old_lines: &[&str],
10907 start: usize,
10908 matcher: Option<&FuncnameMatcher>,
10909) -> Option<usize> {
10910 let mut l = start;
10911 while l < old_lines.len() {
10912 if is_func_line(old_lines[l], matcher) {
10913 return Some(l);
10914 }
10915 l += 1;
10916 }
10917 None
10918}
10919#[expect(clippy::too_many_arguments)]
10920pub fn anchored_unified_diff(
10930 old_content: &str,
10931 new_content: &str,
10932 old_path: &str,
10933 new_path: &str,
10934 context_lines: usize,
10935 anchors: &[String],
10936 algorithm: similar::Algorithm,
10937 use_git_histogram: bool,
10938 indent_heuristic: bool,
10939 quote_path_fully: bool,
10940) -> String {
10941 use crate::quote_path::format_diff_path_with_prefix;
10942 use similar::TextDiff;
10943
10944 let old_lines: Vec<&str> = old_content.lines().collect();
10945 let new_lines: Vec<&str> = new_content.lines().collect();
10946
10947 let mut anchor_pairs: Vec<(usize, usize)> = Vec::new(); for anchor in anchors {
10951 let anchor_str = anchor.as_str();
10952
10953 let old_positions: Vec<usize> = old_lines
10955 .iter()
10956 .enumerate()
10957 .filter(|(_, l)| l.trim_end() == anchor_str)
10958 .map(|(i, _)| i)
10959 .collect();
10960
10961 let new_positions: Vec<usize> = new_lines
10963 .iter()
10964 .enumerate()
10965 .filter(|(_, l)| l.trim_end() == anchor_str)
10966 .map(|(i, _)| i)
10967 .collect();
10968
10969 if old_positions.len() == 1 && new_positions.len() == 1 {
10971 anchor_pairs.push((old_positions[0], new_positions[0]));
10972 }
10973 }
10974
10975 if anchor_pairs.is_empty() {
10977 return unified_diff_with_prefix_and_funcname_and_algorithm(
10978 old_content,
10979 new_content,
10980 old_path,
10981 new_path,
10982 context_lines,
10983 0,
10984 "a/",
10985 "b/",
10986 None,
10987 algorithm,
10988 false,
10989 use_git_histogram,
10990 indent_heuristic,
10991 quote_path_fully,
10992 );
10993 }
10994
10995 anchor_pairs.sort_by_key(|&(old_idx, _)| old_idx);
10997
10998 let mut filtered: Vec<(usize, usize)> = Vec::new();
11001 for &pair in &anchor_pairs {
11002 if filtered.is_empty() || filtered.last().is_some_and(|last| pair.1 > last.1) {
11003 filtered.push(pair);
11004 }
11005 }
11006 let anchor_pairs = filtered;
11007
11008 struct LineDiffOp {
11017 tag: char, line: String,
11019 }
11020
11021 let append_segment_diff =
11022 |ops: &mut Vec<LineDiffOp>, old_seg_input: &str, new_seg_input: &str| {
11023 use similar::ChangeTag;
11024 let old_ls: Vec<&str> = old_seg_input.lines().collect();
11025 let new_ls: Vec<&str> = new_seg_input.lines().collect();
11026 if old_ls.is_empty() && new_ls.is_empty() {
11027 return;
11028 }
11029 let seg_diff = TextDiff::configure()
11030 .algorithm(algorithm)
11031 .diff_slices(&old_ls, &new_ls);
11032 let raw = seg_diff.ops().to_vec();
11033 let compacted = diff_indent_heuristic::apply_change_compact_to_ops(
11034 &raw,
11035 &old_ls,
11036 &new_ls,
11037 indent_heuristic,
11038 );
11039 for op in &compacted {
11040 for ch in op.iter_changes(&old_ls, &new_ls) {
11041 let t = match ch.tag() {
11042 ChangeTag::Equal => ' ',
11043 ChangeTag::Delete => '-',
11044 ChangeTag::Insert => '+',
11045 };
11046 ops.push(LineDiffOp {
11047 tag: t,
11048 line: ch.value().to_string(),
11049 });
11050 }
11051 }
11052 };
11053
11054 let mut ops: Vec<LineDiffOp> = Vec::new();
11055 let mut old_pos = 0usize;
11056 let mut new_pos = 0usize;
11057
11058 for &(old_anchor, new_anchor) in &anchor_pairs {
11059 let old_segment: Vec<&str> = old_lines[old_pos..old_anchor].to_vec();
11061 let new_segment: Vec<&str> = new_lines[new_pos..new_anchor].to_vec();
11062
11063 let old_seg_text = old_segment.join("\n");
11064 let new_seg_text = new_segment.join("\n");
11065
11066 if !old_seg_text.is_empty() || !new_seg_text.is_empty() {
11067 let old_seg_input = if old_seg_text.is_empty() {
11068 String::new()
11069 } else {
11070 format!("{}\n", old_seg_text)
11071 };
11072 let new_seg_input = if new_seg_text.is_empty() {
11073 String::new()
11074 } else {
11075 format!("{}\n", new_seg_text)
11076 };
11077 append_segment_diff(&mut ops, &old_seg_input, &new_seg_input);
11078 }
11079
11080 ops.push(LineDiffOp {
11082 tag: ' ',
11083 line: old_lines[old_anchor].to_string(),
11084 });
11085
11086 old_pos = old_anchor + 1;
11087 new_pos = new_anchor + 1;
11088 }
11089
11090 let old_segment: Vec<&str> = old_lines[old_pos..].to_vec();
11092 let new_segment: Vec<&str> = new_lines[new_pos..].to_vec();
11093 let old_seg_text = old_segment.join("\n");
11094 let new_seg_text = new_segment.join("\n");
11095
11096 if !old_seg_text.is_empty() || !new_seg_text.is_empty() {
11097 let old_seg_input = if old_seg_text.is_empty() {
11098 String::new()
11099 } else {
11100 format!("{}\n", old_seg_text)
11101 };
11102 let new_seg_input = if new_seg_text.is_empty() {
11103 String::new()
11104 } else {
11105 format!("{}\n", new_seg_text)
11106 };
11107 append_segment_diff(&mut ops, &old_seg_input, &new_seg_input);
11108 }
11109
11110 let mut output = String::new();
11112 if old_path == "/dev/null" {
11113 output.push_str("--- /dev/null\n");
11114 } else {
11115 output.push_str("--- ");
11116 output.push_str(&format_diff_path_with_prefix(
11117 "a/",
11118 old_path,
11119 quote_path_fully,
11120 ));
11121 output.push('\n');
11122 }
11123 if new_path == "/dev/null" {
11124 output.push_str("+++ /dev/null\n");
11125 } else {
11126 output.push_str("+++ ");
11127 output.push_str(&format_diff_path_with_prefix(
11128 "b/",
11129 new_path,
11130 quote_path_fully,
11131 ));
11132 output.push('\n');
11133 }
11134
11135 let total_ops = ops.len();
11137 if total_ops == 0 {
11138 return output;
11139 }
11140
11141 let mut hunks: Vec<(usize, usize)> = Vec::new(); let mut i = 0;
11144 while i < total_ops {
11145 if ops[i].tag != ' ' {
11146 let start = i.saturating_sub(context_lines);
11147 let mut end = i;
11148 while end < total_ops {
11150 if ops[end].tag != ' ' {
11151 end += 1;
11152 continue;
11153 }
11154 let mut next_change = end;
11156 while next_change < total_ops && ops[next_change].tag == ' ' {
11157 next_change += 1;
11158 }
11159 if next_change < total_ops && next_change - end <= context_lines * 2 {
11160 end = next_change + 1;
11161 } else {
11162 end = (end + context_lines).min(total_ops);
11163 break;
11164 }
11165 }
11166 if let Some(last) = hunks.last_mut() {
11168 if start <= last.1 {
11169 last.1 = end;
11170 } else {
11171 hunks.push((start, end));
11172 }
11173 } else {
11174 hunks.push((start, end));
11175 }
11176 i = end;
11177 } else {
11178 i += 1;
11179 }
11180 }
11181
11182 for (start, end) in hunks {
11184 let mut old_start = 1usize;
11186 let mut new_start = 1usize;
11187 for op in &ops[..start] {
11189 match op.tag {
11190 ' ' => {
11191 old_start += 1;
11192 new_start += 1;
11193 }
11194 '-' => {
11195 old_start += 1;
11196 }
11197 '+' => {
11198 new_start += 1;
11199 }
11200 _ => {}
11201 }
11202 }
11203 let mut old_count = 0usize;
11204 let mut new_count = 0usize;
11205 for op in &ops[start..end] {
11206 match op.tag {
11207 ' ' => {
11208 old_count += 1;
11209 new_count += 1;
11210 }
11211 '-' => {
11212 old_count += 1;
11213 }
11214 '+' => {
11215 new_count += 1;
11216 }
11217 _ => {}
11218 }
11219 }
11220
11221 output.push_str(&format!(
11222 "@@ -{},{} +{},{} @@\n",
11223 old_start, old_count, new_start, new_count
11224 ));
11225 for op in &ops[start..end] {
11226 output.push(op.tag);
11227 output.push_str(&op.line);
11228 output.push('\n');
11229 }
11230 }
11231
11232 output
11233}
11234
11235fn func_context_line_matches(line: &str, funcname_matcher: Option<&FuncnameMatcher>) -> bool {
11241 if let Some(m) = funcname_matcher {
11242 return m.match_line(line).is_some();
11243 }
11244 is_func_line(line, None)
11245}
11246
11247fn extract_function_context(
11248 header: &str,
11249 old_lines: &[&str],
11250 funcname_matcher: Option<&FuncnameMatcher>,
11251 _source_path: Option<&str>,
11252) -> Option<String> {
11253 let at_pos = header.find("-")?;
11255 let rest = &header[at_pos + 1..];
11256 let comma_or_space = rest.find([',', ' '])?;
11257 let start_str = &rest[..comma_or_space];
11258 let start_line: usize = start_str.parse().ok()?;
11259
11260 let old_token_end = rest.find([' ', '\t']).unwrap_or(rest.len());
11264 let old_token = &rest[..old_token_end];
11265 let old_count: usize = if let Some(comma) = old_token.find(',') {
11266 old_token[comma + 1..].parse().unwrap_or(1)
11267 } else {
11268 1
11269 };
11270
11271 if start_line == 0 {
11272 return None;
11273 }
11274
11275 if start_line == 0 {
11282 return None;
11283 }
11284 let search_end = if old_count == 0 {
11285 start_line.min(old_lines.len())
11286 } else {
11287 if start_line <= 1 {
11288 return None;
11289 }
11290 (start_line - 1).min(old_lines.len())
11291 };
11292 let truncate = |line: &str| {
11293 let mut end = line.len().min(80);
11294 while end > 0 && !line.is_char_boundary(end) {
11295 end -= 1;
11296 }
11297 let mut out = line[..end].to_owned();
11298 let tail = line[end..].trim_start();
11299 if tail.is_empty()
11304 || (!tail.starts_with('"')
11305 && !tail.starts_with('\'')
11306 && !line.starts_with("msgstr")
11307 && !line.starts_with("msgid"))
11308 {
11309 out = out.trim_end().to_owned();
11310 }
11311 out
11312 };
11313
11314 for i in (0..search_end).rev() {
11315 let line = old_lines[i];
11316 if line.is_empty() {
11317 continue;
11318 }
11319 if func_context_line_matches(line, funcname_matcher) {
11320 if let Some(matcher) = funcname_matcher {
11321 if let Some(matched) = matcher.match_line(line) {
11322 return Some(truncate(&matched));
11323 }
11324 continue;
11325 }
11326 return Some(truncate(line));
11327 }
11328 }
11329 None
11330}
11331
11332fn last_line_index_without_trailing_newline(text: &str) -> Option<usize> {
11333 LineSide::from_text(text).incomplete_last_line_index()
11334}
11335
11336fn parse_unified_hunk_starts(header: &str) -> Option<(usize, usize)> {
11337 let rest = header.strip_prefix("@@ ")?;
11338 let (old_part, new_part) = rest.split_once(" +")?;
11339 let old_start = parse_unified_range_start(old_part.strip_prefix('-')?)?;
11340 let new_start = parse_unified_range_start(new_part.split(" @@").next()?)?;
11341 Some((old_start, new_start))
11342}
11343
11344fn parse_unified_range_start(spec: &str) -> Option<usize> {
11345 let spec = spec.trim();
11346 if let Some((start, _)) = spec.split_once(',') {
11347 start.parse().ok()
11348 } else {
11349 spec.parse().ok()
11350 }
11351}
11352
11353struct UnifiedDiffNoNewlineAnnotator {
11355 old_last: Option<usize>,
11356 new_last: Option<usize>,
11357 old_line: usize,
11358 new_line: usize,
11359 passthrough: bool,
11360}
11361
11362impl UnifiedDiffNoNewlineAnnotator {
11363 fn new(old: &str, new: &str) -> Self {
11364 let old_last = last_line_index_without_trailing_newline(old);
11365 let new_last = last_line_index_without_trailing_newline(new);
11366 Self {
11367 old_last,
11368 new_last,
11369 old_line: 1,
11370 new_line: 1,
11371 passthrough: old_last.is_none() && new_last.is_none(),
11372 }
11373 }
11374
11375 fn write_hunk<W: std::io::Write>(&mut self, out: &mut W, patch: &str) -> Result<()> {
11376 if self.passthrough {
11377 out.write_all(patch.as_bytes())?;
11378 return Ok(());
11379 }
11380 let mut lines = patch.lines().peekable();
11381 while let Some(line) = lines.next() {
11382 self.write_annotated_line(out, line, &mut lines)?;
11383 }
11384 Ok(())
11385 }
11386
11387 fn write_annotated_line<W: std::io::Write>(
11388 &mut self,
11389 out: &mut W,
11390 line: &str,
11391 lines: &mut std::iter::Peekable<std::str::Lines<'_>>,
11392 ) -> Result<()> {
11393 if let Some((o, n)) = line
11394 .strip_prefix("@@ ")
11395 .and_then(|_| parse_unified_hunk_starts(line))
11396 {
11397 self.old_line = o;
11398 self.new_line = n;
11399 writeln!(out, "{line}")?;
11400 return Ok(());
11401 }
11402 if line.starts_with("--- ")
11403 || line.starts_with("+++ ")
11404 || line.starts_with("diff --git ")
11405 || line.starts_with("diff --cc ")
11406 || line.starts_with("diff --combined ")
11407 {
11408 writeln!(out, "{line}")?;
11409 return Ok(());
11410 }
11411 if line.starts_with('-') {
11412 writeln!(out, "{line}")?;
11413 let already_marked = lines
11414 .peek()
11415 .is_some_and(|next| *next == "\\ No newline at end of file");
11416 if !already_marked && self.old_last == Some(self.old_line.saturating_sub(1)) {
11417 writeln!(out, "\\ No newline at end of file")?;
11418 }
11419 self.old_line += 1;
11420 return Ok(());
11421 }
11422 if line.starts_with('+') {
11423 writeln!(out, "{line}")?;
11424 let already_marked = lines
11425 .peek()
11426 .is_some_and(|next| *next == "\\ No newline at end of file");
11427 if !already_marked && self.new_last == Some(self.new_line.saturating_sub(1)) {
11428 writeln!(out, "\\ No newline at end of file")?;
11429 }
11430 self.new_line += 1;
11431 return Ok(());
11432 }
11433 if line.starts_with(' ') {
11434 writeln!(out, "{line}")?;
11435 let already_marked = lines
11436 .peek()
11437 .is_some_and(|next| *next == "\\ No newline at end of file");
11438 if !already_marked {
11439 let old_idx = self.old_line.saturating_sub(1);
11440 let new_idx = self.new_line.saturating_sub(1);
11441 if self.old_last == Some(old_idx) || self.new_last == Some(new_idx) {
11442 writeln!(out, "\\ No newline at end of file")?;
11443 }
11444 }
11445 self.old_line += 1;
11446 self.new_line += 1;
11447 return Ok(());
11448 }
11449 writeln!(out, "{line}")?;
11450 Ok(())
11451 }
11452}
11453
11454#[must_use]
11457pub fn annotate_unified_diff_no_newline_at_eof(patch: &str, old: &str, new: &str) -> String {
11458 let mut annotator = UnifiedDiffNoNewlineAnnotator::new(old, new);
11459 if annotator.passthrough {
11460 return patch.to_string();
11461 }
11462 let mut out = Vec::with_capacity(patch.len() + 64);
11463 let mut lines = patch.lines().peekable();
11464 while let Some(line) = lines.next() {
11465 if annotator
11466 .write_annotated_line(&mut out, line, &mut lines)
11467 .is_err()
11468 {
11469 return patch.to_string();
11470 }
11471 }
11472 String::from_utf8(out).unwrap_or_else(|_| patch.to_string())
11473}
11474
11475#[must_use]
11477pub fn normalize_ignore_space_change_line(line: &str) -> String {
11478 let mut result = String::with_capacity(line.len());
11479 let mut in_space = false;
11480 for c in line.chars() {
11481 if c.is_whitespace() {
11482 if !in_space {
11483 result.push(' ');
11484 in_space = true;
11485 }
11486 } else {
11487 result.push(c);
11488 in_space = false;
11489 }
11490 }
11491 while result.ends_with(' ') {
11492 result.pop();
11493 }
11494 result
11495}
11496
11497#[must_use]
11503pub fn normalize_ignore_space_change(content: &str) -> String {
11504 content
11505 .lines()
11506 .map(normalize_ignore_space_change_line)
11507 .collect::<Vec<_>>()
11508 .join("\n")
11509}
11510
11511#[must_use]
11515pub fn count_changes_with_algorithm(
11516 old_content: &str,
11517 new_content: &str,
11518 algorithm: similar::Algorithm,
11519 use_git_histogram: bool,
11520) -> (usize, usize) {
11521 use crate::diff::options::{DiffAlgorithm, DiffOptions};
11522 let diff_algorithm = if use_git_histogram {
11523 DiffAlgorithm::Histogram
11524 } else {
11525 match algorithm {
11526 similar::Algorithm::Myers => DiffAlgorithm::Myers,
11527 similar::Algorithm::Lcs => DiffAlgorithm::Minimal,
11528 similar::Algorithm::Patience => DiffAlgorithm::Patience,
11529 _ => DiffAlgorithm::Myers,
11530 }
11531 };
11532 count_line_changes_with_options(
11533 old_content,
11534 new_content,
11535 &DiffOptions {
11536 algorithm: diff_algorithm,
11537 ..DiffOptions::default()
11538 },
11539 )
11540}
11541
11542#[must_use]
11544pub fn count_changes_git_histogram(old_content: &str, new_content: &str) -> (usize, usize) {
11545 count_changes_with_algorithm(old_content, new_content, similar::Algorithm::Myers, true)
11546}
11547
11548#[must_use]
11553pub fn count_git_lines(data: &[u8]) -> usize {
11554 if data.is_empty() {
11555 return 0;
11556 }
11557 let mut count = 0usize;
11558 let mut nl_just_seen = false;
11559 for &ch in data {
11560 if ch == b'\n' {
11561 count += 1;
11562 nl_just_seen = true;
11563 } else {
11564 nl_just_seen = false;
11565 }
11566 }
11567 if !nl_just_seen {
11568 count += 1;
11569 }
11570 count
11571}
11572
11573pub const GIT_DIFF_MAX_SCORE: u64 = 60_000;
11575const DIFF_MAX_SCORE: u64 = GIT_DIFF_MAX_SCORE;
11576const DIFF_MINIMUM_BREAK_SIZE: usize = 400;
11577const DIFF_DEFAULT_BREAK_SCORE: u64 = 30_000;
11578pub const GIT_DIFF_DEFAULT_BREAK_SCORE: u64 = DIFF_DEFAULT_BREAK_SCORE;
11580const DIFF_HASHBASE: u32 = 107_927;
11581
11582#[derive(Clone, Copy, Default)]
11583struct SpanSlot {
11584 hashval: u32,
11585 cnt: u32,
11586}
11587
11588struct SpanHashTop {
11589 alloc_log2: u8,
11590 free_slots: i32,
11591 data: Vec<SpanSlot>,
11592}
11593
11594impl SpanHashTop {
11595 fn new(initial_log2: u8) -> Self {
11596 let cap = 1usize << initial_log2;
11597 Self {
11598 alloc_log2: initial_log2,
11599 free_slots: initial_free(initial_log2),
11600 data: vec![SpanSlot::default(); cap],
11601 }
11602 }
11603
11604 fn len(&self) -> usize {
11605 1usize << self.alloc_log2
11606 }
11607
11608 fn add_span(&mut self, hashval: u32, cnt: u32) {
11609 loop {
11610 let lim = self.len();
11611 let mut bucket = (hashval as usize) & (lim - 1);
11612 loop {
11613 let h = &mut self.data[bucket];
11614 if h.cnt == 0 {
11615 h.hashval = hashval;
11616 h.cnt = cnt;
11617 self.free_slots -= 1;
11618 if self.free_slots < 0 {
11619 self.rehash();
11620 break;
11621 }
11622 return;
11623 }
11624 if h.hashval == hashval {
11625 h.cnt = h.cnt.saturating_add(cnt);
11626 return;
11627 }
11628 bucket += 1;
11629 if bucket >= lim {
11630 bucket = 0;
11631 }
11632 }
11633 }
11634 }
11635
11636 fn rehash(&mut self) {
11637 let old = std::mem::take(&mut self.data);
11638 let old_log = self.alloc_log2;
11639 self.alloc_log2 = old_log.saturating_add(1);
11640 let new_len = 1usize << self.alloc_log2;
11641 self.free_slots = initial_free(self.alloc_log2);
11642 self.data = vec![SpanSlot::default(); new_len];
11643 let old_sz = 1usize << old_log;
11644 for o in old.iter().take(old_sz) {
11645 let o = *o;
11646 if o.cnt == 0 {
11647 continue;
11648 }
11649 self.add_span_after_rehash(o.hashval, o.cnt);
11650 }
11651 }
11652
11653 fn add_span_after_rehash(&mut self, hashval: u32, cnt: u32) {
11654 loop {
11655 let lim = self.len();
11656 let mut bucket = (hashval as usize) & (lim - 1);
11657 loop {
11658 let h = &mut self.data[bucket];
11659 if h.cnt == 0 {
11660 h.hashval = hashval;
11661 h.cnt = cnt;
11662 self.free_slots -= 1;
11663 if self.free_slots < 0 {
11664 self.rehash();
11665 break;
11666 }
11667 return;
11668 }
11669 if h.hashval == hashval {
11670 h.cnt = h.cnt.saturating_add(cnt);
11671 return;
11672 }
11673 bucket += 1;
11674 if bucket >= lim {
11675 bucket = 0;
11676 }
11677 }
11678 }
11679 }
11680
11681 fn sort_by_hashval(&mut self) {
11682 let sz = self.len();
11683 self.data[..sz].sort_by(|a, b| {
11684 if a.cnt == 0 {
11685 return std::cmp::Ordering::Greater;
11686 }
11687 if b.cnt == 0 {
11688 return std::cmp::Ordering::Less;
11689 }
11690 a.hashval.cmp(&b.hashval)
11691 });
11692 }
11693}
11694
11695fn initial_free(sz_log2: u8) -> i32 {
11696 let sz = sz_log2 as i32;
11697 ((1i32 << sz_log2) * (sz - 3) / sz).max(0)
11698}
11699
11700fn hash_blob_spans(buf: &[u8], is_text: bool) -> SpanHashTop {
11701 let mut hash = SpanHashTop::new(9);
11702 let mut n = 0u32;
11703 let mut accum1: u32 = 0;
11704 let mut accum2: u32 = 0;
11705 let mut i = 0usize;
11706 while i < buf.len() {
11707 let c = buf[i] as u32;
11708 let old_1 = accum1;
11709 i += 1;
11710
11711 if is_text && c == b'\r' as u32 && i < buf.len() && buf[i] == b'\n' {
11712 continue;
11713 }
11714
11715 accum1 = accum1.wrapping_shl(7) ^ accum2.wrapping_shr(25);
11716 accum2 = accum2.wrapping_shl(7) ^ old_1.wrapping_shr(25);
11717 accum1 = accum1.wrapping_add(c);
11718 n += 1;
11719 if n < 64 && c != b'\n' as u32 {
11720 continue;
11721 }
11722 let hashval = (accum1.wrapping_add(accum2.wrapping_mul(0x61))) % DIFF_HASHBASE;
11723 hash.add_span(hashval, n);
11724 n = 0;
11725 accum1 = 0;
11726 accum2 = 0;
11727 }
11728 if n > 0 {
11729 let hashval = (accum1.wrapping_add(accum2.wrapping_mul(0x61))) % DIFF_HASHBASE;
11730 hash.add_span(hashval, n);
11731 }
11732 hash.sort_by_hashval();
11733 hash
11734}
11735
11736#[must_use]
11741pub fn diffcore_count_changes(old: &[u8], new: &[u8]) -> (u64, u64) {
11742 crate::trace::trace_span!("grit.diff.blob");
11743 let src_is_text = !crate::merge_file::is_binary(old);
11744 let dst_is_text = !crate::merge_file::is_binary(new);
11745 let src_count = hash_blob_spans(old, src_is_text);
11746 let dst_count = hash_blob_spans(new, dst_is_text);
11747 let mut sc: u64 = 0;
11748 let mut la: u64 = 0;
11749 let mut si = 0usize;
11750 let mut di = 0usize;
11751 let src_len = src_count.len();
11752 let dst_len = dst_count.len();
11753 loop {
11754 if si >= src_len || src_count.data[si].cnt == 0 {
11755 break;
11756 }
11757 let s_hash = src_count.data[si].hashval;
11758 let s_cnt = u64::from(src_count.data[si].cnt);
11759 while di < dst_len && dst_count.data[di].cnt != 0 && dst_count.data[di].hashval < s_hash {
11760 la += u64::from(dst_count.data[di].cnt);
11761 di += 1;
11762 }
11763 let mut dst_cnt = 0u64;
11764 if di < dst_len && dst_count.data[di].cnt != 0 && dst_count.data[di].hashval == s_hash {
11765 dst_cnt = u64::from(dst_count.data[di].cnt);
11766 di += 1;
11767 }
11768 if s_cnt < dst_cnt {
11769 la += dst_cnt - s_cnt;
11770 sc += s_cnt;
11771 } else {
11772 sc += dst_cnt;
11773 }
11774 si += 1;
11775 }
11776 while di < dst_len && dst_count.data[di].cnt != 0 {
11777 la += u64::from(dst_count.data[di].cnt);
11778 di += 1;
11779 }
11780 (sc, la)
11781}
11782
11783#[must_use]
11787pub fn should_break_rewrite_pair(old: &[u8], new: &[u8], break_score: u64) -> bool {
11788 should_break_rewrite_inner(old, new, break_score)
11789}
11790
11791#[must_use]
11793pub fn apply_diff_filter(
11794 entries: Vec<DiffEntry>,
11795 filter: Option<super::filter::DiffFilter>,
11796) -> Vec<DiffEntry> {
11797 match filter {
11798 Some(f) => f.filter_entries(entries),
11799 None => entries,
11800 }
11801}
11802
11803#[must_use]
11805pub fn parse_diff_rename_threshold_percent(arg: &str) -> Option<u8> {
11806 let token = if arg.is_empty() { "50" } else { arg };
11807 let internal = parse_diff_rename_score_token(token)?;
11808 Some((internal.saturating_mul(100) / GIT_DIFF_MAX_SCORE).min(100) as u8)
11809}
11810
11811pub fn parse_diff_rename_score_token(arg: &str) -> Option<u64> {
11814 let mut num: u64 = 0;
11815 let mut scale: u64 = 1;
11816 let mut dot = false;
11817 let mut saw_digit = false;
11818 for ch in arg.chars() {
11819 if !dot && ch == '.' {
11820 scale = 1;
11821 dot = true;
11822 continue;
11823 }
11824 if ch == '%' {
11825 scale = if dot { scale.saturating_mul(100) } else { 100 };
11826 break;
11827 }
11828 if ch.is_ascii_digit() {
11829 saw_digit = true;
11830 if scale < 100_000 {
11831 scale = scale.saturating_mul(10);
11832 num = num.saturating_mul(10) + u64::from(ch as u8 - b'0');
11833 }
11834 } else {
11835 break;
11836 }
11837 }
11838 if !saw_digit {
11839 return None;
11840 }
11841 Some(if num >= scale {
11842 GIT_DIFF_MAX_SCORE
11843 } else {
11844 GIT_DIFF_MAX_SCORE * num / scale
11845 })
11846}
11847
11848#[must_use]
11851pub fn rewrite_merge_score(old: &[u8], new: &[u8]) -> Option<u64> {
11852 if old.is_empty() {
11853 return None;
11854 }
11855 let max_size = old.len().max(new.len());
11856 if max_size < DIFF_MINIMUM_BREAK_SIZE {
11857 return None;
11858 }
11859 let (src_copied, _) = diffcore_count_changes(old, new);
11860 let src_copied = src_copied.min(old.len() as u64);
11861 let src_removed = (old.len() as u64).saturating_sub(src_copied);
11862 Some(src_removed * DIFF_MAX_SCORE / old.len() as u64)
11863}
11864
11865#[must_use]
11867pub fn rewrite_dissimilarity_index_percent(old: &[u8], new: &[u8]) -> Option<u32> {
11868 let score = rewrite_merge_score(old, new)?;
11869 Some((score * 100 / DIFF_MAX_SCORE).min(100) as u32)
11870}
11871
11872fn should_break_rewrite_inner(src: &[u8], dst: &[u8], break_score: u64) -> bool {
11873 if src.is_empty() {
11874 return false;
11875 }
11876 let max_size = src.len().max(dst.len());
11877 if max_size < DIFF_MINIMUM_BREAK_SIZE {
11878 return false;
11879 }
11880 let (src_copied, literal_added) = diffcore_count_changes(src, dst);
11881 let src_copied = src_copied.min(src.len() as u64);
11882 let mut literal_added = literal_added;
11883 let dst_len = dst.len() as u64;
11884 if src_copied < dst_len && literal_added + src_copied > dst_len {
11885 literal_added = dst_len.saturating_sub(src_copied);
11886 }
11887 let src_removed = (src.len() as u64).saturating_sub(src_copied);
11888 let merge_score = src_removed * DIFF_MAX_SCORE / src.len() as u64;
11889 if merge_score > break_score {
11890 return true;
11891 }
11892 let delta_size = src_removed.saturating_add(literal_added);
11893 if delta_size * DIFF_MAX_SCORE / (max_size as u64) < break_score {
11894 return false;
11895 }
11896 let s = src.len() as u64;
11897 if (s * break_score < src_removed * DIFF_MAX_SCORE)
11898 && (literal_added * 20 < src_removed)
11899 && (literal_added * 20 < src_copied)
11900 {
11901 return false;
11902 }
11903 true
11904}
11905
11906struct FlatEntry {
11910 path: String,
11911 mode: u32,
11912 oid: ObjectId,
11913}
11914
11915fn flatten_tree(odb: &Odb, tree_oid: &ObjectId, prefix: &str) -> Result<Vec<FlatEntry>> {
11916 flatten_tree_with_index(odb, tree_oid, prefix, None)
11917}
11918
11919fn sparse_index_skips_tree_descent(index: &Index, dir_path: &str, tree_oid: &ObjectId) -> bool {
11920 let mut storage = crate::repo_relpath::storage_bytes(dir_path).into_owned();
11921 if !storage.ends_with(b"/") {
11922 storage.push(b'/');
11923 }
11924 index.find_entry_pos(&storage, 0).is_some_and(|pos| {
11925 let entry = &index.entries[pos];
11926 entry.is_sparse_directory_placeholder() && entry.oid == *tree_oid
11927 })
11928}
11929
11930fn subtree_oid_at_path(odb: &Odb, root: &ObjectId, path: &str) -> Result<Option<ObjectId>> {
11932 let mut oid = *root;
11933 for component in path.split('/').filter(|c| !c.is_empty()) {
11934 let entries = read_tree(odb, &oid)?;
11935 let Some(entry) = entries
11936 .iter()
11937 .find(|e| e.name.as_slice() == component.as_bytes())
11938 else {
11939 return Ok(None);
11940 };
11941 if !is_tree_mode(entry.mode) {
11942 return Ok(None);
11943 }
11944 oid = entry.oid;
11945 }
11946 Ok(Some(oid))
11947}
11948
11949fn emit_flattened_subtree_as_added(
11951 odb: &Odb,
11952 subtree: &ObjectId,
11953 prefix: &str,
11954 ignore_submodules: bool,
11955 result: &mut Vec<DiffEntry>,
11956) -> Result<()> {
11957 for entry in flatten_tree(odb, subtree, prefix)? {
11958 if ignore_submodules && entry.mode == 0o160000 {
11959 continue;
11960 }
11961 result.push(DiffEntry {
11962 status: DiffStatus::Added,
11963 old_path: None,
11964 new_path: Some(entry.path),
11965 old_mode: FileMode::ABSENT,
11966 new_mode: FileMode::new(entry.mode),
11967 old_oid: zero_oid(),
11968 new_oid: entry.oid,
11969 score: None,
11970 });
11971 }
11972 Ok(())
11973}
11974
11975fn diff_flattened_subtrees(
11977 head: &[FlatEntry],
11978 index: &[FlatEntry],
11979 ignore_submodules: bool,
11980 result: &mut Vec<DiffEntry>,
11981 stage0_paths: &mut std::collections::BTreeSet<String>,
11982 tree_map: &mut std::collections::BTreeMap<&str, &FlatEntry>,
11983) {
11984 let mut head_map: std::collections::BTreeMap<&str, &FlatEntry> = head
11985 .iter()
11986 .map(|entry| (entry.path.as_str(), entry))
11987 .collect();
11988 for entry in index {
11989 stage0_paths.insert(entry.path.clone());
11990 tree_map.remove(entry.path.as_str());
11991 match head_map.remove(entry.path.as_str()) {
11992 Some(te) => {
11993 if te.oid != entry.oid || te.mode != entry.mode {
11994 result.push(DiffEntry {
11995 status: DiffStatus::for_content_change(te.mode, entry.mode),
11996 old_path: Some(entry.path.clone()),
11997 new_path: Some(entry.path.clone()),
11998 old_mode: FileMode::new(te.mode),
11999 new_mode: FileMode::new(entry.mode),
12000 old_oid: te.oid,
12001 new_oid: entry.oid,
12002 score: None,
12003 });
12004 }
12005 }
12006 None => {
12007 result.push(DiffEntry {
12008 status: DiffStatus::Added,
12009 old_path: None,
12010 new_path: Some(entry.path.clone()),
12011 old_mode: FileMode::ABSENT,
12012 new_mode: FileMode::new(entry.mode),
12013 old_oid: zero_oid(),
12014 new_oid: entry.oid,
12015 score: None,
12016 });
12017 }
12018 }
12019 }
12020 for (path, te) in head_map {
12021 if ignore_submodules && te.mode == 0o160000 {
12022 continue;
12023 }
12024 tree_map.remove(path);
12025 result.push(DiffEntry {
12026 status: DiffStatus::Deleted,
12027 old_path: Some(path.to_owned()),
12028 new_path: None,
12029 old_mode: FileMode::new(te.mode),
12030 new_mode: FileMode::ABSENT,
12031 old_oid: te.oid,
12032 new_oid: zero_oid(),
12033 score: None,
12034 });
12035 }
12036}
12037
12038fn flatten_tree_with_index(
12039 odb: &Odb,
12040 tree_oid: &ObjectId,
12041 prefix: &str,
12042 index: Option<&Index>,
12043) -> Result<Vec<FlatEntry>> {
12044 let entries = read_tree(odb, tree_oid)?;
12045 let mut result = Vec::new();
12046
12047 for entry in entries {
12048 let name_str = String::from_utf8_lossy(&entry.name);
12049 let path = format_path(prefix, &name_str);
12050 if is_tree_mode(entry.mode) {
12051 if index.is_some_and(|idx| sparse_index_skips_tree_descent(idx, &path, &entry.oid)) {
12052 continue;
12053 }
12054 let nested = flatten_tree_with_index(odb, &entry.oid, &path, index)?;
12055 result.extend(nested);
12056 } else {
12057 result.push(FlatEntry {
12058 path,
12059 mode: entry.mode,
12060 oid: entry.oid,
12061 });
12062 }
12063 }
12064
12065 Ok(result)
12066}
12067
12068pub fn head_path_states(
12070 odb: &Odb,
12071 head_tree: Option<&ObjectId>,
12072) -> Result<std::collections::BTreeMap<String, (u32, ObjectId)>> {
12073 let mut m = std::collections::BTreeMap::new();
12074 let Some(t) = head_tree else {
12075 return Ok(m);
12076 };
12077 for fe in flatten_tree(odb, t, "")? {
12078 m.insert(fe.path, (fe.mode, fe.oid));
12079 }
12080 Ok(m)
12081}
12082
12083fn is_tree_mode(mode: u32) -> bool {
12085 crate::index::is_tree_mode(mode)
12086}
12087
12088fn path_order(a: &DiffEntry, b: &DiffEntry) -> std::cmp::Ordering {
12091 use crate::repo_relpath::storage_bytes;
12092 storage_bytes(a.path()).cmp(&storage_bytes(b.path()))
12093}
12094
12095fn entries_sorted_by_path(entries: &[DiffEntry]) -> bool {
12096 entries.is_sorted_by(|a, b| path_order(a, b).is_le())
12097}
12098
12099fn entries_strictly_sorted_by_path(entries: &[DiffEntry]) -> bool {
12100 entries.is_sorted_by(|a, b| path_order(a, b).is_lt())
12101}
12102
12103pub fn sort_entries_by_path(entries: &mut [DiffEntry]) {
12114 if entries_sorted_by_path(entries) {
12115 return;
12116 }
12117 entries.sort_by(path_order);
12118}
12119
12120fn format_path(prefix: &str, name: &str) -> String {
12122 if prefix.is_empty() {
12123 name.to_owned()
12124 } else {
12125 let mut path = String::with_capacity(prefix.len() + 1 + name.len());
12126 path.push_str(prefix);
12127 path.push('/');
12128 path.push_str(name);
12129 path
12130 }
12131}
12132
12133pub fn format_mode(mode: u32) -> String {
12135 format!("{mode:06o}")
12136}
12137
12138#[must_use]
12142pub fn read_submodule_head_for_checkout(sub_dir: &Path) -> Option<ObjectId> {
12143 read_submodule_head(sub_dir)
12144}
12145
12146#[must_use]
12151pub fn submodule_commit_subject_line(c: &CommitData) -> String {
12152 let enc = c.encoding.as_deref().unwrap_or("UTF-8");
12153 let is_latin1 = enc.eq_ignore_ascii_case("ISO8859-1")
12154 || enc.eq_ignore_ascii_case("ISO-8859-1")
12155 || enc.eq_ignore_ascii_case("LATIN1")
12156 || enc.eq_ignore_ascii_case("ISO-8859-15");
12157 if let Some(raw) = c.raw_message.as_deref() {
12158 let line = raw.split(|b| *b == b'\n').next().unwrap_or(raw);
12159 if is_latin1 {
12160 return line
12161 .iter()
12162 .map(|&b| b as char)
12163 .collect::<String>()
12164 .trim()
12165 .to_owned();
12166 }
12167 return String::from_utf8_lossy(line).trim().to_string();
12168 }
12169 c.message.lines().next().unwrap_or("").trim().to_owned()
12170}
12171
12172fn uninitialized_gitlink_worktree_matches_index(sub_dir: &Path) -> bool {
12176 submodule_embedded_git_dir(sub_dir).is_none()
12177}
12178
12179fn read_submodule_head(sub_dir: &Path) -> Option<ObjectId> {
12180 read_submodule_head_oid(sub_dir)
12181}
12182
12183#[must_use]
12185pub fn submodule_embedded_git_dir(sub_dir: &Path) -> Option<PathBuf> {
12186 let gitfile = sub_dir.join(".git");
12187 if gitfile.is_file() {
12188 let content = fs::read_to_string(&gitfile).ok()?;
12189 let gitdir = content
12190 .lines()
12191 .find_map(|l| l.strip_prefix("gitdir: "))?
12192 .trim();
12193 Some(if Path::new(gitdir).is_absolute() {
12194 PathBuf::from(gitdir)
12195 } else {
12196 sub_dir.join(gitdir)
12197 })
12198 } else if gitfile.is_dir() {
12199 Some(gitfile)
12200 } else {
12201 None
12202 }
12203}
12204
12205#[must_use]
12211pub(crate) fn is_unregistered_embedded_git_repo(index: &Index, rel: &str, abs: &Path) -> bool {
12212 if rel.is_empty() {
12213 return false;
12214 }
12215 if index
12216 .entries
12217 .iter()
12218 .any(|e| e.path.as_slice() == rel.as_bytes() && e.mode == crate::index::MODE_GITLINK)
12219 {
12220 return false;
12221 }
12222 let Some(git_dir) = submodule_embedded_git_dir(abs) else {
12223 return false;
12224 };
12225 crate::submodule_gitdir::is_valid_git_dir(&git_dir)
12226}
12227
12228fn find_superproject_git(sub_dir: &Path) -> Option<(PathBuf, PathBuf)> {
12230 let mut cur = sub_dir.parent()?;
12231 loop {
12232 let git_path = cur.join(".git");
12233 if git_path.exists() {
12234 let gd = if git_path.is_file() {
12235 let content = fs::read_to_string(&git_path).ok()?;
12236 let line = content
12237 .lines()
12238 .find_map(|l| l.strip_prefix("gitdir: "))?
12239 .trim();
12240 if Path::new(line).is_absolute() {
12241 PathBuf::from(line)
12242 } else {
12243 cur.join(line)
12244 }
12245 } else {
12246 git_path
12247 };
12248 return Some((cur.to_path_buf(), gd));
12249 }
12250 cur = cur.parent()?;
12251 }
12252}
12253
12254pub fn read_submodule_head_oid(sub_dir: &Path) -> Option<ObjectId> {
12260 let mut git_dir = submodule_embedded_git_dir(sub_dir)?;
12263 if let Some((super_wt, super_git_dir)) = find_superproject_git(sub_dir) {
12264 let rel = sub_dir.strip_prefix(&super_wt).ok()?;
12265 let rel_str = rel.to_string_lossy().replace('\\', "/");
12266 let local_mod = super_git_dir
12267 .join("modules")
12268 .join(rel_str.trim_start_matches('/'));
12269 if local_mod.join("HEAD").exists() {
12270 let sg = super_git_dir
12271 .canonicalize_simplified()
12272 .unwrap_or(super_git_dir);
12273 let cur = git_dir
12274 .canonicalize_simplified()
12275 .unwrap_or_else(|_| git_dir.clone());
12276 if !cur.starts_with(&sg) {
12277 git_dir = local_mod;
12278 }
12279 }
12280 }
12281 let head_content = fs::read_to_string(git_dir.join("HEAD")).ok()?;
12282 let head_trimmed = head_content.trim();
12283 if head_trimmed.starts_with("ref: ") {
12284 match crate::refs::resolve_ref(&git_dir, "HEAD") {
12288 Ok(oid) => Some(oid),
12289 Err(_) => {
12290 let mut found = None;
12291 for branch in ["main", "master"] {
12292 let p = git_dir.join("refs/heads").join(branch);
12293 if let Ok(s) = fs::read_to_string(&p) {
12294 if let Ok(o) = ObjectId::from_hex(s.trim()) {
12295 found = Some(o);
12296 break;
12297 }
12298 }
12299 }
12300 found
12301 }
12302 }
12303 } else {
12304 ObjectId::from_hex(head_trimmed).ok()
12305 }
12306}
12307
12308#[must_use]
12320pub fn submodule_head_object_broken(sub_dir: &Path) -> bool {
12321 let Some(sub_git_dir) = submodule_embedded_git_dir(sub_dir) else {
12322 return false;
12323 };
12324 let Some(head_oid) = read_submodule_head_oid(sub_dir) else {
12325 return false;
12326 };
12327 let odb = Odb::new(&sub_git_dir.join("objects"));
12328 match odb.read(&head_oid) {
12329 Ok(obj) => parse_commit(&obj.data).is_err(),
12332 Err(_) => true,
12333 }
12334}
12335
12336#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
12338pub struct SubmodulePorcelainFlags {
12339 pub new_commits: bool,
12341 pub modified: bool,
12343 pub untracked: bool,
12345}
12346
12347pub fn submodule_porcelain_flags(
12353 super_worktree: &Path,
12354 rel_path: &str,
12355 recorded_oid: ObjectId,
12356) -> SubmodulePorcelainFlags {
12357 let sub_dir = super_worktree.join(rel_path);
12358 let Some(sub_git_dir) = submodule_embedded_git_dir(&sub_dir) else {
12359 return SubmodulePorcelainFlags::default();
12360 };
12361 let Some(sub_head) = read_submodule_head_oid(&sub_dir) else {
12362 return SubmodulePorcelainFlags::default();
12363 };
12364
12365 let new_commits = sub_head != recorded_oid;
12366
12367 let index_path = sub_git_dir.join("index");
12368 let mut sub_index = match crate::index::Index::load(&index_path) {
12369 Ok(ix) => ix,
12370 Err(_) => {
12371 return SubmodulePorcelainFlags {
12372 new_commits,
12373 ..Default::default()
12374 }
12375 }
12376 };
12377
12378 let tracked: std::collections::BTreeSet<String> = sub_index
12379 .entries
12380 .iter()
12381 .filter(|e| e.stage() == 0)
12382 .map(|e| String::from_utf8_lossy(&e.path).into_owned())
12383 .collect();
12384 let untracked = submodule_dir_has_untracked_inner(&sub_dir, &sub_dir, &tracked, &sub_index);
12385
12386 let objects_dir = sub_git_dir.join("objects");
12387 let odb = Odb::new(&objects_dir);
12388
12389 let sub_head_tree = (|| -> Option<ObjectId> {
12390 let h = fs::read_to_string(sub_git_dir.join("HEAD")).ok()?;
12391 let h_str = h.trim();
12392 let commit_oid = if let Some(r) = h_str.strip_prefix("ref: ") {
12393 let oid_hex = fs::read_to_string(sub_git_dir.join(r)).ok()?;
12394 ObjectId::from_hex(oid_hex.trim()).ok()?
12395 } else {
12396 ObjectId::from_hex(h_str).ok()?
12397 };
12398 let obj = odb.read(&commit_oid).ok()?;
12399 let commit = parse_commit(&obj.data).ok()?;
12400 Some(commit.tree)
12401 })();
12402
12403 let staged_dirty = sub_head_tree
12404 .as_ref()
12405 .map(|t| diff_index_to_tree(&odb, &sub_index, Some(t), false).map(|v| !v.is_empty()))
12406 .unwrap_or(Ok(false));
12407 let staged_dirty = staged_dirty.unwrap_or(false);
12408
12409 let unstaged_dirty = diff_index_to_worktree(&odb, &mut sub_index, &sub_dir, false, true)
12410 .map(|v| !v.is_empty())
12411 .unwrap_or(false);
12412
12413 let mut modified = staged_dirty || unstaged_dirty;
12414
12415 for e in &sub_index.entries {
12420 if e.stage() != 0 || e.mode != 0o160000 {
12421 continue;
12422 }
12423 let child = String::from_utf8_lossy(&e.path).into_owned();
12424 let full_rel = if rel_path.is_empty() {
12425 child
12426 } else {
12427 format!("{rel_path}/{child}")
12428 };
12429 let nested = submodule_porcelain_flags(super_worktree, &full_rel, e.oid);
12430 modified |= nested.modified;
12431 }
12432
12433 SubmodulePorcelainFlags {
12434 new_commits,
12435 modified,
12436 untracked,
12437 }
12438}
12439
12440fn submodule_dir_has_untracked_inner(
12441 dir: &Path,
12442 root: &Path,
12443 tracked: &std::collections::BTreeSet<String>,
12444 owning_index: &Index,
12445) -> bool {
12446 let entries = match fs::read_dir(dir) {
12447 Ok(e) => e,
12448 Err(_) => return false,
12449 };
12450 let mut sorted: Vec<_> = entries.filter_map(|e| e.ok()).collect();
12451 sorted.sort_by_key(|e| e.file_name());
12452
12453 for entry in sorted {
12454 let name = entry.file_name().to_string_lossy().to_string();
12455 if name == ".git" {
12456 continue;
12457 }
12458 let path = entry.path();
12459 let rel = path
12460 .strip_prefix(root)
12461 .map(|p| p.to_string_lossy().to_string())
12462 .unwrap_or_else(|_| name.clone());
12463
12464 let is_dir = entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false);
12465 if is_dir {
12466 let is_gitlink = owning_index
12467 .get(rel.as_bytes(), 0)
12468 .is_some_and(|e| e.mode == 0o160000);
12469 if is_gitlink {
12470 let Some(nested_git) = submodule_embedded_git_dir(&path) else {
12471 continue;
12472 };
12473 let nested_index_path = nested_git.join("index");
12474 let Ok(nested_ix) = crate::index::Index::load(&nested_index_path) else {
12475 continue;
12476 };
12477 let nested_tracked: std::collections::BTreeSet<String> = nested_ix
12478 .entries
12479 .iter()
12480 .filter(|e| e.stage() == 0)
12481 .map(|e| String::from_utf8_lossy(&e.path).into_owned())
12482 .collect();
12483 if submodule_dir_has_untracked_inner(&path, &path, &nested_tracked, &nested_ix) {
12484 return true;
12485 }
12486 } else if submodule_dir_has_untracked_inner(&path, root, tracked, owning_index) {
12487 return true;
12488 }
12489 } else if !tracked.contains(&rel) {
12490 return true;
12491 }
12492 }
12493 false
12494}
12495
12496#[cfg(test)]
12497mod line_compare_key_tests {
12498 use super::*;
12499 use crate::diff::unified_emit::line_diff_token;
12500 use crate::diff::WhitespaceMode;
12501
12502 #[test]
12503 fn borrowed_keys_match_owned_line_tokens() {
12504 let texts = [
12505 "",
12506 "\n",
12507 "a",
12508 "a\n",
12509 "a\nb",
12510 "a\r\nb\r\n",
12511 "a\r\nb\r",
12512 "\n\n\nlast",
12513 "x\n\r\n\ry\n",
12514 ];
12515 for text in texts {
12516 let side = LineSide::from_text(text);
12517 let mut normalized = Vec::new();
12518 let keys = line_compare_keys(text, &side, WhitespaceMode::default(), &mut normalized);
12519 let expected: Vec<String> = side.lines.iter().map(line_diff_token).collect();
12520 assert_eq!(keys, expected, "text {text:?}");
12521 let source = text.as_bytes().as_ptr_range();
12522 assert!(
12523 keys.iter()
12524 .all(|key| source.contains(&key.as_ptr()) || key.is_empty()),
12525 "text {text:?}"
12526 );
12527 }
12528 }
12529
12530 #[test]
12531 fn whitespace_modes_compare_normalized_bodies() {
12532 let text = "a b\nab \n";
12533 let side = LineSide::from_text(text);
12534 let ws = WhitespaceMode {
12535 ignore_all_space: true,
12536 ..WhitespaceMode::default()
12537 };
12538 let mut normalized = Vec::new();
12539 let keys = line_compare_keys(text, &side, ws, &mut normalized);
12540 assert_eq!(keys, vec!["ab", "ab"]);
12541 }
12542
12543 #[test]
12544 fn xdiff_marks_only_edited_lines_in_large_file() {
12545 let old: String = (0..5_000).map(|i| format!("line{i}\n")).collect();
12546 let new: String = (0..5_000)
12547 .map(|i| {
12548 if i % 100 == 0 {
12549 format!("LINE{i}\n")
12550 } else {
12551 format!("line{i}\n")
12552 }
12553 })
12554 .collect();
12555 let body =
12556 git_xdiff_unified_body_raw(&old, &new, 3, 0, true, false, WhitespaceMode::default());
12557 assert_eq!(body.matches("\n@@ ").count() + 1, 50);
12558 assert_eq!(body.lines().filter(|l| l.starts_with('-')).count(), 50);
12559 assert_eq!(body.lines().filter(|l| l.starts_with('+')).count(), 50);
12560 assert!(body.starts_with("@@ -1,4 +1,4 @@\n-line0\n+LINE0\n line1\n"));
12561 }
12562}
12563
12564#[cfg(test)]
12565mod entry_order_tests {
12566 use super::*;
12567
12568 fn entry(status: DiffStatus, path: &str, score: Option<u32>) -> DiffEntry {
12569 let mode = |present: bool| {
12570 if present {
12571 FileMode::REGULAR
12572 } else {
12573 FileMode::ABSENT
12574 }
12575 };
12576 DiffEntry {
12577 status,
12578 old_path: (status != DiffStatus::Added).then(|| path.to_owned()),
12579 new_path: (status != DiffStatus::Deleted).then(|| path.to_owned()),
12580 old_mode: mode(status != DiffStatus::Added),
12581 new_mode: mode(status != DiffStatus::Deleted),
12582 old_oid: ObjectId::zero(),
12583 new_oid: ObjectId::zero(),
12584 score,
12585 }
12586 }
12587
12588 #[test]
12589 fn sort_entries_by_path_orders_by_path_and_keeps_equal_paths_in_place() {
12590 let mut entries = vec![
12591 entry(DiffStatus::Modified, "b", None),
12592 entry(DiffStatus::Deleted, "a/x", Some(1)),
12593 entry(DiffStatus::Added, "a/x", Some(2)),
12594 entry(DiffStatus::Added, "a", None),
12595 ];
12596 sort_entries_by_path(&mut entries);
12597 let order: Vec<_> = entries.iter().map(|e| (e.path(), e.score)).collect();
12598 assert_eq!(
12599 order,
12600 [("a", None), ("a/x", Some(1)), ("a/x", Some(2)), ("b", None)]
12601 );
12602 let sorted = entries.clone();
12603 sort_entries_by_path(&mut entries);
12604 assert_eq!(entries, sorted);
12605 sort_entries_by_path(&mut []);
12606 }
12607
12608 #[test]
12609 fn sort_entries_by_path_compares_non_utf8_paths_by_raw_bytes() {
12610 let raw = crate::repo_relpath::format_path_stored("", b"raw-\xff");
12611 assert!(raw.starts_with("grit+pathhex:"));
12612 let mut entries = vec![
12613 entry(DiffStatus::Modified, &raw, None),
12614 entry(DiffStatus::Modified, "node/leaf", None),
12615 entry(DiffStatus::Modified, "zeta", None),
12616 ];
12617 sort_entries_by_path(&mut entries);
12618 let paths: Vec<_> = entries.iter().map(DiffEntry::path).collect();
12619 assert_eq!(paths, ["node/leaf", raw.as_str(), "zeta"]);
12620 }
12621
12622 #[test]
12623 fn rename_pass_returns_unpairable_lists_in_path_order() {
12624 let config = ConfigSet::new();
12625 let policy = ThreadPolicy::default();
12626 let odb = Odb::new(Path::new("/nonexistent-objects"));
12627 let run = |entries: Vec<DiffEntry>| {
12628 detect_renames_with_policy(&odb, None, entries, 50, &config, &policy, None)
12629 .expect("no blob is read without both adds and deletes")
12630 };
12631 let sorted = vec![
12632 entry(DiffStatus::Deleted, "a", None),
12633 entry(DiffStatus::Modified, "b", None),
12634 entry(DiffStatus::Deleted, "c", None),
12635 ];
12636 assert_eq!(run(sorted.clone()), sorted);
12637 let unsorted = vec![
12638 entry(DiffStatus::Added, "z", None),
12639 entry(DiffStatus::Modified, "m", None),
12640 entry(DiffStatus::Added, "a", None),
12641 ];
12642 let paths: Vec<_> = run(unsorted).iter().map(|e| e.path().to_owned()).collect();
12643 assert_eq!(paths, ["a", "m", "z"]);
12644 let duplicate = vec![
12646 entry(DiffStatus::Added, "p", None),
12647 entry(DiffStatus::Modified, "p", None),
12648 ];
12649 let statuses: Vec<_> = run(duplicate).iter().map(|e| e.status).collect();
12650 assert_eq!(statuses, [DiffStatus::Modified, DiffStatus::Added]);
12651 }
12652}
12653
12654#[cfg(test)]
12655mod diff_index_to_tree_cache_tree_tests {
12656 use std::rc::Rc;
12657 use std::sync::atomic::{AtomicUsize, Ordering};
12658
12659 use super::*;
12660 use crate::index::{Index, IndexEntry, MODE_EXECUTABLE, MODE_GITLINK, MODE_REGULAR};
12661 use crate::objects::ObjectKind;
12662 use crate::write_tree::{build_cache_tree_from_index, write_tree_from_index};
12663 use tempfile::TempDir;
12664
12665 struct TreeReadCounter {
12666 counter: Rc<AtomicUsize>,
12667 }
12668
12669 impl TreeReadCounter {
12670 fn start() -> Self {
12671 let counter = Rc::new(AtomicUsize::new(0));
12672 set_diff_tree_read_hook_for_tests(Some(counter.clone()));
12673 Self { counter }
12674 }
12675
12676 fn get(&self) -> usize {
12677 self.counter.load(Ordering::Relaxed)
12678 }
12679 }
12680
12681 impl Drop for TreeReadCounter {
12682 fn drop(&mut self) {
12683 set_diff_tree_read_hook_for_tests(None);
12684 }
12685 }
12686
12687 fn diff_index_to_tree_flatten_reference(
12688 odb: &Odb,
12689 index: &Index,
12690 head_tree: &ObjectId,
12691 ignore_submodules: bool,
12692 ) -> Result<Vec<DiffEntry>> {
12693 let mut flat_index = index.clone();
12694 flat_index.clear_cache_tree();
12695 diff_index_to_tree(odb, &flat_index, Some(head_tree), ignore_submodules)
12696 }
12697
12698 fn index_entry(path: &str, mode: u32, oid: ObjectId) -> IndexEntry {
12699 IndexEntry {
12700 ctime_sec: 0,
12701 ctime_nsec: 0,
12702 mtime_sec: 0,
12703 mtime_nsec: 0,
12704 dev: 0,
12705 ino: 0,
12706 mode,
12707 uid: 0,
12708 gid: 0,
12709 size: 0,
12710 oid,
12711 flags: path.len().min(0xfff) as u16,
12712 flags_extended: None,
12713 path: path.as_bytes().to_vec(),
12714 base_index_pos: 0,
12715 }
12716 }
12717
12718 fn repo_with_tree(depth: usize, width: usize) -> (TempDir, Odb, ObjectId, Index) {
12719 let temp = TempDir::new().expect("tempdir");
12720 let odb = Odb::new(temp.path());
12721 let mut index = Index::new();
12722 for d in 0..depth {
12723 for w in 0..width {
12724 let path = format!("unchanged/d{d}/f{w}.txt");
12725 let oid = odb
12726 .write(ObjectKind::Blob, format!("body-{d}-{w}").as_bytes())
12727 .expect("blob");
12728 index.add_or_replace(index_entry(&path, MODE_REGULAR, oid));
12729 }
12730 }
12731 let head_tree = write_tree_from_index(&odb, &index, "").expect("write tree");
12732 let cache = build_cache_tree_from_index(&odb, &index).expect("cache-tree");
12733 index.set_cache_tree(cache);
12734 index.cache_tree_root = Some(head_tree);
12735 (temp, odb, head_tree, index)
12736 }
12737
12738 #[test]
12739 fn diff_index_to_tree_cache_tree_fast_path_matches_slow() {
12740 let (_temp, odb, head_tree, mut index) = repo_with_tree(4, 5);
12741 assert!(index.cache_tree.as_ref().is_some_and(|c| c.is_valid()));
12742
12743 let mut state: u64 = 0xDEAD_BEEF;
12744 for _ in 0..24 {
12745 state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
12746 let d = (state % 4) as usize;
12747 state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
12748 let w = (state % 5) as usize;
12749 let path = format!("unchanged/d{d}/f{w}.txt");
12750 index.invalidate_cache_tree_for_path(path.as_bytes());
12751 assert!(
12752 index.cache_tree_root.is_none(),
12753 "partial invalidation clears cache_tree_root like Index::add"
12754 );
12755 assert!(
12756 index.cache_tree.as_ref().is_some_and(|r| !r.is_valid()),
12757 "root cache node must be invalid after partial invalidation"
12758 );
12759 if let Some(e) = index.get_mut(path.as_bytes(), 0) {
12760 let new_oid = odb
12761 .write(ObjectKind::Blob, format!("new-{d}-{w}").as_bytes())
12762 .expect("blob");
12763 e.oid = new_oid;
12764 }
12765 let fast = diff_index_to_tree(&odb, &index, Some(&head_tree), false).expect("fast");
12766 let flat = diff_index_to_tree_flatten_reference(&odb, &index, &head_tree, false)
12767 .expect("flat");
12768 assert_eq!(fast, flat, "mismatch after mutating {path}");
12769 }
12770 }
12771
12772 #[test]
12773 fn diff_index_to_tree_skips_unchanged_subtrees() {
12774 let (_temp, odb, head_tree, index) = repo_with_tree(5, 6);
12775 let reads = TreeReadCounter::start();
12776 diff_index_to_tree(&odb, &index, Some(&head_tree), false).expect("cached diff");
12777 let cached_reads = reads.get();
12778
12779 let mut no_cache = index.clone();
12780 no_cache.clear_cache_tree();
12781 let reads = TreeReadCounter::start();
12782 diff_index_to_tree(&odb, &no_cache, Some(&head_tree), false).expect("flat diff");
12783 let flat_reads = reads.get();
12784
12785 assert!(
12786 cached_reads < flat_reads,
12787 "expected fewer tree reads with cache-tree (cached={cached_reads}, flat={flat_reads})"
12788 );
12789 assert_eq!(
12790 cached_reads, 1,
12791 "unchanged repo should only read the root tree"
12792 );
12793 }
12794
12795 #[test]
12796 fn diff_index_to_tree_partial_invalidation_skips_sibling_subtree() {
12797 let (_temp, odb, head_tree, mut index) = repo_with_tree(5, 6);
12798 let path = "unchanged/d1/f2.txt";
12799 index.invalidate_cache_tree_for_path(path.as_bytes());
12800 let new_oid = odb.write(ObjectKind::Blob, b"changed").expect("blob");
12801 index.get_mut(path.as_bytes(), 0).expect("entry").oid = new_oid;
12802
12803 let flat =
12804 diff_index_to_tree_flatten_reference(&odb, &index, &head_tree, false).expect("flat");
12805 let reads = TreeReadCounter::start();
12806 let fast = diff_index_to_tree(&odb, &index, Some(&head_tree), false).expect("fast");
12807 let cached_reads = reads.get();
12808 assert_eq!(fast, flat);
12809
12810 let reads = TreeReadCounter::start();
12811 diff_index_to_tree_flatten_reference(&odb, &index, &head_tree, false).expect("flat");
12812 let flat_reads = reads.get();
12813
12814 assert!(
12815 cached_reads < flat_reads,
12816 "partial cache-tree should skip sibling dirs (cached={cached_reads}, flat={flat_reads})"
12817 );
12818 }
12819
12820 fn cache_leaf(name: &str) -> CacheTreeNode {
12821 CacheTreeNode::valid(name.as_bytes().to_vec(), 1, ObjectId::zero(), Vec::new())
12822 }
12823
12824 #[test]
12825 fn cache_children_lookup_matches_linear_scan_on_wide_unsorted_nodes() {
12826 let mut children: Vec<CacheTreeNode> = (0..CACHE_CHILD_LINEAR_SCAN_MAX * 3)
12827 .rev()
12828 .map(|i| cache_leaf(&format!("d{i:03}")))
12829 .collect();
12830 let mut first_dup = cache_leaf("d007");
12831 first_dup.entry_count = 7;
12832 children.insert(3, first_dup);
12833 let root = CacheTreeNode::valid(Vec::new(), 0, ObjectId::zero(), children);
12834
12835 let lookup = CacheChildren::new(Some(&root));
12836 assert!(
12837 !lookup.sorted.is_empty(),
12838 "wide nodes must use the sorted lookup"
12839 );
12840 for name in ["d000", "d007", "d020", "d047", "zzz", "d", ""] {
12841 let linear = root.children.iter().find(|c| c.name == name.as_bytes());
12842 let fast = lookup.get(name.as_bytes());
12843 assert_eq!(
12844 fast.map(|c| (&c.name, c.entry_count)),
12845 linear.map(|c| (&c.name, c.entry_count)),
12846 "lookup of {name:?}"
12847 );
12848 }
12849 assert_eq!(lookup.get(b"d007").map(|c| c.entry_count), Some(7));
12850
12851 let narrow = CacheTreeNode::valid(
12852 Vec::new(),
12853 0,
12854 ObjectId::zero(),
12855 vec![cache_leaf("b"), cache_leaf("a")],
12856 );
12857 let lookup = CacheChildren::new(Some(&narrow));
12858 assert!(lookup.sorted.is_empty());
12859 assert_eq!(lookup.get(b"a").map(|c| c.name.as_slice()), Some(&b"a"[..]));
12860 assert!(lookup.get(b"c").is_none());
12861 assert!(CacheChildren::new(None).get(b"a").is_none());
12862 }
12863
12864 #[test]
12865 fn index_relative_in_dir_requires_a_component_boundary() {
12866 assert_eq!(index_relative_in_dir(b"a/b", b""), Some(&b"a/b"[..]));
12867 assert_eq!(index_relative_in_dir(b"a/b", b"a"), Some(&b"b"[..]));
12868 assert_eq!(index_relative_in_dir(b"a", b"a"), Some(&b""[..]));
12869 assert_eq!(index_relative_in_dir(b"ab/c", b"a"), None);
12870 assert_eq!(index_relative_in_dir(b"a.c", b"a"), None);
12871 assert_eq!(index_relative_in_dir(b"b", b"a"), None);
12872 assert_eq!(join_index_path(b"", b"x"), b"x".to_vec());
12873 assert_eq!(join_index_path(b"a/b", b"x"), b"a/b/x".to_vec());
12874 }
12875
12876 #[test]
12879 fn diff_index_to_tree_wide_unsorted_cache_tree_matches_flatten() {
12880 let temp = TempDir::new().expect("tempdir");
12881 let odb = Odb::new(temp.path());
12882 let mut index = Index::new();
12883 let width = CACHE_CHILD_LINEAR_SCAN_MAX * 4;
12884 for d in 0..width {
12885 for f in 0..2 {
12886 let path = format!("dir{d:03}/sub/f{f}.txt");
12887 let oid = odb.write(ObjectKind::Blob, path.as_bytes()).expect("blob");
12888 index.add_or_replace(index_entry(&path, MODE_REGULAR, oid));
12889 }
12890 }
12891 let head_tree = write_tree_from_index(&odb, &index, "").expect("write tree");
12892 let mut cache = build_cache_tree_from_index(&odb, &index).expect("cache-tree");
12893 cache.children.reverse();
12894 index.set_cache_tree(cache);
12895 index.cache_tree_root = Some(head_tree);
12896
12897 let changed = [
12898 "dir003/sub/f1.txt",
12899 "dir031/sub/f0.txt",
12900 "dir063/sub/f1.txt",
12901 ];
12902 for path in changed {
12903 index.invalidate_cache_tree_for_path(path.as_bytes());
12904 let oid = odb.write(ObjectKind::Blob, b"changed").expect("blob");
12905 index.get_mut(path.as_bytes(), 0).expect("entry").oid = oid;
12906 }
12907 for path in ["dir010/sub/new.txt", "dir999/new.txt", "top.txt"] {
12908 index.invalidate_cache_tree_for_path(path.as_bytes());
12909 let oid = odb.write(ObjectKind::Blob, path.as_bytes()).expect("blob");
12910 index.add_or_replace(index_entry(path, MODE_REGULAR, oid));
12911 }
12912 for path in [
12913 "dir020/sub/f0.txt",
12914 "dir020/sub/f1.txt",
12915 "dir021/sub/f0.txt",
12916 ] {
12917 index.invalidate_cache_tree_for_path(path.as_bytes());
12918 index.remove(path.as_bytes());
12919 }
12920
12921 let flat =
12922 diff_index_to_tree_flatten_reference(&odb, &index, &head_tree, false).expect("flat");
12923 let reads = TreeReadCounter::start();
12924 let fast = diff_index_to_tree(&odb, &index, Some(&head_tree), false).expect("fast");
12925 let cached_reads = reads.get();
12926 assert_eq!(fast, flat);
12927 assert_eq!(fast.len(), changed.len() + 3 + 3);
12928 assert!(
12929 cached_reads <= 1 + 2 * 6,
12930 "unchanged siblings must be skipped through the cache-tree (reads={cached_reads})"
12931 );
12932 }
12933
12934 #[test]
12935 fn diff_index_to_tree_cache_tree_handles_non_utf8_directory_names() {
12936 let temp = TempDir::new().expect("tempdir");
12937 let odb = Odb::new(temp.path());
12938 let mut index = Index::new();
12939 let raw_dirs: [&[u8]; 3] = [b"caf\xe9", b"caf\xef\xbf\xbd", b"plain"];
12940 for dir in raw_dirs {
12941 for f in [&b"a.txt"[..], b"b.txt"] {
12942 let path = [dir, b"/", f].concat();
12943 let oid = odb.write(ObjectKind::Blob, &path).expect("blob");
12944 let mut entry = index_entry("x", MODE_REGULAR, oid);
12945 entry.flags = path.len().min(0xfff) as u16;
12946 entry.path = path;
12947 index.add_or_replace(entry);
12948 }
12949 }
12950 let head_tree = write_tree_from_index(&odb, &index, "").expect("write tree");
12951 let cache = build_cache_tree_from_index(&odb, &index).expect("cache-tree");
12952 index.set_cache_tree(cache);
12953 index.cache_tree_root = Some(head_tree);
12954
12955 let changed: &[u8] = b"caf\xe9/b.txt";
12956 index.invalidate_cache_tree_for_path(changed);
12957 let oid = odb.write(ObjectKind::Blob, b"changed").expect("blob");
12958 index.get_mut(changed, 0).expect("entry").oid = oid;
12959
12960 let fast = diff_index_to_tree(&odb, &index, Some(&head_tree), false).expect("fast");
12963 assert_eq!(fast.len(), 1, "{fast:?}");
12964 assert_eq!(fast[0].status, DiffStatus::Modified);
12965 assert_eq!(
12966 fast[0].old_oid,
12967 odb.write(ObjectKind::Blob, changed).expect("blob")
12968 );
12969 assert_eq!(fast[0].new_oid, oid);
12970 }
12971
12972 #[test]
12973 fn diff_index_to_tree_reports_mode_gitlink_and_intent_to_add() {
12974 let temp = TempDir::new().expect("tempdir");
12975 let odb = Odb::new(temp.path());
12976 let blob = odb.write(ObjectKind::Blob, b"x").expect("blob");
12977 let mut index = Index::new();
12978 index.add_or_replace(index_entry("tracked.txt", MODE_REGULAR, blob));
12979 let head_tree = write_tree_from_index(&odb, &index, "").expect("tree");
12980 let cache = build_cache_tree_from_index(&odb, &index).expect("cache");
12981 index.set_cache_tree(cache);
12982
12983 let mode_entry = index_entry("tracked.txt", MODE_EXECUTABLE, blob);
12984 index.add_or_replace(mode_entry);
12985 index.invalidate_cache_tree_for_path(b"tracked.txt");
12986
12987 let diffs = diff_index_to_tree(&odb, &index, Some(&head_tree), false).expect("diff");
12988 assert_eq!(diffs.len(), 1);
12989 assert_eq!(diffs[0].status, DiffStatus::Modified);
12990 assert_eq!(diffs[0].old_mode, FileMode::new(MODE_REGULAR));
12991 assert_eq!(diffs[0].new_mode, FileMode::new(MODE_EXECUTABLE));
12992
12993 let mut ita = index_entry("new.txt", MODE_REGULAR, blob);
12995 ita.set_intent_to_add(true);
12996 index.add_or_replace(ita);
12997 let diffs = diff_index_to_tree(&odb, &index, Some(&head_tree), false).expect("diff");
12998 assert!(
12999 diffs.iter().all(|d| d.path() != "new.txt"),
13000 "intent-to-add must not appear in staged diff"
13001 );
13002
13003 let gitlink: ObjectId = "855827c583bc30645ba427885caa40c5b81764d2"
13005 .parse()
13006 .expect("oid");
13007 let mut sub_index = Index::new();
13008 sub_index.add_or_replace(index_entry("sub", MODE_GITLINK, gitlink));
13009 let sub_tree = write_tree_from_index(&odb, &sub_index, "").expect("tree");
13010 let sub_cache = build_cache_tree_from_index(&odb, &sub_index).expect("cache");
13011 sub_index.set_cache_tree(sub_cache);
13012 let diffs =
13013 diff_index_to_tree(&odb, &sub_index, Some(&sub_tree), true).expect("gitlink diff");
13014 assert!(
13015 diffs.is_empty(),
13016 "ignored gitlink should not diff: {diffs:?}"
13017 );
13018 }
13019
13020 #[test]
13021 fn ignore_submodules_skips_gitlink_in_added_directory_cache_walk() {
13022 let temp = TempDir::new().expect("tempdir");
13023 let odb = Odb::new(temp.path());
13024 let gitlink: ObjectId = "855827c583bc30645ba427885caa40c5b81764d2"
13025 .parse()
13026 .expect("oid");
13027 let head_tree = write_tree_from_index(&odb, &Index::new(), "").expect("empty tree");
13028 let mut index = Index::new();
13029 index.add_or_replace(index_entry("newdir/sub", MODE_GITLINK, gitlink));
13030 let cache = build_cache_tree_from_index(&odb, &index).expect("cache");
13031 index.set_cache_tree(cache);
13032 index.invalidate_cache_tree_for_path(b"newdir/sub");
13033
13034 let fast = diff_index_to_tree(&odb, &index, Some(&head_tree), true).expect("fast");
13035 let flat =
13036 diff_index_to_tree_flatten_reference(&odb, &index, &head_tree, true).expect("flat");
13037 assert_eq!(fast, flat);
13038 assert!(
13039 fast.is_empty(),
13040 "nested gitlinks in added dirs must be ignored: {fast:?}"
13041 );
13042 }
13043
13044 #[test]
13045 fn ignore_submodules_skips_gitlink_in_deleted_directory_cache_walk() {
13046 let temp = TempDir::new().expect("tempdir");
13047 let odb = Odb::new(temp.path());
13048 let blob = odb.write(ObjectKind::Blob, b"keep").expect("blob");
13049 let gitlink: ObjectId = "855827c583bc30645ba427885caa40c5b81764d2"
13050 .parse()
13051 .expect("oid");
13052 let mut index = Index::new();
13053 index.add_or_replace(index_entry("keep.txt", MODE_REGULAR, blob));
13054 index.add_or_replace(index_entry("olddir/sub", MODE_GITLINK, gitlink));
13055 let head_tree = write_tree_from_index(&odb, &index, "").expect("head");
13056 let cache = build_cache_tree_from_index(&odb, &index).expect("cache");
13057 index.set_cache_tree(cache);
13058 index
13059 .entries
13060 .retain(|e| !String::from_utf8_lossy(&e.path).starts_with("olddir/"));
13061 index.invalidate_cache_tree_for_path(b"olddir/sub");
13062
13063 let fast = diff_index_to_tree(&odb, &index, Some(&head_tree), true).expect("fast");
13064 let flat =
13065 diff_index_to_tree_flatten_reference(&odb, &index, &head_tree, true).expect("flat");
13066 assert_eq!(fast, flat);
13067 assert!(
13068 fast.is_empty(),
13069 "nested gitlinks in deleted dirs must be ignored: {fast:?}"
13070 );
13071 }
13072
13073 #[test]
13074 fn diff_trees_skips_identical_subtree_oids() {
13075 let temp = TempDir::new().expect("tempdir");
13076 let odb = Odb::new(temp.path());
13077 let leaf = odb.write(ObjectKind::Blob, b"leaf").expect("blob");
13078 let mut deep = Index::new();
13079 for i in 0..20 {
13080 deep.add_or_replace(index_entry(&format!("deep/n{i}.txt"), MODE_REGULAR, leaf));
13081 }
13082 let tree = write_tree_from_index(&odb, &deep, "").expect("tree");
13083 let reads = TreeReadCounter::start();
13084 let entries = diff_trees(&odb, Some(&tree), Some(&tree), "").expect("diff");
13085 assert!(entries.is_empty());
13086 assert_eq!(
13087 reads.get(),
13088 0,
13089 "identical tree oids should short-circuit without reading tree objects"
13090 );
13091 }
13092
13093 #[test]
13094 fn diff_tree_path_change_count_matches_diff_trees() {
13095 let temp = TempDir::new().expect("tempdir");
13096 let odb = Odb::new(temp.path());
13097 let a = odb.write(ObjectKind::Blob, b"a").expect("blob");
13098 let b = odb.write(ObjectKind::Blob, b"b").expect("blob");
13099 let mut left = Index::new();
13100 left.add_or_replace(index_entry("f.txt", MODE_REGULAR, a));
13101 let mut right = Index::new();
13102 right.add_or_replace(index_entry("f.txt", MODE_REGULAR, b));
13103 right.add_or_replace(index_entry("g.txt", MODE_REGULAR, a));
13104 let old_tree = write_tree_from_index(&odb, &left, "").expect("old");
13105 let new_tree = write_tree_from_index(&odb, &right, "").expect("new");
13106 let entries = diff_trees(&odb, Some(&old_tree), Some(&new_tree), "").expect("diff");
13107 let count =
13108 diff_tree_path_change_count(&odb, Some(&old_tree), Some(&new_tree), "").expect("count");
13109 assert_eq!(count, entries.len());
13110 assert_eq!(count, 2);
13111 }
13112
13113 #[test]
13114 fn diff_trees_matches_whether_trees_were_cached_parsed_or_raw() {
13115 let temp = TempDir::new().expect("tempdir");
13116 let a = Odb::new(temp.path())
13117 .write(ObjectKind::Blob, b"a")
13118 .expect("blob");
13119 let b = Odb::new(temp.path())
13120 .write(ObjectKind::Blob, b"b")
13121 .expect("blob");
13122 let mut left = Index::new();
13123 let mut right = Index::new();
13124 for i in 0..50 {
13125 left.add_or_replace(index_entry(&format!("same{i:02}.txt"), MODE_REGULAR, a));
13126 right.add_or_replace(index_entry(&format!("same{i:02}.txt"), MODE_REGULAR, a));
13127 }
13128 left.add_or_replace(index_entry("dir/changed.txt", MODE_REGULAR, a));
13129 left.add_or_replace(index_entry("dir/kept.txt", MODE_REGULAR, a));
13130 left.add_or_replace(index_entry("gone/x.txt", MODE_REGULAR, a));
13131 left.add_or_replace(index_entry("mode.sh", MODE_REGULAR, a));
13132 left.add_or_replace(index_entry("swap", MODE_REGULAR, a));
13133 right.add_or_replace(index_entry("dir/changed.txt", MODE_REGULAR, b));
13134 right.add_or_replace(index_entry("dir/kept.txt", MODE_REGULAR, a));
13135 right.add_or_replace(index_entry("mode.sh", MODE_EXECUTABLE, a));
13136 right.add_or_replace(index_entry("new/y.txt", MODE_REGULAR, b));
13137 right.add_or_replace(index_entry("swap/inner.txt", MODE_REGULAR, b));
13138 let setup = Odb::new(temp.path());
13139 let old_tree = write_tree_from_index(&setup, &left, "").expect("old");
13140 let new_tree = write_tree_from_index(&setup, &right, "").expect("new");
13141
13142 let raw = diff_trees(&Odb::new(temp.path()), Some(&old_tree), Some(&new_tree), "")
13143 .expect("raw diff");
13144 let changed: Vec<(DiffStatus, &str)> = raw.iter().map(|e| (e.status, e.path())).collect();
13145 assert_eq!(
13146 changed,
13147 vec![
13148 (DiffStatus::Modified, "dir/changed.txt"),
13149 (DiffStatus::Deleted, "gone/x.txt"),
13150 (DiffStatus::Modified, "mode.sh"),
13151 (DiffStatus::Added, "new/y.txt"),
13152 (DiffStatus::Deleted, "swap"),
13153 (DiffStatus::Added, "swap/inner.txt"),
13154 ]
13155 );
13156
13157 let parsed = Odb::new(temp.path());
13158 parsed.cached_tree_entries(&old_tree).expect("parse old");
13159 parsed.cached_tree_entries(&new_tree).expect("parse new");
13160 assert_eq!(
13161 diff_trees(&parsed, Some(&old_tree), Some(&new_tree), "").expect("parsed diff"),
13162 raw
13163 );
13164
13165 let mixed = Odb::new(temp.path());
13166 mixed.cached_tree_entries(&new_tree).expect("parse new");
13167 assert_eq!(
13168 diff_trees(&mixed, Some(&old_tree), Some(&new_tree), "").expect("mixed diff"),
13169 raw
13170 );
13171 let reparsed = mixed
13172 .cached_tree_entries(&old_tree)
13173 .expect("parse cached raw");
13174 assert_eq!(
13175 reparsed.as_slice(),
13176 crate::objects::parse_tree(&mixed.read(&old_tree).expect("read").data)
13177 .expect("parse")
13178 .as_slice(),
13179 "a tree cached raw by a diff parses to the same entries"
13180 );
13181 }
13182
13183 #[test]
13184 fn diff_trees_reports_a_corrupt_tree() {
13185 let temp = TempDir::new().expect("tempdir");
13186 let odb = Odb::new(temp.path());
13187 let good = write_tree_from_index(&odb, &Index::new(), "").expect("empty tree");
13188 let corrupt = odb
13189 .write(ObjectKind::Tree, b"100644 name-without-oid")
13190 .expect("tree");
13191 let err = diff_trees(&odb, Some(&good), Some(&corrupt), "").expect_err("corrupt");
13192 assert!(matches!(err, Error::CorruptObject(_)), "{err:?}");
13193 }
13194}
13195
13196#[cfg(test)]
13197mod changed_flags_shared_tests {
13198 use super::git_xdiff;
13199 use crate::line_interner::LineInterner;
13200
13201 #[test]
13204 fn shared_counts_stand_in_for_records_left_out() {
13205 let mut old: Vec<String> = Vec::new();
13208 let mut new: Vec<String> = Vec::new();
13209 for i in 0..60 {
13210 for side in [&mut old, &mut new] {
13211 side.push(format!("fn f{i}() {{\n"));
13212 side.push("}\n".to_owned());
13213 }
13214 }
13215 old.extend(
13216 [
13217 "u1\n", "u2\n", "u3\n", "u4\n", "}\n", "u5\n", "u6\n", "u7\n",
13218 ]
13219 .map(str::to_owned),
13220 );
13221 new.extend(
13222 [
13223 "v1\n", "v2\n", "v3\n", "v4\n", "}\n", "v5\n", "v6\n", "v7\n",
13224 ]
13225 .map(str::to_owned),
13226 );
13227 for i in 0..40 {
13228 for side in [&mut old, &mut new] {
13229 side.push(format!("fn g{i}() {{\n"));
13230 side.push("}\n".to_owned());
13231 }
13232 }
13233 let old: Vec<&str> = old.iter().map(String::as_str).collect();
13234 let new: Vec<&str> = new.iter().map(String::as_str).collect();
13235 let (head, tail) = (120, 80);
13236
13237 let mut interner = LineInterner::with_capacity(old.len() + new.len());
13238 let ids1 = interner.intern_all(&old);
13239 let ids2 = interner.intern_all(&new);
13240 let (full1, full2) = git_xdiff::changed_flags(&ids1, &ids2, false);
13241
13242 let mut cut = LineInterner::default();
13243 let cut1 = cut.intern_all(&old[head..old.len() - tail]);
13244 let cut2 = cut.intern_all(&new[head..new.len() - tail]);
13245 let mut shared = vec![0u32; cut.len()];
13246 for line in old[..head].iter().chain(&old[old.len() - tail..]) {
13247 if let Some(id) = cut.get(line) {
13248 shared[id as usize] += 1;
13249 }
13250 }
13251 let (part1, part2) =
13252 git_xdiff::changed_flags_with_shared(&cut1, &cut2, &shared, head + tail, false);
13253 assert_eq!(part1, full1[head..old.len() - tail]);
13254 assert_eq!(part2, full2[head..new.len() - tail]);
13255 assert!(full1[..head]
13256 .iter()
13257 .chain(&full1[old.len() - tail..])
13258 .all(|c| !c));
13259 assert!(full1[head..old.len() - tail].iter().all(|&c| c));
13262 let (alone1, _) = git_xdiff::changed_flags(&cut1, &cut2, false);
13263 assert!(
13264 !alone1[4],
13265 "counted alone, the cut keeps the `}}` as a match"
13266 );
13267 }
13268}
13269
13270#[cfg(test)]
13271mod gitlink_tree_worktree_tests {
13272 use std::fs;
13273
13274 use crate::index::{Index, IndexEntry, MODE_GITLINK};
13275 use crate::objects::{ObjectId, ObjectKind};
13276 use crate::repo::init_repository;
13277
13278 use super::diff_tree_to_worktree;
13279
13280 fn encode_tree_gitlink(name: &str, oid: &ObjectId) -> Vec<u8> {
13281 let mut data = format!("160000 {name}\0").into_bytes();
13282 data.extend_from_slice(oid.as_bytes());
13283 data
13284 }
13285
13286 #[test]
13287 fn unpopulated_gitlink_matching_index_produces_no_diff_entry() {
13288 let dir = tempfile::tempdir().expect("tempdir");
13289 let work_tree = dir.path();
13290 fs::create_dir(work_tree.join("sub")).expect("sub placeholder");
13291
13292 let repo = init_repository(
13293 work_tree,
13294 false,
13295 "main",
13296 None,
13297 crate::RefStorageFormat::Files,
13298 )
13299 .expect("init");
13300 let gitlink: ObjectId = "855827c583bc30645ba427885caa40c5b81764d2"
13301 .parse()
13302 .expect("gitlink oid");
13303 let tree_oid = repo
13304 .odb()
13305 .write(ObjectKind::Tree, &encode_tree_gitlink("sub", &gitlink))
13306 .expect("write tree");
13307
13308 let path = b"sub".to_vec();
13309 let mut index = Index::new();
13310 index.add_or_replace(IndexEntry {
13311 ctime_sec: 0,
13312 ctime_nsec: 0,
13313 mtime_sec: 0,
13314 mtime_nsec: 0,
13315 dev: 0,
13316 ino: 0,
13317 mode: MODE_GITLINK,
13318 uid: 0,
13319 gid: 0,
13320 size: 0,
13321 oid: gitlink,
13322 flags: (path.len().min(0xfff)) as u16,
13323 flags_extended: None,
13324 path,
13325 base_index_pos: 0,
13326 });
13327
13328 let entries = diff_tree_to_worktree(&repo.odb(), Some(&tree_oid), work_tree, &index)
13329 .expect("diff_tree_to_worktree");
13330 assert!(
13331 entries.is_empty(),
13332 "clean uninitialized gitlink should not appear in tree→worktree diff: {entries:?}"
13333 );
13334 }
13335
13336 #[test]
13337 fn unpopulated_gitlink_with_stray_file_produces_no_diff_entry() {
13338 let dir = tempfile::tempdir().expect("tempdir");
13339 let work_tree = dir.path();
13340 let sub = work_tree.join("sub");
13341 fs::create_dir(&sub).expect("sub placeholder");
13342 fs::write(sub.join("x.txt"), b"stray\n").expect("stray file");
13343
13344 let repo = init_repository(
13345 work_tree,
13346 false,
13347 "main",
13348 None,
13349 crate::RefStorageFormat::Files,
13350 )
13351 .expect("init");
13352 let gitlink: ObjectId = "855827c583bc30645ba427885caa40c5b81764d2"
13353 .parse()
13354 .expect("gitlink oid");
13355 let tree_oid = repo
13356 .odb()
13357 .write(ObjectKind::Tree, &encode_tree_gitlink("sub", &gitlink))
13358 .expect("write tree");
13359
13360 let path = b"sub".to_vec();
13361 let mut index = Index::new();
13362 index.add_or_replace(IndexEntry {
13363 ctime_sec: 0,
13364 ctime_nsec: 0,
13365 mtime_sec: 0,
13366 mtime_nsec: 0,
13367 dev: 0,
13368 ino: 0,
13369 mode: MODE_GITLINK,
13370 uid: 0,
13371 gid: 0,
13372 size: 0,
13373 oid: gitlink,
13374 flags: (path.len().min(0xfff)) as u16,
13375 flags_extended: None,
13376 path,
13377 base_index_pos: 0,
13378 });
13379
13380 let tree_entries = diff_tree_to_worktree(&repo.odb(), Some(&tree_oid), work_tree, &index)
13381 .expect("diff_tree_to_worktree");
13382 assert!(
13383 tree_entries.is_empty(),
13384 "uninitialized gitlink with stray files should not diff: {tree_entries:?}"
13385 );
13386
13387 let wt_entries =
13388 super::diff_index_to_worktree(&repo.odb(), &mut index.clone(), work_tree, false, true)
13389 .expect("diff_index_to_worktree");
13390 assert!(
13391 wt_entries.is_empty(),
13392 "uninitialized gitlink with stray files should not diff: {wt_entries:?}"
13393 );
13394
13395 let ie = index.entries()[0].clone();
13396 assert!(
13397 !super::worktree_differs_from_index_entry(
13398 repo.odb(),
13399 repo.git_dir(),
13400 work_tree,
13401 &ie,
13402 false,
13403 None,
13404 None,
13405 )
13406 .expect("differs"),
13407 "stray files in uninitialized submodule should not differ from index"
13408 );
13409 }
13410
13411 #[cfg_attr(
13412 not(feature = "full-tests"),
13413 ignore = "starts system git or is heavy; run with --features full-tests"
13414 )]
13415 #[test]
13416 fn untracked_only_in_checked_out_submodule_produces_no_diff_entry() {
13417 use std::process::Command;
13418
13419 let dir = tempfile::tempdir().expect("tempdir");
13420 let work_tree = dir.path();
13421 fs::create_dir(work_tree.join("sub")).expect("sub dir");
13422
13423 let repo = init_repository(
13424 work_tree,
13425 false,
13426 "main",
13427 None,
13428 crate::RefStorageFormat::Files,
13429 )
13430 .expect("init");
13431 let sub = work_tree.join("sub");
13432 for args in [
13433 &["init", "-q"][..],
13434 &["config", "user.name", "T"][..],
13435 &["config", "user.email", "t@e.com"][..],
13436 ] {
13437 let status = Command::new("git")
13438 .current_dir(&sub)
13439 .args(args)
13440 .status()
13441 .expect("git");
13442 assert!(status.success(), "git {args:?} failed");
13443 }
13444 fs::write(sub.join("tracked.txt"), b"v1\n").expect("write");
13445 for args in [&["add", "tracked.txt"][..], &["commit", "-qm", "sub"][..]] {
13446 let status = Command::new("git")
13447 .current_dir(&sub)
13448 .args(args)
13449 .status()
13450 .expect("git");
13451 assert!(status.success());
13452 }
13453 let head_out = Command::new("git")
13454 .current_dir(&sub)
13455 .args(["rev-parse", "HEAD"])
13456 .output()
13457 .expect("rev-parse");
13458 let gitlink = std::str::from_utf8(&head_out.stdout)
13459 .expect("utf8")
13460 .trim()
13461 .parse::<ObjectId>()
13462 .expect("oid");
13463
13464 let tree_oid = repo
13465 .odb()
13466 .write(ObjectKind::Tree, &encode_tree_gitlink("sub", &gitlink))
13467 .expect("tree");
13468 let path = b"sub".to_vec();
13469 let mut index = Index::new();
13470 index.add_or_replace(IndexEntry {
13471 ctime_sec: 0,
13472 ctime_nsec: 0,
13473 mtime_sec: 0,
13474 mtime_nsec: 0,
13475 dev: 0,
13476 ino: 0,
13477 mode: MODE_GITLINK,
13478 uid: 0,
13479 gid: 0,
13480 size: 0,
13481 oid: gitlink,
13482 flags: (path.len().min(0xfff)) as u16,
13483 flags_extended: None,
13484 path,
13485 base_index_pos: 0,
13486 });
13487
13488 fs::write(sub.join("untracked.txt"), b"extra\n").expect("untracked");
13489
13490 let entries = diff_tree_to_worktree(&repo.odb(), Some(&tree_oid), work_tree, &index)
13491 .expect("diff_tree_to_worktree");
13492 assert!(
13493 entries.is_empty(),
13494 "untracked-only submodule dirt must not appear in default tree→worktree diff: {entries:?}"
13495 );
13496 }
13497}
13498
13499#[cfg(test)]
13500mod racy_index_mtime_diff_tests {
13501 use super::*;
13502 use crate::index::{entry_from_metadata, Index, MODE_REGULAR};
13503 use crate::objects::{HashAlgo, ObjectKind};
13504 use filetime::FileTime;
13505 use std::fs;
13506 use std::path::PathBuf;
13507
13508 const INDEX_MTIME: (u32, u32) = (1_700_000_000, 123_456_789);
13509
13510 fn pin_mtime(path: &Path, sec: u32, nsec: u32) {
13511 filetime::set_file_mtime(path, FileTime::from_unix_time(i64::from(sec), nsec))
13512 .expect("set mtime");
13513 }
13514
13515 fn minimal_sha256_repo(work_tree: &Path) -> (PathBuf, Odb) {
13516 let git_dir = work_tree.join(".git");
13517 fs::create_dir_all(git_dir.join("objects")).expect("objects dir");
13518 fs::write(
13519 git_dir.join("config"),
13520 "[extensions]\n\tobjectformat = sha256\n",
13521 )
13522 .expect("config");
13523 let odb = Odb::new(&git_dir.join("objects")).with_config_git_dir(git_dir.clone());
13524 (git_dir, odb)
13525 }
13526
13527 #[test]
13528 fn detects_same_size_change_when_entry_mtime_matches_index_mtime() {
13529 let td = tempfile::tempdir().expect("tempdir");
13530 let wt = td.path();
13531 let content_a = b"same length!!";
13532 let content_b = b"same length??";
13533 assert_eq!(content_a.len(), content_b.len());
13534
13535 fs::write(wt.join("f.txt"), content_a).expect("write");
13536 pin_mtime(&wt.join("f.txt"), INDEX_MTIME.0, INDEX_MTIME.1);
13537 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
13538 let git_dir = wt.join(".git");
13539 fs::create_dir_all(git_dir.join("objects")).expect("objects dir");
13540 let odb = Odb::new(&git_dir.join("objects"));
13541 let oid_a = odb.hash(ObjectKind::Blob, content_a).expect("hash");
13542 let entry = entry_from_metadata(&meta, b"f.txt", oid_a, MODE_REGULAR);
13543
13544 let mut index = Index::new();
13545 index.source_mtime = Some(INDEX_MTIME);
13546 index.entries.push(entry);
13547
13548 fs::write(wt.join("f.txt"), content_b).expect("rewrite");
13549 pin_mtime(&wt.join("f.txt"), INDEX_MTIME.0, INDEX_MTIME.1);
13550
13551 let diff = diff_index_to_worktree(&odb, &mut index, wt, false, false).expect("diff");
13552 assert_eq!(
13553 diff.len(),
13554 1,
13555 "racy entry must be re-hashed, not stat-trusted"
13556 );
13557 assert_eq!(diff[0].path(), "f.txt");
13558 }
13559
13560 #[test]
13561 #[cfg(unix)]
13562 fn non_utf8_symlink_clean_racy_equal_mtime_reports_no_diff() {
13563 use crate::index::MODE_SYMLINK;
13564 use std::os::unix::ffi::OsStrExt as _;
13565 use std::os::unix::fs::symlink;
13566
13567 let td = tempfile::tempdir().expect("tempdir");
13568 let wt = td.path();
13569 let target = std::ffi::OsStr::from_bytes(b"bad-\xff-target");
13570 let link_path = wt.join("link");
13571 symlink(target, &link_path).expect("symlink");
13572
13573 let ft = FileTime::from_unix_time(i64::from(INDEX_MTIME.0), INDEX_MTIME.1);
13574 filetime::set_symlink_file_times(&link_path, ft, ft).expect("set symlink mtime");
13575 let meta = fs::symlink_metadata(&link_path).expect("stat");
13576 let git_dir = wt.join(".git");
13577 fs::create_dir_all(git_dir.join("objects")).expect("objects dir");
13578 let odb = Odb::new(&git_dir.join("objects"));
13579 let oid = odb.hash(ObjectKind::Blob, target.as_bytes()).expect("hash");
13580 let entry = entry_from_metadata(&meta, b"link", oid, MODE_SYMLINK);
13581
13582 let mut index = Index::new();
13583 index.source_mtime = Some(INDEX_MTIME);
13584 index.entries.push(entry);
13585
13586 let diff = diff_index_to_worktree(&odb, &mut index, wt, false, false).expect("diff");
13587 assert!(
13588 diff.is_empty(),
13589 "clean non-UTF-8 symlink must not be reported modified: {diff:?}"
13590 );
13591 }
13592
13593 #[test]
13594 fn sha256_clean_racy_equal_mtime_reports_no_diff() {
13595 let td = tempfile::tempdir().expect("tempdir");
13596 let wt = td.path();
13597 let (_git_dir, odb) = minimal_sha256_repo(wt);
13598 assert_eq!(odb.hash_algo(), HashAlgo::Sha256);
13599
13600 let bytes = b"sha256 width!!";
13601 fs::write(wt.join("f.txt"), bytes).expect("write");
13602 pin_mtime(&wt.join("f.txt"), INDEX_MTIME.0, INDEX_MTIME.1);
13603 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
13604 let oid = odb.hash(ObjectKind::Blob, bytes).expect("hash");
13605 let entry = entry_from_metadata(&meta, b"f.txt", oid, MODE_REGULAR);
13606
13607 let mut index = Index::new();
13608 index.hash_algo = HashAlgo::Sha256;
13609 index.source_mtime = Some(INDEX_MTIME);
13610 index.entries.push(entry);
13611
13612 let diff = diff_index_to_worktree(&odb, &mut index, wt, false, false).expect("diff");
13613 assert!(
13614 diff.is_empty(),
13615 "clean SHA-256 racy entry must not be reported modified: {diff:?}"
13616 );
13617 }
13618}
13619
13620#[cfg(test)]
13621mod smudge_racily_clean_tests {
13622 use super::*;
13623 use crate::config::ConfigSet;
13624 use crate::crlf::{self, FileAttrs};
13625 use crate::index::{entry_from_metadata, Index, MODE_REGULAR};
13626 use crate::objects::{HashAlgo, ObjectKind};
13627 use filetime::FileTime;
13628 use std::fs;
13629 use std::path::PathBuf;
13630
13631 const INDEX_MTIME_PIN: (u32, u32) = (1_700_000_000, 123_456_789);
13632
13633 fn mtime_from_metadata(meta: &fs::Metadata) -> (u32, u32) {
13634 #[cfg(unix)]
13635 {
13636 use std::os::unix::fs::MetadataExt;
13637 (meta.mtime() as u32, meta.mtime_nsec() as u32)
13638 }
13639 #[cfg(not(unix))]
13640 {
13641 use std::time::UNIX_EPOCH;
13642 let mtime = meta
13643 .modified()
13644 .ok()
13645 .and_then(|t| t.duration_since(UNIX_EPOCH).ok())
13646 .unwrap_or_default();
13647 (mtime.as_secs() as u32, mtime.subsec_nanos())
13648 }
13649 }
13650
13651 fn pin_mtime_and_sample(path: &Path, sec: u32, nsec: u32) -> (u32, u32) {
13653 filetime::set_file_mtime(path, FileTime::from_unix_time(i64::from(sec), nsec))
13654 .expect("set mtime");
13655 let meta = fs::symlink_metadata(path).expect("stat file");
13656 mtime_from_metadata(&meta)
13657 }
13658
13659 fn setup_index_mtime(work_tree: &Path) -> (u32, u32) {
13661 let probe = work_tree.join(".grit-index-mtime-probe");
13662 fs::write(&probe, b"").expect("probe file");
13663 pin_mtime_and_sample(&probe, INDEX_MTIME_PIN.0, INDEX_MTIME_PIN.1)
13664 }
13665
13666 fn minimal_repo(work_tree: &Path, config: &str) -> (PathBuf, Odb) {
13667 let git_dir = work_tree.join(".git");
13668 fs::create_dir_all(git_dir.join("objects")).expect("objects dir");
13669 fs::write(git_dir.join("config"), config).expect("config");
13670 let odb = Odb::new(&git_dir.join("objects")).with_config_git_dir(git_dir.clone());
13671 (git_dir, odb)
13672 }
13673
13674 fn staged_entry_for_file(
13675 odb: &Odb,
13676 work_tree: &Path,
13677 rel: &str,
13678 bytes: &[u8],
13679 mtime: (u32, u32),
13680 ) -> IndexEntry {
13681 let path = work_tree.join(rel);
13682 fs::write(&path, bytes).expect("write file");
13683 pin_mtime_and_sample(&path, mtime.0, mtime.1);
13684 let meta = fs::symlink_metadata(&path).expect("stat file");
13685 let oid = odb.hash(ObjectKind::Blob, bytes).expect("hash");
13686 entry_from_metadata(&meta, rel.as_bytes(), oid, MODE_REGULAR)
13687 }
13688
13689 fn staged_entry_with_oid(
13690 work_tree: &Path,
13691 rel: &str,
13692 worktree_bytes: &[u8],
13693 oid: crate::objects::ObjectId,
13694 mtime: (u32, u32),
13695 ) -> IndexEntry {
13696 let path = work_tree.join(rel);
13697 fs::write(&path, worktree_bytes).expect("write file");
13698 pin_mtime_and_sample(&path, mtime.0, mtime.1);
13699 let meta = fs::symlink_metadata(&path).expect("stat file");
13700 entry_from_metadata(&meta, rel.as_bytes(), oid, MODE_REGULAR)
13701 }
13702
13703 #[test]
13704 fn index_stat_cache_trustworthy_rejects_unknown_mtime() {
13705 assert!(!super::index_stat_cache_trustworthy(None));
13706 assert!(!super::index_stat_cache_trustworthy(Some((0, 0))));
13707 assert!(super::index_stat_cache_trustworthy(Some((1, 0))));
13708 }
13709
13710 #[test]
13711 fn index_entry_skips_content_hash_after_stat_match_when_trusted_and_not_racy() {
13712 use crate::index::StatMatchPolicy;
13713 let td = tempfile::tempdir().expect("tempdir");
13714 let wt = td.path();
13715 let (git_dir, odb) = minimal_repo(wt, "[core]\n\tfilemode = true\n");
13716 let rel = "trusted.txt";
13717 let file_mtime = (1_700_000_000, 0);
13718 let mut entry = staged_entry_for_file(&odb, wt, rel, b"payload\n", file_mtime);
13719 let index_mtime = (file_mtime.0 + 1, 0);
13720 let policy = StatMatchPolicy::from_config(None);
13721 assert!(
13722 super::index_entry_skips_worktree_content_hash_after_stat_match(
13723 &entry,
13724 Some(index_mtime),
13725 policy,
13726 )
13727 );
13728 entry.size = 0;
13729 entry.mtime_sec = 0;
13730 entry.mtime_nsec = 0;
13731 assert!(
13732 !super::index_entry_skips_worktree_content_hash_after_stat_match(
13733 &entry,
13734 Some(index_mtime),
13735 policy,
13736 )
13737 );
13738 let _ = git_dir;
13739 }
13740
13741 #[cfg(unix)]
13742 #[test]
13743 fn symlink_dir_cache_detects_symlinked_ancestors_across_threads() {
13744 let td = tempfile::tempdir().expect("tempdir");
13745 let wt = td.path();
13746 fs::create_dir_all(wt.join("real/deep")).expect("mkdir");
13747 fs::create_dir_all(wt.join("target/deep")).expect("mkdir");
13748 std::os::unix::fs::symlink(wt.join("target"), wt.join("linked")).expect("symlink");
13749 std::thread::scope(|scope| {
13750 for _ in 0..4 {
13751 scope.spawn(|| {
13752 let mut cache = super::SymlinkDirCache::default();
13753 assert!(!cache.has_symlink_in_path_bytes(wt, b"top.txt", false, false));
13754 assert!(!cache.has_symlink_in_path_bytes(wt, b"linked", false, false));
13755 assert!(!cache.has_symlink_in_path_bytes(wt, b"real/deep/f", false, false));
13756 assert!(cache.has_symlink_in_path_bytes(wt, b"linked/f", false, false));
13757 assert!(cache.has_symlink_in_path_bytes(wt, b"linked/deep/f", false, false));
13758 assert!(!cache.has_symlink_in_path_bytes(wt, b"\xff\xfe", false, false));
13759 });
13760 }
13761 });
13762 }
13763
13764 #[cfg(unix)]
13765 #[test]
13766 fn symlink_dir_cache_accepts_a_work_tree_reached_through_a_symlink() {
13767 let td = tempfile::tempdir().expect("tempdir");
13768 let real = td.path().join("real");
13769 fs::create_dir_all(real.join("a/b/c")).expect("mkdir");
13770 fs::create_dir_all(real.join("t/b/c")).expect("mkdir");
13771 std::os::unix::fs::symlink(real.join("t"), real.join("l")).expect("symlink");
13772 let wt = td.path().join("alias");
13773 std::os::unix::fs::symlink(&real, &wt).expect("symlink");
13774 let mut cache = super::SymlinkDirCache::default();
13775 assert!(!cache.has_symlink_in_path_bytes(&wt, b"a/b/c/f", false, false));
13776 assert!(cache.has_symlink_in_path_bytes(&wt, b"l/b/c/f", false, false));
13777 assert!(!cache.has_symlink_in_path_bytes(&real, b"a/b/c/f", false, false));
13778 assert!(cache.has_symlink_in_path_bytes(&real, b"l/b/c/f", false, false));
13779 }
13780
13781 #[cfg(all(target_os = "linux", target_pointer_width = "64"))]
13782 #[test]
13783 fn real_dir_chain_confirms_only_symlink_free_directories() {
13784 let td = tempfile::tempdir().expect("tempdir");
13785 let wt = td.path();
13786 fs::create_dir_all(wt.join("a/b/c")).expect("mkdir");
13787 fs::create_dir_all(wt.join("t/c")).expect("mkdir");
13788 fs::write(wt.join("a/file"), b"f").expect("write");
13789 std::os::unix::fs::symlink(wt.join("t"), wt.join("a/l")).expect("symlink");
13790 let mut chain = crate::worktree_scan::RealDirChain::open(wt);
13791 assert_eq!(chain.work_tree(), wt);
13792 assert!(chain.confirms(b"a"));
13793 assert!(chain.confirms(b"a/b/c"));
13794 assert!(!chain.confirms(b"a/l"));
13795 assert!(!chain.confirms(b"a/l/c"));
13796 assert!(!chain.confirms(b"a/file"));
13797 assert!(!chain.confirms(b"a/missing/c"));
13798 assert!(!chain.confirms(b""));
13799 assert!(!chain.confirms(b"a/\0b"));
13800 assert!(chain.confirms(b"a/b"));
13801 }
13802
13803 #[test]
13804 fn symlink_dir_cache_probes_each_new_directory_once_in_index_order() {
13805 let td = tempfile::tempdir().expect("tempdir");
13806 let wt = td.path();
13807 let depth = 12;
13808 let chains = 20;
13809 let mut paths = Vec::new();
13810 for chain in 0..chains {
13811 let dir: Vec<String> = (0..depth)
13812 .map(|level| format!("c{chain:02}l{level:02}"))
13813 .collect();
13814 let dir = format!("shared/{}", dir.join("/"));
13815 fs::create_dir_all(wt.join(&dir)).expect("mkdir");
13816 paths.extend((0..3).map(|file| format!("{dir}/f{file}")));
13817 }
13818 paths.sort();
13819 let mut cache = super::SymlinkDirCache::default();
13820 for rel in &paths {
13821 assert!(!cache.has_symlink_in_path_bytes(wt, rel.as_bytes(), false, false));
13822 }
13823 let unique_dirs = 1 + chains * depth;
13825 assert!(
13826 cache.fs_queries <= unique_dirs,
13827 "{} filesystem queries for {unique_dirs} directories",
13828 cache.fs_queries
13829 );
13830 #[cfg(all(target_os = "linux", target_pointer_width = "64"))]
13831 assert!(
13832 cache.fs_queries <= chains + 1,
13833 "one chain walk per new directory: {} queries for {chains} chains",
13834 cache.fs_queries
13835 );
13836 }
13837
13838 #[test]
13839 fn common_dir_prefix_len_stops_at_whole_components() {
13840 use super::{common_dir_prefix_len, path_within_dir};
13841 assert_eq!(common_dir_prefix_len(b"a/b/c", b"a/b/c"), 5);
13842 assert_eq!(common_dir_prefix_len(b"a/b/c", b"a/b"), 3);
13843 assert_eq!(common_dir_prefix_len(b"a/b", b"a/b/c"), 3);
13844 assert_eq!(common_dir_prefix_len(b"a/bc/d", b"a/b"), 1);
13845 assert_eq!(common_dir_prefix_len(b"a/b", b"a/bc/d"), 1);
13846 assert_eq!(common_dir_prefix_len(b"ab", b"a"), 0);
13847 assert_eq!(common_dir_prefix_len(b"a/b", b""), 0);
13848 assert!(path_within_dir(b"a/b", b"a/b"));
13849 assert!(path_within_dir(b"a/b/c", b"a/b"));
13850 assert!(!path_within_dir(b"a/bc", b"a/b"));
13851 assert!(!path_within_dir(b"a", b"a/b"));
13852 }
13853
13854 #[cfg(unix)]
13856 fn naive_has_symlink_ancestor(wt: &Path, rel: &str) -> bool {
13857 let Some((dir, _)) = rel.rsplit_once('/') else {
13858 return false;
13859 };
13860 let mut prefix = wt.to_path_buf();
13861 dir.split('/').any(|component| {
13862 prefix.push(component);
13863 fs::symlink_metadata(&prefix).is_ok_and(|m| m.file_type().is_symlink())
13864 })
13865 }
13866
13867 #[cfg(unix)]
13868 #[test]
13869 fn symlink_dir_cache_matches_uncached_probe_in_any_query_order() {
13870 let td = tempfile::tempdir().expect("tempdir");
13871 let wt = td.path();
13872 fs::create_dir_all(wt.join("a/b/c/d")).expect("mkdir");
13873 fs::create_dir_all(wt.join("a/bb/c")).expect("mkdir");
13874 fs::create_dir_all(wt.join("a/b2")).expect("mkdir");
13875 fs::create_dir_all(wt.join("t/c/d")).expect("mkdir");
13876 fs::create_dir_all(wt.join("x/y")).expect("mkdir");
13877 fs::write(wt.join("a/file"), b"f").expect("write");
13878 fs::write(wt.join("x/y/plain"), b"f").expect("write");
13879 std::os::unix::fs::symlink(wt.join("t"), wt.join("a/b/link")).expect("symlink");
13880 std::os::unix::fs::symlink(wt.join("t"), wt.join("x/l")).expect("symlink");
13881 std::os::unix::fs::symlink(wt.join("t"), wt.join("top")).expect("symlink");
13882
13883 let paths = [
13884 "a/b/c/d/f",
13885 "a/b/c/f",
13886 "a/b/f",
13887 "a/b/link/f",
13888 "a/b/link/c/d/f",
13889 "a/b/linker/f",
13890 "a/b2/f",
13891 "a/bb/c/f",
13892 "a/file/under/f",
13893 "a/missing/deeper/f",
13894 "a/missing/f",
13895 "a/f",
13896 "f",
13897 "top/c/f",
13898 "top/f",
13899 "topper/f",
13900 "x/l/c/d/f",
13901 "x/y/f",
13902 "x/y/plain/f",
13903 ];
13904 let mut orders: Vec<Vec<&str>> = vec![paths.to_vec()];
13905 orders.push(paths.iter().rev().copied().collect());
13906 orders.push(
13907 paths
13908 .iter()
13909 .step_by(2)
13910 .chain(paths.iter().skip(1).step_by(2))
13911 .copied()
13912 .collect(),
13913 );
13914 orders.push(paths.iter().flat_map(|p| [*p, *p]).collect());
13915 for order in orders {
13916 let mut cache = super::SymlinkDirCache::default();
13917 for rel in order {
13918 assert_eq!(
13919 cache.has_symlink_in_path_bytes(wt, rel.as_bytes(), false, false),
13920 naive_has_symlink_ancestor(wt, rel),
13921 "ancestor symlink answer for {rel}"
13922 );
13923 }
13924 }
13925 }
13926
13927 #[test]
13928 fn content_hash_skip_trusts_empty_blob_but_not_smudged_size() {
13929 use crate::index::StatMatchPolicy;
13930 let td = tempfile::tempdir().expect("tempdir");
13931 let wt = td.path();
13932 let (_git_dir, odb) = minimal_repo(wt, "[core]\n\tfilemode = false\n");
13933 let file_mtime = (1_700_000_000, 0);
13934 let index_mtime = Some((file_mtime.0 + 1, 0));
13935 let policy = StatMatchPolicy::from_config(None);
13936
13937 let empty = staged_entry_for_file(&odb, wt, "empty.txt", b"", file_mtime);
13938 assert_eq!(empty.size, 0);
13939 assert!(
13940 super::index_entry_skips_worktree_content_hash_after_stat_match(
13941 &empty,
13942 index_mtime,
13943 policy,
13944 )
13945 );
13946
13947 let smudged_oid = odb.hash(ObjectKind::Blob, b"abc\n").expect("hash");
13948 let smudged = staged_entry_with_oid(wt, "smudged.txt", b"", smudged_oid, file_mtime);
13949 assert_eq!(smudged.size, 0);
13950 assert!(
13951 !super::index_entry_skips_worktree_content_hash_after_stat_match(
13952 &smudged,
13953 index_mtime,
13954 policy,
13955 )
13956 );
13957 }
13958
13959 #[test]
13960 fn status_diff_reports_smudged_entry_whose_file_became_empty() {
13961 let td = tempfile::tempdir().expect("tempdir");
13962 let wt = td.path();
13963 let (git_dir, odb) = minimal_repo(wt, "[core]\n\tfilemode = false\n");
13964 let oid = odb.write(ObjectKind::Blob, b"abc\n").expect("write blob");
13965 let file_mtime = (1_700_000_000, 0);
13966 let mut index = Index::new();
13967 index.entries.push(staged_entry_with_oid(
13968 wt,
13969 "smudged.txt",
13970 b"",
13971 oid,
13972 file_mtime,
13973 ));
13974 let (entries, _) = super::diff_index_to_worktree_with_options(
13975 &odb,
13976 &mut index,
13977 wt,
13978 super::DiffIndexToWorktreeOptions {
13979 repository_git_dir: Some(git_dir),
13980 index_mtime: Some((file_mtime.0 + 1, 0)),
13981 for_status: true,
13982 stat_parallel_threads: Some(1),
13983 ..Default::default()
13984 },
13985 )
13986 .expect("diff");
13987 assert_eq!(entries.len(), 1, "{entries:?}");
13988 assert_eq!(entries[0].status, DiffStatus::Modified);
13989 }
13990
13991 #[test]
13992 fn status_diff_verifies_content_when_index_stat_cache_unset() {
13993 let td = tempfile::tempdir().expect("tempdir");
13994 let wt = td.path();
13995 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
13996 let rel = "unset-stat.txt";
13997 let bytes = b"same bytes\n";
13998 let oid = odb.hash(ObjectKind::Blob, bytes).expect("hash");
13999 fs::write(wt.join(rel), bytes).expect("write");
14000 let mut index = Index::new();
14001 let mut entry = staged_entry_for_file(&odb, wt, rel, bytes, (100, 0));
14002 entry.size = 0;
14003 entry.mtime_sec = 0;
14004 entry.mtime_nsec = 0;
14005 entry.oid = oid;
14006 index.entries.push(entry);
14007 let (entries, _) = super::diff_index_to_worktree_with_options(
14008 &odb,
14009 &mut index,
14010 wt,
14011 super::DiffIndexToWorktreeOptions {
14012 repository_git_dir: Some(git_dir),
14013 for_status: true,
14014 ..Default::default()
14015 },
14016 )
14017 .expect("diff");
14018 assert!(
14019 entries.is_empty(),
14020 "unset stat must not size-mismatch a matching worktree: {entries:?}"
14021 );
14022 }
14023
14024 #[cfg_attr(
14025 not(feature = "full-tests"),
14026 ignore = "starts system git or is heavy; run with --features full-tests"
14027 )]
14028 #[test]
14029 fn status_reports_modified_when_ctime_differs_but_mtime_sec_and_size_match_non_racy() {
14030 let td = tempfile::tempdir().expect("tempdir");
14031 let wt = td.path();
14032 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
14033 let rel = "tracked.txt";
14034 let original = b"original\n";
14035 let modified = b"modified\n";
14036 assert_eq!(original.len(), modified.len());
14037
14038 let entry_mtime = (1_700_000_000u32, 0u32);
14039 let index_file_mtime = (1_700_000_100u32, 0u32);
14040
14041 std::process::Command::new("git")
14042 .args(["-C", &wt.to_string_lossy(), "init", "-q"])
14043 .status()
14044 .expect("git init");
14045 std::process::Command::new("git")
14046 .args([
14047 "-C",
14048 &wt.to_string_lossy(),
14049 "config",
14050 "user.email",
14051 "t@example.com",
14052 ])
14053 .status()
14054 .expect("git config email");
14055 std::process::Command::new("git")
14056 .args(["-C", &wt.to_string_lossy(), "config", "user.name", "T"])
14057 .status()
14058 .expect("git config name");
14059
14060 fs::write(wt.join(rel), original).expect("write original");
14061 pin_mtime_and_sample(&wt.join(rel), entry_mtime.0, entry_mtime.1);
14062 std::process::Command::new("git")
14063 .args(["-C", &wt.to_string_lossy(), "add", rel])
14064 .status()
14065 .expect("git add");
14066 std::process::Command::new("git")
14067 .args([
14068 "-C",
14069 &wt.to_string_lossy(),
14070 "commit",
14071 "-qm",
14072 "seed",
14073 "--no-gpg-sign",
14074 ])
14075 .status()
14076 .expect("git commit");
14077
14078 let mut index = Index::new();
14079 index.source_mtime = Some(index_file_mtime);
14080 let staged = staged_entry_for_file(&odb, wt, rel, original, entry_mtime);
14081 let oid_original = staged.oid;
14082 index.entries.push(staged);
14083
14084 fs::write(wt.join(rel), modified).expect("modify worktree");
14085 let git_dirty = std::process::Command::new("git")
14086 .args([
14087 "-C",
14088 &wt.to_string_lossy(),
14089 "--no-optional-locks",
14090 "status",
14091 "--short",
14092 ])
14093 .output()
14094 .expect("git status");
14095 assert!(
14096 git_dirty.status.success(),
14097 "git status failed: {}",
14098 String::from_utf8_lossy(&git_dirty.stderr)
14099 );
14100 assert!(
14101 String::from_utf8_lossy(&git_dirty.stdout).contains("tracked.txt"),
14102 "system git must see modification before mtime pin: {}",
14103 String::from_utf8_lossy(&git_dirty.stdout)
14104 );
14105
14106 pin_mtime_and_sample(&wt.join(rel), entry_mtime.0, entry_mtime.1);
14107
14108 let meta = fs::symlink_metadata(wt.join(rel)).expect("stat");
14109 let ie = &mut index.entries[0];
14110 assert_eq!(ie.oid, oid_original);
14111 if stat_matches(ie, &meta) {
14112 ie.ctime_sec = ie.ctime_sec.wrapping_sub(3600);
14113 }
14114 assert!(
14115 !stat_matches(ie, &meta),
14116 "ctime/inode change must fail full stat match"
14117 );
14118 let stat_policy = crate::index::StatMatchPolicy::default();
14119 assert!(
14120 !entry_is_racy_with_policy(ie, Some(index_file_mtime), stat_policy),
14121 "fixture must be non-racy"
14122 );
14123
14124 let (entries, index_changed) = super::diff_index_to_worktree_with_options(
14125 &odb,
14126 &mut index,
14127 wt,
14128 super::DiffIndexToWorktreeOptions {
14129 index_mtime: Some(index_file_mtime),
14130 repository_git_dir: Some(git_dir),
14131 refresh_index_stat_in_pass: true,
14132 for_status: true,
14133 ..Default::default()
14134 },
14135 )
14136 .expect("diff");
14137 assert_eq!(
14138 entries.len(),
14139 1,
14140 "must report modified, not refresh stat clean: {entries:?}, index_changed={index_changed}"
14141 );
14142 assert_eq!(entries[0].path(), rel);
14143 }
14144
14145 #[test]
14146 fn diff_name_status_skips_big_worktree_read_on_size_mismatch() {
14147 let td = tempfile::tempdir().expect("tempdir");
14148 let wt = td.path();
14149 let (git_dir, odb) = minimal_repo(wt, "[core]\n\tbigFileThreshold = 4k\n");
14150 let rel = "big.bin";
14151 let indexed = vec![b'x'; 100];
14152 let mut index = Index::new();
14153 index
14154 .entries
14155 .push(staged_entry_for_file(&odb, wt, rel, &indexed, (100, 0)));
14156 fs::write(wt.join(rel), vec![b'y'; 5000]).expect("rewrite bigger");
14157 let metrics = odb.hot_path_metrics();
14158 metrics.reset_blob_content_reads();
14159 metrics.set_blob_counting(true);
14160 let (entries, _) = super::diff_index_to_worktree_with_options(
14161 &odb,
14162 &mut index,
14163 wt,
14164 super::DiffIndexToWorktreeOptions {
14165 repository_git_dir: Some(git_dir),
14166 for_status: false,
14167 ..Default::default()
14168 },
14169 )
14170 .expect("diff");
14171 metrics.set_blob_counting(false);
14172 assert_eq!(entries.len(), 1);
14173 assert_eq!(entries[0].status, super::DiffStatus::Modified);
14174 assert_eq!(entries[0].new_oid, super::zero_oid());
14175 assert_eq!(
14176 metrics.blob_content_reads(),
14177 0,
14178 "large size-only drift must not hash or read the worktree"
14179 );
14180 }
14181
14182 #[test]
14183 fn status_diff_skips_hash_when_worktree_size_differs() {
14184 let td = tempfile::tempdir().expect("tempdir");
14185 let wt = td.path();
14186 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
14187 let rel = "size-change.txt";
14188 let original = b"short\n";
14189 let mut index = Index::new();
14190 index
14191 .entries
14192 .push(staged_entry_for_file(&odb, wt, rel, original, (100, 0)));
14193 fs::write(wt.join(rel), b"longer content\n").expect("rewrite");
14194 let metrics = odb.hot_path_metrics();
14195 metrics.reset_blob_content_reads();
14196 metrics.set_blob_counting(true);
14197 let (entries, _) = super::diff_index_to_worktree_with_options(
14198 &odb,
14199 &mut index,
14200 wt,
14201 super::DiffIndexToWorktreeOptions {
14202 repository_git_dir: Some(git_dir),
14203 for_status: true,
14204 ..Default::default()
14205 },
14206 )
14207 .expect("diff");
14208 metrics.set_blob_counting(false);
14209 assert_eq!(entries.len(), 1);
14210 assert_eq!(entries[0].status, super::DiffStatus::Modified);
14211 assert_eq!(entries[0].path(), rel);
14212 assert_eq!(entries[0].old_oid, index.entries[0].oid);
14213 assert_eq!(entries[0].new_oid, super::zero_oid());
14214 assert_eq!(
14215 metrics.blob_content_reads(),
14216 0,
14217 "identity conversion: a size change alone proves the file modified"
14218 );
14219 }
14220
14221 #[test]
14222 fn status_diff_hashes_size_change_when_eol_conversion_restores_blob() {
14223 for refresh_index_stat_in_pass in [false, true] {
14224 let td = tempfile::tempdir().expect("tempdir");
14225 let wt = td.path();
14226 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
14227 fs::write(wt.join(".gitattributes"), "*.txt text eol=crlf\n").expect("attrs");
14228 let rel = "n.txt";
14229 let blob = odb
14230 .write(ObjectKind::Blob, b"one\ntwo\n")
14231 .expect("write blob");
14232 let mut index = Index::new();
14233 index.entries.push(staged_entry_with_oid(
14234 wt,
14235 rel,
14236 b"one\r\ntwo\r\n",
14237 blob,
14238 (100, 0),
14239 ));
14240 fs::write(wt.join(rel), b"one\ntwo\n").expect("rewrite as LF");
14241 let (entries, index_changed) = super::diff_index_to_worktree_with_options(
14242 &odb,
14243 &mut index,
14244 wt,
14245 super::DiffIndexToWorktreeOptions {
14246 index_mtime: Some((200, 0)),
14247 repository_git_dir: Some(git_dir),
14248 refresh_index_stat_in_pass,
14249 for_status: true,
14250 ..Default::default()
14251 },
14252 )
14253 .expect("diff");
14254 assert!(
14255 entries.is_empty(),
14256 "refresh={refresh_index_stat_in_pass}: LF worktree cleans to the indexed blob: \
14257 {entries:?}"
14258 );
14259 if refresh_index_stat_in_pass {
14260 assert!(index_changed, "verified entry adopts the new stat");
14261 assert_eq!(index.entries[0].size, 8);
14262 }
14263 }
14264 }
14265
14266 #[test]
14267 fn status_diff_reads_size_changed_converted_file_once() {
14268 for refresh_index_stat_in_pass in [false, true] {
14269 let td = tempfile::tempdir().expect("tempdir");
14270 let wt = td.path();
14271 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
14272 fs::write(wt.join(".gitattributes"), "*.txt text eol=crlf\n").expect("attrs");
14273 let rel = "n.txt";
14274 let blob = odb
14275 .write(ObjectKind::Blob, b"one\ntwo\n")
14276 .expect("write blob");
14277 let mut index = Index::new();
14278 index.entries.push(staged_entry_with_oid(
14279 wt,
14280 rel,
14281 b"one\r\ntwo\r\n",
14282 blob,
14283 (100, 0),
14284 ));
14285 fs::write(wt.join(rel), b"one\ntwo\nthree\n").expect("real edit");
14286 let metrics = odb.hot_path_metrics();
14287 metrics.reset_blob_content_reads();
14288 metrics.set_blob_counting(true);
14289 let (entries, _) = super::diff_index_to_worktree_with_options(
14290 &odb,
14291 &mut index,
14292 wt,
14293 super::DiffIndexToWorktreeOptions {
14294 index_mtime: Some((200, 0)),
14295 repository_git_dir: Some(git_dir),
14296 refresh_index_stat_in_pass,
14297 for_status: true,
14298 ..Default::default()
14299 },
14300 )
14301 .expect("diff");
14302 metrics.set_blob_counting(false);
14303 assert_eq!(entries.len(), 1, "refresh={refresh_index_stat_in_pass}");
14304 assert_eq!(entries[0].status, super::DiffStatus::Modified);
14305 assert_eq!(entries[0].new_oid, zero_oid());
14306 assert_eq!(
14307 index.entries[0].size, 10,
14308 "a modified file keeps its stale stat"
14309 );
14310 assert_eq!(
14311 metrics.blob_content_reads(),
14312 1,
14313 "refresh={refresh_index_stat_in_pass}: read once, not again after the refresh"
14314 );
14315 }
14316 }
14317
14318 #[test]
14319 fn stat_matches_ignores_subsecond_nsec_by_default() {
14320 let td = tempfile::tempdir().expect("tempdir");
14321 let wt = td.path();
14322 fs::write(wt.join("f.txt"), b"x\n").expect("write");
14323 pin_mtime_and_sample(&wt.join("f.txt"), 100, 0);
14324 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
14325 let mut entry = entry_from_metadata(&meta, b"f.txt", zero_oid(), MODE_REGULAR);
14326 entry.mtime_nsec = entry.mtime_nsec.wrapping_add(1_000_000);
14327 assert!(stat_matches(&entry, &meta));
14328 }
14329
14330 #[test]
14331 fn classify_add_up_to_date_when_stat_matches() {
14332 let td = tempfile::tempdir().expect("tempdir");
14333 let wt = td.path();
14334 let (_git_dir, odb) = minimal_repo(wt, "[core]\n");
14335 let bytes = b"hello world\n";
14336 let entry = staged_entry_for_file(&odb, wt, "f.txt", bytes, (100, 0));
14337 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
14338 let conv = crlf::ConversionConfig::from_config(&ConfigSet::new());
14339 let attrs = FileAttrs::default();
14340 let params = WorktreeAddRefreshParams {
14341 odb: &odb,
14342 ie: &entry,
14343 meta: &meta,
14344 abs_path: &wt.join("f.txt"),
14345 rel_path: "f.txt",
14346 conv: &conv,
14347 file_attrs: &attrs,
14348 index_mtime: Some((200, 0)),
14349 staged_mode: MODE_REGULAR,
14350 filter_process: None,
14351 };
14352 let refresh = classify_worktree_entry_for_add(¶ms).expect("classify");
14353 assert_eq!(refresh, WorktreeAddRefresh::UpToDate);
14354 }
14355
14356 #[test]
14357 fn classify_add_rehashes_when_index_mtime_unknown_and_stat_matches() {
14358 let td = tempfile::tempdir().expect("tempdir");
14359 let wt = td.path();
14360 let (_git_dir, odb) = minimal_repo(wt, "[core]\n");
14361 let entry = staged_entry_for_file(&odb, wt, "f.txt", b"old content\n", (100, 0));
14362 fs::write(wt.join("f.txt"), b"new content\n").expect("rewrite");
14363 pin_mtime_and_sample(&wt.join("f.txt"), 100, 0);
14364 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
14365 let conv = crlf::ConversionConfig::from_config(&ConfigSet::new());
14366 let attrs = FileAttrs::default();
14367 let params = WorktreeAddRefreshParams {
14368 odb: &odb,
14369 ie: &entry,
14370 meta: &meta,
14371 abs_path: &wt.join("f.txt"),
14372 rel_path: "f.txt",
14373 conv: &conv,
14374 file_attrs: &attrs,
14375 index_mtime: None,
14376 staged_mode: MODE_REGULAR,
14377 filter_process: None,
14378 };
14379 let refresh = classify_worktree_entry_for_add(¶ms).expect("classify");
14380 assert_eq!(refresh, WorktreeAddRefresh::NeedsRestage);
14381 }
14382
14383 #[test]
14384 fn classify_add_needs_restaging_when_content_differs() {
14385 let td = tempfile::tempdir().expect("tempdir");
14386 let wt = td.path();
14387 let (_git_dir, odb) = minimal_repo(wt, "[core]\n");
14388 let entry = staged_entry_for_file(&odb, wt, "f.txt", b"old content\n", (100, 0));
14389 fs::write(wt.join("f.txt"), b"new content\n").expect("rewrite");
14390 pin_mtime_and_sample(&wt.join("f.txt"), 101, 0);
14391 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
14392 let conv = crlf::ConversionConfig::from_config(&ConfigSet::new());
14393 let attrs = FileAttrs::default();
14394 let params = WorktreeAddRefreshParams {
14395 odb: &odb,
14396 ie: &entry,
14397 meta: &meta,
14398 abs_path: &wt.join("f.txt"),
14399 rel_path: "f.txt",
14400 conv: &conv,
14401 file_attrs: &attrs,
14402 index_mtime: Some((200, 0)),
14403 staged_mode: MODE_REGULAR,
14404 filter_process: None,
14405 };
14406 let refresh = classify_worktree_entry_for_add(¶ms).expect("classify");
14407 assert_eq!(refresh, WorktreeAddRefresh::NeedsRestage);
14408 }
14409
14410 #[test]
14411 fn smudges_racily_clean_modified_entry() {
14412 let td = tempfile::tempdir().expect("tempdir");
14413 let wt = td.path();
14414 let index_mtime = setup_index_mtime(wt);
14415 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
14416 let content_a = b"same length!!";
14417 let content_b = b"same length??";
14418 assert_eq!(content_a.len(), content_b.len());
14419
14420 let mut index = Index::new();
14421 let entry = staged_entry_for_file(&odb, wt, "f.txt", content_a, index_mtime);
14422 let stale_oid = entry.oid;
14423
14424 fs::write(wt.join("f.txt"), content_b).expect("rewrite");
14425 pin_mtime_and_sample(&wt.join("f.txt"), index_mtime.0, index_mtime.1);
14426 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
14427 let entry = entry_from_metadata(&meta, b"f.txt", stale_oid, MODE_REGULAR);
14429 index.entries.push(entry.clone());
14430
14431 assert!(stat_matches(&entry, &meta));
14432 assert!(entry_is_racy(&entry, Some(index_mtime)));
14433
14434 let smudged = smudge_racily_clean_entries(
14435 &odb,
14436 &git_dir,
14437 &mut index,
14438 wt,
14439 Some(index_mtime),
14440 None,
14441 None,
14442 );
14443 assert!(smudged);
14444 assert_eq!(index.entries[0].size, 0);
14445
14446 let diff = diff_index_to_worktree(&odb, &mut index, wt, false, false).expect("diff");
14447 assert_eq!(diff.len(), 1);
14448 assert_eq!(diff[0].path(), "f.txt");
14449 }
14450
14451 fn racy_stale_entry(wt: &Path) -> (PathBuf, Odb, Index, (u32, u32), ObjectId) {
14453 let index_mtime = setup_index_mtime(wt);
14454 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
14455 let entry = staged_entry_for_file(&odb, wt, "f.txt", b"same length!!", index_mtime);
14456 let stale_oid = entry.oid;
14457 fs::write(wt.join("f.txt"), b"same length??").expect("rewrite");
14458 pin_mtime_and_sample(&wt.join("f.txt"), index_mtime.0, index_mtime.1);
14459 let meta = fs::symlink_metadata(wt.join("f.txt")).expect("stat");
14460 let mut index = Index::new();
14461 index.entries.push(entry_from_metadata(
14462 &meta,
14463 b"f.txt",
14464 stale_oid,
14465 MODE_REGULAR,
14466 ));
14467 (git_dir, odb, index, index_mtime, stale_oid)
14468 }
14469
14470 #[test]
14471 fn content_verified_entry_is_not_reread_and_the_mark_is_consumed() {
14472 let td = tempfile::tempdir().expect("tempdir");
14473 let wt = td.path();
14474 let (git_dir, odb, mut index, index_mtime, stale_oid) = racy_stale_entry(wt);
14475 index.mark_content_verified(b"f.txt", stale_oid);
14476
14477 let smudge = |index: &mut Index| {
14478 smudge_racily_clean_entries(&odb, &git_dir, index, wt, Some(index_mtime), None, None)
14479 };
14480 assert!(!smudge(&mut index), "a verified entry is trusted");
14481 assert_ne!(index.entries[0].size, 0);
14482 assert!(index.content_verified.is_empty());
14483 assert!(smudge(&mut index), "without the mark the entry is checked");
14484 assert_eq!(index.entries[0].size, 0);
14485 }
14486
14487 #[test]
14488 fn content_verified_entry_bit_is_trusted_and_consumed() {
14489 let td = tempfile::tempdir().expect("tempdir");
14490 let wt = td.path();
14491 let (git_dir, odb, mut index, index_mtime, _) = racy_stale_entry(wt);
14492 index.entries[0].mark_content_verified();
14493
14494 let smudge = |index: &mut Index| {
14495 smudge_racily_clean_entries(&odb, &git_dir, index, wt, Some(index_mtime), None, None)
14496 };
14497 assert!(!smudge(&mut index), "a marked entry is trusted");
14498 assert_ne!(index.entries[0].size, 0);
14499 assert_eq!(
14500 index.entries[0].flags_extended, None,
14501 "the mark is consumed"
14502 );
14503 assert!(smudge(&mut index), "without the mark the entry is checked");
14504 assert_eq!(index.entries[0].size, 0);
14505 }
14506
14507 #[test]
14508 fn content_verified_mark_for_another_oid_does_not_skip_the_check() {
14509 let td = tempfile::tempdir().expect("tempdir");
14510 let wt = td.path();
14511 let (git_dir, odb, mut index, index_mtime, _) = racy_stale_entry(wt);
14512 let other = odb.hash(ObjectKind::Blob, b"other").expect("hash");
14513 index.mark_content_verified(b"f.txt", other);
14514 assert!(smudge_racily_clean_entries(
14515 &odb,
14516 &git_dir,
14517 &mut index,
14518 wt,
14519 Some(index_mtime),
14520 None,
14521 None,
14522 ));
14523 assert_eq!(index.entries[0].size, 0);
14524 }
14525
14526 #[test]
14527 fn leaves_non_racy_entry_alone() {
14528 let td = tempfile::tempdir().expect("tempdir");
14529 let wt = td.path();
14530 let index_mtime = setup_index_mtime(wt);
14531 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
14532 let content_a = b"same length!!";
14533 let content_b = b"same length??";
14534 let entry_mtime = (index_mtime.0 - 10, 0);
14535
14536 let mut index = Index::new();
14537 let entry = staged_entry_for_file(&odb, wt, "f.txt", content_a, entry_mtime);
14538 index.entries.push(entry.clone());
14539
14540 fs::write(wt.join("f.txt"), content_b).expect("rewrite");
14541 pin_mtime_and_sample(&wt.join("f.txt"), entry_mtime.0, entry_mtime.1);
14542
14543 assert!(!entry_is_racy(&entry, Some(index_mtime)));
14544 let smudged = smudge_racily_clean_entries(
14545 &odb,
14546 &git_dir,
14547 &mut index,
14548 wt,
14549 Some(index_mtime),
14550 None,
14551 None,
14552 );
14553 assert!(!smudged);
14554 assert_eq!(index.entries[0].size, entry.size);
14555 }
14556
14557 #[test]
14558 fn leaves_matching_content_alone() {
14559 let td = tempfile::tempdir().expect("tempdir");
14560 let wt = td.path();
14561 let index_mtime = setup_index_mtime(wt);
14562 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
14563 let bytes = b"unchanged!!!!";
14564
14565 let mut index = Index::new();
14566 let entry = staged_entry_for_file(&odb, wt, "f.txt", bytes, index_mtime);
14567 index.entries.push(entry.clone());
14568
14569 assert!(entry_is_racy(&entry, Some(index_mtime)));
14570 let smudged = smudge_racily_clean_entries(
14571 &odb,
14572 &git_dir,
14573 &mut index,
14574 wt,
14575 Some(index_mtime),
14576 None,
14577 None,
14578 );
14579 assert!(!smudged);
14580 assert_eq!(index.entries[0].size, entry.size);
14581 }
14582
14583 #[test]
14584 fn status_diff_skips_refresh_rewrite_for_racy_clean_matching_stat() {
14585 let td = tempfile::tempdir().expect("tempdir");
14586 let wt = td.path();
14587 let index_mtime = setup_index_mtime(wt);
14588 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
14589 let bytes = b"racy clean payload\n";
14590
14591 let mut index = Index::new();
14592 index.source_mtime = Some(index_mtime);
14593 let entry = staged_entry_for_file(&odb, wt, "f.txt", bytes, index_mtime);
14594 index.entries.push(entry.clone());
14595 assert!(entry_is_racy(&entry, Some(index_mtime)));
14596
14597 let (entries, index_changed) = super::diff_index_to_worktree_with_options(
14598 &odb,
14599 &mut index,
14600 wt,
14601 super::DiffIndexToWorktreeOptions {
14602 repository_git_dir: Some(git_dir),
14603 index_mtime: Some(index_mtime),
14604 refresh_index_stat_in_pass: true,
14605 for_status: true,
14606 ..Default::default()
14607 },
14608 )
14609 .expect("status diff");
14610 assert!(entries.is_empty(), "unexpected unstaged: {entries:?}");
14611 assert!(
14612 !index_changed,
14613 "matching stat on a racy entry must skip refresh without rewriting the index"
14614 );
14615 }
14616
14617 #[test]
14618 fn leaves_matching_sha256_racy_content_alone() {
14619 let td = tempfile::tempdir().expect("tempdir");
14620 let wt = td.path();
14621 let index_mtime = setup_index_mtime(wt);
14622 let (git_dir, odb) = minimal_repo(wt, "[extensions]\n\tobjectformat = sha256\n");
14623 assert_eq!(odb.hash_algo(), HashAlgo::Sha256);
14624
14625 let bytes = b"sha256 width!!";
14626 let oid = odb.hash(ObjectKind::Blob, bytes).expect("hash");
14627 let mut index = Index::new();
14628 let entry = staged_entry_with_oid(wt, "f.txt", bytes, oid, index_mtime);
14629 index.entries.push(entry.clone());
14630
14631 assert!(entry_is_racy(&entry, Some(index_mtime)));
14632 let smudged = smudge_racily_clean_entries(
14633 &odb,
14634 &git_dir,
14635 &mut index,
14636 wt,
14637 Some(index_mtime),
14638 None,
14639 None,
14640 );
14641 assert!(!smudged, "matching SHA-256 blob must not be smudged");
14642 assert_eq!(index.entries[0].size, entry.size);
14643 }
14644
14645 #[test]
14646 fn materialize_big_binary_under_autocrlf_streams_into_pack() {
14647 use std::io::Write;
14648
14649 let td = tempfile::tempdir().expect("tempdir");
14650 let wt = td.path();
14651 let (git_dir, odb) =
14652 minimal_repo(wt, "[core]\n\tautocrlf = true\n\tbigFileThreshold = 64k\n");
14653 let path = wt.join("big.bin");
14654 {
14655 let mut f = fs::File::create(&path).expect("create");
14656 f.write_all(&[0u8; 9000]).expect("nul prefix");
14657 let chunk = vec![1u8; 256 * 1024];
14658 for _ in 0..20 {
14659 f.write_all(&chunk).expect("chunk");
14660 }
14661 }
14662 let meta = fs::metadata(&path).expect("stat");
14663 let mut conv = crlf::ConversionConfig::from_config(&ConfigSet::new());
14664 conv.autocrlf = crlf::AutoCrlf::True;
14665 let attrs = FileAttrs::default();
14666 let oid =
14667 materialize_worktree_blob(&odb, &path, &meta, &conv, &attrs, "big.bin", None, None)
14668 .expect("materialize");
14669 assert!(
14670 !odb.test_loose_object_path(&oid).exists(),
14671 "big binary under autocrlf should pack, not write loose"
14672 );
14673 let pack_dir = git_dir.join("objects/pack");
14674 let names: Vec<_> = fs::read_dir(&pack_dir)
14675 .expect("pack dir")
14676 .map(|e| e.expect("entry").file_name())
14677 .collect();
14678 assert!(
14679 names.iter().any(|n| n.to_string_lossy().ends_with(".pack")),
14680 "{names:?}"
14681 );
14682 assert!(odb.read(&oid).expect("read blob").data.len() > 9000);
14683 }
14684
14685 #[test]
14686 fn indexed_binary_under_autocrlf_rehash_streams() {
14687 use std::io::Write;
14688
14689 let td = tempfile::tempdir().expect("tempdir");
14690 let wt = td.path();
14691 let (_git_dir, odb) =
14692 minimal_repo(wt, "[core]\n\tautocrlf = true\n\tbigFileThreshold = 1m\n");
14693 let path = wt.join("big.bin");
14694 {
14695 let mut f = fs::File::create(&path).expect("create");
14696 f.write_all(&[0u8; 9000]).expect("nul prefix");
14697 let chunk = vec![1u8; 512 * 1024];
14698 for _ in 0..3 {
14699 f.write_all(&chunk).expect("chunk");
14700 }
14701 }
14702 let meta = fs::symlink_metadata(&path).expect("stat");
14703 let oid = hash_worktree_file(
14704 &odb,
14705 &path,
14706 &meta,
14707 &crlf::ConversionConfig::from_config(&ConfigSet::new()),
14708 &FileAttrs::default(),
14709 "big.bin",
14710 None,
14711 None,
14712 )
14713 .expect("hash");
14714 let entry = entry_from_metadata(&meta, b"big.bin", oid, MODE_REGULAR);
14715 let mut conv = crlf::ConversionConfig::from_config(&ConfigSet::new());
14716 conv.autocrlf = crlf::AutoCrlf::True;
14717 let attrs = FileAttrs::default();
14718 let content = read_regular_worktree_blob(
14719 &odb,
14720 &path,
14721 &conv,
14722 &attrs,
14723 "big.bin",
14724 Some(&entry),
14725 None,
14726 1024 * 1024,
14727 )
14728 .expect("read blob");
14729 assert!(
14730 matches!(content, WorktreeBlobContent::Stream { .. }),
14731 "indexed binary re-hash must stream, not buffer"
14732 );
14733 assert_eq!(content.hash(&odb).expect("hash"), entry.oid);
14734 }
14735
14736 #[test]
14737 fn leaves_matching_crlf_worktree_content_alone() {
14738 let td = tempfile::tempdir().expect("tempdir");
14739 let wt = td.path();
14740 let index_mtime = setup_index_mtime(wt);
14741 let (git_dir, odb) = minimal_repo(wt, "[core]\n\tautocrlf = true\n");
14742 let git_form = b"line one\nline two\n";
14743 let worktree_form = b"line one\r\nline two\r\n";
14744 let oid = odb.hash(ObjectKind::Blob, git_form).expect("hash");
14745
14746 let mut index = Index::new();
14747 let entry = staged_entry_with_oid(wt, "f.txt", worktree_form, oid, index_mtime);
14748 index.entries.push(entry.clone());
14749
14750 assert!(entry_is_racy(&entry, Some(index_mtime)));
14751 let smudged = smudge_racily_clean_entries(
14752 &odb,
14753 &git_dir,
14754 &mut index,
14755 wt,
14756 Some(index_mtime),
14757 None,
14758 None,
14759 );
14760 assert!(
14761 !smudged,
14762 "CRLF worktree matching clean index OID must not be smudged"
14763 );
14764 assert_eq!(index.entries[0].size, entry.size);
14765 }
14766
14767 #[test]
14768 fn skips_skip_worktree_and_intent_to_add() {
14769 let td = tempfile::tempdir().expect("tempdir");
14770 let wt = td.path();
14771 let index_mtime = setup_index_mtime(wt);
14772 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
14773 let content_a = b"same length!!";
14774 let content_b = b"same length??";
14775
14776 let mut index = Index::new();
14777 let mut entry = staged_entry_for_file(&odb, wt, "f.txt", content_a, index_mtime);
14778 entry.set_skip_worktree(true);
14779 index.entries.push(entry);
14780 fs::write(wt.join("f.txt"), content_b).expect("rewrite");
14781 pin_mtime_and_sample(&wt.join("f.txt"), index_mtime.0, index_mtime.1);
14782 assert!(!smudge_racily_clean_entries(
14783 &odb,
14784 &git_dir,
14785 &mut index,
14786 wt,
14787 Some(index_mtime),
14788 None,
14789 None,
14790 ));
14791 assert_ne!(index.entries[0].size, 0);
14792
14793 let mut index = Index::new();
14794 let mut entry = staged_entry_for_file(&odb, wt, "f.txt", content_a, index_mtime);
14795 entry.set_intent_to_add(true);
14796 index.entries.push(entry);
14797 fs::write(wt.join("f.txt"), content_b).expect("rewrite");
14798 pin_mtime_and_sample(&wt.join("f.txt"), index_mtime.0, index_mtime.1);
14799 assert!(!smudge_racily_clean_entries(
14800 &odb,
14801 &git_dir,
14802 &mut index,
14803 wt,
14804 Some(index_mtime),
14805 None,
14806 None,
14807 ));
14808 assert_ne!(index.entries[0].size, 0);
14809 }
14810
14811 #[test]
14812 fn smudges_nothing_without_prior_index_mtime() {
14813 let td = tempfile::tempdir().expect("tempdir");
14814 let wt = td.path();
14815 let index_mtime = setup_index_mtime(wt);
14816 let (git_dir, odb) = minimal_repo(wt, "[core]\n");
14817 let mut index = Index::new();
14818 let entry = staged_entry_for_file(&odb, wt, "f.txt", b"same length!!", index_mtime);
14819 index.entries.push(entry);
14820
14821 fs::write(wt.join("f.txt"), b"same length??").expect("rewrite");
14822 pin_mtime_and_sample(&wt.join("f.txt"), index_mtime.0, index_mtime.1);
14823
14824 let smudged = smudge_racily_clean_entries(&odb, &git_dir, &mut index, wt, None, None, None);
14825 assert!(!smudged);
14826 assert_ne!(index.entries[0].size, 0);
14827 }
14828}
14829
14830#[cfg(all(test, unix))]
14831mod read_worktree_bytes_for_mode_tests {
14832 use super::*;
14833 use crate::index::MODE_SYMLINK;
14834 use std::fs;
14835 use std::os::unix::fs::symlink;
14836
14837 #[test]
14838 fn symlink_mode_reads_link_target_not_followed_file() {
14839 let td = tempfile::tempdir().expect("tempdir");
14840 let wt = td.path();
14841 fs::write(wt.join("payload.txt"), b"secret\n").expect("write");
14842 symlink("payload.txt", wt.join("link")).expect("symlink");
14843 let bytes =
14844 read_worktree_bytes_for_mode(&wt.join("link"), MODE_SYMLINK).expect("read link");
14845 assert_eq!(bytes, b"payload.txt");
14846 }
14847
14848 #[test]
14849 fn symlink_mode_reads_plain_file_holding_the_target() {
14850 let td = tempfile::tempdir().expect("tempdir");
14851 let path = td.path().join("link");
14852 fs::write(&path, b"payload.txt").expect("write");
14853 let bytes = read_worktree_bytes_for_mode(&path, MODE_SYMLINK).expect("read plain link");
14854 assert_eq!(bytes, b"payload.txt");
14855 }
14856
14857 #[test]
14858 fn regular_mode_reads_file_contents() {
14859 let td = tempfile::tempdir().expect("tempdir");
14860 let path = td.path().join("f.txt");
14861 fs::write(&path, b"hello\n").expect("write");
14862 let bytes = read_worktree_bytes_for_mode(&path, crate::index::MODE_REGULAR).expect("read");
14863 assert_eq!(bytes, b"hello\n");
14864 }
14865
14866 #[test]
14867 fn annotate_marks_context_line_when_eof_line_is_unchanged() {
14868 let old = "a\nb\nc";
14869 let new = "a\nx\nc";
14870 let raw = super::unified_diff_histogram_hunks_only(old, new, 3, 0, false);
14871 let out = annotate_unified_diff_no_newline_at_eof(&raw, old, new);
14872 assert!(
14873 out.contains(" c\n\\ No newline at end of file"),
14874 "histogram hunk should mark trailing context line:\n{out}"
14875 );
14876 }
14877
14878 #[test]
14879 fn annotate_does_not_treat_removed_dash_lines_as_file_headers() {
14880 use super::unified_diff_with_prefix_and_funcname_and_algorithm;
14881 use similar::Algorithm;
14882
14883 let old = "--old";
14884 let new = "--new";
14885 let raw = unified_diff_with_prefix_and_funcname_and_algorithm(
14886 old,
14887 new,
14888 "a/x",
14889 "b/x",
14890 3,
14891 0,
14892 "",
14893 "",
14894 None,
14895 Algorithm::Myers,
14896 false,
14897 false,
14898 false,
14899 false,
14900 );
14901 assert_eq!(
14902 raw.matches("\\ No newline at end of file").count(),
14903 2,
14904 "expected two markers like git, got:\n{raw}"
14905 );
14906 }
14907}
14908
14909#[cfg(test)]
14910mod annotate_no_newline_tests {
14911 use super::annotate_unified_diff_no_newline_at_eof;
14912
14913 #[test]
14914 fn annotate_marks_both_sides_without_trailing_newline() {
14915 let raw = "@@ -1 +1 @@\n-foo\n+f o o\n";
14916 let out = annotate_unified_diff_no_newline_at_eof(raw, "foo", "f o o");
14917 assert!(out.contains("\\ No newline at end of file"));
14918 assert_eq!(out.matches("\\ No newline at end of file").count(), 2);
14919 }
14920}
14921
14922#[cfg(test)]
14923mod blob_diff_algorithm_smoke {
14924 use super::{
14925 anchored_unified_diff, count_changes_with_algorithm, unified_diff_histogram_hunks_only,
14926 unified_diff_with_prefix_and_funcname_and_algorithm, unified_hunk_body_for_diff_options,
14927 };
14928 use similar::Algorithm;
14929
14930 const OLD: &str = "alpha\nbeta\ngamma\n";
14931 const NEW: &str = "alpha\nBETA\ngamma\ndelta\n";
14932
14933 #[test]
14934 fn seed15_histogram_u0_body_matches_git() {
14935 use crate::diff::options::{DiffAlgorithm, DiffOptions};
14936 use std::collections::hash_map::DefaultHasher;
14937 use std::hash::{Hash, Hasher};
14938 let seed = 15u64;
14939 let mut hasher = DefaultHasher::new();
14940 seed.hash(&mut hasher);
14941 let h = hasher.finish();
14942 let line_count = 4 + (h as usize % 12);
14943 let mut old = String::new();
14944 let mut new = String::new();
14945 for i in 0..line_count {
14946 let token = (h.wrapping_add(i as u64 * 131) % 7) as u8 + b'a';
14947 let ch = token as char;
14948 old.push(ch);
14949 if (h >> (i % 8)) & 1 == 1 {
14950 new.push(((token as u8).wrapping_add(1)) as char);
14951 } else {
14952 new.push(ch);
14953 }
14954 old.push('\n');
14955 new.push('\n');
14956 }
14957 let opts = DiffOptions {
14958 context_lines: 0,
14959 algorithm: DiffAlgorithm::Histogram,
14960 indent_heuristic: false,
14961 ..DiffOptions::default()
14962 };
14963 let body = unified_hunk_body_for_diff_options(&old, &new, &opts);
14964 assert_eq!(
14965 body.matches("@@ ").count(),
14966 2,
14967 "git 2.55 splits u0 histogram hunks at seed 15: {body}"
14968 );
14969 }
14970
14971 #[test]
14972 fn t4071_histogram_changed_flags_git_myers_completes() {
14973 let old: Vec<&str> = "x\nx\nx\nx\n".lines().collect();
14974 let new: Vec<&str> = "x\nx\nx\nA\nB\nC\nD\nx\nE\nF\nG\n".lines().collect();
14975 let (c1, c2) = super::git_xdiff::histogram_changed_flags(&old, &new);
14976 assert_eq!(c1.len(), 4);
14977 assert_eq!(c2.len(), 11);
14978 let ops = super::changed_flags_to_ops(&c1, &c2, old.len(), new.len());
14979 assert!(!ops.is_empty());
14980 }
14981
14982 #[test]
14983 fn t4071_histogram_compacted_ops_completes() {
14984 use crate::diff::WhitespaceMode;
14985 let old = "x\nx\nx\nx\n";
14986 let new = "x\nx\nx\nA\nB\nC\nD\nx\nE\nF\nG\n";
14987 let ops = super::histogram_compacted_line_ops(old, new, false, WhitespaceMode::default());
14988 assert!(!ops.is_empty(), "expected diff ops");
14989 }
14990
14991 #[test]
14992 fn t4071_histogram_u0_body_completes() {
14993 use crate::diff::options::{DiffAlgorithm, DiffOptions};
14994 let old = "x\nx\nx\nx\n";
14995 let new = "x\nx\nx\nA\nB\nC\nD\nx\nE\nF\nG\n";
14996 let opts = DiffOptions {
14997 context_lines: 0,
14998 algorithm: DiffAlgorithm::Histogram,
14999 indent_heuristic: false,
15000 ..DiffOptions::default()
15001 };
15002 let body = unified_hunk_body_for_diff_options(old, new, &opts);
15003 assert!(body.contains("@@"), "expected hunk header: {body}");
15004 }
15005
15006 #[test]
15007 fn t4071_minimal_u0_matches_git_hunk_headers() {
15008 use crate::diff::options::{DiffAlgorithm, DiffOptions};
15009 let old = "x\nx\nx\nx\n";
15010 let new = "x\nx\nx\nA\nB\nC\nD\nx\nE\nF\nG\n";
15011 let opts = DiffOptions {
15012 context_lines: 0,
15013 algorithm: DiffAlgorithm::Minimal,
15014 indent_heuristic: false,
15015 ..DiffOptions::default()
15016 };
15017 let body = unified_hunk_body_for_diff_options(old, new, &opts);
15018 assert_eq!(
15019 body.matches("@@ ").count(),
15020 2,
15021 "git 2.55+ splits u0 minimal hunks at repeated anchors: {body}"
15022 );
15023 }
15024
15025 #[test]
15026 fn hunks_do_not_slide_onto_final_line_without_newline() {
15027 use crate::diff::options::{DiffAlgorithm, DiffOptions};
15028
15029 let old = "{\n \"a\": 1\n}\n";
15030 let new = "{\n \"a\": 2\n}\n{\n \"b\": 2\n}";
15031 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";
15032 for algorithm in [
15033 DiffAlgorithm::Myers,
15034 DiffAlgorithm::Minimal,
15035 DiffAlgorithm::Patience,
15036 DiffAlgorithm::Histogram,
15037 ] {
15038 for indent_heuristic in [true, false] {
15039 let options = DiffOptions {
15040 algorithm,
15041 indent_heuristic,
15042 ..DiffOptions::default()
15043 };
15044 assert_eq!(
15045 unified_hunk_body_for_diff_options(old, new, &options),
15046 expected,
15047 "{algorithm:?}, indent heuristic {indent_heuristic}"
15048 );
15049 }
15050 }
15051 }
15052
15053 #[test]
15054 fn myers_patience_histogram_minimal_emit_unified_hunks() {
15055 for algo in [Algorithm::Myers, Algorithm::Patience, Algorithm::Histogram] {
15056 let patch = unified_diff_with_prefix_and_funcname_and_algorithm(
15057 OLD,
15058 NEW,
15059 "a/old.txt",
15060 "b/new.txt",
15061 3,
15062 0,
15063 "a/",
15064 "b/",
15065 None,
15066 algo,
15067 false,
15068 false,
15069 true,
15070 false,
15071 );
15072 assert!(patch.contains("@@"), "algo {algo:?} should emit hunks");
15073 assert!(patch.contains("-beta") || patch.contains("-BETA"));
15074 }
15075 }
15076
15077 #[test]
15078 fn git_histogram_printer_and_anchored_diff_smoke() {
15079 let hist = unified_diff_histogram_hunks_only(OLD, NEW, 3, 0, false);
15080 assert!(!hist.is_empty());
15081 let anchors = vec!["beta".to_string()];
15082 let anchored = anchored_unified_diff(
15083 OLD,
15084 NEW,
15085 "old",
15086 "new",
15087 3,
15088 &anchors,
15089 Algorithm::Myers,
15090 false,
15091 true,
15092 false,
15093 );
15094 assert!(anchored.contains("@@"));
15095 }
15096
15097 #[test]
15098 fn count_changes_git_histogram_vs_similar() {
15099 let hist = count_changes_with_algorithm(OLD, NEW, Algorithm::Myers, true);
15100 let myers = count_changes_with_algorithm(OLD, NEW, Algorithm::Myers, false);
15101 assert!(hist.0 + hist.1 > 0);
15102 assert!(myers.0 + myers.1 > 0);
15103 }
15104}
15105
15106#[cfg(test)]
15107mod line_coverage_tests {
15108 use super::*;
15109 use crate::config::ConfigSet;
15110 use crate::index::StatMatchPolicy;
15111 use crate::index::{Index, IndexEntry, MODE_REGULAR};
15112 use crate::objects::ObjectKind;
15113 use crate::parallel::ThreadPolicy;
15114 use crate::write_tree::write_tree_from_index;
15115 use similar::Algorithm;
15116 use tempfile::TempDir;
15117
15118 fn index_entry(path: &str, mode: u32, oid: ObjectId) -> IndexEntry {
15119 IndexEntry {
15120 ctime_sec: 0,
15121 ctime_nsec: 0,
15122 mtime_sec: 0,
15123 mtime_nsec: 0,
15124 dev: 0,
15125 ino: 0,
15126 mode,
15127 uid: 0,
15128 gid: 0,
15129 size: 0,
15130 oid,
15131 flags: path.len().min(0xfff) as u16,
15132 flags_extended: None,
15133 path: path.as_bytes().to_vec(),
15134 base_index_pos: 0,
15135 }
15136 }
15137
15138 fn write_tree(odb: &Odb, entries: &[(&str, ObjectId)]) -> ObjectId {
15139 let mut body = Vec::new();
15140 for (name, oid) in entries {
15141 body.extend_from_slice(format!("100644 {name}\0").as_bytes());
15142 body.extend_from_slice(oid.as_bytes());
15143 }
15144 odb.write(ObjectKind::Tree, &body).expect("tree")
15145 }
15146
15147 fn pair_of_trees_with_rename() -> (TempDir, Odb, ObjectId, ObjectId) {
15148 let temp = TempDir::new().expect("tempdir");
15149 let odb = Odb::new(temp.path());
15150 let old_blob = odb
15151 .write(ObjectKind::Blob, b"line one\nline two\n")
15152 .expect("old");
15153 let new_blob = odb
15154 .write(ObjectKind::Blob, b"line one\nline TWO\nline three\n")
15155 .expect("new");
15156 let old_tree = write_tree(&odb, &[("old.txt", old_blob)]);
15157 let new_tree = write_tree(&odb, &[("new.txt", new_blob), ("kept.txt", old_blob)]);
15158 (temp, odb, old_tree, new_tree)
15159 }
15160
15161 #[test]
15162 fn detect_copies_reports_source_modes_without_tree_entries() {
15163 let temp = TempDir::new().expect("tempdir");
15164 let odb = Odb::new(temp.path());
15165 let content = odb
15166 .write(ObjectKind::Blob, b"shared content\n")
15167 .expect("blob");
15168 let changed = odb.write(ObjectKind::Blob, b"changed\n").expect("blob");
15169 let old_tree = write_tree(&odb, &[("gone.txt", content), ("mod.txt", content)]);
15170 let new_tree = write_tree(
15171 &odb,
15172 &[
15173 ("copy.txt", content),
15174 ("mod.txt", changed),
15175 ("moved.txt", content),
15176 ],
15177 );
15178 let entries = diff_trees(&odb, Some(&old_tree), Some(&new_tree), "").expect("diff");
15179 let result = detect_copies(&odb, None, entries, 50, false, &[]).expect("copies");
15180 let pairs: Vec<_> = result
15181 .iter()
15182 .filter(|e| matches!(e.status, DiffStatus::Renamed | DiffStatus::Copied))
15183 .map(|e| (e.old_mode, e.new_mode))
15184 .collect();
15185 assert_eq!(pairs.len(), 2, "{result:?}");
15186 assert!(
15187 pairs
15188 .iter()
15189 .all(|p| *p == (FileMode::REGULAR, FileMode::REGULAR)),
15190 "{pairs:?}"
15191 );
15192 }
15193
15194 #[test]
15195 fn detect_copies_keeps_modifications_as_modifications() {
15196 let temp = TempDir::new().expect("tempdir");
15199 let odb = Odb::new(temp.path());
15200 let one = odb.write(ObjectKind::Blob, b"1\n").expect("blob");
15201 let two = odb.write(ObjectKind::Blob, b"2\n").expect("blob");
15202 let old_tree = write_tree(&odb, &[("a", one), ("b", two)]);
15203 let new_tree = write_tree(&odb, &[("a", two), ("b", one)]);
15204 let entries = diff_trees(&odb, Some(&old_tree), Some(&new_tree), "").expect("diff");
15205 let result = detect_copies(&odb, None, entries, 50, false, &[]).expect("copies");
15206 let statuses: Vec<_> = result.iter().map(|e| (e.path(), e.status)).collect();
15207 assert_eq!(
15208 statuses,
15209 [("a", DiffStatus::Modified), ("b", DiffStatus::Modified)]
15210 );
15211 }
15212
15213 #[test]
15214 fn diff_helper_and_format_sweep() {
15215 let cfg = ConfigSet::new();
15216 assert!(resolve_indent_heuristic(&cfg, false, false));
15217 assert!(!resolve_indent_heuristic(&cfg, false, true));
15218
15219 let old = "alpha beta\ngamma\n";
15220 let new = "alpha BETA\ngamma delta\n";
15221 let _ = normalize_ignore_space_change(old);
15222 let _ = normalize_ignore_space_change_line("- alpha beta ");
15223 let _ = count_git_lines(old.as_bytes());
15224 let _ = diffcore_count_changes(old.as_bytes(), new.as_bytes());
15225 assert!(parse_diff_rename_score_token("50%").is_some());
15226 let _ = rewrite_merge_score(b"a", b"b");
15227 let _ = rewrite_dissimilarity_index_percent(b"abc", b"abd");
15228 assert_eq!(format_mode(0o100644), "100644");
15229
15230 let old_lines = ["a", "b", "c"];
15231 let new_lines = ["a", "b", "d"];
15232 let compact = diff_slice_ops_compacted(&old_lines, &new_lines, Algorithm::Myers, false);
15233 assert!(!compact.is_empty());
15234 }
15235
15236 #[test]
15237 fn tree_diff_rename_copy_postprocess_matrix() -> Result<()> {
15238 let (_temp, odb, old_tree, new_tree) = pair_of_trees_with_rename();
15239 let raw = diff_trees(&odb, Some(&old_tree), Some(&new_tree), "")?;
15240 assert!(!raw.is_empty());
15241 let with_renames = diff_trees_with_renames(&odb, Some(&old_tree), Some(&new_tree), "", 50)?;
15242 assert!(!with_renames.is_empty());
15243 let config = ConfigSet::new();
15244 let policy = ThreadPolicy::default();
15245 let detected =
15246 detect_renames_with_policy(&odb, None, raw.clone(), 50, &config, &policy, None)?;
15247 let source_flat = flatten_tree_entries(&odb, &old_tree)?;
15248 let _ = detect_copies_with_policy(
15249 &odb,
15250 None,
15251 detected.clone(),
15252 50,
15253 true,
15254 &source_flat,
15255 &config,
15256 &policy,
15257 )?;
15258 let opts = DiffPostprocessOptions {
15259 break_score: Some(50),
15260 rename_threshold: 50,
15261 copy_threshold: Some(50),
15262 find_copies_harder: true,
15263 materialize_break_worktree_new_oid: false,
15264 };
15265 let processed = postprocess_diff_entries(
15266 &odb,
15267 raw,
15268 Some(&old_tree),
15269 None,
15270 None,
15271 &config,
15272 &policy,
15273 &opts,
15274 )?;
15275 assert!(!processed.is_empty());
15276 let _ = status_apply_rename_copy_detection(
15277 &odb,
15278 with_renames.clone(),
15279 50,
15280 true,
15281 Some(&old_tree),
15282 &config,
15283 &policy,
15284 None,
15285 )?;
15286 let flat = flatten_tree_entries(&odb, &new_tree)?;
15287 assert!(!flat.is_empty());
15288 let _ = diff_trees_show_tree_entries(&odb, Some(&old_tree), Some(&new_tree), "")?;
15289 Ok(())
15290 }
15291
15292 #[test]
15293 fn unified_diff_flag_matrix() {
15294 let old = "fn main() {\n println!(\"hi\");\n}\n";
15295 let new = "fn main() {\n println!(\"bye\");\n}\n";
15296 for algo in [Algorithm::Myers, Algorithm::Patience, Algorithm::Histogram] {
15297 for (ws, ws_all, fc, ig) in [(true, false, false, false), (false, true, true, true)] {
15298 let patch = unified_diff_with_prefix_and_funcname_and_algorithm(
15299 old,
15300 new,
15301 "a/main.rs",
15302 "b/main.rs",
15303 3,
15304 0,
15305 "a/",
15306 "b/",
15307 None,
15308 algo,
15309 ws,
15310 ws_all,
15311 fc,
15312 ig,
15313 );
15314 assert!(patch.contains("@@"), "algo {algo:?}");
15315 }
15316 }
15317 let hist = unified_diff_histogram_ignore_with_prefix_and_funcname(
15318 old,
15319 new,
15320 "a/x",
15321 "b/x",
15322 3,
15323 0,
15324 "",
15325 "",
15326 None,
15327 false,
15328 false,
15329 false,
15330 |_old, _new| false,
15331 );
15332 assert!(!hist.is_empty());
15333 let raw = format_raw_abbrev(
15334 &DiffEntry {
15335 status: DiffStatus::Modified,
15336 old_path: Some("x".into()),
15337 new_path: Some("x".into()),
15338 old_mode: FileMode::REGULAR,
15339 new_mode: FileMode::REGULAR,
15340 old_oid: empty_blob_oid(),
15341 new_oid: empty_blob_oid(),
15342 score: Some(80),
15343 },
15344 7,
15345 );
15346 assert!(raw.contains('M') || raw.contains('x'));
15347 }
15348
15349 #[test]
15350 fn index_and_commit_diff_paths() -> Result<()> {
15351 let temp = TempDir::new().expect("tempdir");
15352 let odb = Odb::new(temp.path());
15353 let b1 = odb.write(ObjectKind::Blob, b"v1\n").expect("b1");
15354 let b2 = odb.write(ObjectKind::Blob, b"v2\n").expect("b2");
15355 let b3 = odb.write(ObjectKind::Blob, b"v3\n").expect("b3");
15356 let mut index = Index::new();
15357 index.add_or_replace(index_entry("file.txt", MODE_REGULAR, b2));
15358 let head = write_tree_from_index(&odb, &index, "").expect("tree");
15359 index.entries_mut()[0].oid = b1;
15360 let idx_diff = diff_index_to_tree(&odb, &index, Some(&head), false)?;
15361 assert!(!idx_diff.is_empty());
15362 let commit_body = format!(
15363 "tree {head}\nauthor T <t@e.com> 1 +0000\ncommitter T <t@e.com> 1 +0000\n\nm\n"
15364 );
15365 let c1 = odb
15366 .write(ObjectKind::Commit, commit_body.as_bytes())
15367 .expect("c1");
15368 let mut index2 = Index::new();
15369 index2.add_or_replace(index_entry("file.txt", MODE_REGULAR, b3));
15370 let head2 = write_tree_from_index(&odb, &index2, "").expect("head2");
15371 let c2_body = format!(
15372 "tree {head2}\nparent {c1}\nauthor T <t@e.com> 2 +0000\ncommitter T <t@e.com> 2 +0000\n\nm2\n"
15373 );
15374 let c2 = odb
15375 .write(ObjectKind::Commit, c2_body.as_bytes())
15376 .expect("c2");
15377 let commit_diff = diff_commits_with_renames(&odb, &c1, &c2, 50)?;
15378 assert!(!commit_diff.is_empty());
15379 let _ = tree_oid_of_commit(&odb, &c2)?;
15380 let mtime = index_mtime_for_diff(&index, Some((1, 0)));
15381 let _ = entry_is_racy(&index.entries()[0], mtime);
15382 let _ = entry_is_racy_with_policy(&index.entries()[0], mtime, StatMatchPolicy::default());
15383 Ok(())
15384 }
15385
15386 #[test]
15387 fn tree_oid_of_commit_reports_invalid_ref_for_non_commits() {
15388 let temp = TempDir::new().expect("tempdir");
15389 let odb = Odb::new(temp.path());
15390 let tree = odb.write(ObjectKind::Tree, &[]).expect("tree");
15391 let body = format!(
15392 "tree {tree}\nauthor T <t@e.com> 1 +0000\ncommitter T <t@e.com> 1 +0000\n\nm\n"
15393 );
15394 let commit = odb
15395 .write(ObjectKind::Commit, body.as_bytes())
15396 .expect("commit");
15397 let blob = odb.write(ObjectKind::Blob, b"x\n").expect("blob");
15398
15399 assert_eq!(tree_oid_of_commit(&odb, &commit).expect("tree"), tree);
15400 assert!(matches!(
15401 tree_oid_of_commit(&odb, &blob),
15402 Err(Error::InvalidRef(_))
15403 ));
15404 assert!(matches!(
15405 tree_oid_of_commit(&odb, &tree),
15406 Err(Error::InvalidRef(_))
15407 ));
15408 }
15409
15410 #[test]
15411 fn worktree_index_diff_and_rename_score() -> Result<()> {
15412 let temp = TempDir::new().expect("tempdir");
15413 let wt = temp.path();
15414 let git_dir = wt.join(".git");
15415 std::fs::create_dir_all(git_dir.join("objects")).expect("objects");
15416 let objects_dir = git_dir.join("objects");
15417 let odb = Odb::new(&objects_dir);
15418 let content = b"hello world\n";
15419 std::fs::write(wt.join("tracked.txt"), content).expect("write");
15420 let oid = odb.write(ObjectKind::Blob, content).expect("blob");
15421 let mut index = Index::new();
15422 index.add_or_replace(index_entry("tracked.txt", MODE_REGULAR, oid));
15423 let head = write_tree_from_index(&odb, &index, "").expect("tree");
15424 std::fs::write(wt.join("tracked.txt"), b"hello WORLD\n").expect("mutate");
15425 let mut index_mut = index.clone();
15426 let diff = diff_index_to_worktree(&odb, &mut index_mut, wt, false, false)?;
15427 assert!(!diff.is_empty());
15428 let _ = diff_tree_to_worktree(&odb, Some(&head), wt, &index)?;
15429 assert_eq!(rename_similarity_score(b"a", b"a\n"), 0);
15430 assert_eq!(rename_similarity_score(b"a\n", b"a"), 0);
15431 let score = rename_similarity_score(b"line one\nline two\n", b"line one\nline TWO\n");
15432 assert!(score >= 50);
15433 let _ = detect_renames(&odb, Some(wt), diff, 50);
15434 Ok(())
15435 }
15436
15437 #[test]
15438 fn extended_diff_helpers_sweep() -> Result<()> {
15439 use crate::crlf::{ConversionConfig, FileAttrs};
15440 let temp = TempDir::new().expect("tempdir");
15441 let wt = temp.path();
15442 let git_dir = wt.join(".git");
15443 std::fs::create_dir_all(git_dir.join("objects")).expect("objects");
15444 let objects_dir = git_dir.join("objects");
15445 let odb = Odb::new(&objects_dir);
15446 let b_old = odb.write(ObjectKind::Blob, b"alpha\nbeta\n").expect("old");
15447 let b_new = odb
15448 .write(ObjectKind::Blob, b"alpha\nBETA\ngamma\n")
15449 .expect("new");
15450 let old_tree = write_tree(&odb, &[("a.txt", b_old), ("b.txt", b_old)]);
15451 let new_tree = write_tree(
15452 &odb,
15453 &[("a.txt", b_new), ("b.txt", b_new), ("c.txt", b_new)],
15454 );
15455 let entries = diff_trees(&odb, Some(&old_tree), Some(&new_tree), "")?;
15456 assert!(entries.len() >= 2);
15457
15458 let cfg = ConfigSet::new();
15459 assert!(indent_heuristic_from_config(&cfg));
15460 let _ = unified_diff_histogram_hunks_only("a\nb\n", "a\nB\n", 3, 0, false);
15461 let _ = unified_diff_histogram_with_prefix_and_funcname_indent(
15462 "x\n", "y\n", "a/f", "b/f", 3, 0, "", "", None, false, false,
15463 );
15464 let _ = count_changes_with_algorithm("a\n", "b\n", Algorithm::Myers, false);
15465 let _ = should_break_rewrite_pair(b"short", b"totally different body", 50);
15466 let _ = head_path_states(&odb, Some(&new_tree))?;
15467 std::fs::write(wt.join("tracked.txt"), b"hello\n").expect("wt");
15468 let abs = wt.join("tracked.txt");
15469 let meta = std::fs::metadata(&abs).expect("meta");
15470 let mut index = Index::new();
15471 index.add_or_replace(index_entry("tracked.txt", MODE_REGULAR, b_old));
15472 let _ = stat_matches_with_policy(&index.entries()[0], &meta, StatMatchPolicy::default());
15473 let _ = worktree_differs_from_index_entry(
15474 &odb,
15475 &git_dir,
15476 wt,
15477 &index.entries()[0],
15478 false,
15479 None,
15480 None,
15481 )?;
15482 let conv = ConversionConfig::from_config(&cfg);
15483 let attrs = FileAttrs::default();
15484 let params = WorktreeAddRefreshParams {
15485 odb: &odb,
15486 ie: &index.entries()[0],
15487 meta: &meta,
15488 abs_path: &abs,
15489 rel_path: "tracked.txt",
15490 conv: &conv,
15491 file_attrs: &attrs,
15492 index_mtime: None,
15493 staged_mode: MODE_REGULAR,
15494 filter_process: None,
15495 };
15496 let _ = classify_worktree_entry_for_add(¶ms)?;
15497 let _ = hash_worktree_file(
15498 &odb,
15499 &abs,
15500 &meta,
15501 &conv,
15502 &attrs,
15503 "tracked.txt",
15504 Some(&index.entries()[0]),
15505 None,
15506 )?;
15507 let _ = materialize_worktree_blob(
15508 &odb,
15509 &abs,
15510 &meta,
15511 &conv,
15512 &attrs,
15513 "tracked.txt",
15514 Some(&index.entries()[0]),
15515 None,
15516 )?;
15517 let _ = read_worktree_bytes_for_mode(&abs, MODE_REGULAR);
15518 let _ = mode_from_metadata(&meta);
15519 let _ = smudge_racily_clean_entries(&odb, &git_dir, &mut index, wt, None, Some(&cfg), None);
15520 let _ = refresh_index_stat_content_verified(
15521 &odb,
15522 &git_dir,
15523 &mut index,
15524 wt,
15525 None,
15526 Some(&cfg),
15527 None,
15528 )?;
15529 let _ = refresh_index_stat_content_verified_with_rules(
15530 &odb,
15531 &git_dir,
15532 &mut index,
15533 wt,
15534 None,
15535 Some(&cfg),
15536 None,
15537 None,
15538 )?;
15539 let _ = path_checkout_skip_blob_write_when_up_to_date_with_config(
15540 &odb,
15541 &git_dir,
15542 wt,
15543 &index,
15544 &index.entries()[0],
15545 &b_old,
15546 MODE_REGULAR,
15547 "tracked.txt",
15548 None,
15549 None,
15550 )?;
15551 let patch = anchored_unified_diff(
15552 "a\nb\n",
15553 "a\nB\n",
15554 "f",
15555 "f",
15556 3,
15557 &[String::from("b")],
15558 Algorithm::Myers,
15559 false,
15560 true,
15561 false,
15562 );
15563 assert!(patch.contains("@@"));
15564 let _ = annotate_unified_diff_no_newline_at_eof("@@\n+a\n\\ No newline\n", "a", "a\n");
15565 Ok(())
15566 }
15567
15568 #[test]
15569 fn unmerged_only_tree_worktree_diff_and_func_heuristics() -> Result<()> {
15570 use similar::DiffOp;
15571 assert!(inserted_block_starts_with_c_like_function_definition(
15572 "static int foo(int bar) {"
15573 ));
15574 assert!(!inserted_block_starts_with_c_like_function_definition(
15575 "printf(hi);"
15576 ));
15577 let ops = vec![
15578 DiffOp::Equal {
15579 old_index: 0,
15580 new_index: 0,
15581 len: 4,
15582 },
15583 DiffOp::Insert {
15584 old_index: 4,
15585 new_index: 4,
15586 new_len: 2,
15587 },
15588 ];
15589 let new_lines = ["a\n", "b\n", "c\n", "d\n", "void added() {\n", "}\n"];
15590 assert!(append_with_whole_function_added(
15591 &ops, 4, 6, &new_lines, None
15592 ));
15593 let _ = hunk_old_change_end_exclusive(&ops);
15594
15595 let temp = TempDir::new().expect("tempdir");
15596 let wt = temp.path();
15597 let git_dir = wt.join(".git");
15598 let objects_dir = git_dir.join("objects");
15599 std::fs::create_dir_all(&objects_dir).expect("objects");
15600 let odb = Odb::new(&objects_dir);
15601 let tree_blob = odb.write(ObjectKind::Blob, b"base\n").expect("blob");
15602 std::fs::write(wt.join("conflict.txt"), b"worktree\n").expect("wt");
15603 let tree = write_tree(&odb, &[("conflict.txt", tree_blob)]);
15604 let mut index = Index::new();
15605 let mut stage1 = index_entry("conflict.txt", MODE_REGULAR, tree_blob);
15606 stage1.flags = (stage1.flags & 0x0FFF) | (1 << 12);
15607 index.entries_mut().push(stage1);
15608 let cfg = ConfigSet::new();
15609 let diff = diff_tree_to_worktree_with_git_dir(
15610 &odb,
15611 Some(&tree),
15612 wt,
15613 &git_dir,
15614 &index,
15615 Some(&cfg),
15616 )?;
15617 assert!(!diff.is_empty());
15618 Ok(())
15619 }
15620
15621 #[test]
15622 fn high_volume_unified_diff_and_postprocess() -> Result<()> {
15623 let (_temp, odb, old_tree, new_tree) = pair_of_trees_with_rename();
15624 let config = ConfigSet::new();
15625 let policy = ThreadPolicy::default();
15626 for i in 0..600u32 {
15627 let old = format!("seed {i}\nalpha\n");
15628 let new = format!("seed {i}\nalpha\nvoid fn_{i}(int arg) {{\n return arg;\n}}\n");
15629 let _ = unified_diff_with_prefix_and_funcname_and_algorithm(
15630 &old,
15631 &new,
15632 "a/f.c",
15633 "b/f.c",
15634 3,
15635 0,
15636 "a/",
15637 "b/",
15638 None,
15639 Algorithm::Myers,
15640 i % 2 == 0,
15641 i % 3 == 0,
15642 i % 5 == 0,
15643 i % 7 == 0,
15644 );
15645 let threshold = 30 + (i % 40);
15646 let raw = diff_trees(&odb, Some(&old_tree), Some(&new_tree), "")?;
15647 let opts = DiffPostprocessOptions {
15648 rename_threshold: threshold,
15649 copy_threshold: Some(threshold),
15650 find_copies_harder: i % 2 == 0,
15651 break_score: Some(threshold as u64),
15652 materialize_break_worktree_new_oid: i % 4 == 0,
15653 };
15654 let _ = postprocess_diff_entries(
15655 &odb,
15656 raw,
15657 Some(&old_tree),
15658 None,
15659 None,
15660 &config,
15661 &policy,
15662 &opts,
15663 )?;
15664 }
15665 Ok(())
15666 }
15667}
15668
15669#[cfg(test)]
15670mod refresh_checks_tests {
15671 use super::{RefreshChecks, RefreshContent};
15672 use crate::objects::ObjectId;
15673
15674 #[test]
15675 fn empty_checks_answer_none_for_every_entry() {
15676 let checks = RefreshChecks::default();
15677 assert!(checks.0.is_empty());
15678 assert!(checks.get(0).is_none());
15679 assert!(checks.get(9_999).is_none());
15680 }
15681
15682 #[test]
15683 fn filled_checks_answer_by_entry_position() {
15684 let oid = ObjectId::from_bytes(&[7u8; 20]).unwrap();
15685 let other = ObjectId::from_bytes(&[8u8; 20]).unwrap();
15686 let checks = RefreshChecks(vec![None, Some(RefreshContent::Hashed(oid)), None]);
15687 assert!(checks.get(0).is_none());
15688 assert!(RefreshContent::matches(checks.get(1), &oid));
15689 assert!(RefreshContent::differs(checks.get(1), &other));
15690 assert!(
15691 checks.get(3).is_none(),
15692 "positions past the end have no check"
15693 );
15694 }
15695}