1use std::ops::RangeInclusive;
23use std::sync::OnceLock;
24
25use similar::{Algorithm as SimilarAlgorithm, DiffOp};
26
27#[path = "blame_ignore_revs.rs"]
28mod blame_ignore_revs;
29pub use crate::line_range::{resolve_line_range, LineRangeError};
30pub use blame_ignore_revs::{IgnoreRevs, IgnoreRevsSources};
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34pub enum BlameDiffAlgorithm {
35 Myers,
37 Histogram,
39 Patience,
41 Minimal,
43}
44
45impl BlameDiffAlgorithm {
46 pub fn to_similar(self) -> SimilarAlgorithm {
48 match self {
49 BlameDiffAlgorithm::Myers => SimilarAlgorithm::Myers,
53 BlameDiffAlgorithm::Histogram => SimilarAlgorithm::Patience,
54 BlameDiffAlgorithm::Patience => SimilarAlgorithm::Patience,
55 BlameDiffAlgorithm::Minimal => SimilarAlgorithm::Lcs,
56 }
57 }
58}
59
60pub fn parse_diff_algorithm_name(name: &str) -> Option<BlameDiffAlgorithm> {
62 match name.to_ascii_lowercase().as_str() {
63 "myers" | "default" => Some(BlameDiffAlgorithm::Myers),
64 "histogram" => Some(BlameDiffAlgorithm::Histogram),
65 "patience" => Some(BlameDiffAlgorithm::Patience),
66 "minimal" => Some(BlameDiffAlgorithm::Minimal),
67 _ => None,
68 }
69}
70
71fn affix_parent_index(
73 child_idx: usize,
74 prefix: usize,
75 suffix: usize,
76 old_len: usize,
77 new_len: usize,
78) -> usize {
79 let suffix_start = new_len.saturating_sub(suffix);
80 if child_idx < prefix {
81 child_idx
82 } else {
83 debug_assert!(suffix > 0 && child_idx >= suffix_start);
84 let off = child_idx - suffix_start;
85 old_len.saturating_sub(suffix) + off
86 }
87}
88
89fn partition_pending_by_affix(
94 affix: Affix,
95 old_len: usize,
96 new_len: usize,
97 pending: Vec<TrackedLine>,
98) -> (Vec<TrackedLine>, Vec<TrackedLine>) {
99 let (prefix, suffix) = (affix.safe_prefix, affix.safe_suffix);
100 let suffix_start = new_len.saturating_sub(suffix);
101 let mut affix_mapped = Vec::new();
102 let mut need_diff = Vec::new();
103 for t in pending {
104 let i = t.current_idx;
105 if i < prefix || (suffix > 0 && i >= suffix_start) {
106 affix_mapped.push(TrackedLine {
107 final_lineno: t.final_lineno,
108 current_idx: affix_parent_index(i, prefix, suffix, old_len, new_len),
109 ignored: t.ignored,
110 source_path: t.source_path,
111 origin: t.origin,
112 });
113 } else {
114 need_diff.push(t);
115 }
116 }
117 (affix_mapped, need_diff)
118}
119
120fn slide_safe_affix<L: BlameLines + ?Sized>(
128 old: &L,
129 new: &L,
130 ws: crate::diff::WhitespaceMode,
131) -> Affix {
132 let (prefix, suffix) = common_affix_len(old, new, ws);
133 let new_hi = new.len() - suffix;
134 let old_hi = old.len() - suffix;
135 let mut affix = Affix {
136 prefix,
137 suffix,
138 safe_prefix: prefix,
139 safe_suffix: suffix,
140 };
141 if prefix == new_hi && prefix == old_hi {
142 return affix;
143 }
144 let mut middle = MiddleLines {
145 old,
146 new,
147 old_range: prefix..old_hi,
148 new_range: prefix..new_hi,
149 ws,
150 scans_left: if ws.normalizes_line_compare() { 0 } else { 8 },
152 set: None,
153 };
154 let prefix_reach = (0..prefix)
155 .rev()
156 .take_while(|&i| middle.contains(new.comparison_line(i, ws)))
157 .count();
158 let suffix_reach = (new_hi..new.len())
159 .take_while(|&i| middle.contains(new.comparison_line(i, ws)))
160 .count();
161 affix.safe_prefix -= prefix_reach;
162 affix.safe_suffix -= suffix_reach;
163 affix
164}
165
166#[derive(Debug, Clone, Copy, PartialEq, Eq)]
169struct Affix {
170 prefix: usize,
171 suffix: usize,
172 safe_prefix: usize,
173 safe_suffix: usize,
174}
175
176struct MiddleLines<'a, L: BlameLines + ?Sized> {
181 old: &'a L,
182 new: &'a L,
183 old_range: std::ops::Range<usize>,
184 new_range: std::ops::Range<usize>,
185 ws: crate::diff::WhitespaceMode,
186 scans_left: usize,
187 set: Option<hashbrown::HashSet<std::borrow::Cow<'a, str>>>,
188}
189
190impl<'a, L: BlameLines + ?Sized> MiddleLines<'a, L> {
191 fn lines(&self) -> impl Iterator<Item = &'a str> + '_ {
192 let (old, new, ws) = (self.old, self.new, self.ws);
193 self.old_range
194 .clone()
195 .map(move |i| old.comparison_line(i, ws))
196 .chain(
197 self.new_range
198 .clone()
199 .map(move |i| new.comparison_line(i, ws)),
200 )
201 }
202
203 fn contains(&mut self, line: &str) -> bool {
204 let ws = self.ws;
205 if self.set.is_none() && self.scans_left > 0 {
206 self.scans_left -= 1;
207 return self.lines().any(|m| blame_lines_equal(m, line, ws));
208 }
209 let set = match self.set.take() {
210 Some(set) => set,
211 None => self
212 .lines()
213 .map(|m| ws.normalize_line_for_compare(m))
214 .collect(),
215 };
216 let found = set.contains(&ws.normalize_line_for_compare(line));
217 self.set = Some(set);
218 found
219 }
220}
221
222trait BlameLines {
224 fn len(&self) -> usize;
225 fn comparison_line(&self, idx: usize, ws: crate::diff::WhitespaceMode) -> &str;
227 fn exact_len(&self, idx: usize) -> usize;
229 fn stored_line(&self, idx: usize) -> StoredLine<'_>;
231 fn tail_bytes(&self, lines: usize) -> usize;
233}
234
235#[derive(Clone, Copy, PartialEq, Eq)]
237struct StoredLine<'a> {
238 text: &'a [u8],
239 implied_newline: bool,
240}
241
242impl<'a> StoredLine<'a> {
243 fn rev_bytes(self) -> impl Iterator<Item = u8> + 'a {
244 self.implied_newline
245 .then_some(b'\n')
246 .into_iter()
247 .chain(self.text.iter().rev().copied())
248 }
249}
250
251impl BlameLines for [&str] {
252 fn len(&self) -> usize {
253 <[&str]>::len(self)
254 }
255
256 fn comparison_line(&self, idx: usize, _ws: crate::diff::WhitespaceMode) -> &str {
257 self[idx]
258 }
259
260 fn exact_len(&self, idx: usize) -> usize {
261 self[idx].len()
262 }
263
264 fn stored_line(&self, idx: usize) -> StoredLine<'_> {
266 StoredLine {
267 text: self[idx].as_bytes(),
268 implied_newline: !self[idx].ends_with('\n'),
269 }
270 }
271
272 fn tail_bytes(&self, lines: usize) -> usize {
273 self[self.len() - lines..]
274 .iter()
275 .map(|l| l.len() + usize::from(!l.ends_with('\n')))
276 .sum()
277 }
278}
279
280impl BlameLines for BlobLines {
281 fn len(&self) -> usize {
282 BlobLines::len(self)
283 }
284
285 fn comparison_line(&self, idx: usize, ws: crate::diff::WhitespaceMode) -> &str {
286 BlobLines::comparison_line(self, idx, ws)
287 }
288
289 fn exact_len(&self, idx: usize) -> usize {
290 let start = self.line_start(idx);
291 (self.line_ends[idx] + 1).min(self.bytes().len()) - start
292 }
293
294 fn stored_line(&self, idx: usize) -> StoredLine<'_> {
295 StoredLine {
296 text: BlobLines::comparison_line(self, idx, crate::diff::WhitespaceMode::default())
297 .as_bytes(),
298 implied_newline: false,
299 }
300 }
301
302 fn tail_bytes(&self, lines: usize) -> usize {
303 if lines == 0 {
304 return 0;
305 }
306 self.bytes().len() - self.line_start(self.len() - lines)
307 }
308}
309
310const BLAME_TAIL_BLOCK: usize = 1024;
312
313fn blame_trimmed_tail_lines<L: BlameLines + ?Sized>(
321 old: &L,
322 new: &L,
323 affix: Affix,
324 ws: crate::diff::WhitespaceMode,
325) -> usize {
326 let (old_len, new_len) = (old.len(), new.len());
327 let shorter = old_len.min(new_len);
328 let equal_lines = if !ws.normalizes_line_compare() && affix.prefix + affix.suffix < shorter {
330 affix.suffix
331 } else {
332 (0..shorter)
333 .take_while(|&j| old.stored_line(old_len - 1 - j) == new.stored_line(new_len - 1 - j))
334 .count()
335 };
336 let mut common_bytes = new.tail_bytes(equal_lines);
337 if equal_lines < shorter {
338 let a = old.stored_line(old_len - 1 - equal_lines);
339 let b = new.stored_line(new_len - 1 - equal_lines);
340 common_bytes += a
341 .rev_bytes()
342 .zip(b.rev_bytes())
343 .take_while(|(x, y)| x == y)
344 .count();
345 }
346 let trimmed = common_bytes / BLAME_TAIL_BLOCK * BLAME_TAIL_BLOCK;
347 if trimmed == 0 {
348 return 0;
349 }
350 let (mut lo, mut hi) = (0usize, equal_lines);
353 while lo < hi {
354 let mid = (lo + hi).div_ceil(2);
355 if new.tail_bytes(mid) < trimmed {
356 lo = mid;
357 } else {
358 hi = mid - 1;
359 }
360 }
361 lo
362}
363
364const COMPACTION_CONTEXT: usize = 21;
369
370fn blame_lines_equal(a: &str, b: &str, ws: crate::diff::WhitespaceMode) -> bool {
371 if !ws.normalizes_line_compare() {
372 a == b
373 } else {
374 ws.normalize_line_for_compare(a) == ws.normalize_line_for_compare(b)
375 }
376}
377
378fn blame_compacted_line_ops<'s>(
387 old: &[&str],
388 new: &[&str],
389 shared: impl Iterator<Item = &'s str>,
390 shared_records: usize,
391 diff_algorithm: BlameDiffAlgorithm,
392 indent_heuristic: bool,
393 ws: crate::diff::WhitespaceMode,
394) -> Vec<DiffOp> {
395 use similar::TextDiff;
396
397 let normalized = ws.normalizes_line_compare().then(|| {
398 let norm = |lines: &[&str]| -> Vec<String> {
399 lines
400 .iter()
401 .map(|l| ws.normalize_line_for_compare(l).into_owned())
402 .collect()
403 };
404 (norm(old), norm(new))
405 });
406 let normalized_refs: Option<(Vec<&str>, Vec<&str>)> = normalized.as_ref().map(|(o, n)| {
407 (
408 o.iter().map(String::as_str).collect(),
409 n.iter().map(String::as_str).collect(),
410 )
411 });
412 let (old_cmp, new_cmp) = match &normalized_refs {
413 Some((o, n)) => (o.as_slice(), n.as_slice()),
414 None => (old, new),
415 };
416 let (prefix, suffix) = common_affix_len(old_cmp, new_cmp, ws);
417 let old_mid = &old_cmp[prefix..old_cmp.len() - suffix];
418 let new_mid = &new_cmp[prefix..new_cmp.len() - suffix];
419 if diff_algorithm == BlameDiffAlgorithm::Myers {
420 use crate::line_interner::LineInterner;
421 let mut interner = LineInterner::with_capacity(old_cmp.len() + new_cmp.len());
422 let ids1 = interner.intern_all(old_cmp);
423 let ids2 = interner.intern_all(new_cmp);
424 let mut shared_counts = vec![0u32; interner.len()];
425 for line in shared {
426 if let Some(id) = interner.get(&ws.normalize_line_for_compare(line)) {
427 shared_counts[id as usize] += 1;
428 }
429 }
430 return crate::diff::line_diff_ops_git_xdiff_compacted_ids(
431 &ids1,
432 &ids2,
433 &shared_counts,
434 shared_records,
435 old,
436 new,
437 indent_heuristic,
438 ws,
439 );
440 }
441 let mid_ops: Vec<DiffOp> = TextDiff::configure()
443 .algorithm(diff_algorithm.to_similar())
444 .diff_slices(old_mid, new_mid)
445 .ops()
446 .iter()
447 .map(|op| shift_diff_op(op, prefix))
448 .collect();
449 crate::diff_indent_heuristic::apply_change_compact_to_ops_with_ws(
450 &mid_ops,
451 old,
452 new,
453 indent_heuristic,
454 ws,
455 )
456}
457
458fn shift_diff_op(op: &DiffOp, by: usize) -> DiffOp {
460 match *op {
461 DiffOp::Equal {
462 old_index,
463 new_index,
464 len,
465 } => DiffOp::Equal {
466 old_index: old_index + by,
467 new_index: new_index + by,
468 len,
469 },
470 DiffOp::Delete {
471 old_index,
472 old_len,
473 new_index,
474 } => DiffOp::Delete {
475 old_index: old_index + by,
476 old_len,
477 new_index: new_index + by,
478 },
479 DiffOp::Insert {
480 old_index,
481 new_index,
482 new_len,
483 } => DiffOp::Insert {
484 old_index: old_index + by,
485 new_index: new_index + by,
486 new_len,
487 },
488 DiffOp::Replace {
489 old_index,
490 old_len,
491 new_index,
492 new_len,
493 } => DiffOp::Replace {
494 old_index: old_index + by,
495 old_len,
496 new_index: new_index + by,
497 new_len,
498 },
499 }
500}
501
502fn common_affix_len<L: BlameLines + ?Sized>(
504 old: &L,
505 new: &L,
506 ws: crate::diff::WhitespaceMode,
507) -> (usize, usize) {
508 let mut prefix = 0usize;
509 while prefix < old.len()
510 && prefix < new.len()
511 && blame_lines_equal(
512 old.comparison_line(prefix, ws),
513 new.comparison_line(prefix, ws),
514 ws,
515 )
516 {
517 prefix += 1;
518 }
519 let mut suffix = 0usize;
520 while suffix < old.len().saturating_sub(prefix)
521 && suffix < new.len().saturating_sub(prefix)
522 && blame_lines_equal(
523 old.comparison_line(old.len() - 1 - suffix, ws),
524 new.comparison_line(new.len() - 1 - suffix, ws),
525 ws,
526 )
527 {
528 suffix += 1;
529 }
530 (prefix, suffix)
531}
532
533pub fn build_line_map(
541 old: &[&str],
542 new: &[&str],
543 diff_algorithm: BlameDiffAlgorithm,
544 indent_heuristic: bool,
545) -> Vec<Option<usize>> {
546 build_line_map_with_changed_flags(
547 old,
548 new,
549 diff_algorithm,
550 indent_heuristic,
551 false,
552 crate::diff::WhitespaceMode::default(),
553 )
554 .0
555}
556
557fn build_line_map_with_changed_flags_owned(
561 old: &BlobLines,
562 new: &BlobLines,
563 affix: Affix,
564 diff_algorithm: BlameDiffAlgorithm,
565 indent_heuristic: bool,
566 with_changed_flags: bool,
567 ws: crate::diff::WhitespaceMode,
568) -> (Vec<Option<usize>>, Vec<bool>) {
569 windowed_line_map(
570 old,
571 new,
572 affix,
573 diff_algorithm,
574 indent_heuristic,
575 with_changed_flags,
576 ws,
577 )
578}
579
580fn build_line_map_with_changed_flags(
583 old: &[&str],
584 new: &[&str],
585 diff_algorithm: BlameDiffAlgorithm,
586 indent_heuristic: bool,
587 with_changed_flags: bool,
588 ws: crate::diff::WhitespaceMode,
589) -> (Vec<Option<usize>>, Vec<bool>) {
590 windowed_line_map(
591 old,
592 new,
593 slide_safe_affix(old, new, ws),
594 diff_algorithm,
595 indent_heuristic,
596 with_changed_flags,
597 ws,
598 )
599}
600
601fn windowed_line_map<L: BlameLines + ?Sized>(
608 old: &L,
609 new: &L,
610 affix: Affix,
611 diff_algorithm: BlameDiffAlgorithm,
612 indent_heuristic: bool,
613 with_changed_flags: bool,
614 ws: crate::diff::WhitespaceMode,
615) -> (Vec<Option<usize>>, Vec<bool>) {
616 let trimmed_tail = blame_trimmed_tail_lines(old, new, affix, ws);
617 let (safe_prefix, safe_suffix) = (affix.safe_prefix, affix.safe_suffix);
618 let tail = safe_suffix
619 .saturating_sub(COMPACTION_CONTEXT)
620 .max(trimmed_tail);
621 let (old_hi, new_hi) = (old.len() - tail, new.len() - tail);
622 let lo = safe_prefix
623 .saturating_sub(COMPACTION_CONTEXT)
624 .min(old_hi)
625 .min(new_hi);
626 let old_w: Vec<&str> = (lo..old_hi).map(|i| old.comparison_line(i, ws)).collect();
627 let new_w: Vec<&str> = (lo..new_hi).map(|i| new.comparison_line(i, ws)).collect();
628 let shared_tail = new_hi..new.len() - trimmed_tail;
629 let shared_records = lo + shared_tail.len();
630 let mid_lens = (!ws.normalizes_line_compare()).then(|| {
633 let lens = (affix.prefix..old.len() - affix.suffix)
634 .map(|i| old.exact_len(i))
635 .chain((affix.prefix..new.len() - affix.suffix).map(|i| new.exact_len(i)));
636 let mut seen = Vec::new();
637 for len in lens {
638 if seen.len() <= len {
639 seen.resize(len + 1, false);
640 }
641 seen[len] = true;
642 }
643 seen
644 });
645 let shared = (0..lo)
646 .chain(shared_tail)
647 .filter(|&i| {
648 mid_lens
649 .as_ref()
650 .is_none_or(|seen| seen.get(new.exact_len(i)).copied().unwrap_or(false))
651 })
652 .map(|i| new.comparison_line(i, ws));
653 let ops = blame_compacted_line_ops(
656 &old_w,
657 &new_w,
658 shared,
659 shared_records,
660 diff_algorithm,
661 indent_heuristic,
662 ws,
663 );
664 let window_map = crate::diff_indent_heuristic::map_new_to_old_from_ops(&ops, new_w.len());
665 let map = (0..lo)
666 .map(Some)
667 .chain(window_map.into_iter().map(|m| m.map(|i| i + lo)))
668 .chain((old_hi..old.len()).map(Some))
669 .collect();
670 let flags = if with_changed_flags {
671 let mut flags = vec![false; new.len()];
672 let window_flags =
673 crate::diff_indent_heuristic::new_line_changed_flags_from_ops(&ops, new_w.len());
674 flags[lo..new_hi].copy_from_slice(&window_flags);
675 flags
676 } else {
677 Vec::new()
678 };
679 (map, flags)
680}
681
682fn fill_exact_text_relocate(
684 old: &[&str],
685 new: &[&str],
686 map: &mut [Option<usize>],
687 used_old: &mut [usize],
688 ws: crate::diff::WhitespaceMode,
689) {
690 let mut by_content_norm: HashMap<String, Vec<usize>> = HashMap::new();
691 if ws.normalizes_line_compare() {
692 for (idx, line) in old.iter().enumerate() {
693 let key = ws.normalize_line_for_compare(line).into_owned();
694 by_content_norm.entry(key).or_default().push(idx);
695 }
696 } else {
697 let mut by_content: hashbrown::HashMap<&str, Vec<usize>> = map
700 .iter()
701 .zip(new)
702 .filter(|(slot, _)| slot.is_none())
703 .map(|(_, line)| (*line, Vec::new()))
704 .collect();
705 if by_content.is_empty() {
706 return;
707 }
708 let mut lens: Vec<usize> = by_content.keys().map(|line| line.len()).collect();
709 lens.sort_unstable();
710 lens.dedup();
711 for (idx, line) in old.iter().enumerate() {
712 if lens.binary_search(&line.len()).is_ok() {
713 if let Some(indices) = by_content.get_mut(line) {
714 indices.push(idx);
715 }
716 }
717 }
718 fill_exact_text_relocate_with_lookup(old, new, map, used_old, |line| {
719 by_content
720 .get(line)
721 .filter(|indices| !indices.is_empty())
722 .map(Vec::as_slice)
723 });
724 return;
725 }
726
727 fill_exact_text_relocate_with_lookup(old, new, map, used_old, |line| {
728 let key = ws.normalize_line_for_compare(line).into_owned();
729 by_content_norm.get(&key).map(Vec::as_slice)
730 });
731}
732
733fn fill_exact_text_relocate_with_lookup<'a>(
734 _old: &[&str],
735 new: &[&str],
736 map: &mut [Option<usize>],
737 used_old: &mut [usize],
738 mut lookup: impl FnMut(&str) -> Option<&'a [usize]>,
739) {
740 let mut windowed: Vec<usize> = Vec::new();
741 for new_idx in 0..map.len() {
742 if map[new_idx].is_some() {
743 continue;
744 }
745 let Some(candidates) = lookup(new[new_idx]) else {
746 continue;
747 };
748 let mut best: Option<(usize, usize, usize)> = None;
749 let scan: &[usize] = if candidates.len() > 32 {
750 windowed.clear();
751 const WINDOW: usize = 768;
752 let lo = new_idx.saturating_sub(WINDOW);
753 let hi = new_idx.saturating_add(WINDOW);
754 windowed.extend(
755 candidates
756 .iter()
757 .copied()
758 .filter(|&idx| (lo..=hi).contains(&idx)),
759 );
760 if windowed.is_empty() {
761 candidates
762 } else {
763 &windowed
764 }
765 } else {
766 candidates
767 };
768 for &old_idx in scan {
769 let candidate = (used_old[old_idx], old_idx.abs_diff(new_idx), old_idx);
770 if best.is_none_or(|b| candidate < b) {
771 best = Some(candidate);
772 }
773 }
774 if let Some((_, _, old_idx)) = best {
775 map[new_idx] = Some(old_idx);
776 used_old[old_idx] += 1;
777 }
778 }
779}
780
781pub fn build_exact_relocate_line_map(
785 old: &[&str],
786 new: &[&str],
787 exact_map: &[Option<usize>],
788) -> Vec<Option<usize>> {
789 build_exact_relocate_line_map_ws(old, new, exact_map, crate::diff::WhitespaceMode::default())
790}
791
792fn build_exact_relocate_line_map_ws(
793 old: &[&str],
794 new: &[&str],
795 exact_map: &[Option<usize>],
796 ws: crate::diff::WhitespaceMode,
797) -> Vec<Option<usize>> {
798 let mut fuzzy_map = exact_map.to_vec();
799 let mut used_old = vec![0usize; old.len()];
800 for old_idx in fuzzy_map.iter().flatten() {
801 if *old_idx < used_old.len() {
802 used_old[*old_idx] += 1;
803 }
804 }
805
806 fill_exact_text_relocate(old, new, &mut fuzzy_map, &mut used_old, ws);
807
808 fuzzy_map
809}
810
811fn blame_stops_on_current_commit(
817 diff_map: &[Option<usize>],
818 relocate_map: &[Option<usize>],
819 xdiff_changed: &[bool],
820) -> Vec<bool> {
821 let reposition = blame_reposition_on_current_commit(relocate_map);
822 let mut stop = vec![false; diff_map.len()];
823 for child_idx in 0..diff_map.len() {
824 if reposition.get(child_idx).copied().unwrap_or(false) {
825 stop[child_idx] = true;
826 continue;
827 }
828 if xdiff_changed.get(child_idx).copied().unwrap_or(false) {
829 if let Some(parent_idx) = diff_map[child_idx] {
830 if parent_idx == child_idx {
831 stop[child_idx] = true;
832 }
833 }
834 }
835 if stop[child_idx] {
836 continue;
837 }
838 if diff_map[child_idx].is_none() {
839 stop[child_idx] = true;
842 }
843 }
844 stop
845}
846
847#[allow(clippy::manual_isolate_lowest_one)]
849fn fenwick_max_prefix(len: &[usize], arg: &[usize], idx: usize) -> (usize, usize) {
850 let mut pos = idx;
851 let mut best_len = 0usize;
852 let mut best_i = usize::MAX;
853 while pos > 0 {
854 if len[pos] > best_len {
855 best_len = len[pos];
856 best_i = arg[pos];
857 }
858 pos &= pos - 1;
859 }
860 (best_len, best_i)
861}
862
863#[allow(clippy::manual_isolate_lowest_one)]
864fn fenwick_max_set(len: &mut [usize], arg: &mut [usize], idx: usize, value: usize, i: usize) {
865 let mut pos = idx;
866 while pos < len.len() {
867 if value > len[pos] {
868 len[pos] = value;
869 arg[pos] = i;
870 }
871 pos += pos & pos.wrapping_neg();
872 }
873}
874
875fn blame_reposition_on_current_commit(line_map: &[Option<usize>]) -> Vec<bool> {
877 let n = line_map.len();
878 let pairs: Vec<(usize, usize)> = line_map
879 .iter()
880 .enumerate()
881 .filter_map(|(child, parent)| parent.map(|p| (child, p)))
882 .collect();
883
884 if pairs.is_empty() || pairs.iter().all(|&(c, p)| c == p) {
885 return vec![false; n];
886 }
887
888 let mut in_lis = vec![false; n];
889 if !pairs.is_empty() {
890 let len = pairs.len();
891 let mut lis_len = vec![1usize; len];
892 let mut pred = vec![usize::MAX; len];
893 let max_parent = pairs.iter().map(|(_, p)| *p).max().unwrap_or(0);
894 let fw_size = max_parent + 2;
895 let mut fw_len = vec![0usize; fw_size];
896 let mut fw_idx = vec![usize::MAX; fw_size];
897 for i in 0..len {
898 let p = pairs[i].1;
899 let (prev_len, prev_i) = fenwick_max_prefix(&fw_len, &fw_idx, p);
900 if prev_len > 0 {
901 pred[i] = prev_i;
902 }
903 lis_len[i] = prev_len + 1;
904 fenwick_max_set(&mut fw_len, &mut fw_idx, p + 1, lis_len[i], i);
905 }
906 let max_lis = *lis_len.iter().max().unwrap_or(&1);
907 let end = (0..len)
908 .filter(|&i| lis_len[i] == max_lis)
909 .max_by_key(|&i| pairs[i].1)
910 .unwrap_or(0);
911
912 let mut cur = end;
913 loop {
914 in_lis[pairs[cur].0] = true;
915 if pred[cur] == usize::MAX {
916 break;
917 }
918 cur = pred[cur];
919 }
920 }
921
922 let mut blame_current = vec![false; n];
923 for (child_idx, parent_idx) in line_map
924 .iter()
925 .enumerate()
926 .filter_map(|(c, p)| p.map(|p| (c, p)))
927 {
928 if parent_idx == child_idx {
929 continue;
930 }
931 if !in_lis[child_idx] {
932 blame_current[child_idx] = true;
933 }
934 }
935 blame_current
936}
937
938pub fn build_fuzzy_line_map(
940 old: &[&str],
941 new: &[&str],
942 exact_map: &[Option<usize>],
943) -> Vec<Option<usize>> {
944 build_fuzzy_line_map_ws(old, new, exact_map, crate::diff::WhitespaceMode::default())
945}
946
947fn build_fuzzy_line_map_ws(
948 old: &[&str],
949 new: &[&str],
950 exact_map: &[Option<usize>],
951 ws: crate::diff::WhitespaceMode,
952) -> Vec<Option<usize>> {
953 let mut fuzzy_map = exact_map.to_vec();
954 let mut used_old = vec![0usize; old.len()];
955 for old_idx in fuzzy_map.iter().flatten() {
956 if *old_idx < used_old.len() {
957 used_old[*old_idx] += 1;
958 }
959 }
960
961 fill_exact_text_relocate(old, new, &mut fuzzy_map, &mut used_old, ws);
965
966 let mut anchors: Vec<(usize, usize)> = exact_map
967 .iter()
968 .enumerate()
969 .filter_map(|(new_idx, old_idx)| old_idx.map(|old| (new_idx, old)))
970 .collect();
971 anchors.sort_unstable();
972
973 let mut prev_new = usize::MAX;
974 let mut prev_old = usize::MAX;
975 for (next_new, next_old) in anchors
976 .iter()
977 .copied()
978 .chain(std::iter::once((new.len(), old.len())))
979 {
980 let new_start = if prev_new == usize::MAX {
981 0
982 } else {
983 prev_new + 1
984 };
985 let new_end = next_new;
986 let old_start = if prev_old == usize::MAX {
987 0
988 } else {
989 prev_old + 1
990 };
991 let old_end = next_old;
992
993 if new_start < new_end && old_start < old_end {
994 let segment_matches =
995 fuzzy_match_segment(old, old_start, old_end, new, new_start, new_end);
996 for (new_idx, old_idx) in segment_matches {
997 if fuzzy_map[new_idx].is_none() {
998 fuzzy_map[new_idx] = Some(old_idx);
999 used_old[old_idx] += 1;
1000 }
1001 }
1002 }
1003
1004 prev_new = next_new;
1005 prev_old = next_old;
1006 }
1007
1008 for new_idx in 0..fuzzy_map.len() {
1012 if fuzzy_map[new_idx].is_some() {
1013 continue;
1014 }
1015 let prev_old = (0..new_idx).rev().find_map(|i| fuzzy_map[i]);
1016 let next_old = ((new_idx + 1)..fuzzy_map.len()).find_map(|i| fuzzy_map[i]);
1017
1018 let mut candidates = Vec::new();
1019 if let Some(o) = prev_old {
1020 candidates.push(o);
1021 }
1022 if let Some(o) = next_old {
1023 if candidates.last().copied() != Some(o) {
1024 candidates.push(o);
1025 }
1026 }
1027
1028 let mut best: Option<(f64, usize)> = None;
1029 for old_idx in candidates {
1030 if old_idx >= old.len() {
1031 continue;
1032 }
1033 let (sim, lcs) = line_similarity_and_lcs(old[old_idx], new[new_idx]);
1034 let exact_text = old[old_idx].trim() == new[new_idx].trim();
1035 if !exact_text && lcs < 6 && !(sim >= 0.35 && lcs >= 3) {
1037 continue;
1038 }
1039
1040 let mut score = lcs as f64 + sim * 10.0;
1041 score -= 0.2 * used_old[old_idx] as f64;
1042
1043 if let (Some(lo), Some(hi)) = (prev_old, next_old) {
1044 if lo <= hi && (old_idx < lo || old_idx > hi) {
1045 score -= 3.0;
1046 }
1047 }
1048
1049 if best.is_none_or(|(best_score, best_old)| {
1050 score > best_score
1051 || ((score - best_score).abs() < 1e-9
1052 && old_idx.abs_diff(new_idx) < best_old.abs_diff(new_idx))
1053 }) {
1054 best = Some((score, old_idx));
1055 }
1056 }
1057
1058 if let Some((_, old_idx)) = best {
1059 fuzzy_map[new_idx] = Some(old_idx);
1060 used_old[old_idx] += 1;
1061 }
1062 }
1063
1064 for new_idx in 0..fuzzy_map.len() {
1068 if fuzzy_map[new_idx].is_some() {
1069 continue;
1070 }
1071 let prev_old = (0..new_idx).rev().find_map(|i| fuzzy_map[i]);
1072 let next_old = ((new_idx + 1)..fuzzy_map.len()).find_map(|i| fuzzy_map[i]);
1073
1074 let mut best: Option<(f64, usize)> = None;
1075 for old_idx in 0..old.len() {
1076 let (sim, lcs) = line_similarity_and_lcs(old[old_idx], new[new_idx]);
1077 let exact_text = old[old_idx].trim() == new[new_idx].trim();
1078 let new_len = new[new_idx].trim().chars().count();
1079 if !exact_text && (new_len < 3 || sim < 0.45 || lcs < 2) {
1080 continue;
1081 }
1082
1083 let mut score = sim;
1084 score -= 0.004 * old_idx.abs_diff(new_idx) as f64;
1085 score -= 0.08 * used_old[old_idx] as f64;
1086
1087 if let (Some(lo), Some(hi)) = (prev_old, next_old) {
1090 if lo <= hi {
1091 if old_idx < lo {
1092 score -= 0.20 * (lo - old_idx) as f64;
1093 } else if old_idx > hi {
1094 score -= 0.20 * (old_idx - hi) as f64;
1095 }
1096 }
1097 } else if let Some(lo) = prev_old {
1098 if old_idx < lo {
1099 score -= 0.20 * (lo - old_idx) as f64;
1100 }
1101 } else if let Some(hi) = next_old {
1102 if old_idx > hi {
1103 score -= 0.20 * (old_idx - hi) as f64;
1104 }
1105 }
1106
1107 if best.is_none_or(|(best_score, best_idx)| {
1108 score > best_score
1109 || ((score - best_score).abs() < 1e-9
1110 && old_idx.abs_diff(new_idx) < best_idx.abs_diff(new_idx))
1111 }) {
1112 best = Some((score, old_idx));
1113 }
1114 }
1115
1116 if let Some((_, old_idx)) = best {
1117 fuzzy_map[new_idx] = Some(old_idx);
1118 used_old[old_idx] += 1;
1119 }
1120 }
1121
1122 fuzzy_map
1123}
1124
1125fn fuzzy_match_segment(
1126 old: &[&str],
1127 old_start: usize,
1128 old_end: usize,
1129 new: &[&str],
1130 new_start: usize,
1131 new_end: usize,
1132) -> Vec<(usize, usize)> {
1133 let m = old_end.saturating_sub(old_start);
1134 let n = new_end.saturating_sub(new_start);
1135 if m == 0 || n == 0 {
1136 return Vec::new();
1137 }
1138
1139 let states = m + 1;
1144 let neg_inf = f64::NEG_INFINITY;
1145 let mut dp = vec![neg_inf; states];
1146 dp[0] = 0.0;
1147
1148 let mut back_prev = vec![vec![usize::MAX; states]; n + 1];
1149 let mut back_pick = vec![vec![None; states]; n + 1];
1150
1151 for j in 0..n {
1152 let mut next_dp = vec![neg_inf; states];
1153
1154 for state in 0..states {
1155 let base = dp[state];
1156 if !base.is_finite() {
1157 continue;
1158 }
1159
1160 if base > next_dp[state] {
1162 next_dp[state] = base;
1163 back_prev[j + 1][state] = state;
1164 back_pick[j + 1][state] = None;
1165 }
1166
1167 let start_k = if state == 0 { 0 } else { state - 1 };
1169 for k in start_k..m {
1170 let old_idx = old_start + k;
1171 let new_idx = new_start + j;
1172 let (sim, lcs) = line_similarity_and_lcs(old[old_idx], new[new_idx]);
1173 let exact_text = old[old_idx].trim() == new[new_idx].trim();
1174 let new_len = new[new_idx].trim().chars().count();
1175 if !exact_text && (new_len < 3 || sim < 0.45 || lcs < 2) {
1176 continue;
1177 }
1178
1179 let distance = k.abs_diff(j) as f64;
1181 let score = base + sim - 0.002 * distance;
1182 let next_state = k + 1;
1183 if score > next_dp[next_state] {
1184 next_dp[next_state] = score;
1185 back_prev[j + 1][next_state] = state;
1186 back_pick[j + 1][next_state] = Some(k);
1187 }
1188 }
1189 }
1190
1191 dp = next_dp;
1192 }
1193
1194 let mut best_state = 0usize;
1195 for state in 1..states {
1196 if dp[state] > dp[best_state] {
1197 best_state = state;
1198 }
1199 }
1200
1201 let mut matches = Vec::new();
1202 let mut state = best_state;
1203 for j in (1..=n).rev() {
1204 if let Some(k) = back_pick[j][state] {
1205 matches.push((new_start + (j - 1), old_start + k));
1206 }
1207 let prev = back_prev[j][state];
1208 if prev == usize::MAX {
1209 break;
1210 }
1211 state = prev;
1212 }
1213 matches.reverse();
1214 matches
1215}
1216
1217fn line_similarity_and_lcs(a: &str, b: &str) -> (f64, usize) {
1218 let a = a.trim();
1219 let b = b.trim();
1220 if a.is_empty() || b.is_empty() {
1221 return (0.0, 0);
1222 }
1223 if a == b {
1224 return (1.0, a.chars().count());
1225 }
1226
1227 let a = a.to_ascii_lowercase();
1228 let b = b.to_ascii_lowercase();
1229 let a_chars: Vec<char> = a.chars().collect();
1230 let b_chars: Vec<char> = b.chars().collect();
1231 let n = b_chars.len();
1232 if a_chars.is_empty() || b_chars.is_empty() {
1233 return (0.0, 0);
1234 }
1235
1236 let mut prev = vec![0usize; n + 1];
1237 let mut curr = vec![0usize; n + 1];
1238 for i in 1..=a_chars.len() {
1239 for j in 1..=n {
1240 curr[j] = if a_chars[i - 1] == b_chars[j - 1] {
1241 prev[j - 1] + 1
1242 } else {
1243 prev[j].max(curr[j - 1])
1244 };
1245 }
1246 std::mem::swap(&mut prev, &mut curr);
1247 curr.fill(0);
1248 }
1249
1250 let lcs = prev[n];
1251 let sim = (2.0 * lcs as f64) / (a_chars.len() as f64 + b_chars.len() as f64);
1252 (sim, lcs)
1253}
1254
1255use std::collections::{BinaryHeap, HashMap, HashSet};
1256use std::fs::OpenOptions;
1257use std::io::{self, Write};
1258use std::path::Path;
1259use std::sync::Arc;
1260
1261use crate::attributes::{load_gitattributes_stack, MacroTable, ParsedGitAttributes};
1262use crate::commit_graph_file::CommitGraphChain;
1263use crate::config::ConfigSet;
1264use crate::crlf::{
1265 convert_to_worktree_eager, file_attrs_from_gitattributes, load_gitattributes_from_index,
1266 load_gitattributes_parsed, ConversionConfig, GitAttributes,
1267};
1268use crate::diff::{detect_renames, diff_trees, DiffEntry, DiffStatus};
1269use crate::error::{Error as LibError, Result};
1270use crate::index::Index;
1271use crate::objects::{
1272 commit_author_and_message_lenient, find_tree_entry_by_name_dir_or_file_in_data,
1273 parse_commit_tree_and_parents_lenient, parse_commit_tree_and_parents_prefix, parse_tree,
1274 Object, ObjectId, ObjectKind,
1275};
1276use crate::odb::Odb;
1277use crate::pack::SingleLocalPackReader;
1278use crate::parallel::ThreadPolicy;
1279use crate::repo::Repository;
1280use crate::repo_relpath::{format_path_stored, storage_bytes, storage_string};
1281use crate::wildmatch::wildmatch;
1282
1283pub type PromisorHydrateHook = fn(&Repository, ObjectId);
1288
1289#[derive(Debug, Clone, PartialEq, Eq)]
1291pub struct BlameLine {
1292 pub oid: ObjectId,
1294 pub final_lineno: usize,
1296 pub orig_lineno: usize,
1298 pub content: String,
1300 pub encoding_lossy: bool,
1302 pub content_bytes: Option<Vec<u8>>,
1304 pub original_path: String,
1308 pub source_file: Option<String>,
1310 pub ignored: bool,
1312 pub unblamable: bool,
1314 pub not_committed: bool,
1317}
1318
1319fn final_line_display(final_text: &BlobLines, final_lineno: usize) -> BlameLineDisplay {
1320 final_text.line_display(final_lineno - 1)
1321}
1322
1323#[allow(clippy::too_many_arguments)]
1324fn blame_line_at_path(
1325 oid: ObjectId,
1326 final_lineno: usize,
1327 orig_lineno: usize,
1328 display: BlameLineDisplay,
1329 blame_path: &str,
1330 source_file: Option<String>,
1331 ignored: bool,
1332 unblamable: bool,
1333) -> BlameLine {
1334 let original_path = source_file.clone().unwrap_or_else(|| blame_path.to_owned());
1335 BlameLine {
1336 oid,
1337 final_lineno,
1338 orig_lineno,
1339 content: display.content,
1340 encoding_lossy: display.encoding_lossy,
1341 content_bytes: display.content_bytes,
1342 original_path,
1343 source_file,
1344 ignored,
1345 unblamable,
1346 not_committed: false,
1347 }
1348}
1349
1350fn blame_line_from_parent(
1351 pb: &BlameLine,
1352 final_lineno: usize,
1353 display: BlameLineDisplay,
1354 source_file: Option<String>,
1355 ignored: bool,
1356 unblamable: bool,
1357) -> BlameLine {
1358 BlameLine {
1359 oid: pb.oid,
1360 final_lineno,
1361 orig_lineno: pb.orig_lineno,
1362 content: display.content,
1363 encoding_lossy: display.encoding_lossy,
1364 content_bytes: display.content_bytes,
1365 original_path: pb.original_path.clone(),
1366 source_file,
1367 ignored,
1368 unblamable,
1369 not_committed: false,
1370 }
1371}
1372
1373fn resolve_path_in_tree_entry(
1375 odb: &Odb,
1376 tree_oid: &ObjectId,
1377 path: &str,
1378) -> Result<Option<(ObjectId, u32)>> {
1379 Ok(odb
1380 .tree_entry_at_path(tree_oid, &storage_bytes(path))?
1381 .filter(|e| e.mode != 0o040000)
1382 .map(|e| (e.oid, e.mode)))
1383}
1384
1385#[cfg(test)]
1388fn content_lines(s: &str) -> Vec<&str> {
1389 if s.is_empty() {
1390 return Vec::new();
1391 }
1392 let mut out: Vec<&str> = s.split('\n').collect();
1393 if out.last() == Some(&"") {
1394 out.pop();
1395 }
1396 out
1397}
1398
1399#[derive(Debug, Clone)]
1403struct TrackedLine {
1404 final_lineno: usize,
1405 current_idx: usize,
1406 ignored: bool,
1407 source_path: Option<String>,
1408 origin: u32,
1411}
1412
1413struct BlameSuspect {
1415 oid: ObjectId,
1416 blob: ObjectId,
1417 mode: u32,
1418 path: String,
1419 lines: Option<Arc<BlobLines>>,
1421 pending: Vec<TrackedLine>,
1422}
1423
1424struct BlameOverlay {
1427 lines: Arc<BlobLines>,
1428 parents: Vec<ObjectId>,
1430 copy_paths: Option<HashSet<String>>,
1432}
1433
1434struct OverlayWalk<'a> {
1436 path: &'a str,
1437 ignore_revs: &'a HashSet<ObjectId>,
1438 diff_algorithm: BlameDiffAlgorithm,
1439 textconv_ctx: Option<&'a BlameTextconvContext>,
1440 use_textconv: bool,
1441 copy_depth: usize,
1442 first_parent_only: bool,
1443 grafts: &'a HashMap<ObjectId, Vec<ObjectId>>,
1444 indent_heuristic: bool,
1445}
1446
1447#[allow(clippy::too_many_arguments)]
1454fn pass_overlay_to_parents(
1455 odb: &Odb,
1456 session: &mut BlameSession<'_>,
1457 overlay: &BlameOverlay,
1458 walk: &OverlayWalk<'_>,
1459 pending: Vec<TrackedLine>,
1460 final_text: &BlobLines,
1461 commit_cache: &mut BlameCommitCache,
1462 queue: &mut SuspectQueue,
1463 next_origin: &mut u32,
1464 result: &mut Vec<BlameLine>,
1465) -> Result<Option<BlameSuspect>> {
1466 let ws = session.whitespace;
1467 let cur = overlay.lines.as_ref();
1468 if session.merge_line_stamps.len() < overlay.parents.len() {
1469 session
1470 .merge_line_stamps
1471 .resize_with(overlay.parents.len(), LineUseStamp::default);
1472 }
1473 let mut remaining = pending;
1474 let mut first = None;
1475 let parent_count = if walk.first_parent_only {
1476 1
1477 } else {
1478 overlay.parents.len()
1479 };
1480 for (i, &parent) in overlay.parents.iter().take(parent_count).enumerate() {
1481 if remaining.is_empty() {
1482 break;
1483 }
1484 let Some((blob, mode)) =
1485 session.resolve_blob_at_commit(odb, parent, walk.path, commit_cache)?
1486 else {
1487 continue;
1488 };
1489 let par = session.load_blob_lines(blob, walk.path, mode)?;
1490 let (map, _) = build_line_map_with_changed_flags_owned(
1491 &par,
1492 cur,
1493 slide_safe_affix(par.as_ref(), cur, ws),
1494 walk.diff_algorithm,
1495 walk.indent_heuristic,
1496 false,
1497 ws,
1498 );
1499 session.merge_line_stamps[i].begin_pass(par.len());
1500 let origin = if i == 0 {
1501 0
1502 } else {
1503 *next_origin += 1;
1504 *next_origin
1505 };
1506 let mut handed = Vec::new();
1507 let mut unmatched = Vec::new();
1508 for t in remaining {
1509 let matched = map.get(t.current_idx).copied().flatten().filter(|&p_idx| {
1510 p_idx < par.len()
1511 && session.merge_line_stamps[i].is_unused(p_idx)
1512 && blame_lines_equal(
1513 par.comparison_line(p_idx, ws),
1514 cur.comparison_line(t.current_idx, ws),
1515 ws,
1516 )
1517 });
1518 match matched {
1519 Some(p_idx) => {
1520 session.merge_line_stamps[i].mark(p_idx);
1521 handed.push(TrackedLine {
1522 current_idx: p_idx,
1523 origin,
1524 ..t
1525 });
1526 }
1527 None => unmatched.push(t),
1528 }
1529 }
1530 remaining = unmatched;
1531 if handed.is_empty() {
1532 continue;
1533 }
1534 let suspect = BlameSuspect {
1535 oid: parent,
1536 blob,
1537 mode,
1538 path: walk.path.to_owned(),
1539 lines: Some(par),
1540 pending: handed,
1541 };
1542 if i == 0 {
1543 first = Some(suspect);
1544 } else {
1545 queue.push(suspect, || blame_suspect_date(session, &parent));
1546 }
1547 }
1548
1549 if walk.copy_depth >= 1 && !remaining.is_empty() {
1550 if let Some(&parent) = overlay.parents.first() {
1551 let parent_commit =
1552 get_blame_commit(odb, parent, commit_cache, session.commit_graph.as_deref())?;
1553 let (left, _handoff, copy_lines) = copy_detect_attribute_pending(
1554 odb,
1555 session,
1556 remaining,
1557 &overlay.lines,
1558 final_text,
1559 walk.path,
1560 parent,
1561 &parent_commit.tree,
1562 &parent_commit.tree,
1563 walk.path,
1564 walk.copy_depth,
1565 false,
1566 Some(overlay.copy_paths.as_ref()),
1567 None,
1568 walk.ignore_revs,
1569 walk.diff_algorithm,
1570 walk.textconv_ctx,
1571 walk.use_textconv,
1572 walk.first_parent_only,
1573 walk.grafts,
1574 walk.indent_heuristic,
1575 )?;
1576 result.extend(copy_lines);
1577 remaining = left;
1578 }
1579 }
1580
1581 let null = ObjectId::null(odb.hash_algo());
1582 for t in remaining {
1583 result.push(BlameLine {
1584 oid: null,
1585 final_lineno: t.final_lineno,
1586 orig_lineno: t.current_idx + 1,
1587 content: final_line_display(final_text, t.final_lineno).content,
1588 encoding_lossy: final_line_display(final_text, t.final_lineno).encoding_lossy,
1589 content_bytes: final_line_display(final_text, t.final_lineno).content_bytes,
1590 original_path: walk.path.to_owned(),
1591 source_file: None,
1592 ignored: false,
1593 unblamable: false,
1594 not_committed: true,
1595 });
1596 }
1597 Ok(first)
1598}
1599
1600#[derive(PartialEq, Eq)]
1602struct SuspectSlot {
1603 date: i64,
1604 seq: u64,
1605 oid: ObjectId,
1606 path: String,
1607}
1608
1609impl Ord for SuspectSlot {
1610 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1611 self.date
1612 .cmp(&other.date)
1613 .then_with(|| other.seq.cmp(&self.seq))
1614 }
1615}
1616
1617impl PartialOrd for SuspectSlot {
1618 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1619 Some(self.cmp(other))
1620 }
1621}
1622
1623#[derive(Default)]
1630struct SuspectQueue {
1631 heap: BinaryHeap<SuspectSlot>,
1632 entries: HashMap<(ObjectId, String), BlameSuspect>,
1633 seq: u64,
1634}
1635
1636impl SuspectQueue {
1637 fn is_empty(&self) -> bool {
1638 self.heap.is_empty()
1639 }
1640
1641 fn push(&mut self, suspect: BlameSuspect, date: impl FnOnce() -> i64) {
1645 let key = (suspect.oid, suspect.path);
1646 if let Some(queued) = self.entries.get_mut(&key) {
1647 queued.pending.extend(suspect.pending);
1648 if queued.lines.is_none() {
1649 queued.lines = suspect.lines;
1650 }
1651 return;
1652 }
1653 self.heap.push(SuspectSlot {
1654 date: date(),
1655 seq: self.seq,
1656 oid: key.0,
1657 path: key.1.clone(),
1658 });
1659 self.seq += 1;
1660 self.entries.insert(
1661 key.clone(),
1662 BlameSuspect {
1663 oid: key.0,
1664 path: key.1,
1665 ..suspect
1666 },
1667 );
1668 }
1669
1670 fn pop(&mut self) -> Option<BlameSuspect> {
1672 let slot = self.heap.pop()?;
1673 self.entries.remove(&(slot.oid, slot.path))
1674 }
1675}
1676
1677fn blame_suspect_date(session: &mut BlameSession<'_>, oid: &ObjectId) -> i64 {
1679 if let Some((_, date)) = session
1680 .commit_graph
1681 .as_ref()
1682 .and_then(|chain| chain.graph_commit(oid))
1683 {
1684 return date;
1685 }
1686 session
1687 .pack
1688 .read(session.odb, oid)
1689 .ok()
1690 .and_then(|obj| {
1691 crate::objects::CommitRef::parse(&obj.data)
1692 .ok()
1693 .map(|c| c.committer_unix_seconds_for_ordering())
1694 })
1695 .unwrap_or(0)
1696}
1697
1698#[derive(Debug, Clone)]
1700pub struct BlameTextconvContext {
1701 pub config: ConfigSet,
1703 conversion: ConversionConfig,
1704 pub attrs: GitAttributes,
1706 attr_macros: MacroTable,
1707 diff_attrs: Vec<DiffAttrRule>,
1708 filter_process: std::sync::Arc<crate::repo_caches::RepoCaches>,
1709}
1710
1711impl BlameTextconvContext {
1712 pub fn new(repo: &Repository) -> Self {
1714 let config = repo.config().map(|c| (*c).clone()).unwrap_or_default();
1715 let conversion = ConversionConfig::from_config(&config);
1716 let parsed = load_attr_parsed(repo);
1717 let attrs = parsed.rules.clone();
1718 let attr_macros = parsed.macros;
1719 let diff_attrs = load_diff_attr_rules(repo);
1720 Self {
1721 config,
1722 conversion,
1723 attrs,
1724 attr_macros,
1725 diff_attrs,
1726 filter_process: std::sync::Arc::clone(repo.caches()),
1727 }
1728 }
1729
1730 fn filters(&self) -> &crate::filter_process::FilterProcessState {
1731 self.filter_process.filters()
1732 }
1733}
1734
1735#[derive(Debug, Clone)]
1736struct DiffAttrRule {
1737 pattern: String,
1738 value: DiffAttrValue,
1739}
1740
1741#[derive(Debug, Clone)]
1742enum DiffAttrValue {
1743 Unset,
1744 Set,
1745 Driver(String),
1746}
1747
1748fn load_attr_parsed(repo: &Repository) -> ParsedGitAttributes {
1749 if let Some(work_tree) = repo.work_tree() {
1750 if let Ok(p) = load_gitattributes_stack(repo, work_tree) {
1751 if !p.rules.is_empty() {
1752 return p;
1753 }
1754 }
1755 let p = load_gitattributes_parsed(work_tree);
1756 if !p.rules.is_empty() {
1757 return p;
1758 }
1759 }
1760
1761 if let Ok(index) = repo.load_index() {
1762 if let Some(wt) = repo.work_tree() {
1763 if let Ok(p) = crate::attributes::load_gitattributes_from_index(&index, repo.odb(), wt)
1764 {
1765 return p;
1766 }
1767 }
1768 return ParsedGitAttributes {
1769 rules: load_gitattributes_from_index(&index, repo.odb()),
1770 ..ParsedGitAttributes::default()
1771 };
1772 }
1773
1774 ParsedGitAttributes::default()
1775}
1776
1777fn load_diff_attr_rules(repo: &Repository) -> Vec<DiffAttrRule> {
1778 let mut rules = Vec::new();
1779
1780 if let Some(work_tree) = repo.work_tree() {
1781 parse_diff_attr_file(&work_tree.join(".gitattributes"), &mut rules);
1782 parse_diff_attr_file(&work_tree.join(".git/info/attributes"), &mut rules);
1783 }
1784
1785 if rules.is_empty() {
1786 if let Ok(index) = repo.load_index() {
1787 if let Some(entry) = index.get(b".gitattributes", 0) {
1788 if let Ok(obj) = repo.odb().read(&entry.oid) {
1789 if let Ok(content) = String::from_utf8(obj.data) {
1790 parse_diff_attr_content(&content, &mut rules);
1791 }
1792 }
1793 }
1794 }
1795 parse_diff_attr_file(&repo.git_dir().join("info/attributes"), &mut rules);
1796 }
1797
1798 rules
1799}
1800
1801pub fn read_object_for_blame(
1803 odb: &Odb,
1804 oid: &ObjectId,
1805 repo: Option<&Repository>,
1806) -> Result<Object> {
1807 match odb.read(oid) {
1808 Ok(obj) => Ok(obj),
1809 Err(LibError::ObjectNotFound(_)) => {
1810 if let Some(repo) = repo {
1811 if let Some(hook) = repo.promisor_hydrate_hook() {
1812 hook(repo, *oid);
1813 return odb.read(oid);
1814 }
1815 }
1816 Err(LibError::ObjectNotFound(oid.to_hex()))
1817 }
1818 Err(err) => Err(err),
1819 }
1820}
1821
1822fn parse_diff_attr_file(path: &Path, rules: &mut Vec<DiffAttrRule>) {
1823 if let Ok(content) = std::fs::read_to_string(path) {
1824 parse_diff_attr_content(&content, rules);
1825 }
1826}
1827
1828fn parse_diff_attr_content(content: &str, rules: &mut Vec<DiffAttrRule>) {
1829 for line in content.lines() {
1830 let line = line.trim();
1831 if line.is_empty() || line.starts_with('#') {
1832 continue;
1833 }
1834
1835 let mut parts = line.split_whitespace();
1836 let Some(pattern) = parts.next() else {
1837 continue;
1838 };
1839
1840 let mut value: Option<DiffAttrValue> = None;
1841 for token in parts {
1842 if token == "binary" || token == "-diff" {
1843 value = Some(DiffAttrValue::Unset);
1844 } else if token == "diff" {
1845 value = Some(DiffAttrValue::Set);
1846 } else if let Some(driver) = token.strip_prefix("diff=") {
1847 value = Some(DiffAttrValue::Driver(driver.to_owned()));
1848 }
1849 }
1850
1851 if let Some(value) = value {
1852 rules.push(DiffAttrRule {
1853 pattern: pattern.to_owned(),
1854 value,
1855 });
1856 }
1857 }
1858}
1859
1860fn resolve_textconv_command(ctx: &BlameTextconvContext, path: &str) -> Option<String> {
1861 let mut selected: Option<DiffAttrValue> = None;
1862 for rule in &ctx.diff_attrs {
1863 if diff_attr_pattern_matches(&rule.pattern, path) {
1864 selected = Some(rule.value.clone());
1865 }
1866 }
1867
1868 match selected {
1869 Some(DiffAttrValue::Driver(driver)) => ctx.config.get(&format!("diff.{driver}.textconv")),
1870 _ => None,
1871 }
1872}
1873
1874fn diff_attr_pattern_matches(pattern: &str, path: &str) -> bool {
1875 if pattern.contains('/') {
1876 return wildmatch(pattern.as_bytes(), path.as_bytes(), 0);
1877 }
1878 let basename = path.rsplit('/').next().unwrap_or(path);
1879 wildmatch(pattern.as_bytes(), basename.as_bytes(), 0)
1880}
1881
1882fn is_regular_mode(mode: u32) -> bool {
1883 mode & 0o170000 == 0o100000
1884}
1885
1886fn run_textconv_command(command: &str, input_data: &[u8]) -> Result<Vec<u8>> {
1887 use crate::command_runner::{run_sh_dash_c, system_command_runner, CommandStdin, CommandStdio};
1888 let temp_path = create_temp_textconv_file(input_data)?;
1889 let quoted = shell_quote(temp_path.to_string_lossy().as_ref());
1890 let shell_command = format!("{command} {quoted}");
1891
1892 let output = run_sh_dash_c(
1893 system_command_runner().as_ref(),
1894 &shell_command,
1895 None,
1896 CommandStdin::Null,
1897 CommandStdio::Inherit,
1898 )
1899 .map_err(|e| LibError::Message(format!("running textconv command '{command}': {e}")))?;
1900
1901 let _ = std::fs::remove_file(&temp_path);
1902
1903 if !output.status.success {
1904 return Err(LibError::Message(format!(
1905 "textconv command exited with status {}",
1906 output.status.code.unwrap_or(-1)
1907 )));
1908 }
1909
1910 Ok(output.stdout)
1911}
1912
1913fn create_temp_textconv_file(data: &[u8]) -> Result<std::path::PathBuf> {
1914 let pid = std::process::id();
1915 for attempt in 0..32u32 {
1916 let now = crate::git_date::tm::process_wall_clock_sec() as u128;
1917 let path = std::env::temp_dir().join(format!("grit-blame-textconv-{pid}-{now}-{attempt}"));
1918 match OpenOptions::new().create_new(true).write(true).open(&path) {
1920 Ok(mut file) => {
1921 file.write_all(data)?;
1922 return Ok(path);
1923 }
1924 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => continue,
1925 Err(err) => return Err(err.into()),
1926 }
1927 }
1928 Err(LibError::Message(
1929 "failed to create temporary textconv input file".to_string(),
1930 ))
1931}
1932
1933fn shell_quote(text: &str) -> String {
1934 format!("'{}'", text.replace('\'', "'\\''"))
1935}
1936
1937fn read_blob_content_for_blame(
1938 odb: &Odb,
1939 oid: &ObjectId,
1940 path: &str,
1941 mode: u32,
1942 textconv_ctx: Option<&BlameTextconvContext>,
1943 use_textconv: bool,
1944) -> Result<Vec<u8>> {
1945 let obj = read_object_for_blame(odb, oid, None)?;
1946 if obj.kind != ObjectKind::Blob {
1947 return Err(LibError::Message("expected blob object".to_string()));
1948 }
1949
1950 if !use_textconv || !is_regular_mode(mode) {
1951 return Ok(obj.data);
1952 }
1953
1954 let Some(ctx) = textconv_ctx else {
1955 return Ok(obj.data);
1956 };
1957 let Some(command) = resolve_textconv_command(ctx, path) else {
1958 return Ok(obj.data);
1959 };
1960
1961 let attrs =
1962 file_attrs_from_gitattributes(&ctx.attrs, &ctx.attr_macros, path, false, &ctx.config);
1963 let oid_hex = oid.to_string();
1964 let worktree_data = convert_to_worktree_eager(
1965 &obj.data,
1966 path,
1967 &ctx.conversion,
1968 &attrs,
1969 Some(&oid_hex),
1970 None,
1971 Some(ctx.filters()),
1972 )
1973 .map_err(LibError::Filter)?;
1974 run_textconv_command(&command, &worktree_data)
1975 .or_else(|_| run_textconv_command(&command, &obj.data))
1976}
1977
1978#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1979struct BlobCacheKey {
1980 oid: ObjectId,
1981 path: String,
1982 mode: u32,
1983}
1984
1985#[derive(Debug, Clone, PartialEq, Eq)]
1987struct BlameLineDisplay {
1988 content: String,
1989 encoding_lossy: bool,
1990 content_bytes: Option<Vec<u8>>,
1991}
1992
1993fn latin1_bytes_to_string(bytes: &[u8]) -> String {
1994 bytes.iter().map(|&b| char::from(b)).collect()
1995}
1996
1997#[derive(Clone)]
1998enum BlobStorage {
1999 Utf8(Arc<str>),
2000 Binary(Arc<Vec<u8>>),
2001}
2002
2003#[derive(Clone)]
2005struct BlobLines {
2006 storage: BlobStorage,
2007 line_ends: Arc<Vec<usize>>,
2009 compare_bodies: OnceLock<Arc<Vec<String>>>,
2010 compare_records: OnceLock<Arc<Vec<String>>>,
2011}
2012
2013impl BlobLines {
2014 fn from_bytes(content: Vec<u8>) -> Self {
2015 let unterminated = !content.is_empty() && !content.ends_with(b"\n");
2016 let count = memchr::memchr_iter(b'\n', &content).count() + usize::from(unterminated);
2017 let mut line_ends = Vec::with_capacity(count);
2018 line_ends.extend(memchr::memchr_iter(b'\n', &content));
2019 if unterminated {
2020 line_ends.push(content.len());
2021 }
2022 let storage = match std::str::from_utf8(&content) {
2023 Ok(text) => BlobStorage::Utf8(Arc::from(text)),
2024 Err(_) => BlobStorage::Binary(Arc::new(content)),
2025 };
2026 Self {
2027 storage,
2028 line_ends: Arc::new(line_ends),
2029 compare_bodies: OnceLock::new(),
2030 compare_records: OnceLock::new(),
2031 }
2032 }
2033
2034 #[cfg(test)]
2035 fn from_content(content: String) -> Self {
2036 Self::from_bytes(content.into_bytes())
2037 }
2038
2039 fn bytes(&self) -> &[u8] {
2040 match &self.storage {
2041 BlobStorage::Utf8(s) => s.as_bytes(),
2042 BlobStorage::Binary(b) => b.as_ref(),
2043 }
2044 }
2045
2046 fn utf8(&self) -> Option<&str> {
2047 match &self.storage {
2048 BlobStorage::Utf8(s) => Some(s.as_ref()),
2049 BlobStorage::Binary(_) => None,
2050 }
2051 }
2052
2053 fn len(&self) -> usize {
2054 self.line_ends.len()
2055 }
2056
2057 fn line_start(&self, idx: usize) -> usize {
2058 idx.checked_sub(1)
2059 .map_or(0, |prev| self.line_ends[prev] + 1)
2060 }
2061
2062 fn line_body_bytes(&self, idx: usize) -> &[u8] {
2063 &self.bytes()[self.line_start(idx)..self.line_ends[idx]]
2064 }
2065
2066 fn line(&self, idx: usize) -> &str {
2067 if let Some(text) = self.utf8() {
2068 let start = self.line_start(idx);
2069 let end = self.line_ends[idx];
2070 return &text[start..end];
2071 }
2072 self.compare_bodies()
2074 .get(idx)
2075 .map(String::as_str)
2076 .unwrap_or("")
2077 }
2078
2079 fn line_display(&self, idx: usize) -> BlameLineDisplay {
2080 let body = self.line_body_bytes(idx);
2081 let encoding_lossy = std::str::from_utf8(body).is_err();
2082 let content = String::from_utf8_lossy(body).into_owned();
2083 let content_bytes = encoding_lossy.then(|| body.to_vec());
2084 BlameLineDisplay {
2085 content,
2086 encoding_lossy,
2087 content_bytes,
2088 }
2089 }
2090
2091 fn compare_bodies(&self) -> &Arc<Vec<String>> {
2092 self.compare_bodies.get_or_init(|| {
2093 Arc::new(
2094 (0..self.len())
2095 .map(|i| latin1_bytes_to_string(self.line_body_bytes(i)))
2096 .collect(),
2097 )
2098 })
2099 }
2100
2101 fn compare_records(&self) -> &Arc<Vec<String>> {
2102 self.compare_records.get_or_init(|| {
2103 Arc::new(
2104 (0..self.len())
2105 .map(|i| {
2106 let start = self.line_start(i);
2107 let end = (self.line_ends[i] + 1).min(self.bytes().len());
2108 latin1_bytes_to_string(&self.bytes()[start..end])
2109 })
2110 .collect(),
2111 )
2112 })
2113 }
2114
2115 fn comparison_line(&self, idx: usize, ws: crate::diff::WhitespaceMode) -> &str {
2117 if let Some(text) = self.utf8() {
2118 if ws.normalizes_line_compare() {
2119 let start = self.line_start(idx);
2120 let end = self.line_ends[idx];
2121 return &text[start..end];
2122 }
2123 let end = (self.line_ends[idx] + 1).min(text.len());
2124 return &text[self.line_start(idx)..end];
2125 }
2126 if ws.normalizes_line_compare() {
2127 return &self.compare_bodies()[idx];
2128 }
2129 &self.compare_records()[idx]
2130 }
2131
2132 fn comparison_refs(&self, ws: crate::diff::WhitespaceMode) -> Vec<&str> {
2133 (0..self.len())
2134 .map(|i| self.comparison_line(i, ws))
2135 .collect()
2136 }
2137
2138 fn to_vec(&self) -> Vec<String> {
2139 (0..self.len())
2140 .map(|i| self.line_display(i).content)
2141 .collect()
2142 }
2143}
2144
2145#[derive(Default)]
2147struct LineUseStamp {
2148 generation: u32,
2149 marks: Vec<u32>,
2150}
2151
2152impl LineUseStamp {
2153 fn begin_pass(&mut self, min_len: usize) {
2154 self.generation = self.generation.wrapping_add(1);
2155 if self.generation == 0 {
2156 self.marks.fill(0);
2157 self.generation = 1;
2158 }
2159 if self.marks.len() < min_len {
2160 self.marks.resize(min_len, 0);
2161 }
2162 }
2163
2164 fn mark(&mut self, idx: usize) {
2165 if idx >= self.marks.len() {
2166 self.marks.resize(idx + 1, 0);
2167 }
2168 self.marks[idx] = self.generation;
2169 }
2170
2171 fn is_unused(&self, idx: usize) -> bool {
2172 self.marks.get(idx).is_none_or(|&g| g != self.generation)
2173 }
2174}
2175
2176#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2177struct PathCacheKey {
2178 tree: ObjectId,
2179 path: String,
2180}
2181
2182#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2183struct PairMapCacheKey {
2184 parent: ObjectId,
2185 parent_path: String,
2186 parent_mode: u32,
2187 child: ObjectId,
2188 child_path: String,
2189 child_mode: u32,
2190 diff_algorithm: BlameDiffAlgorithm,
2191 indent_heuristic: bool,
2192 fuzzy: bool,
2193 include_stop_blame: bool,
2194 whitespace: crate::diff::WhitespaceMode,
2195}
2196
2197type BloomKeyCacheEntry = (String, u32, Arc<Vec<Vec<u32>>>);
2198
2199#[derive(Clone)]
2200struct PairMapCacheValue {
2201 diff_line_map: Arc<Vec<Option<usize>>>,
2202 line_map: Arc<Vec<Option<usize>>>,
2203 stop_blame_on_current: Arc<Vec<bool>>,
2204}
2205
2206const BLOB_LINES_CACHE_SOFT_LIMIT: usize = 64;
2208
2209struct BlameSession<'a> {
2211 odb: &'a Odb,
2212 textconv_ctx: Option<&'a BlameTextconvContext>,
2213 use_textconv: bool,
2214 whitespace: crate::diff::WhitespaceMode,
2215 commit_graph: Option<Arc<CommitGraphChain>>,
2216 bloom_pathspec: Vec<String>,
2217 bloom_key_cache: Option<BloomKeyCacheEntry>,
2218 blob_lines: HashMap<BlobCacheKey, Arc<BlobLines>>,
2219 path_entries: HashMap<PathCacheKey, Option<(ObjectId, u32)>>,
2220 commit_path_blob: HashMap<(ObjectId, String), Option<(ObjectId, u32)>>,
2221 pair_maps: HashMap<PairMapCacheKey, PairMapCacheValue>,
2222 cache_pair_maps: bool,
2225 identity_line_maps: HashMap<usize, Arc<Vec<Option<usize>>>>,
2226 pack: SessionPackReader,
2227 merge_line_stamps: Vec<LineUseStamp>,
2228 #[cfg(test)]
2229 diffed_pairs: Vec<(ObjectId, ObjectId)>,
2230}
2231
2232impl<'a> BlameSession<'a> {
2233 fn new(
2234 odb: &'a Odb,
2235 textconv_ctx: Option<&'a BlameTextconvContext>,
2236 use_textconv: bool,
2237 thread_policy: &ThreadPolicy,
2238 ) -> Self {
2239 let _ = thread_policy;
2240 Self {
2241 odb,
2242 textconv_ctx,
2243 use_textconv,
2244 whitespace: crate::diff::WhitespaceMode::default(),
2245 commit_graph: None,
2246 bloom_pathspec: Vec::new(),
2247 bloom_key_cache: None,
2248 blob_lines: HashMap::new(),
2249 path_entries: HashMap::new(),
2250 commit_path_blob: HashMap::new(),
2251 pair_maps: HashMap::new(),
2252 cache_pair_maps: false,
2253 identity_line_maps: HashMap::new(),
2254 pack: SessionPackReader::default(),
2255 merge_line_stamps: Vec::new(),
2256 #[cfg(test)]
2257 diffed_pairs: Vec::new(),
2258 }
2259 }
2260
2261 fn identity_line_map(&mut self, len: usize) -> Arc<Vec<Option<usize>>> {
2262 if let Some(map) = self.identity_line_maps.get(&len) {
2263 return Arc::clone(map);
2264 }
2265 let map: Arc<Vec<Option<usize>>> = Arc::new((0..len).map(Some).collect());
2266 self.identity_line_maps.insert(len, Arc::clone(&map));
2267 map
2268 }
2269
2270 fn tree_entry(&mut self, tree_oid: ObjectId, name: &[u8]) -> Result<Option<(ObjectId, u32)>> {
2272 let obj = self.pack.read(self.odb, &tree_oid)?;
2276 if obj.kind != ObjectKind::Tree {
2277 return Err(LibError::Message("expected tree".to_string()));
2278 }
2279 let oid_len = self.odb.hash_algo().len();
2280 Ok(
2281 find_tree_entry_by_name_dir_or_file_in_data(&obj.data, oid_len, name)?
2282 .map(|e| (e.oid, e.mode)),
2283 )
2284 }
2285
2286 fn prune_blob_lines(&mut self) {
2289 if self.blob_lines.len() > BLOB_LINES_CACHE_SOFT_LIMIT {
2290 self.blob_lines
2291 .retain(|_, lines| Arc::strong_count(lines) > 1);
2292 }
2293 }
2294
2295 fn set_bloom_path(&mut self, path: &str) {
2296 if self.bloom_pathspec.len() == 1 && self.bloom_pathspec[0] == path {
2297 return;
2298 }
2299 self.bloom_pathspec.clear();
2300 self.bloom_pathspec.push(path.to_owned());
2301 self.bloom_key_cache = None;
2302 }
2303
2304 fn bloom_keys_for_path(
2305 &mut self,
2306 path: &str,
2307 settings: &crate::bloom::BloomFilterSettings,
2308 ) -> Arc<Vec<Vec<u32>>> {
2309 let version = settings.hash_version;
2310 let needs_refresh = self
2311 .bloom_key_cache
2312 .as_ref()
2313 .is_none_or(|(p, v, _)| p != path || *v != version);
2314 if needs_refresh {
2315 let bytes = storage_bytes(path);
2316 let keys = crate::bloom::bloom_keyvec_for_bytes(&bytes, settings);
2317 self.bloom_key_cache = Some((path.to_owned(), version, Arc::new(keys)));
2318 }
2319 if let Some((_, _, keys)) = &self.bloom_key_cache {
2320 return Arc::clone(keys);
2321 }
2322 Arc::new(Vec::new())
2323 }
2324
2325 fn resolve_blob_at_commit(
2326 &mut self,
2327 odb: &Odb,
2328 commit: ObjectId,
2329 path: &str,
2330 commit_cache: &mut BlameCommitCache,
2331 ) -> Result<Option<(ObjectId, u32)>> {
2332 let cache_key = (commit, path.to_owned());
2333 if let Some(resolved) = self.commit_path_blob.get(&cache_key) {
2334 return Ok(*resolved);
2335 }
2336 let meta = get_blame_commit(odb, commit, commit_cache, self.commit_graph.as_deref())?;
2337 let resolved = self.resolve_path_in_tree(meta.tree, path)?;
2338 self.commit_path_blob.insert(cache_key, resolved);
2339 Ok(resolved)
2340 }
2341
2342 fn line_refs(lines: &BlobLines) -> Vec<&str> {
2343 (0..lines.len()).map(|i| lines.line(i)).collect()
2344 }
2345
2346 fn load_blob_lines(&mut self, oid: ObjectId, path: &str, mode: u32) -> Result<Arc<BlobLines>> {
2347 let key = BlobCacheKey {
2348 oid,
2349 path: path.to_owned(),
2350 mode,
2351 };
2352 if let Some(lines) = self.blob_lines.get(&key) {
2353 return Ok(Arc::clone(lines));
2354 }
2355 let content = read_blob_content_for_blame(
2356 self.odb,
2357 &oid,
2358 path,
2359 mode,
2360 self.textconv_ctx,
2361 self.use_textconv,
2362 )?;
2363 let lines = Arc::new(BlobLines::from_bytes(content));
2364 self.blob_lines.insert(key, Arc::clone(&lines));
2365 Ok(lines)
2366 }
2367
2368 fn resolve_path_in_tree(
2369 &mut self,
2370 tree_oid: ObjectId,
2371 path: &str,
2372 ) -> Result<Option<(ObjectId, u32)>> {
2373 let bytes = storage_bytes(path);
2374 self.resolve_path_in_tree_bytes(tree_oid, &bytes)
2375 }
2376
2377 fn resolve_path_in_tree_bytes(
2380 &mut self,
2381 tree_oid: ObjectId,
2382 path: &[u8],
2383 ) -> Result<Option<(ObjectId, u32)>> {
2384 let key = PathCacheKey {
2385 tree: tree_oid,
2386 path: storage_string(path),
2387 };
2388 if let Some(entry) = self.path_entries.get(&key) {
2389 return Ok(*entry);
2390 }
2391 let (head, rest) = match path.iter().position(|&b| b == b'/') {
2394 Some(pos) => (&path[..pos], Some(&path[pos + 1..])),
2395 None => (path, None),
2396 };
2397 let resolved = match (self.tree_entry(tree_oid, head)?, rest) {
2398 (Some((oid, 0o040000)), Some(rest)) => self.resolve_path_in_tree_bytes(oid, rest)?,
2399 (Some((oid, mode)), None) if mode != 0o040000 => Some((oid, mode)),
2400 _ => None,
2401 };
2402 self.path_entries.insert(key, resolved);
2403 Ok(resolved)
2404 }
2405
2406 #[allow(clippy::too_many_arguments)]
2407 fn parent_child_line_maps(
2408 &mut self,
2409 parent_oid: ObjectId,
2410 parent_path: &str,
2411 parent_mode: u32,
2412 child_oid: ObjectId,
2413 child_path: &str,
2414 child_mode: u32,
2415 par_lines: &BlobLines,
2416 cur_lines: &BlobLines,
2417 affix: Affix,
2418 diff_algorithm: BlameDiffAlgorithm,
2419 indent_heuristic: bool,
2420 is_ignored: bool,
2421 include_stop_blame: bool,
2422 ) -> PairMapCacheValue {
2423 #[cfg(test)]
2424 self.diffed_pairs.push((parent_oid, child_oid));
2425 let ws = self.whitespace;
2426 let key = PairMapCacheKey {
2427 parent: parent_oid,
2428 parent_path: parent_path.to_owned(),
2429 parent_mode,
2430 child: child_oid,
2431 child_path: child_path.to_owned(),
2432 child_mode,
2433 diff_algorithm,
2434 indent_heuristic,
2435 fuzzy: is_ignored,
2436 include_stop_blame,
2437 whitespace: ws,
2438 };
2439 if self.cache_pair_maps {
2440 if let Some(maps) = self.pair_maps.get(&key) {
2441 return maps.clone();
2442 }
2443 }
2444 let identity_map = parent_oid == child_oid && par_lines.len() == cur_lines.len();
2445 let (diff_line_map, xdiff_changed) = if identity_map {
2446 (
2447 (0..cur_lines.len()).map(Some).collect(),
2448 vec![false; cur_lines.len()],
2449 )
2450 } else if include_stop_blame {
2451 build_line_map_with_changed_flags_owned(
2452 par_lines,
2453 cur_lines,
2454 affix,
2455 diff_algorithm,
2456 indent_heuristic,
2457 true,
2458 ws,
2459 )
2460 } else {
2461 (
2462 build_line_map_with_changed_flags_owned(
2463 par_lines,
2464 cur_lines,
2465 affix,
2466 diff_algorithm,
2467 indent_heuristic,
2468 false,
2469 ws,
2470 )
2471 .0,
2472 Vec::new(),
2473 )
2474 };
2475 let (line_map, stop_blame_on_current) = if include_stop_blame {
2476 if !is_ignored
2477 && par_lines.len() == cur_lines.len()
2478 && diff_line_map.iter().all(|m| m.is_some())
2479 && xdiff_changed.iter().all(|&f| !f)
2480 {
2481 (diff_line_map.clone(), vec![false; cur_lines.len()])
2482 } else {
2483 let par_refs = par_lines.comparison_refs(ws);
2484 let cur_refs = cur_lines.comparison_refs(ws);
2485 let mut line_map =
2486 build_exact_relocate_line_map_ws(&par_refs, &cur_refs, &diff_line_map, ws);
2487 if is_ignored {
2488 line_map = build_fuzzy_line_map_ws(
2490 &Self::line_refs(par_lines),
2491 &Self::line_refs(cur_lines),
2492 &line_map,
2493 ws,
2494 );
2495 }
2496 let stop = if par_lines.len() == cur_lines.len() {
2497 blame_stops_on_current_commit(&diff_line_map, &line_map, &xdiff_changed)
2498 } else {
2499 Vec::new()
2500 };
2501 (line_map, stop)
2502 }
2503 } else {
2504 (Vec::new(), Vec::new())
2505 };
2506 let value = PairMapCacheValue {
2507 diff_line_map: Arc::new(diff_line_map),
2508 line_map: Arc::new(line_map),
2509 stop_blame_on_current: Arc::new(stop_blame_on_current),
2510 };
2511 if self.cache_pair_maps {
2512 self.pair_maps.insert(key, value.clone());
2513 }
2514 value
2515 }
2516}
2517
2518const BLAME_RENAME_SIMILARITY: u32 = 50;
2520
2521#[derive(Debug, Clone)]
2523struct BlameCommitMeta {
2524 tree: ObjectId,
2525 parents: Vec<ObjectId>,
2526}
2527
2528#[derive(Default)]
2534struct SessionPackReader {
2535 reader: Option<SingleLocalPackReader>,
2536 opened: bool,
2537}
2538
2539impl SessionPackReader {
2540 fn get(&mut self, odb: &Odb) -> Option<&mut SingleLocalPackReader> {
2541 if !self.opened {
2542 self.opened = true;
2543 self.reader = odb.open_session_pack_reader().ok().flatten();
2544 }
2545 self.reader.as_mut()
2546 }
2547
2548 fn read(&mut self, odb: &Odb, oid: &ObjectId) -> Result<Object> {
2550 if let Some(Ok(obj)) = self.get(odb).map(|r| r.read_object(oid)) {
2551 return Ok(obj);
2552 }
2553 read_object_for_blame(odb, oid, None)
2554 }
2555
2556 fn commit_tree_and_parents(
2562 &mut self,
2563 odb: &Odb,
2564 oid: &ObjectId,
2565 ) -> Result<(ObjectId, Vec<ObjectId>)> {
2566 let hex_len = odb.hash_algo().hex_len();
2567 if let Some(reader) = self.get(odb) {
2568 if let Ok(Some(prefix)) = reader.read_commit_author_prefix_unordered(oid) {
2569 if let Some(parsed) = parse_commit_tree_and_parents_prefix(prefix, hex_len) {
2570 return Ok(parsed);
2571 }
2572 }
2573 reader.clear_commit_header_prefix();
2574 }
2575 let mut data = Vec::new();
2576 if odb.read_into(oid, &mut data)? != ObjectKind::Commit {
2577 return Err(LibError::CorruptObject(format!(
2578 "{} is not a commit",
2579 oid.to_hex()
2580 )));
2581 }
2582 parse_commit_tree_and_parents_lenient(&data, hex_len).ok_or_else(|| {
2583 LibError::CorruptObject(format!("commit {} has no valid tree header", oid.to_hex()))
2584 })
2585 }
2586}
2587
2588#[derive(Default)]
2590struct BlameCommitCache {
2591 metas: HashMap<ObjectId, Arc<BlameCommitMeta>>,
2592 pack: SessionPackReader,
2593}
2594
2595#[expect(clippy::too_many_arguments)]
2606pub fn compute_blame(
2607 odb: &Odb,
2608 start_oid: ObjectId,
2609 file_path: &str,
2610 ignore_revs: &HashSet<ObjectId>,
2611 diff_algorithm: BlameDiffAlgorithm,
2612 textconv_ctx: Option<&BlameTextconvContext>,
2613 use_textconv: bool,
2614 copy_depth: usize,
2615 first_parent_only: bool,
2616 grafts: &HashMap<ObjectId, Vec<ObjectId>>,
2617 indent_heuristic: bool,
2618 line_range: Option<RangeInclusive<usize>>,
2619 whitespace: crate::diff::WhitespaceMode,
2620 thread_policy: &ThreadPolicy,
2621) -> Result<Vec<BlameLine>> {
2622 let bounded_odb = line_range.as_ref().map(|_| odb.with_bounded_pack_cache());
2623 let odb = bounded_odb.as_ref().unwrap_or(odb);
2624 let mut session = BlameSession::new(odb, textconv_ctx, use_textconv, thread_policy);
2625 session.whitespace = whitespace;
2626 session.cache_pair_maps = !ignore_revs.is_empty() || copy_depth > 0;
2627 let revision_label = start_oid.to_hex();
2628 let run = || {
2629 compute_blame_with_session(
2630 odb,
2631 start_oid,
2632 &revision_label,
2633 file_path,
2634 ignore_revs,
2635 diff_algorithm,
2636 textconv_ctx,
2637 use_textconv,
2638 copy_depth,
2639 first_parent_only,
2640 grafts,
2641 indent_heuristic,
2642 line_range,
2643 None,
2644 &mut session,
2645 )
2646 };
2647 if bounded_odb.is_some() {
2648 odb.with_pack_read_context(run)
2649 } else {
2650 run()
2651 }
2652}
2653
2654fn blame_path_missing_in_revision(path: &str, revision: &str) -> LibError {
2655 crate::revwalk::rev_parse_error::RevParseError::PathNotInRevision {
2656 path: path.to_owned(),
2657 revision: revision.to_owned(),
2658 }
2659 .into()
2660}
2661
2662#[allow(clippy::too_many_arguments)]
2664fn fast_forward_same_blob(
2665 odb: &Odb,
2666 session: &mut BlameSession<'_>,
2667 mut oid: ObjectId,
2668 blob_oid: ObjectId,
2669 blob_mode: u32,
2670 path: &str,
2671 grafts: &HashMap<ObjectId, Vec<ObjectId>>,
2672 commit_cache: &mut BlameCommitCache,
2673) -> Result<ObjectId> {
2674 session.set_bloom_path(path);
2675 loop {
2676 if grafts.contains_key(&oid) {
2677 break;
2678 }
2679 let parent_oid = if let Some(chain) = session.commit_graph.as_deref() {
2680 if let Some((parents, _)) = chain.graph_commit(&oid) {
2681 parents.first().copied()
2682 } else {
2683 commit_parents_for_blame(odb, oid, grafts, commit_cache, session)?
2684 .first()
2685 .copied()
2686 }
2687 } else {
2688 commit_parents_for_blame(odb, oid, grafts, commit_cache, session)?
2689 .first()
2690 .copied()
2691 };
2692 let Some(parent_oid) = parent_oid else {
2693 break;
2694 };
2695 if session.commit_graph.is_some()
2696 && bloom_may_skip_to_first_parent(
2697 session,
2698 odb,
2699 oid,
2700 parent_oid,
2701 path,
2702 grafts,
2703 commit_cache,
2704 true,
2705 )?
2706 {
2707 session
2708 .commit_path_blob
2709 .insert((parent_oid, path.to_owned()), Some((blob_oid, blob_mode)));
2710 oid = parent_oid;
2711 continue;
2712 }
2713 let parent_tree = if let Some(chain) = session.commit_graph.as_deref() {
2714 if let Some(tree) = chain.graph_commit_tree(&parent_oid) {
2715 tree
2716 } else {
2717 get_blame_commit(odb, parent_oid, commit_cache, Some(chain))?.tree
2718 }
2719 } else {
2720 get_blame_commit(odb, parent_oid, commit_cache, None)?.tree
2721 };
2722 let Some((p_blob, _mode)) = session.resolve_path_in_tree(parent_tree, path)? else {
2723 break;
2724 };
2725 if p_blob != blob_oid {
2726 break;
2727 }
2728 oid = parent_oid;
2729 }
2730 Ok(oid)
2731}
2732
2733#[expect(clippy::too_many_arguments)]
2734fn compute_blame_with_session(
2735 odb: &Odb,
2736 start_oid: ObjectId,
2737 revision_label: &str,
2738 file_path: &str,
2739 ignore_revs: &HashSet<ObjectId>,
2740 diff_algorithm: BlameDiffAlgorithm,
2741 textconv_ctx: Option<&BlameTextconvContext>,
2742 use_textconv: bool,
2743 copy_depth: usize,
2744 first_parent_only: bool,
2745 grafts: &HashMap<ObjectId, Vec<ObjectId>>,
2746 indent_heuristic: bool,
2747 line_range: Option<RangeInclusive<usize>>,
2748 overlay: Option<&BlameOverlay>,
2749 session: &mut BlameSession<'_>,
2750) -> Result<Vec<BlameLine>> {
2751 let blamed_path = file_path.to_owned();
2752 let mut commit_cache = BlameCommitCache::default();
2753 let (start_lines, blob_oid, blob_mode) = match overlay {
2754 Some(overlay) => (
2755 Arc::clone(&overlay.lines),
2756 ObjectId::null(odb.hash_algo()),
2757 0,
2758 ),
2759 None => {
2760 let start_commit = get_blame_commit(
2761 odb,
2762 start_oid,
2763 &mut commit_cache,
2764 session.commit_graph.as_deref(),
2765 )?;
2766 let (blob_oid, blob_mode) = session
2767 .resolve_path_in_tree(start_commit.tree, file_path)?
2768 .ok_or_else(|| blame_path_missing_in_revision(file_path, revision_label))?;
2769 let lines = session.load_blob_lines(blob_oid, file_path, blob_mode)?;
2770 (lines, blob_oid, blob_mode)
2771 }
2772 };
2773 let num_lines = start_lines.len();
2774
2775 if num_lines == 0 {
2776 return Ok(Vec::new());
2777 }
2778
2779 let mut pending: Vec<TrackedLine> = match &line_range {
2781 Some(range) => range
2782 .clone()
2783 .filter_map(|lineno| {
2784 if lineno == 0 || lineno > num_lines {
2785 return None;
2786 }
2787 Some(TrackedLine {
2788 final_lineno: lineno,
2789 current_idx: lineno - 1,
2790 ignored: false,
2791 source_path: None,
2792 origin: 0,
2793 })
2794 })
2795 .collect(),
2796 None => (0..num_lines)
2797 .map(|i| TrackedLine {
2798 final_lineno: i + 1,
2799 current_idx: i,
2800 ignored: false,
2801 source_path: None,
2802 origin: 0,
2803 })
2804 .collect(),
2805 };
2806
2807 if pending.is_empty() {
2808 return Ok(Vec::new());
2809 }
2810
2811 let mut result: Vec<BlameLine> = Vec::with_capacity(num_lines);
2812 let final_text: &BlobLines = &start_lines;
2813
2814 let mut current_oid = start_oid;
2815 let mut current_blob_oid = blob_oid;
2816 let mut current_blob_mode = blob_mode;
2817 let mut current_path = file_path.to_string();
2818 let mut queue = SuspectQueue::default();
2819 let mut next_origin: u32 = 0;
2820 let merge_aware = !first_parent_only;
2821
2822 session.set_bloom_path(¤t_path);
2823 let mut walk_lines: Option<(ObjectId, Arc<BlobLines>)> =
2824 Some((current_blob_oid, Arc::clone(&start_lines)));
2825
2826 if let Some(overlay) = overlay {
2827 let walk = OverlayWalk {
2828 path: file_path,
2829 ignore_revs,
2830 diff_algorithm,
2831 textconv_ctx,
2832 use_textconv,
2833 copy_depth,
2834 first_parent_only,
2835 grafts,
2836 indent_heuristic,
2837 };
2838 let first = pass_overlay_to_parents(
2839 odb,
2840 session,
2841 overlay,
2842 &walk,
2843 std::mem::take(&mut pending),
2844 final_text,
2845 &mut commit_cache,
2846 &mut queue,
2847 &mut next_origin,
2848 &mut result,
2849 )?;
2850 walk_lines = None;
2851 if let Some(first) = first {
2852 current_oid = first.oid;
2853 current_blob_oid = first.blob;
2854 current_blob_mode = first.mode;
2855 walk_lines = first.lines.map(|lines| (first.blob, lines));
2856 pending = first.pending;
2857 }
2858 }
2859
2860 'blame_loop: loop {
2865 if pending.is_empty() || !queue.is_empty() {
2866 if !pending.is_empty() {
2867 let lines = walk_lines
2868 .take()
2869 .and_then(|(oid, lines)| (oid == current_blob_oid).then_some(lines));
2870 let oid = current_oid;
2871 queue.push(
2872 BlameSuspect {
2873 oid,
2874 blob: current_blob_oid,
2875 mode: current_blob_mode,
2876 path: std::mem::take(&mut current_path),
2877 lines,
2878 pending: std::mem::take(&mut pending),
2879 },
2880 || blame_suspect_date(session, &oid),
2881 );
2882 }
2883 let Some(next) = queue.pop() else {
2884 break;
2885 };
2886 current_oid = next.oid;
2887 current_blob_oid = next.blob;
2888 current_blob_mode = next.mode;
2889 current_path = next.path;
2890 walk_lines = next.lines.map(|lines| (next.blob, lines));
2891 pending = next.pending;
2892 session.set_bloom_path(¤t_path);
2893 }
2894 session.prune_blob_lines();
2895
2896 if !ignore_revs.contains(¤t_oid) {
2897 let (new_oid, new_mode) = advance_across_unchanged_first_parents(
2898 session,
2899 odb,
2900 current_oid,
2901 current_blob_oid,
2902 current_blob_mode,
2903 ¤t_path,
2904 grafts,
2905 &mut commit_cache,
2906 )?;
2907 if new_oid != current_oid {
2908 current_oid = new_oid;
2909 current_blob_mode = new_mode;
2910 continue 'blame_loop;
2911 }
2912 }
2913
2914 let commit = get_blame_commit(
2915 odb,
2916 current_oid,
2917 &mut commit_cache,
2918 session.commit_graph.as_deref(),
2919 )?;
2920 let mut parents =
2921 commit_parents_for_blame(odb, current_oid, grafts, &mut commit_cache, session)?;
2922 let is_ignored = ignore_revs.contains(¤t_oid);
2923
2924 if first_parent_only && parents.len() > 1 {
2925 parents.truncate(1);
2926 }
2927
2928 if merge_aware && !is_ignored && parents.len() >= 2 {
2930 if let Some(first_parent) = parents.first().copied() {
2931 if session.commit_graph.is_some()
2932 && bloom_may_skip_to_first_parent(
2933 session,
2934 odb,
2935 current_oid,
2936 first_parent,
2937 ¤t_path,
2938 grafts,
2939 &mut commit_cache,
2940 true,
2941 )?
2942 {
2943 session.commit_path_blob.insert(
2944 (first_parent, current_path.clone()),
2945 Some((current_blob_oid, current_blob_mode)),
2946 );
2947 current_oid = fast_forward_same_blob(
2948 odb,
2949 session,
2950 first_parent,
2951 current_blob_oid,
2952 current_blob_mode,
2953 ¤t_path,
2954 grafts,
2955 &mut commit_cache,
2956 )?;
2957 continue 'blame_loop;
2958 }
2959 }
2960 for parent_oid in &parents {
2961 if let Some((blob, mode)) = session.resolve_blob_at_commit(
2962 odb,
2963 *parent_oid,
2964 ¤t_path,
2965 &mut commit_cache,
2966 )? {
2967 if blob == current_blob_oid {
2968 current_oid = fast_forward_same_blob(
2969 odb,
2970 session,
2971 *parent_oid,
2972 current_blob_oid,
2973 current_blob_mode,
2974 ¤t_path,
2975 grafts,
2976 &mut commit_cache,
2977 )?;
2978 current_blob_mode = mode;
2979 continue 'blame_loop;
2980 }
2981 }
2982 }
2983 }
2984
2985 if merge_aware && !ignore_revs.contains(¤t_oid) && parents.len() >= 2 {
2986 if let Some((first_blob, _)) =
2987 session.resolve_blob_at_commit(odb, parents[0], ¤t_path, &mut commit_cache)?
2988 {
2989 let mut all_same = true;
2990 for parent_oid in parents.iter().skip(1) {
2991 match session.resolve_blob_at_commit(
2992 odb,
2993 *parent_oid,
2994 ¤t_path,
2995 &mut commit_cache,
2996 )? {
2997 Some((blob, _)) if blob == first_blob => {}
2998 _ => {
2999 all_same = false;
3000 break;
3001 }
3002 }
3003 }
3004 if all_same {
3005 parents = vec![parents[0]];
3006 }
3007 }
3008 }
3009
3010 if is_ignored && parents.len() > 1 {
3013 let cur_lines_arc =
3014 session.load_blob_lines(current_blob_oid, ¤t_path, current_blob_mode)?;
3015 let cur_lines = BlameSession::line_refs(cur_lines_arc.as_ref());
3016
3017 let mut parent_lines: Vec<Option<Vec<String>>> = Vec::new();
3018 let mut parent_blames: Vec<Option<Vec<BlameLine>>> = Vec::new();
3019 for parent_oid in &parents {
3020 let parent_commit = get_blame_commit(
3021 odb,
3022 *parent_oid,
3023 &mut commit_cache,
3024 session.commit_graph.as_deref(),
3025 )?;
3026 if let Some((p_blob_oid, p_blob_mode)) =
3027 session.resolve_path_in_tree(parent_commit.tree, ¤t_path)?
3028 {
3029 let p_lines_arc =
3030 session.load_blob_lines(p_blob_oid, ¤t_path, p_blob_mode)?;
3031 let p_lines = p_lines_arc.to_vec();
3032 let parent_revision = parent_oid.to_hex();
3033 let p_blame = compute_blame_with_session(
3034 odb,
3035 *parent_oid,
3036 &parent_revision,
3037 ¤t_path,
3038 ignore_revs,
3039 diff_algorithm,
3040 textconv_ctx,
3041 use_textconv,
3042 copy_depth,
3043 first_parent_only,
3044 grafts,
3045 indent_heuristic,
3046 None,
3047 None,
3048 session,
3049 )?;
3050 parent_lines.push(Some(p_lines));
3051 parent_blames.push(Some(p_blame));
3052 } else {
3053 parent_lines.push(None);
3054 parent_blames.push(None);
3055 }
3056 }
3057
3058 for t in pending.drain(..) {
3059 let idx = t.current_idx;
3060 let Some(cur_line) = cur_lines.get(idx).copied() else {
3061 result.push(blame_line_at_path(
3062 current_oid,
3063 t.final_lineno,
3064 idx + 1,
3065 final_line_display(final_text, t.final_lineno),
3066 ¤t_path,
3067 None,
3068 t.ignored,
3069 true,
3070 ));
3071 continue;
3072 };
3073
3074 let mut picked: Option<BlameLine> = None;
3075 for i in 0..parents.len() {
3076 let Some(lines) = parent_lines.get(i).and_then(|v| v.as_ref()) else {
3077 continue;
3078 };
3079 if idx >= lines.len() || lines[idx] != cur_line {
3080 continue;
3081 }
3082 let Some(blames) = parent_blames.get(i).and_then(|v| v.as_ref()) else {
3083 continue;
3084 };
3085 if let Some(line_blame) = blames.iter().find(|b| b.final_lineno == idx + 1) {
3086 picked = Some(line_blame.clone());
3087 break;
3088 }
3089 }
3090
3091 if let Some(pb) = picked {
3092 result.push(blame_line_from_parent(
3093 &pb,
3094 t.final_lineno,
3095 final_line_display(final_text, t.final_lineno),
3096 pb.source_file.clone(),
3097 true,
3098 pb.unblamable,
3099 ));
3100 } else {
3101 result.push(blame_line_at_path(
3102 current_oid,
3103 t.final_lineno,
3104 idx + 1,
3105 final_line_display(final_text, t.final_lineno),
3106 ¤t_path,
3107 None,
3108 t.ignored,
3109 true,
3110 ));
3111 }
3112 }
3113 continue 'blame_loop;
3114 }
3115
3116 if parents.is_empty() {
3117 for t in pending.drain(..) {
3119 result.push(blame_line_at_path(
3120 current_oid,
3121 t.final_lineno,
3122 t.current_idx + 1,
3123 final_line_display(final_text, t.final_lineno),
3124 &blamed_path,
3125 t.source_path.clone(),
3126 t.ignored,
3127 false,
3128 ));
3129 }
3130 continue 'blame_loop;
3131 }
3132
3133 if merge_aware && !is_ignored && parents.len() >= 2 {
3138 let cur_lines_arc = match walk_lines.take() {
3139 Some((oid, lines)) if oid == current_blob_oid => lines,
3140 _ => session.load_blob_lines(current_blob_oid, ¤t_path, current_blob_mode)?,
3141 };
3142 let line_count = cur_lines_arc.len();
3143
3144 let mut parent_blobs: Vec<Option<(ObjectId, ObjectId, u32, String)>> =
3145 vec![None; parents.len()];
3146 let mut par_lines_vec: Vec<Option<Arc<BlobLines>>> = vec![None; parents.len()];
3147 let mut maps: Vec<Option<Arc<Vec<Option<usize>>>>> = vec![None; parents.len()];
3148
3149 let mut buckets: Vec<Vec<TrackedLine>> = vec![Vec::new(); parents.len()];
3150 let mut attributed: Vec<BlameLine> = Vec::new();
3151
3152 if session.merge_line_stamps.len() < parents.len() {
3153 session
3154 .merge_line_stamps
3155 .resize_with(parents.len(), LineUseStamp::default);
3156 }
3157
3158 let mut remaining = std::mem::take(&mut pending);
3159 remaining.sort_by_key(|t| t.origin);
3162 let mut any_parent_has_path = false;
3163 for i in 0..parents.len() {
3164 if remaining.is_empty() {
3165 break;
3166 }
3167 let p = parents[i];
3168 let parent_commit =
3169 get_blame_commit(odb, p, &mut commit_cache, session.commit_graph.as_deref())?;
3170 let mut parent_path = current_path.clone();
3171 let mut resolved =
3172 session.resolve_blob_at_commit(odb, p, ¤t_path, &mut commit_cache)?;
3173 if resolved.is_none() {
3174 if let Some((renamed_path, renamed_mode)) = find_path_by_oid_in_tree(
3175 odb,
3176 &parent_commit.tree,
3177 &commit.tree,
3178 ¤t_blob_oid,
3179 ¤t_path,
3180 )? {
3181 parent_path = renamed_path;
3182 resolved = session.resolve_blob_at_commit(
3183 odb,
3184 p,
3185 &parent_path,
3186 &mut commit_cache,
3187 )?;
3188 if resolved.is_none() {
3189 resolved = Some((current_blob_oid, renamed_mode));
3190 }
3191 } else if let Some(entry) = find_rename_entry_for_path(
3192 odb,
3193 &parent_commit.tree,
3194 &commit.tree,
3195 ¤t_path,
3196 BLAME_RENAME_SIMILARITY,
3197 )? {
3198 parent_path = entry.old_path.clone().unwrap_or_default();
3199 let p_blob_oid = entry.old_oid;
3200 let p_blob_mode = entry.old_mode.bits();
3201 resolved = Some((p_blob_oid, p_blob_mode));
3202 }
3203 }
3204 let Some((blob, mode)) = resolved else {
3205 continue;
3206 };
3207 let renamed_parent = parent_path != current_path;
3208 any_parent_has_path = true;
3209 parent_blobs[i] = Some((p, blob, mode, parent_path.clone()));
3210 if blob == current_blob_oid {
3211 par_lines_vec[i] = Some(Arc::clone(&cur_lines_arc));
3212 maps[i] = Some(session.identity_line_map(line_count));
3213 let mut next_remaining: Vec<TrackedLine> = Vec::new();
3214 let mut pass_origin = None;
3215 for t in remaining {
3216 if pass_origin != Some(t.origin) {
3217 pass_origin = Some(t.origin);
3218 session.merge_line_stamps[i].begin_pass(line_count);
3219 }
3220 if t.current_idx < line_count
3221 && session.merge_line_stamps[i].is_unused(t.current_idx)
3222 {
3223 session.merge_line_stamps[i].mark(t.current_idx);
3224 let source_path = if renamed_parent {
3225 Some(parent_path.clone())
3226 } else {
3227 t.source_path.clone()
3228 };
3229 buckets[i].push(TrackedLine {
3230 final_lineno: t.final_lineno,
3231 current_idx: t.current_idx,
3232 ignored: t.ignored,
3233 source_path,
3234 origin: t.origin,
3235 });
3236 } else {
3237 next_remaining.push(t);
3238 }
3239 }
3240 remaining = next_remaining;
3241 continue;
3242 }
3243 let pl_arc = session.load_blob_lines(blob, &parent_path, mode)?;
3244 let map_algo = diff_algorithm;
3245 let pair = session.parent_child_line_maps(
3246 blob,
3247 &parent_path,
3248 mode,
3249 current_blob_oid,
3250 ¤t_path,
3251 current_blob_mode,
3252 pl_arc.as_ref(),
3253 cur_lines_arc.as_ref(),
3254 slide_safe_affix(pl_arc.as_ref(), cur_lines_arc.as_ref(), session.whitespace),
3255 map_algo,
3256 indent_heuristic,
3257 false,
3258 true,
3259 );
3260 par_lines_vec[i] = Some(pl_arc);
3261 maps[i] = Some(Arc::clone(&pair.diff_line_map));
3262
3263 let mut next_remaining: Vec<TrackedLine> = Vec::new();
3264 let Some(map) = maps[i].as_ref() else {
3265 return Err(LibError::Message(
3266 "internal: missing merge parent line map".to_string(),
3267 ));
3268 };
3269 let Some(par_lines) = par_lines_vec[i].as_ref() else {
3270 return Err(LibError::Message(
3271 "internal: missing merge parent lines".to_string(),
3272 ));
3273 };
3274 let parent_line_count = par_lines.len();
3275 let mut pass_origin = None;
3276 for t in remaining {
3277 if pass_origin != Some(t.origin) {
3278 pass_origin = Some(t.origin);
3279 session.merge_line_stamps[i].begin_pass(parent_line_count);
3280 }
3281 let idx = t.current_idx;
3282 let cur_line = cur_lines_arc.comparison_line(idx, session.whitespace);
3283 let m = map.get(idx).copied().flatten();
3284 if let Some(p_idx) = m {
3285 let text_ok = blame_lines_equal(
3286 par_lines.comparison_line(p_idx, session.whitespace),
3287 cur_line,
3288 session.whitespace,
3289 );
3290 if text_ok
3291 && p_idx < parent_line_count
3292 && session.merge_line_stamps[i].is_unused(p_idx)
3293 {
3294 session.merge_line_stamps[i].mark(p_idx);
3295 let source_path = if renamed_parent {
3296 Some(parent_path.clone())
3297 } else {
3298 t.source_path.clone()
3299 };
3300 buckets[i].push(TrackedLine {
3301 final_lineno: t.final_lineno,
3302 current_idx: p_idx,
3303 ignored: t.ignored,
3304 source_path,
3305 origin: t.origin,
3306 });
3307 } else {
3308 next_remaining.push(t);
3309 }
3310 } else {
3311 next_remaining.push(t);
3312 }
3313 }
3314 remaining = next_remaining;
3315 }
3316
3317 if any_parent_has_path {
3318 for t in remaining {
3319 let idx = t.current_idx;
3320 attributed.push(blame_line_at_path(
3321 current_oid,
3322 t.final_lineno,
3323 idx + 1,
3324 final_line_display(final_text, t.final_lineno),
3325 ¤t_path,
3326 None,
3327 t.ignored,
3328 false,
3329 ));
3330 }
3331
3332 for bl in attributed {
3333 result.push(bl);
3334 }
3335
3336 for i in 1..parents.len() {
3337 if buckets[i].is_empty() {
3338 continue;
3339 }
3340 let Some((p, blob, mode, parent_path)) = parent_blobs[i].clone() else {
3341 continue;
3342 };
3343 next_origin += 1;
3344 let mut handed_off = std::mem::take(&mut buckets[i]);
3345 for t in &mut handed_off {
3346 t.origin = next_origin;
3347 }
3348 queue.push(
3349 BlameSuspect {
3350 oid: p,
3351 blob,
3352 mode,
3353 path: parent_path,
3354 lines: par_lines_vec[i].take(),
3355 pending: handed_off,
3356 },
3357 || blame_suspect_date(session, &p),
3358 );
3359 }
3360
3361 if !buckets[0].is_empty() {
3362 let Some((p, blob, mode, parent_path)) = parent_blobs[0].clone() else {
3363 continue 'blame_loop;
3364 };
3365 if parent_path != current_path {
3366 note_blame_source_path(&mut buckets[0], &parent_path);
3367 current_path = parent_path;
3368 session.set_bloom_path(¤t_path);
3369 }
3370 current_oid = p;
3371 current_blob_oid = blob;
3372 current_blob_mode = mode;
3373 walk_lines = par_lines_vec[0].take().map(|lines| (blob, lines));
3374 pending = std::mem::take(&mut buckets[0]);
3375 }
3376 continue;
3377 }
3378 pending = remaining;
3379 }
3380
3381 let parent_oid = parents[0];
3382 let parent_commit = get_blame_commit(
3383 odb,
3384 parent_oid,
3385 &mut commit_cache,
3386 session.commit_graph.as_deref(),
3387 )?;
3388 let mut parent_blob_entry =
3389 session.resolve_blob_at_commit(odb, parent_oid, ¤t_path, &mut commit_cache)?;
3390 let mut parent_blob_path = current_path.clone();
3391
3392 if parent_blob_entry.is_none() && !is_ignored {
3393 if let Some((renamed_path, renamed_mode)) = find_path_by_oid_in_tree(
3394 odb,
3395 &parent_commit.tree,
3396 &commit.tree,
3397 ¤t_blob_oid,
3398 ¤t_path,
3399 )? {
3400 note_blame_source_path(&mut pending, &renamed_path);
3401 current_path = renamed_path;
3402 session.set_bloom_path(¤t_path);
3403 current_oid = parent_oid;
3404 current_blob_mode = renamed_mode;
3405 continue;
3406 }
3407 if let Some(entry) = find_rename_entry_for_path(
3408 odb,
3409 &parent_commit.tree,
3410 &commit.tree,
3411 ¤t_path,
3412 BLAME_RENAME_SIMILARITY,
3413 )? {
3414 let old_path = entry.old_path.clone().unwrap_or_default();
3415 let p_blob_oid = entry.old_oid;
3416 let p_blob_mode = entry.old_mode.bits();
3417 if p_blob_oid == current_blob_oid {
3418 note_blame_source_path(&mut pending, &old_path);
3419 current_path = old_path;
3420 session.set_bloom_path(¤t_path);
3421 current_oid = parent_oid;
3422 current_blob_mode = p_blob_mode;
3423 continue;
3424 }
3425 parent_blob_entry = Some((p_blob_oid, p_blob_mode));
3426 parent_blob_path = old_path;
3427 }
3428 }
3429
3430 match parent_blob_entry {
3431 None if !is_ignored => {
3432 if copy_depth >= 1 {
3433 let cur_lines_arc = session.load_blob_lines(
3434 current_blob_oid,
3435 ¤t_path,
3436 current_blob_mode,
3437 )?;
3438 let pending_batch = std::mem::take(&mut pending);
3439 let (remaining, _handoff, copy_lines) = copy_detect_attribute_pending(
3440 odb,
3441 session,
3442 pending_batch,
3443 &cur_lines_arc,
3444 final_text,
3445 &blamed_path,
3446 parent_oid,
3447 &parent_commit.tree,
3448 &commit.tree,
3449 ¤t_path,
3450 copy_depth,
3451 true,
3452 None,
3453 None,
3454 ignore_revs,
3455 diff_algorithm,
3456 textconv_ctx,
3457 use_textconv,
3458 first_parent_only,
3459 grafts,
3460 indent_heuristic,
3461 )?;
3462 result.extend(copy_lines);
3463 for t in remaining {
3464 result.push(blame_line_at_path(
3465 current_oid,
3466 t.final_lineno,
3467 t.current_idx + 1,
3468 final_line_display(final_text, t.final_lineno),
3469 &blamed_path,
3470 t.source_path.clone(),
3471 t.ignored,
3472 false,
3473 ));
3474 }
3475 continue 'blame_loop;
3476 }
3477
3478 for t in pending.drain(..) {
3480 result.push(blame_line_at_path(
3481 current_oid,
3482 t.final_lineno,
3483 t.current_idx + 1,
3484 final_line_display(final_text, t.final_lineno),
3485 &blamed_path,
3486 t.source_path.clone(),
3487 t.ignored,
3488 false,
3489 ));
3490 }
3491 continue 'blame_loop;
3492 }
3493 None => {
3494 for t in pending.drain(..) {
3497 result.push(blame_line_at_path(
3498 current_oid,
3499 t.final_lineno,
3500 t.current_idx + 1,
3501 final_line_display(final_text, t.final_lineno),
3502 ¤t_path,
3503 None,
3504 t.ignored,
3505 true,
3506 ));
3507 }
3508 continue 'blame_loop;
3509 }
3510 Some((p_blob_oid, p_blob_mode)) if p_blob_oid == current_blob_oid => {
3511 current_oid = fast_forward_same_blob(
3512 odb,
3513 session,
3514 parent_oid,
3515 current_blob_oid,
3516 p_blob_mode,
3517 ¤t_path,
3518 grafts,
3519 &mut commit_cache,
3520 )?;
3521 current_blob_mode = p_blob_mode;
3522 continue;
3523 }
3524 Some((p_blob_oid, p_blob_mode)) => {
3525 let cur_lines_arc = match walk_lines.take() {
3527 Some((oid, lines)) if oid == current_blob_oid => lines,
3528 _ => session.load_blob_lines(
3529 current_blob_oid,
3530 ¤t_path,
3531 current_blob_mode,
3532 )?,
3533 };
3534 let par_lines_arc =
3535 session.load_blob_lines(p_blob_oid, &parent_blob_path, p_blob_mode)?;
3536 let pending_batch = std::mem::take(&mut pending);
3537 let affix = slide_safe_affix(
3538 par_lines_arc.as_ref(),
3539 cur_lines_arc.as_ref(),
3540 session.whitespace,
3541 );
3542 let (affix_mapped, need_diff) = partition_pending_by_affix(
3543 affix,
3544 par_lines_arc.len(),
3545 cur_lines_arc.len(),
3546 pending_batch,
3547 );
3548 if need_diff.is_empty() {
3552 pending = affix_mapped;
3553 current_oid = parent_oid;
3554 current_blob_oid = p_blob_oid;
3555 current_blob_mode = p_blob_mode;
3556 walk_lines = Some((p_blob_oid, Arc::clone(&par_lines_arc)));
3557 if parent_blob_path != current_path {
3558 note_blame_source_path(&mut pending, &parent_blob_path);
3559 current_path = parent_blob_path;
3560 session.set_bloom_path(¤t_path);
3561 }
3562 continue 'blame_loop;
3563 }
3564 let mut still_pending = affix_mapped;
3565 pending = need_diff;
3566 let maps = session.parent_child_line_maps(
3567 p_blob_oid,
3568 &parent_blob_path,
3569 p_blob_mode,
3570 current_blob_oid,
3571 ¤t_path,
3572 current_blob_mode,
3573 par_lines_arc.as_ref(),
3574 cur_lines_arc.as_ref(),
3575 affix,
3576 diff_algorithm,
3577 indent_heuristic,
3578 is_ignored,
3579 true,
3580 );
3581 let diff_line_map = maps.diff_line_map.as_slice();
3582 let line_map = maps.line_map.as_slice();
3583 let stop_blame_on_current = maps.stop_blame_on_current.as_slice();
3584 let inserted_line_duplicates_parent = pending.iter().any(|t| {
3585 t.current_idx < cur_lines_arc.len()
3586 && (0..par_lines_arc.len()).any(|i| {
3587 blame_lines_equal(
3588 cur_lines_arc.comparison_line(t.current_idx, session.whitespace),
3589 par_lines_arc.comparison_line(i, session.whitespace),
3590 session.whitespace,
3591 )
3592 })
3593 });
3594 let mut introduced_for_copy = Vec::new();
3595
3596 for t in pending.drain(..) {
3597 if t.current_idx < line_map.len() {
3598 if diff_line_map.get(t.current_idx).and_then(|m| *m).is_none()
3599 && !is_ignored
3600 {
3601 if copy_depth >= 1 && !inserted_line_duplicates_parent {
3602 introduced_for_copy.push(t);
3603 } else {
3604 result.push(blame_line_at_path(
3605 current_oid,
3606 t.final_lineno,
3607 t.current_idx + 1,
3608 final_line_display(final_text, t.final_lineno),
3609 ¤t_path,
3610 None,
3611 t.ignored,
3612 false,
3613 ));
3614 }
3615 continue;
3616 }
3617 if let Some(parent_idx) = line_map[t.current_idx] {
3618 let carried_ignored = if is_ignored {
3620 let unchanged = parent_idx == t.current_idx
3621 && par_lines_arc.line(parent_idx)
3622 == cur_lines_arc.line(t.current_idx);
3623 if unchanged {
3624 t.ignored
3625 } else {
3626 true
3627 }
3628 } else {
3629 t.ignored
3630 };
3631 if stop_blame_on_current
3632 .get(t.current_idx)
3633 .copied()
3634 .unwrap_or(false)
3635 && !is_ignored
3636 {
3637 let duplicates_earlier_parent_line =
3638 (0..par_lines_arc.len()).any(|i| {
3639 i != t.current_idx
3640 && blame_lines_equal(
3641 cur_lines_arc.comparison_line(
3642 t.current_idx,
3643 session.whitespace,
3644 ),
3645 par_lines_arc
3646 .comparison_line(i, session.whitespace),
3647 session.whitespace,
3648 )
3649 });
3650 if duplicates_earlier_parent_line {
3651 still_pending.push(TrackedLine {
3652 final_lineno: t.final_lineno,
3653 current_idx: parent_idx,
3654 ignored: t.ignored,
3655 source_path: t.source_path.clone(),
3656 origin: t.origin,
3657 });
3658 continue;
3659 }
3660 result.push(blame_line_at_path(
3661 current_oid,
3662 t.final_lineno,
3663 t.current_idx + 1,
3664 final_line_display(final_text, t.final_lineno),
3665 &blamed_path,
3666 t.source_path.clone(),
3667 t.ignored,
3668 false,
3669 ));
3670 continue;
3671 }
3672 still_pending.push(TrackedLine {
3673 final_lineno: t.final_lineno,
3674 current_idx: parent_idx,
3675 ignored: carried_ignored,
3676 source_path: t.source_path.clone(),
3677 origin: t.origin,
3678 });
3679 } else if is_ignored {
3680 if t.current_idx < par_lines_arc.len()
3685 && cur_lines_arc.line(t.current_idx)
3686 == par_lines_arc.line(t.current_idx)
3687 {
3688 still_pending.push(TrackedLine {
3689 final_lineno: t.final_lineno,
3690 current_idx: t.current_idx,
3691 ignored: t.ignored,
3692 source_path: t.source_path.clone(),
3693 origin: t.origin,
3694 });
3695 } else {
3696 result.push(blame_line_at_path(
3697 current_oid,
3698 t.final_lineno,
3699 t.current_idx + 1,
3700 final_line_display(final_text, t.final_lineno),
3701 ¤t_path,
3702 None,
3703 t.ignored,
3704 true,
3705 ));
3706 }
3707 } else {
3708 let line_text =
3709 cur_lines_arc.comparison_line(t.current_idx, session.whitespace);
3710 if let Some(parent_idx) = (0..par_lines_arc.len()).find(|&i| {
3711 blame_lines_equal(
3712 par_lines_arc.comparison_line(i, session.whitespace),
3713 line_text,
3714 session.whitespace,
3715 )
3716 }) {
3717 still_pending.push(TrackedLine {
3718 final_lineno: t.final_lineno,
3719 current_idx: parent_idx,
3720 ignored: t.ignored,
3721 source_path: t.source_path.clone(),
3722 origin: t.origin,
3723 });
3724 continue;
3725 }
3726 if copy_depth >= 1 && !inserted_line_duplicates_parent {
3727 introduced_for_copy.push(t);
3728 } else {
3729 result.push(blame_line_at_path(
3730 current_oid,
3731 t.final_lineno,
3732 t.current_idx + 1,
3733 final_line_display(final_text, t.final_lineno),
3734 &blamed_path,
3735 t.source_path.clone(),
3736 t.ignored,
3737 false,
3738 ));
3739 }
3740 }
3741 } else if is_ignored {
3742 result.push(blame_line_at_path(
3743 current_oid,
3744 t.final_lineno,
3745 t.current_idx + 1,
3746 final_line_display(final_text, t.final_lineno),
3747 ¤t_path,
3748 None,
3749 t.ignored,
3750 true,
3751 ));
3752 } else {
3753 result.push(blame_line_at_path(
3755 current_oid,
3756 t.final_lineno,
3757 t.current_idx + 1,
3758 final_line_display(final_text, t.final_lineno),
3759 ¤t_path,
3760 None,
3761 t.ignored,
3762 false,
3763 ));
3764 }
3765 }
3766
3767 if copy_depth >= 1 && !introduced_for_copy.is_empty() {
3768 let (left, handoff, copy_lines) = copy_detect_attribute_pending(
3769 odb,
3770 session,
3771 introduced_for_copy,
3772 &cur_lines_arc,
3773 final_text,
3774 &blamed_path,
3775 parent_oid,
3776 &parent_commit.tree,
3777 &commit.tree,
3778 ¤t_path,
3779 copy_depth,
3780 false,
3781 None,
3782 Some(&par_lines_arc),
3783 ignore_revs,
3784 diff_algorithm,
3785 textconv_ctx,
3786 use_textconv,
3787 first_parent_only,
3788 grafts,
3789 indent_heuristic,
3790 )?;
3791 result.extend(copy_lines);
3792 for t in left {
3793 result.push(blame_line_at_path(
3794 current_oid,
3795 t.final_lineno,
3796 t.current_idx + 1,
3797 final_line_display(final_text, t.final_lineno),
3798 &blamed_path,
3799 t.source_path.clone(),
3800 t.ignored,
3801 false,
3802 ));
3803 }
3804 still_pending.extend(handoff);
3805 }
3806
3807 pending = still_pending;
3808 current_oid = parent_oid;
3809 current_blob_oid = p_blob_oid;
3810 current_blob_mode = p_blob_mode;
3811 walk_lines = Some((p_blob_oid, Arc::clone(&par_lines_arc)));
3812 if parent_blob_path != current_path {
3813 note_blame_source_path(&mut pending, &parent_blob_path);
3814 current_path = parent_blob_path;
3815 session.set_bloom_path(¤t_path);
3816 }
3817 }
3818 }
3819 }
3820
3821 result.sort_by_key(|b| b.final_lineno);
3822 Ok(result)
3823}
3824
3825const BLAME_DEFAULT_COPY_SCORE: usize = 40;
3827const BLAME_DEFAULT_MOVE_SCORE: usize = 20;
3829
3830fn blame_copy_content_score(line: &str) -> usize {
3831 line.chars().filter(|c| c.is_ascii_alphanumeric()).count()
3832}
3833
3834fn blame_entry_score_lines(lines: &[&str]) -> usize {
3836 1 + lines
3837 .iter()
3838 .map(|line| blame_copy_content_score(line))
3839 .sum::<usize>()
3840}
3841
3842#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3844struct ContiguousCopyMatch {
3845 suspect_start: usize,
3846 parent_start: usize,
3847 len: usize,
3848 score: usize,
3849}
3850
3851fn find_best_contiguous_copy_match(
3852 suspect: &[&str],
3853 parent: &[&str],
3854 ws: crate::diff::WhitespaceMode,
3855) -> Option<ContiguousCopyMatch> {
3856 let mut best: Option<ContiguousCopyMatch> = None;
3857 for s_start in 0..suspect.len() {
3858 for p_start in 0..parent.len() {
3859 let mut len = 0usize;
3860 while s_start + len < suspect.len()
3861 && p_start + len < parent.len()
3862 && blame_lines_equal(suspect[s_start + len], parent[p_start + len], ws)
3863 {
3864 len += 1;
3865 }
3866 if len == 0 {
3867 continue;
3868 }
3869 let score = blame_entry_score_lines(&suspect[s_start..s_start + len]);
3870 let replace = best.is_none_or(|b| score > b.score || (score == b.score && len > b.len));
3871 if replace {
3872 best = Some(ContiguousCopyMatch {
3873 suspect_start: s_start,
3874 parent_start: p_start,
3875 len,
3876 score,
3877 });
3878 }
3879 }
3880 }
3881 best
3882}
3883
3884fn group_contiguous_tracked(pending: &[TrackedLine]) -> Vec<(usize, usize)> {
3886 if pending.is_empty() {
3887 return Vec::new();
3888 }
3889 let mut order: Vec<usize> = (0..pending.len()).collect();
3890 order.sort_by_key(|&i| pending[i].current_idx);
3891 let mut groups = Vec::new();
3892 let mut start = pending[order[0]].current_idx;
3893 let mut end = start + 1;
3894 for &i in order.iter().skip(1) {
3895 let idx = pending[i].current_idx;
3896 if idx == end {
3897 end += 1;
3898 } else {
3899 groups.push((start, end));
3900 start = idx;
3901 end = idx + 1;
3902 }
3903 }
3904 groups.push((start, end));
3905 groups
3906}
3907
3908type CopySourceCache = HashMap<String, (Arc<BlobLines>, Vec<BlameLine>)>;
3909
3910#[expect(clippy::too_many_arguments)]
3911fn load_copy_source_blame(
3912 odb: &Odb,
3913 parent_oid: ObjectId,
3914 path: &str,
3915 oid: ObjectId,
3916 mode: u32,
3917 ignore_revs: &HashSet<ObjectId>,
3918 diff_algorithm: BlameDiffAlgorithm,
3919 textconv_ctx: Option<&BlameTextconvContext>,
3920 use_textconv: bool,
3921 copy_depth: usize,
3922 first_parent_only: bool,
3923 grafts: &HashMap<ObjectId, Vec<ObjectId>>,
3924 indent_heuristic: bool,
3925 session: &mut BlameSession<'_>,
3926 cache: &mut CopySourceCache,
3927) -> Result<(Arc<BlobLines>, Vec<BlameLine>)> {
3928 if let Some(entry) = cache.get(path) {
3929 return Ok((Arc::clone(&entry.0), entry.1.clone()));
3930 }
3931 let source_lines = session.load_blob_lines(oid, path, mode)?;
3932 let parent_revision = parent_oid.to_hex();
3933 let source_blame = compute_blame_with_session(
3934 odb,
3935 parent_oid,
3936 &parent_revision,
3937 path,
3938 ignore_revs,
3939 diff_algorithm,
3940 textconv_ctx,
3941 use_textconv,
3942 copy_depth,
3943 first_parent_only,
3944 grafts,
3945 indent_heuristic,
3946 None,
3947 None,
3948 session,
3949 )?;
3950 cache.insert(
3951 path.to_owned(),
3952 (Arc::clone(&source_lines), source_blame.clone()),
3953 );
3954 Ok((source_lines, source_blame))
3955}
3956
3957#[expect(clippy::too_many_arguments)]
3959fn copy_detect_attribute_pending(
3960 odb: &Odb,
3961 session: &mut BlameSession<'_>,
3962 pending: Vec<TrackedLine>,
3963 cur_lines: &Arc<BlobLines>,
3964 final_text: &BlobLines,
3965 _blamed_path: &str,
3966 parent_oid: ObjectId,
3967 parent_tree: &ObjectId,
3968 child_tree: &ObjectId,
3969 current_path: &str,
3970 copy_depth: usize,
3971 blaming_new_file: bool,
3972 explicit_allowed_paths: Option<Option<&HashSet<String>>>,
3974 move_parent_lines: Option<&Arc<BlobLines>>,
3975 ignore_revs: &HashSet<ObjectId>,
3976 diff_algorithm: BlameDiffAlgorithm,
3977 textconv_ctx: Option<&BlameTextconvContext>,
3978 use_textconv: bool,
3979 first_parent_only: bool,
3980 grafts: &HashMap<ObjectId, Vec<ObjectId>>,
3981 indent_heuristic: bool,
3982) -> Result<(Vec<TrackedLine>, Vec<TrackedLine>, Vec<BlameLine>)> {
3983 if copy_depth == 0 || pending.is_empty() {
3984 return Ok((pending, Vec::new(), Vec::new()));
3985 }
3986 let ws = session.whitespace;
3987 let cur_refs = cur_lines.comparison_refs(ws);
3988 let allowed = match explicit_allowed_paths {
3989 Some(paths) => paths.cloned(),
3990 None => {
3991 copy_allowed_parent_paths(odb, parent_tree, child_tree, copy_depth, blaming_new_file)?
3992 }
3993 };
3994 let mut entries = Vec::new();
3995 collect_tree_file_entries(odb, parent_tree, "", &mut entries)?;
3996 let mut candidates: Vec<(String, ObjectId, u32)> = Vec::new();
3997 for (path, oid, mode) in entries {
3998 if path == current_path || !is_regular_mode(mode) {
3999 continue;
4000 }
4001 if !path_allowed_for_copy_search(&path, allowed.as_ref()) {
4002 continue;
4003 }
4004 candidates.push((path, oid, mode));
4005 }
4006 let mut source_cache: CopySourceCache = HashMap::new();
4007 let mut result = Vec::new();
4008 let mut parent_handoff = Vec::new();
4009 let mut work = pending;
4010 let copy_threshold = BLAME_DEFAULT_COPY_SCORE;
4011 let move_threshold = BLAME_DEFAULT_MOVE_SCORE;
4012
4013 if let Some(par_lines) = move_parent_lines {
4014 let par_refs = par_lines.comparison_refs(ws);
4015 let mut next_work = Vec::new();
4016 for group in group_contiguous_tracked(&work) {
4017 let slice: Vec<&str> = (group.0..group.1).map(|i| cur_refs[i]).collect();
4018 if blame_entry_score_lines(&slice) <= move_threshold {
4019 next_work.extend(
4020 work.iter()
4021 .filter(|t| t.current_idx >= group.0 && t.current_idx < group.1)
4022 .cloned(),
4023 );
4024 continue;
4025 }
4026 let Some(m) = find_best_contiguous_copy_match(&slice, &par_refs, ws) else {
4027 next_work.extend(
4028 work.iter()
4029 .filter(|t| t.current_idx >= group.0 && t.current_idx < group.1)
4030 .cloned(),
4031 );
4032 continue;
4033 };
4034 if m.score <= move_threshold {
4035 next_work.extend(
4036 work.iter()
4037 .filter(|t| t.current_idx >= group.0 && t.current_idx < group.1)
4038 .cloned(),
4039 );
4040 continue;
4041 }
4042 for t in work
4043 .iter()
4044 .filter(|t| t.current_idx >= group.0 && t.current_idx < group.1)
4045 {
4046 let off = t.current_idx - group.0;
4047 if off < m.suspect_start || off >= m.suspect_start + m.len {
4048 next_work.push(t.clone());
4049 continue;
4050 }
4051 let parent_line = m.parent_start + (off - m.suspect_start);
4052 parent_handoff.push(TrackedLine {
4053 current_idx: parent_line,
4054 ..t.clone()
4055 });
4056 }
4057 }
4058 work = next_work;
4059 }
4060
4061 loop {
4062 let mut made_progress = false;
4063 let groups = group_contiguous_tracked(&work);
4064 let mut still_pending = Vec::new();
4065 for (start_idx, end_idx) in groups {
4066 let slice: Vec<&str> = (start_idx..end_idx).map(|i| cur_refs[i]).collect();
4067 let group_lines: Vec<TrackedLine> = work
4068 .iter()
4069 .filter(|t| t.current_idx >= start_idx && t.current_idx < end_idx)
4070 .cloned()
4071 .collect();
4072 let mut subranges = vec![(0usize, slice.len())];
4073 let mut group_pending = Vec::new();
4074 while let Some((off, len)) = subranges.pop() {
4075 if len == 0 {
4076 continue;
4077 }
4078 let sub = &slice[off..off + len];
4079 if blame_entry_score_lines(sub) <= copy_threshold {
4080 for t in &group_lines {
4081 let rel = t.current_idx - start_idx;
4082 if rel >= off && rel < off + len {
4083 group_pending.push(t.clone());
4084 }
4085 }
4086 continue;
4087 }
4088 let mut best: Option<(ContiguousCopyMatch, String, ObjectId, u32)> = None;
4089 for (path, oid, mode) in &candidates {
4090 let source_lines = session.load_blob_lines(*oid, path, *mode)?;
4091 let src_refs = source_lines.comparison_refs(ws);
4092 let Some(m) = find_best_contiguous_copy_match(sub, &src_refs, ws) else {
4093 continue;
4094 };
4095 let replace = best.as_ref().is_none_or(|(b, _, _, _)| {
4096 m.score > b.score || (m.score == b.score && m.len > b.len)
4097 });
4098 if replace {
4099 best = Some((m, path.clone(), *oid, *mode));
4100 }
4101 }
4102 let Some((m, source_path, source_oid, source_mode)) = best else {
4103 for t in &group_lines {
4104 let rel = t.current_idx - start_idx;
4105 if rel >= off && rel < off + len {
4106 group_pending.push(t.clone());
4107 }
4108 }
4109 continue;
4110 };
4111 if m.score <= copy_threshold {
4112 for t in &group_lines {
4113 let rel = t.current_idx - start_idx;
4114 if rel >= off && rel < off + len {
4115 group_pending.push(t.clone());
4116 }
4117 }
4118 continue;
4119 }
4120 let (_source_lines, source_blame) = load_copy_source_blame(
4121 odb,
4122 parent_oid,
4123 &source_path,
4124 source_oid,
4125 source_mode,
4126 ignore_revs,
4127 diff_algorithm,
4128 textconv_ctx,
4129 use_textconv,
4130 copy_depth.saturating_sub(1),
4131 first_parent_only,
4132 grafts,
4133 indent_heuristic,
4134 session,
4135 &mut source_cache,
4136 )?;
4137 made_progress = true;
4138 if m.suspect_start > 0 {
4139 subranges.push((off, m.suspect_start));
4140 }
4141 if m.suspect_start + m.len < len {
4142 subranges.push((off + m.suspect_start + m.len, len - m.suspect_start - m.len));
4143 }
4144 for t in &group_lines {
4145 let rel = t.current_idx - start_idx;
4146 if rel < off + m.suspect_start || rel >= off + m.suspect_start + m.len {
4147 continue;
4148 }
4149 let parent_line = m.parent_start + (rel - off - m.suspect_start);
4150 let pb = source_blame
4151 .iter()
4152 .find(|l| l.final_lineno == parent_line + 1)
4153 .cloned()
4154 .unwrap_or_else(|| {
4155 blame_line_at_path(
4156 parent_oid,
4157 parent_line + 1,
4158 parent_line + 1,
4159 final_line_display(final_text, t.final_lineno),
4160 &source_path,
4161 None,
4162 false,
4163 false,
4164 )
4165 });
4166 result.push(blame_line_from_parent(
4167 &pb,
4168 t.final_lineno,
4169 final_line_display(final_text, t.final_lineno),
4170 pb.source_file.clone().or_else(|| Some(source_path.clone())),
4171 pb.ignored || t.ignored,
4172 pb.unblamable,
4173 ));
4174 }
4175 }
4176 still_pending.extend(group_pending);
4177 }
4178 work = still_pending;
4179 if !made_progress {
4180 break;
4181 }
4182 }
4183 Ok((work, parent_handoff, result))
4184}
4185
4186fn copy_changed_parent_paths(
4187 odb: &Odb,
4188 parent_tree: &ObjectId,
4189 child_tree: &ObjectId,
4190) -> Result<HashSet<String>> {
4191 let mut out = HashSet::new();
4192 for entry in diff_trees(odb, Some(parent_tree), Some(child_tree), "")? {
4193 if let Some(path) = entry.old_path {
4194 out.insert(path);
4195 }
4196 if let Some(path) = entry.new_path {
4197 out.insert(path);
4198 }
4199 }
4200 Ok(out)
4201}
4202
4203fn copy_allowed_parent_paths(
4204 odb: &Odb,
4205 parent_tree: &ObjectId,
4206 child_tree: &ObjectId,
4207 copy_depth: usize,
4208 blaming_new_file: bool,
4209) -> Result<Option<HashSet<String>>> {
4210 if copy_depth >= 3 {
4211 return Ok(None);
4212 }
4213 if copy_depth >= 2 && blaming_new_file {
4214 return Ok(None);
4215 }
4216 Ok(Some(copy_changed_parent_paths(
4217 odb,
4218 parent_tree,
4219 child_tree,
4220 )?))
4221}
4222
4223fn path_allowed_for_copy_search(path: &str, allowed: Option<&HashSet<String>>) -> bool {
4224 allowed.is_none_or(|set| set.contains(path))
4225}
4226
4227fn note_blame_source_path(pending: &mut [TrackedLine], path: &str) {
4228 let path = path.to_owned();
4229 for t in pending.iter_mut() {
4230 t.source_path = Some(path.clone());
4231 }
4232}
4233
4234fn find_rename_entry_for_path(
4235 odb: &Odb,
4236 parent_tree: &ObjectId,
4237 commit_tree: &ObjectId,
4238 new_path: &str,
4239 threshold: u32,
4240) -> Result<Option<DiffEntry>> {
4241 if threshold >= 100 {
4242 return Ok(None);
4243 }
4244 let entries = diff_trees(odb, Some(parent_tree), Some(commit_tree), "")?;
4245 let entries = detect_renames(odb, None, entries, threshold);
4246 Ok(entries
4247 .into_iter()
4248 .find(|e| e.status == DiffStatus::Renamed && e.new_path.as_deref() == Some(new_path)))
4249}
4250
4251fn find_path_by_oid_in_tree(
4252 odb: &Odb,
4253 tree_oid: &ObjectId,
4254 current_tree_oid: &ObjectId,
4255 needle_oid: &ObjectId,
4256 exclude_path: &str,
4257) -> Result<Option<(String, u32)>> {
4258 let mut entries = Vec::new();
4259 collect_tree_file_entries(odb, tree_oid, "", &mut entries)?;
4260 for (path, oid, mode) in entries {
4261 if path != exclude_path
4262 && &oid == needle_oid
4263 && resolve_path_in_tree_entry(odb, current_tree_oid, &path)?.is_none()
4264 {
4265 return Ok(Some((path, mode)));
4266 }
4267 }
4268 Ok(None)
4269}
4270
4271fn collect_tree_file_entries(
4272 odb: &Odb,
4273 tree_oid: &ObjectId,
4274 prefix: &str,
4275 out: &mut Vec<(String, ObjectId, u32)>,
4276) -> Result<()> {
4277 let obj = read_object_for_blame(odb, tree_oid, None)?;
4278 if obj.kind != ObjectKind::Tree {
4279 return Err(LibError::Message("expected tree".to_string()));
4280 }
4281 let entries = parse_tree(&obj.data)?;
4282 for entry in entries {
4283 let path = format_path_stored(prefix, &entry.name);
4284 if entry.mode == 0o040000 {
4285 collect_tree_file_entries(odb, &entry.oid, &path, out)?;
4286 } else {
4287 out.push((path, entry.oid, entry.mode));
4288 }
4289 }
4290 Ok(())
4291}
4292
4293fn get_blame_commit(
4294 odb: &Odb,
4295 oid: ObjectId,
4296 cache: &mut BlameCommitCache,
4297 graph: Option<&CommitGraphChain>,
4298) -> Result<Arc<BlameCommitMeta>> {
4299 if let Some(c) = cache.metas.get(&oid) {
4300 return Ok(Arc::clone(c));
4301 }
4302 let meta = if let Some(chain) = graph {
4303 if let Some((tree, parents)) = chain.graph_commit_meta(&oid) {
4304 Arc::new(BlameCommitMeta { tree, parents })
4305 } else {
4306 load_blame_commit_from_object(odb, oid, &mut cache.pack)?
4307 }
4308 } else {
4309 load_blame_commit_from_object(odb, oid, &mut cache.pack)?
4310 };
4311 cache.metas.insert(oid, Arc::clone(&meta));
4312 Ok(meta)
4313}
4314
4315fn load_blame_commit_from_object(
4318 odb: &Odb,
4319 oid: ObjectId,
4320 pack: &mut SessionPackReader,
4321) -> Result<Arc<BlameCommitMeta>> {
4322 let (tree, parents) = pack.commit_tree_and_parents(odb, &oid)?;
4323 Ok(Arc::new(BlameCommitMeta { tree, parents }))
4324}
4325
4326#[allow(clippy::too_many_arguments)]
4328fn advance_across_unchanged_first_parents(
4329 session: &mut BlameSession<'_>,
4330 odb: &Odb,
4331 mut current_oid: ObjectId,
4332 blob_oid: ObjectId,
4333 mut blob_mode: u32,
4334 path: &str,
4335 grafts: &HashMap<ObjectId, Vec<ObjectId>>,
4336 commit_cache: &mut BlameCommitCache,
4337) -> Result<(ObjectId, u32)> {
4338 session.set_bloom_path(path);
4339 let chain = session.commit_graph.clone();
4340 loop {
4341 if grafts.contains_key(¤t_oid) {
4342 break;
4343 }
4344 let parent_oid = if let Some(ref chain) = chain {
4345 if let Some((parents, _)) = chain.graph_commit(¤t_oid) {
4346 parents.first().copied()
4347 } else {
4348 commit_parents_for_blame(odb, current_oid, grafts, commit_cache, session)?
4349 .first()
4350 .copied()
4351 }
4352 } else {
4353 commit_parents_for_blame(odb, current_oid, grafts, commit_cache, session)?
4354 .first()
4355 .copied()
4356 };
4357 let Some(parent_oid) = parent_oid else {
4358 break;
4359 };
4360 if chain.is_some()
4361 && bloom_may_skip_to_first_parent(
4362 session,
4363 odb,
4364 current_oid,
4365 parent_oid,
4366 path,
4367 grafts,
4368 commit_cache,
4369 true,
4370 )?
4371 {
4372 session
4373 .commit_path_blob
4374 .insert((parent_oid, path.to_owned()), Some((blob_oid, blob_mode)));
4375 current_oid = parent_oid;
4376 continue;
4377 }
4378 if let Some((p_blob, p_mode)) =
4379 session.resolve_blob_at_commit(odb, parent_oid, path, commit_cache)?
4380 {
4381 if p_blob == blob_oid {
4382 current_oid = fast_forward_same_blob(
4383 odb,
4384 session,
4385 parent_oid,
4386 blob_oid,
4387 p_mode,
4388 path,
4389 grafts,
4390 commit_cache,
4391 )?;
4392 blob_mode = p_mode;
4393 continue;
4394 }
4395 }
4396 break;
4397 }
4398 Ok((current_oid, blob_mode))
4399}
4400
4401#[allow(clippy::too_many_arguments)]
4404fn bloom_may_skip_to_first_parent(
4405 session: &mut BlameSession<'_>,
4406 odb: &Odb,
4407 current_oid: ObjectId,
4408 parent_oid: ObjectId,
4409 path: &str,
4410 grafts: &HashMap<ObjectId, Vec<ObjectId>>,
4411 commit_cache: &mut BlameCommitCache,
4412 parent_is_first_in_graph: bool,
4413) -> Result<bool> {
4414 if grafts.contains_key(¤t_oid) {
4415 return Ok(false);
4416 }
4417 if !parent_is_first_in_graph {
4418 let object_parents = commit_object_parents(session, odb, current_oid, commit_cache)?;
4419 if object_parents.first() != Some(&parent_oid) {
4420 return Ok(false);
4421 }
4422 }
4423 let Some(chain) = session.commit_graph.clone() else {
4424 return Ok(false);
4425 };
4426 session.set_bloom_path(path);
4427 Ok(bloom_precheck_definitely_not(
4428 session,
4429 &chain,
4430 current_oid,
4431 path,
4432 ))
4433}
4434
4435fn bloom_precheck_definitely_not(
4436 session: &mut BlameSession<'_>,
4437 chain: &CommitGraphChain,
4438 oid: ObjectId,
4439 path: &str,
4440) -> bool {
4441 let Some(settings) = chain.bloom_settings_for_commit(&oid) else {
4442 return false;
4443 };
4444 let keys = session.bloom_keys_for_path(path, &settings);
4445 chain.bloom_definitely_not_for_keys(oid, keys.as_slice())
4446}
4447
4448fn commit_object_parents(
4450 session: &BlameSession<'_>,
4451 odb: &Odb,
4452 oid: ObjectId,
4453 cache: &mut BlameCommitCache,
4454) -> Result<Vec<ObjectId>> {
4455 if let Some(chain) = session.commit_graph.as_ref() {
4456 if let Some((parents, _commit_time)) = chain.graph_commit(&oid) {
4457 return Ok(parents);
4458 }
4459 }
4460 Ok(
4461 get_blame_commit(odb, oid, cache, session.commit_graph.as_deref())?
4462 .parents
4463 .clone(),
4464 )
4465}
4466
4467fn commit_parents_for_blame(
4469 odb: &Odb,
4470 oid: ObjectId,
4471 grafts: &HashMap<ObjectId, Vec<ObjectId>>,
4472 cache: &mut BlameCommitCache,
4473 session: &BlameSession<'_>,
4474) -> Result<Vec<ObjectId>> {
4475 if let Some(p) = grafts.get(&oid) {
4476 return Ok(p.clone());
4477 }
4478 if let Some(chain) = session.commit_graph.as_ref() {
4479 if let Some((parents, _)) = chain.graph_commit(&oid) {
4480 return Ok(parents);
4481 }
4482 }
4483 Ok(
4484 get_blame_commit(odb, oid, cache, session.commit_graph.as_deref())?
4485 .parents
4486 .clone(),
4487 )
4488}
4489
4490pub fn resolve_blame_line_range(
4500 repo: &crate::repo::Repository,
4501 start: crate::objects::ObjectId,
4502 path: &str,
4503 spec: &str,
4504) -> std::result::Result<RangeInclusive<usize>, LineRangeError> {
4505 resolve_blame_line_range_with_options(
4506 repo,
4507 path,
4508 spec,
4509 &BlameOptions {
4510 start: Some(start),
4511 ..BlameOptions::default()
4512 },
4513 )
4514}
4515
4516pub fn resolve_blame_line_range_with_options(
4527 repo: &crate::repo::Repository,
4528 path: &str,
4529 spec: &str,
4530 options: &BlameOptions,
4531) -> std::result::Result<RangeInclusive<usize>, LineRangeError> {
4532 use crate::attributes::load_gitattributes_for_check_attr_source;
4533 use crate::diff::merge_diff::read_blob_at_path;
4534 use crate::userdiff::matcher_for_path_parsed;
4535
4536 let invalid = |detail: String| LineRangeError::InvalidSpec {
4537 spec: spec.to_owned(),
4538 detail,
4539 };
4540 let file_path = normalize_blame_path(repo, path);
4541 let start = resolve_blame_start(repo, options.start).map_err(|e| invalid(e.to_string()))?;
4542 let commit = get_blame_commit(repo.odb(), start, &mut BlameCommitCache::default(), None)
4543 .map_err(|e| invalid(e.to_string()))?;
4544 let overlay = load_blame_overlay(
4545 repo,
4546 start,
4547 &blame_revision_label(options.start, start),
4548 &file_path,
4549 &options.final_source,
4550 CopyDetection::Off,
4551 )
4552 .map_err(|e| invalid(e.to_string()))?;
4553 let content: std::borrow::Cow<'_, str> = match &overlay {
4554 Some(overlay) => overlay
4555 .lines
4556 .utf8()
4557 .map(std::borrow::Cow::Borrowed)
4558 .unwrap_or_else(|| {
4559 String::from_utf8_lossy(overlay.lines.bytes())
4560 .into_owned()
4561 .into()
4562 }),
4563 None => {
4564 let Some(content_bytes) = read_blob_at_path(repo.odb(), &commit.tree, &file_path)
4565 else {
4566 return Err(invalid(format!("file '{file_path}' not found in revision")));
4567 };
4568 String::from_utf8_lossy(&content_bytes).into_owned().into()
4569 }
4570 };
4571 let config = repo.config().map(|c| (*c).clone()).unwrap_or_default();
4572 let parsed = load_gitattributes_for_check_attr_source(repo, &commit.tree)
4573 .map_err(|e| invalid(e.to_string()))?;
4574 let matcher =
4575 matcher_for_path_parsed(&config, &parsed.rules, &parsed.macros, &file_path, false)
4576 .map_err(invalid)?;
4577 resolve_line_range(spec, &content, &file_path, &config, matcher.as_ref(), 1)
4578}
4579
4580#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
4582pub enum CopyDetection {
4583 #[default]
4585 Off,
4586 FromModifiedFiles,
4588 FromCreatingCommit,
4590 FromAnyCommit,
4592}
4593
4594impl CopyDetection {
4595 #[must_use]
4597 pub const fn count(self) -> usize {
4598 match self {
4599 Self::Off => 0,
4600 Self::FromModifiedFiles => 1,
4601 Self::FromCreatingCommit => 2,
4602 Self::FromAnyCommit => 3,
4603 }
4604 }
4605
4606 pub fn from_count(count: usize) -> Result<Self> {
4612 match count {
4613 0 => Ok(Self::Off),
4614 1 => Ok(Self::FromModifiedFiles),
4615 2 => Ok(Self::FromCreatingCommit),
4616 3 => Ok(Self::FromAnyCommit),
4617 _ => Err(LibError::Message(
4618 "blame copy detection: at most three -C flags are supported".into(),
4619 )),
4620 }
4621 }
4622
4623 const fn depth(self) -> usize {
4624 self.count()
4625 }
4626}
4627
4628#[derive(Debug, Clone, Default, PartialEq, Eq)]
4630pub struct BlameOptions {
4631 pub start: Option<ObjectId>,
4633 pub line_range: Option<RangeInclusive<usize>>,
4639 pub first_parent_only: bool,
4641 pub ignore: IgnoreRevs,
4643 pub whitespace: crate::diff::WhitespaceMode,
4645 pub copy_detection: CopyDetection,
4647 pub final_source: BlameFinalSource,
4650 pub now: Option<time::OffsetDateTime>,
4653}
4654
4655#[derive(Debug, Clone, Default, PartialEq, Eq)]
4665pub enum BlameFinalSource {
4666 #[default]
4668 Revision,
4669 Worktree,
4672 Contents(Vec<u8>),
4675}
4676
4677#[derive(Debug, Clone, PartialEq, Eq)]
4679pub struct BlameAttributedLine {
4680 pub commit: ObjectId,
4682 pub original_line: usize,
4684 pub final_line: usize,
4686 pub original_path: String,
4690 pub content: String,
4692 pub encoding_lossy: bool,
4694 pub content_bytes: Option<Vec<u8>>,
4696 pub ignored: bool,
4698 pub unblamable: bool,
4700 pub not_committed: bool,
4704}
4705
4706#[derive(Debug, Clone, PartialEq, Eq)]
4708pub struct BlameHunk {
4709 pub commit: ObjectId,
4711 pub original_path: String,
4715 pub original_start: usize,
4717 pub final_start: usize,
4719 pub count: usize,
4721 pub ignored: bool,
4723 pub unblamable: bool,
4725}
4726
4727#[derive(Debug, Clone, PartialEq, Eq)]
4729pub struct BlameCommitInfo {
4730 pub author: String,
4733 pub author_time: i64,
4735 pub summary: String,
4737}
4738
4739#[derive(Debug, Clone, PartialEq, Eq)]
4741pub struct BlameResult {
4742 pub lines: Vec<BlameAttributedLine>,
4744 pub commits: HashMap<ObjectId, BlameCommitInfo>,
4746}
4747
4748impl BlameResult {
4749 #[must_use]
4751 pub fn hunks(&self) -> Vec<BlameHunk> {
4752 if self.lines.is_empty() {
4753 return Vec::new();
4754 }
4755 let mut out = Vec::new();
4756 let mut start = 0usize;
4757 for i in 1..=self.lines.len() {
4758 let extend =
4759 i < self.lines.len() && blame_lines_share_hunk(&self.lines[i - 1], &self.lines[i]);
4760 if !extend {
4761 let slice = &self.lines[start..i];
4762 out.push(BlameHunk {
4763 commit: slice[0].commit,
4764 original_path: slice[0].original_path.clone(),
4765 original_start: slice[0].original_line,
4766 final_start: slice[0].final_line,
4767 count: slice.len(),
4768 ignored: slice[0].ignored,
4769 unblamable: slice[0].unblamable,
4770 });
4771 start = i;
4772 }
4773 }
4774 out
4775 }
4776}
4777
4778fn blame_lines_share_hunk(prev: &BlameAttributedLine, next: &BlameAttributedLine) -> bool {
4779 prev.commit == next.commit
4780 && prev.original_path == next.original_path
4781 && prev.ignored == next.ignored
4782 && prev.unblamable == next.unblamable
4783 && next.original_line == prev.original_line + 1
4784 && next.final_line == prev.final_line + 1
4785}
4786
4787pub fn blame_file(repo: &Repository, path: &str, options: &BlameOptions) -> Result<BlameResult> {
4816 let start_oid = resolve_blame_start(repo, options.start)?;
4817 let file_path = normalize_blame_path(repo, path);
4818 let config = repo.config().map(|cfg| (*cfg).clone()).unwrap_or_default();
4819 let grafts = crate::rev_parse::load_graft_parents(repo.git_dir());
4820 let diff_algorithm = config
4821 .get("diff.algorithm")
4822 .as_ref()
4823 .and_then(|name| parse_diff_algorithm_name(name))
4824 .unwrap_or(BlameDiffAlgorithm::Myers);
4825 let indent_heuristic = crate::diff::indent_heuristic_from_config(&config);
4826 let ignore_revs: HashSet<ObjectId> = options.ignore.as_set().clone();
4827
4828 let bounded_odb = options
4829 .line_range
4830 .as_ref()
4831 .map(|_| repo.odb().with_bounded_pack_cache());
4832 let odb = bounded_odb.as_ref().unwrap_or(repo.odb());
4833 let thread_policy = repo.thread_policy();
4834 let mut session = BlameSession::new(odb, None, false, &thread_policy);
4835 session.whitespace = options.whitespace;
4836 session.commit_graph = repo.odb().commit_graph_chain();
4837 let revision_label = blame_revision_label(options.start, start_oid);
4838 let overlay = load_blame_overlay(
4839 repo,
4840 start_oid,
4841 &revision_label,
4842 &file_path,
4843 &options.final_source,
4844 options.copy_detection,
4845 )?;
4846 let copy_depth = options.copy_detection.depth();
4847 session.cache_pair_maps = !ignore_revs.is_empty() || copy_depth > 0;
4848 let (lines, commits) = odb.with_pack_read_context(
4849 || -> Result<(Vec<BlameAttributedLine>, HashMap<ObjectId, BlameCommitInfo>)> {
4850 let raw = compute_blame_with_session(
4851 odb,
4852 start_oid,
4853 &revision_label,
4854 &file_path,
4855 &ignore_revs,
4856 diff_algorithm,
4857 None,
4858 false,
4859 copy_depth,
4860 options.first_parent_only,
4861 &grafts,
4862 indent_heuristic,
4863 options.line_range.clone(),
4864 overlay.as_ref(),
4865 &mut session,
4866 )?;
4867
4868 let lines: Vec<BlameAttributedLine> = raw
4869 .into_iter()
4870 .map(|line| BlameAttributedLine {
4871 commit: line.oid,
4872 original_line: line.orig_lineno,
4873 final_line: line.final_lineno,
4874 original_path: line.original_path,
4875 content: line.content,
4876 encoding_lossy: line.encoding_lossy,
4877 content_bytes: line.content_bytes,
4878 ignored: line.ignored,
4879 unblamable: line.unblamable,
4880 not_committed: line.not_committed,
4881 })
4882 .collect();
4883
4884 let mut commits = HashMap::new();
4885 for line in &lines {
4886 commits.entry(line.commit).or_insert_with(|| {
4887 if line.not_committed {
4888 BlameCommitInfo {
4889 author: String::new(),
4890 author_time: options.now.map_or(0, |now| now.unix_timestamp()),
4891 summary: String::new(),
4892 }
4893 } else {
4894 load_blame_commit_info(odb, line.commit)
4895 }
4896 });
4897 }
4898 Ok((lines, commits))
4899 },
4900 )?;
4901
4902 Ok(BlameResult { lines, commits })
4903}
4904
4905fn blame_revision_label(start: Option<ObjectId>, start_oid: ObjectId) -> String {
4907 if start.is_some() {
4908 start_oid.to_hex()
4909 } else {
4910 "HEAD".into()
4911 }
4912}
4913
4914fn load_blame_overlay(
4919 repo: &Repository,
4920 start_oid: ObjectId,
4921 revision_label: &str,
4922 file_path: &str,
4923 source: &BlameFinalSource,
4924 copy_detection: CopyDetection,
4925) -> Result<Option<BlameOverlay>> {
4926 let supplied = match source {
4927 BlameFinalSource::Revision => return Ok(None),
4928 BlameFinalSource::Worktree => None,
4929 BlameFinalSource::Contents(bytes) => Some(bytes.as_slice()),
4930 };
4931 let odb = repo.odb();
4932 let config = repo.config().map(|c| (*c).clone()).unwrap_or_default();
4933 let path_bytes = storage_bytes(file_path);
4934 let rel_match = crate::repo_relpath::as_utf8_for_matching(file_path);
4935 let conv = crate::crlf::ConversionConfig::from_config(&config);
4936 let attrs = repo.work_tree().map(|work_tree| {
4937 let rules = crate::crlf::load_gitattributes(work_tree);
4938 crate::crlf::get_file_attrs(&rules, rel_match.as_ref(), false, &config)
4939 });
4940 let mut index = None;
4942 let bytes = match (supplied, repo.work_tree(), attrs.as_ref()) {
4943 (None, Some(work_tree), Some(attrs)) => {
4944 let abs = crate::repo_relpath::worktree_abs(work_tree, file_path);
4945 let meta = std::fs::symlink_metadata(&abs).map_err(|source| {
4946 crate::worktree::map_worktree_io(
4947 rel_match.as_ref(),
4948 crate::worktree::WorktreePathOperation::Metadata,
4949 source,
4950 )
4951 })?;
4952 let index_entry = if crate::crlf::clean_uses_autocrlf_index_guard(attrs, &conv) {
4953 loaded_index(repo, &mut index)?.get(&path_bytes, 0)
4954 } else {
4955 None
4956 };
4957 crate::diff::worktree_blob_bytes(
4958 odb,
4959 &abs,
4960 &meta,
4961 &conv,
4962 attrs,
4963 rel_match.as_ref(),
4964 index_entry,
4965 None,
4966 )?
4967 }
4968 (None, _, _) => return Err(LibError::BlameWorktreeBareRepository),
4969 (Some(bytes), _, Some(attrs)) => {
4970 crate::crlf::convert_to_git(bytes, rel_match.as_ref(), &conv, attrs)
4971 .map_err(LibError::Filter)?
4972 }
4973 (Some(bytes), _, None) => bytes.to_vec(),
4974 };
4975
4976 let start_tree = get_blame_commit(odb, start_oid, &mut BlameCommitCache::default(), None)?.tree;
4977 let start_blob = resolve_path_in_tree_entry(odb, &start_tree, file_path)?;
4978 if let Some((blob, _)) = start_blob {
4979 if odb.hash(ObjectKind::Blob, &bytes)? == blob {
4980 return Ok(None);
4981 }
4982 }
4983 let mut parents = vec![start_oid];
4984 parents.extend(crate::state::read_merge_heads(repo.git_dir())?);
4985 if start_blob.is_none() {
4986 let staged = loaded_index(repo, &mut index)?;
4987 let mut known = (0..=3).any(|stage| staged.get(&path_bytes, stage).is_some());
4988 for parent in &parents[1..] {
4989 if known {
4990 break;
4991 }
4992 let tree = get_blame_commit(odb, *parent, &mut BlameCommitCache::default(), None)?.tree;
4993 known = resolve_path_in_tree_entry(odb, &tree, file_path)?.is_some();
4994 }
4995 if !known {
4996 return Err(blame_path_missing_in_revision(file_path, revision_label));
4997 }
4998 }
4999
5000 let copy_depth = copy_detection.depth();
5001 let search_all_paths =
5002 copy_depth == 0 || copy_depth >= 3 || (copy_depth == 2 && start_blob.is_none());
5003 let copy_paths = if search_all_paths {
5004 None
5005 } else {
5006 Some(
5008 crate::diff::diff_index_to_tree(
5009 odb,
5010 loaded_index(repo, &mut index)?,
5011 Some(&start_tree),
5012 true,
5013 )?
5014 .into_iter()
5015 .flat_map(|entry| [entry.old_path, entry.new_path])
5016 .flatten()
5017 .collect(),
5018 )
5019 };
5020 Ok(Some(BlameOverlay {
5021 lines: Arc::new(BlobLines::from_bytes(bytes)),
5022 parents,
5023 copy_paths,
5024 }))
5025}
5026
5027fn loaded_index<'a>(repo: &Repository, slot: &'a mut Option<Index>) -> Result<&'a Index> {
5029 if slot.is_none() {
5030 *slot = Some(repo.load_index()?);
5031 }
5032 Ok(slot.get_or_insert_with(Index::default))
5033}
5034
5035fn resolve_blame_start(repo: &Repository, start: Option<ObjectId>) -> Result<ObjectId> {
5036 if let Some(oid) = start {
5037 return crate::revwalk::rev_parse::peel_to_commit_for_merge_base(repo, oid);
5038 }
5039 use crate::state::{resolve_head, HeadState};
5040 match resolve_head(repo.git_dir())? {
5041 HeadState::Detached { oid } => Ok(oid),
5042 HeadState::Branch { oid: Some(oid), .. } => Ok(oid),
5043 HeadState::Branch { oid: None, .. } | HeadState::Invalid => Err(LibError::Message(
5044 "cannot blame: HEAD does not point to a commit".to_string(),
5045 )),
5046 }
5047}
5048
5049fn normalize_blame_path(repo: &Repository, path: &str) -> String {
5050 if let std::borrow::Cow::Owned(bytes) = storage_bytes(path) {
5051 return storage_string(&bytes);
5054 }
5055 let normalized = if let Some(work_tree) = repo.work_tree() {
5056 crate::pathspec::normalize_worktree_file_path_with_git_dir(
5057 path,
5058 work_tree,
5059 None,
5060 Some(repo.git_dir()),
5061 )
5062 } else {
5063 path.to_string()
5064 };
5065 if normalized.starts_with(crate::repo_relpath::STORAGE_PREFIX) {
5067 storage_string(normalized.as_bytes())
5068 } else {
5069 normalized
5070 }
5071}
5072
5073fn load_blame_commit_info(odb: &Odb, oid: ObjectId) -> BlameCommitInfo {
5077 let (author, message) = match read_object_for_blame(odb, &oid, None) {
5078 Ok(obj) if obj.kind == ObjectKind::Commit => commit_author_and_message_lenient(&obj.data),
5079 _ => (None, String::new()),
5080 };
5081 let author = author.unwrap_or_default();
5082 BlameCommitInfo {
5083 author_time: author_epoch_from_identity(&author),
5084 author: author.trim().to_string(),
5085 summary: message
5086 .lines()
5087 .find(|line| !line.trim().is_empty())
5088 .unwrap_or("")
5089 .trim()
5090 .to_string(),
5091 }
5092}
5093
5094fn author_epoch_from_identity(identity: &str) -> i64 {
5095 let parts: Vec<&str> = identity.rsplitn(3, ' ').collect();
5096 if parts.len() >= 2 {
5097 parts[1].parse().unwrap_or(0)
5098 } else {
5099 0
5100 }
5101}
5102
5103#[cfg(test)]
5104mod tests {
5105 use super::*;
5106
5107 #[test]
5108 fn blob_lines_preserves_interior_and_trailing_blank_lines() {
5109 let cases = [
5110 "alpha\n\nbeta\n\ngamma\n",
5111 "only\n",
5112 "\n",
5113 "a\n\n\nb\n",
5114 "head\n\n",
5115 ];
5116 for content in cases {
5117 let blob = BlobLines::from_content(content.to_owned());
5118 let expected: Vec<String> = content_lines(content)
5119 .into_iter()
5120 .map(str::to_owned)
5121 .collect();
5122 assert_eq!(blob.to_vec(), expected, "content={content:?}");
5123 }
5124 }
5125
5126 #[test]
5127 fn blob_lines_compare_records_without_changing_bodies() {
5128 let exact = crate::diff::WhitespaceMode::default();
5129 let ws = crate::diff::WhitespaceMode::ignore_all_space();
5130 for text in ["", "hello", "hello\n", "a\r\nb\r", "\n", "a\n\n"] {
5131 let lines = BlobLines::from_content(text.to_owned());
5132 assert_eq!(
5133 lines.comparison_refs(exact),
5134 text.split_inclusive('\n').collect::<Vec<_>>()
5135 );
5136 assert_eq!(lines.comparison_refs(ws), content_lines(text));
5137 assert_eq!(lines.comparison_refs(exact).concat(), text);
5138 }
5139 }
5140
5141 #[test]
5142 fn final_newline_is_changed_in_affix_and_exact_maps() {
5143 let exact = crate::diff::WhitespaceMode::default();
5144 let ws = crate::diff::WhitespaceMode::ignore_all_space();
5145 for (before, after) in [
5146 ("first\nlast", "first\nlast\n"),
5147 ("first\nlast\n", "first\nlast"),
5148 ] {
5149 let old = BlobLines::from_content(before.to_owned());
5150 let new = BlobLines::from_content(after.to_owned());
5151 assert_eq!(common_affix_len(&old, &new, exact), (1, 0));
5152 let pending = vec![TrackedLine {
5153 final_lineno: 2,
5154 current_idx: 1,
5155 ignored: false,
5156 source_path: None,
5157 origin: 0,
5158 }];
5159 let (affix, need) = partition_pending_by_affix(
5160 slide_safe_affix(&old, &new, exact),
5161 old.len(),
5162 new.len(),
5163 pending,
5164 );
5165 assert!(affix.is_empty());
5166 assert_eq!(need.len(), 1);
5167 for algorithm in [
5168 BlameDiffAlgorithm::Myers,
5169 BlameDiffAlgorithm::Histogram,
5170 BlameDiffAlgorithm::Patience,
5171 BlameDiffAlgorithm::Minimal,
5172 ] {
5173 let (map, flags) = build_line_map_with_changed_flags_owned(
5174 &old,
5175 &new,
5176 slide_safe_affix(&old, &new, exact),
5177 algorithm,
5178 true,
5179 true,
5180 exact,
5181 );
5182 assert_eq!(map, vec![Some(0), None]);
5183 assert_eq!(flags, vec![false, true]);
5184 assert_eq!(
5185 build_exact_relocate_line_map_ws(
5186 &old.comparison_refs(exact),
5187 &new.comparison_refs(exact),
5188 &map,
5189 exact
5190 ),
5191 map
5192 );
5193 let (map, flags) = build_line_map_with_changed_flags_owned(
5194 &old,
5195 &new,
5196 slide_safe_affix(&old, &new, ws),
5197 algorithm,
5198 true,
5199 true,
5200 ws,
5201 );
5202 assert_eq!(map, vec![Some(0), Some(1)]);
5203 assert_eq!(flags, vec![false, false]);
5204 }
5205 }
5206 }
5207
5208 #[test]
5209 fn shuffle_commit_stops_blame_on_moved_include_line() {
5210 let old = [
5211 "",
5212 "#include \"cache.h\"",
5213 "#include \"builtin.h\"",
5214 "#include \"advice.h\"",
5215 "#include \"pathspec.h\"",
5216 "static int chmod_pathspec(struct pathspec_item *item, void *cb)",
5217 "{",
5218 " return 0;",
5219 "}",
5220 "#include \"run-command.h\"",
5221 "#include \"builtin.h\"",
5222 ];
5223 let new = [
5224 "",
5225 "#include \"builtin.h\"",
5226 "#include \"advice.h\"",
5227 "#include \"pathspec.h\"",
5228 "#include \"run-command.h\"",
5229 "static int chmod_pathspec(struct pathspec_item *item, void *cb)",
5230 "{",
5231 " return 0;",
5232 "}",
5233 ];
5234 let diff = build_line_map(&old, &new, BlameDiffAlgorithm::Myers, true);
5235 let map = build_exact_relocate_line_map(&old, &new, &diff);
5236 let (_, flags) = build_line_map_with_changed_flags(
5237 &old,
5238 &new,
5239 BlameDiffAlgorithm::Myers,
5240 true,
5241 true,
5242 crate::diff::WhitespaceMode::default(),
5243 );
5244 let stop = blame_stops_on_current_commit(&diff, &map, &flags);
5245 assert_eq!(diff[4], None);
5246 assert_eq!(
5247 map[4],
5248 Some(9),
5249 "exact relocate pairs moved text with old index"
5250 );
5251 assert!(
5252 stop[4],
5253 "moved include stops on shuffle commit: stop={stop:?} map={map:?} flags={flags:?}"
5254 );
5255 }
5256
5257 #[test]
5258 fn build_line_map_include_shuffle_matches_git_with_indent_heuristic() {
5259 let old = [
5260 "",
5261 "#include \"cache.h\"",
5262 "#include \"builtin.h\"",
5263 "#include \"advice.h\"",
5264 "static void foo(void)",
5265 "{",
5266 "}",
5267 ];
5268 let new = [
5269 "",
5270 "#include \"builtin.h\"",
5271 "#include \"advice.h\"",
5272 "static void foo(void)",
5273 "{",
5274 "}",
5275 ];
5276 let map = build_line_map(&old, &new, BlameDiffAlgorithm::Myers, true);
5277 assert_eq!(
5278 map,
5279 vec![Some(0), Some(2), Some(3), Some(4), Some(5), Some(6),],
5280 "git keeps matching includes through equal hunks"
5281 );
5282 }
5283
5284 #[test]
5285 fn build_line_map_maps_unchanged_lines_and_drops_replaced() {
5286 let old = ["alpha", "beta", "gamma"];
5287 let new = ["alpha", "BETA", "gamma"];
5288 let map = build_line_map(&old, &new, BlameDiffAlgorithm::Myers, false);
5289 assert_eq!(map, vec![Some(0), None, Some(2)]);
5291 }
5292
5293 #[test]
5294 fn build_line_map_slides_insertion_into_unchanged_suffix() {
5295 let old = [
5296 "PROG=a b c",
5297 "\t$(CC) -o cat-file",
5298 "",
5299 "read-cache.o: cache.h",
5300 ];
5301 let new = [
5302 "PROG=a b c fsck-cache",
5303 "\t$(CC) -o cat-file",
5304 "",
5305 "fsck-cache: fsck-cache.o",
5306 "\t$(CC) -o fsck-cache",
5307 "",
5308 "read-cache.o: cache.h",
5309 ];
5310 for algorithm in [BlameDiffAlgorithm::Myers, BlameDiffAlgorithm::Histogram] {
5311 for indent_heuristic in [true, false] {
5312 let map = build_line_map(&old, &new, algorithm, indent_heuristic);
5313 assert_eq!(
5314 map,
5315 vec![None, Some(1), Some(2), None, None, None, Some(3)],
5316 "{algorithm:?} indent_heuristic={indent_heuristic}"
5317 );
5318 }
5319 }
5320 let old_lines = BlobLines::from_content(old.map(|l| format!("{l}\n")).concat());
5321 let new_lines = BlobLines::from_content(new.map(|l| format!("{l}\n")).concat());
5322 let exact = crate::diff::WhitespaceMode::default();
5323 let (map, flags) = build_line_map_with_changed_flags_owned(
5324 &old_lines,
5325 &new_lines,
5326 slide_safe_affix(&old_lines, &new_lines, exact),
5327 BlameDiffAlgorithm::Myers,
5328 true,
5329 true,
5330 exact,
5331 );
5332 assert_eq!(map, vec![None, Some(1), Some(2), None, None, None, Some(3)]);
5333 assert_eq!(flags, vec![true, false, false, true, true, true, false]);
5334 let pending = (0..new.len())
5335 .map(|i| TrackedLine {
5336 final_lineno: i + 1,
5337 current_idx: i,
5338 ignored: false,
5339 source_path: None,
5340 origin: 0,
5341 })
5342 .collect();
5343 let (affix, need) = partition_pending_by_affix(
5344 slide_safe_affix(&old_lines, &new_lines, exact),
5345 old.len(),
5346 new.len(),
5347 pending,
5348 );
5349 let affix: Vec<(usize, usize)> = affix
5350 .iter()
5351 .map(|t| (t.final_lineno, t.current_idx))
5352 .collect();
5353 assert_eq!(
5354 affix,
5355 vec![(7, 3)],
5356 "the trailing blank can slide, so it needs the diff"
5357 );
5358 assert_eq!(need.len(), 6);
5359 }
5360
5361 #[test]
5362 fn blame_tail_trim_follows_git_block_rule() {
5363 let exact = crate::diff::WhitespaceMode::default();
5364 let old_text = format!("a\n{}", "b\n".repeat(1000));
5365 let new_text = format!("a\n{}", "b\n".repeat(1001));
5366 let old = BlobLines::from_content(old_text.clone());
5367 let new = BlobLines::from_content(new_text.clone());
5368 let affix = slide_safe_affix(&old, &new, exact);
5371 assert_eq!(blame_trimmed_tail_lines(&old, &new, affix, exact), 511);
5372 let old_refs: Vec<&str> = old_text.lines().collect();
5373 let new_refs: Vec<&str> = new_text.lines().collect();
5374 let slice_affix = slide_safe_affix(old_refs.as_slice(), new_refs.as_slice(), exact);
5375 assert_eq!(
5376 blame_trimmed_tail_lines(old_refs.as_slice(), new_refs.as_slice(), slice_affix, exact),
5377 511
5378 );
5379 let ws = crate::diff::WhitespaceMode::ignore_all_space();
5380 let ws_affix = slide_safe_affix(&old, &new, ws);
5381 assert_eq!(blame_trimmed_tail_lines(&old, &new, ws_affix, ws), 511);
5382
5383 let map = build_line_map(&old_refs, &new_refs, BlameDiffAlgorithm::Myers, true);
5384 let inserted: Vec<usize> = (0..map.len()).filter(|&i| map[i].is_none()).collect();
5385 assert_eq!(
5386 inserted,
5387 vec![490],
5388 "the insertion stops where git cut the tail"
5389 );
5390
5391 let short_old = BlobLines::from_content("a\nb\n".to_owned());
5392 let short_new = BlobLines::from_content("x\nb\n".to_owned());
5393 let affix = slide_safe_affix(&short_old, &short_new, exact);
5394 assert_eq!(
5395 blame_trimmed_tail_lines(&short_old, &short_new, affix, exact),
5396 0
5397 );
5398 }
5399
5400 struct Lcg(u64);
5402
5403 impl Lcg {
5404 fn next(&mut self, bound: usize) -> usize {
5405 self.0 = self
5406 .0
5407 .wrapping_mul(6364136223846793005)
5408 .wrapping_add(1442695040888963407);
5409 ((self.0 >> 33) as usize) % bound
5410 }
5411 }
5412
5413 #[test]
5414 fn windowed_line_map_matches_compacting_git_view() {
5415 let pool = [
5418 "",
5419 "",
5420 "}",
5421 "\t}",
5422 "#",
5423 "\t\treturn 0;",
5424 "\tif (x) {",
5425 "\tx = 1;",
5426 "fn a() {",
5427 ];
5428 let line = |rng: &mut Lcg, i: usize| -> String {
5429 let pick = rng.next(pool.len() * 2);
5430 pool.get(pick)
5431 .map_or_else(|| format!("line {i} {}", rng.next(4)), |l| (*l).to_owned())
5432 };
5433 let mut rng = Lcg(7);
5434 for case in 0..400 {
5435 let len = 60 + rng.next(500);
5436 let old: Vec<String> = (0..len).map(|i| line(&mut rng, i)).collect();
5437 let mut new = old.clone();
5438 for _ in 0..1 + rng.next(4) {
5439 let at = rng.next(new.len());
5440 let n = 1 + rng.next(6);
5441 match rng.next(3) {
5442 0 => {
5443 for k in 0..n {
5444 let l = line(&mut rng, k);
5445 new.insert(at, l);
5446 }
5447 }
5448 1 => {
5449 new.drain(at..(at + n).min(new.len()));
5450 }
5451 _ => {
5452 let end = (at + n).min(new.len());
5453 for slot in &mut new[at..end] {
5454 *slot = line(&mut rng, case);
5455 }
5456 }
5457 }
5458 }
5459 let old: Vec<&str> = old.iter().map(String::as_str).collect();
5460 let new: Vec<&str> = new.iter().map(String::as_str).collect();
5461 assert_window_matches_git_view(&old, &new, &format!("case {case}"));
5462 }
5463 }
5464
5465 #[test]
5467 fn windowed_line_map_keeps_blank_context_for_indent_heuristic() {
5468 let block = ["\tif (x) {", "\t\ty();", "\t}"];
5469 let mut old = vec!["top"];
5470 old.extend(std::iter::repeat_n("", 25));
5471 old.extend(block);
5472 old.push("end");
5473 let mut new = old.clone();
5474 new.splice(29..29, block);
5475 assert_window_matches_git_view(&old, &new, "blank run");
5476 }
5477
5478 #[test]
5480 fn windowed_line_map_counts_lines_outside_the_window() {
5481 let version = |middle: [&'static str; 8]| -> Vec<String> {
5482 let mut lines = vec!["}".to_owned(); 60];
5483 lines.extend((0..30).map(|i| format!("head {i}")));
5484 lines.extend(middle.map(str::to_owned));
5485 lines.extend((0..30).map(|i| format!("tail {i}")));
5486 lines
5487 };
5488 let old = version(["u1", "u2", "u3", "u4", "}", "u5", "u6", "u7"]);
5489 let new = version(["v1", "v2", "v3", "v4", "}", "v5", "v6", "v7"]);
5490 let old: Vec<&str> = old.iter().map(String::as_str).collect();
5491 let new: Vec<&str> = new.iter().map(String::as_str).collect();
5492 assert_window_matches_git_view(&old, &new, "frequent brace");
5493 let map = build_line_map(&old, &new, BlameDiffAlgorithm::Myers, true);
5494 assert!(
5495 map[90..98].iter().all(Option::is_none),
5496 "{:?}",
5497 &map[88..100]
5498 );
5499 }
5500
5501 fn assert_window_matches_git_view(old: &[&str], new: &[&str], label: &str) {
5503 let exact = crate::diff::WhitespaceMode::default();
5504 for algorithm in [BlameDiffAlgorithm::Myers, BlameDiffAlgorithm::Histogram] {
5505 for indent_heuristic in [true, false] {
5506 let affix = slide_safe_affix(old, new, exact);
5507 let tail = blame_trimmed_tail_lines(old, new, affix, exact);
5508 let (old_v, new_v) = (&old[..old.len() - tail], &new[..new.len() - tail]);
5509 let ops = blame_compacted_line_ops(
5510 old_v,
5511 new_v,
5512 std::iter::empty(),
5513 0,
5514 algorithm,
5515 indent_heuristic,
5516 exact,
5517 );
5518 let mut expected =
5519 crate::diff_indent_heuristic::map_new_to_old_from_ops(&ops, new_v.len());
5520 expected.extend((old.len() - tail..old.len()).map(Some));
5521 let mut expected_flags =
5522 crate::diff_indent_heuristic::new_line_changed_flags_from_ops(
5523 &ops,
5524 new_v.len(),
5525 );
5526 expected_flags.resize(new.len(), false);
5527 let (map, flags) =
5528 windowed_line_map(old, new, affix, algorithm, indent_heuristic, true, exact);
5529 assert_eq!(map, expected, "{label} {algorithm:?} {indent_heuristic}");
5530 assert_eq!(
5531 flags, expected_flags,
5532 "{label} {algorithm:?} {indent_heuristic}"
5533 );
5534 }
5535 }
5536 }
5537
5538 #[test]
5539 fn build_line_map_identity_for_equal_files() {
5540 let lines = ["one", "two", "three"];
5541 let map = build_line_map(&lines, &lines, BlameDiffAlgorithm::Histogram, false);
5542 assert_eq!(map, vec![Some(0), Some(1), Some(2)]);
5543 }
5544
5545 #[test]
5546 fn myers_does_not_map_case_changed_line_to_parent() {
5547 let par = ["alpha one", "beta two", " gamma three"];
5548 let cur = ["alpha ONE", "beta two", " gamma three"];
5549 let map = build_line_map(&par, &cur, BlameDiffAlgorithm::Myers, false);
5550 assert_eq!(map[0], None);
5551 }
5552
5553 #[test]
5554 fn reposition_blame_flags_rotated_head_line_only() {
5555 let par = ["a", "b", "c", "d"];
5556 let cur = ["d", "a", "b", "c"];
5557 let diff = build_line_map(&par, &cur, BlameDiffAlgorithm::Myers, false);
5558 let map = build_exact_relocate_line_map(&par, &cur, &diff);
5559 let xdiff = crate::diff::xdiff_new_line_changed_flags(&par, &cur, false);
5560 let blame = blame_stops_on_current_commit(&diff, &map, &xdiff);
5561 assert_eq!(blame, vec![true, false, false, false]);
5562 }
5563
5564 #[test]
5565 fn reposition_blame_swapped_inserted_line_in_three_line_file() {
5566 let par = ["alpha ONE", "inserted", " gamma three"];
5567 let cur = ["alpha ONE", " gamma three", "inserted"];
5568 let diff = build_line_map(&par, &cur, BlameDiffAlgorithm::Myers, false);
5569 let map = build_exact_relocate_line_map(&par, &cur, &diff);
5570 let xdiff = crate::diff::xdiff_new_line_changed_flags(&par, &cur, false);
5571 let blame = blame_stops_on_current_commit(&diff, &map, &xdiff);
5572 assert_eq!(blame, vec![false, false, true]);
5573 }
5574
5575 #[test]
5576 fn reposition_blame_dacb_permutation() {
5577 let par = ["a", "b", "c", "d"];
5578 let cur = ["d", "a", "c", "b"];
5579 let diff = build_line_map(&par, &cur, BlameDiffAlgorithm::Myers, false);
5580 let map = build_exact_relocate_line_map(&par, &cur, &diff);
5581 let xdiff = crate::diff::xdiff_new_line_changed_flags(&par, &cur, false);
5582 let blame = blame_stops_on_current_commit(&diff, &map, &xdiff);
5583 assert_eq!(blame, vec![true, false, false, true]);
5584 }
5585
5586 #[test]
5587 fn reposition_blame_flags_same_index_diff_insertion() {
5588 let par = ["a", "b", "c", "d"];
5589 let cur = ["a", "d", "c", "b"];
5590 let diff = build_line_map(&par, &cur, BlameDiffAlgorithm::Myers, false);
5591 let map = build_exact_relocate_line_map(&par, &cur, &diff);
5592 let xdiff = crate::diff::xdiff_new_line_changed_flags(&par, &cur, false);
5593 let blame = blame_stops_on_current_commit(&diff, &map, &xdiff);
5594 assert_eq!(blame, vec![false, false, true, true]);
5595 }
5596
5597 #[test]
5598 fn fuzzy_line_map_recovers_moved_unchanged_line() {
5599 let par = ["alpha ONE", "inserted", " gamma three"];
5600 let cur = ["alpha ONE", " gamma three", "inserted"];
5601 let map = build_exact_relocate_line_map(
5602 &par,
5603 &cur,
5604 &build_line_map(&par, &cur, BlameDiffAlgorithm::Myers, false),
5605 );
5606 assert_eq!(map[1], Some(2));
5607 }
5608
5609 #[test]
5610 fn partition_pending_by_affix_maps_prefix_and_suffix() {
5611 let old = BlobLines::from_content("a\nb\nc\nd\ne\n".to_owned());
5612 let new = BlobLines::from_content("a\nb\nx\nd\ne\n".to_owned());
5613 let pending = vec![TrackedLine {
5614 final_lineno: 1,
5615 current_idx: 0,
5616 ignored: false,
5617 source_path: None,
5618 origin: 0,
5619 }];
5620 let exact = crate::diff::WhitespaceMode::default();
5621 let (affix, need) = partition_pending_by_affix(
5622 slide_safe_affix(&old, &new, exact),
5623 old.len(),
5624 new.len(),
5625 pending,
5626 );
5627 assert!(need.is_empty());
5628 assert_eq!(affix[0].current_idx, 0);
5629
5630 let pending_mid = vec![TrackedLine {
5631 final_lineno: 3,
5632 current_idx: 2,
5633 ignored: false,
5634 source_path: None,
5635 origin: 0,
5636 }];
5637 let (affix, need) = partition_pending_by_affix(
5638 slide_safe_affix(&old, &new, exact),
5639 old.len(),
5640 new.len(),
5641 pending_mid,
5642 );
5643 assert!(affix.is_empty());
5644 assert_eq!(need.len(), 1);
5645 }
5646
5647 #[test]
5648 fn blame_result_hunks_group_contiguous_runs() {
5649 use crate::objects::ObjectId;
5650
5651 let c1 = ObjectId::from_hex("1111111111111111111111111111111111111111").unwrap();
5652 let c2 = ObjectId::from_hex("2222222222222222222222222222222222222222").unwrap();
5653 let result = BlameResult {
5654 lines: vec![
5655 BlameAttributedLine {
5656 commit: c1,
5657 original_line: 1,
5658 final_line: 1,
5659 original_path: "a.txt".to_owned(),
5660 content: "one".to_owned(),
5661 encoding_lossy: false,
5662 content_bytes: None,
5663 ignored: false,
5664 unblamable: false,
5665 not_committed: false,
5666 },
5667 BlameAttributedLine {
5668 commit: c1,
5669 original_line: 2,
5670 final_line: 2,
5671 original_path: "a.txt".to_owned(),
5672 content: "two".to_owned(),
5673 encoding_lossy: false,
5674 content_bytes: None,
5675 ignored: false,
5676 unblamable: false,
5677 not_committed: false,
5678 },
5679 BlameAttributedLine {
5680 commit: c2,
5681 original_line: 4,
5682 final_line: 3,
5683 original_path: "b.txt".to_owned(),
5684 content: "three".to_owned(),
5685 encoding_lossy: false,
5686 content_bytes: None,
5687 ignored: false,
5688 unblamable: false,
5689 not_committed: false,
5690 },
5691 ],
5692 commits: HashMap::new(),
5693 };
5694 let hunks = result.hunks();
5695 assert_eq!(hunks.len(), 2);
5696 assert_eq!(hunks[0].count, 2);
5697 assert_eq!(hunks[0].original_start, 1);
5698 assert_eq!(hunks[0].final_start, 1);
5699 assert_eq!(hunks[1].count, 1);
5700 assert_eq!(hunks[1].original_path, "b.txt");
5701 }
5702
5703 #[test]
5704 fn parse_diff_algorithm_name_known_and_unknown() {
5705 assert_eq!(
5706 parse_diff_algorithm_name("myers"),
5707 Some(BlameDiffAlgorithm::Myers)
5708 );
5709 assert_eq!(
5710 parse_diff_algorithm_name("histogram"),
5711 Some(BlameDiffAlgorithm::Histogram)
5712 );
5713 assert_eq!(parse_diff_algorithm_name("nope"), None);
5714 }
5715
5716 #[test]
5717 fn blame_file_resolves_nested_path_in_wide_root_tree() -> Result<()> {
5718 use crate::objects::{serialize_tree, tree_entry_cmp, ObjectKind, TreeEntry};
5719 use crate::repo::init_repository;
5720 use crate::RefStorageFormat;
5721 use std::fs;
5722 use tempfile::tempdir;
5723
5724 let dir = tempdir().map_err(crate::error::Error::Io)?;
5725 let repo = init_repository(dir.path(), false, "main", None, RefStorageFormat::Files)?;
5726 let hello = repo.odb().write(ObjectKind::Blob, b"hello\n")?;
5727 let mut sub_body = b"100644 inner.txt\0".to_vec();
5728 sub_body.extend_from_slice(hello.as_bytes());
5729 let sub_tree = repo.odb().write(ObjectKind::Tree, &sub_body)?;
5730 let filler = repo.odb().write(ObjectKind::Blob, b"1\n")?;
5731 let mut entries: Vec<TreeEntry> = (1..=60)
5732 .map(|i| TreeEntry {
5733 mode: 0o100644,
5734 name: format!("f{i}.txt").into_bytes(),
5735 oid: filler,
5736 })
5737 .collect();
5738 entries.push(TreeEntry {
5739 mode: 0o040000,
5740 name: b"sub".to_vec(),
5741 oid: sub_tree,
5742 });
5743 entries.sort_by(|a, b| {
5744 tree_entry_cmp(&a.name, a.mode == 0o040000, &b.name, b.mode == 0o040000)
5745 });
5746 let root_tree = repo
5747 .odb()
5748 .write(ObjectKind::Tree, &serialize_tree(&entries))?;
5749 let commit_body = format!(
5750 "tree {root_tree}\nauthor T <t@t.com> 1 +0000\ncommitter T <t@t.com> 1 +0000\n\ninit\n"
5751 );
5752 let tip = repo
5753 .odb()
5754 .write(ObjectKind::Commit, commit_body.as_bytes())?;
5755 fs::write(repo.git_dir().join("refs/heads/main"), format!("{tip}\n"))
5756 .map_err(crate::error::Error::Io)?;
5757 let result = blame_file(
5758 &repo,
5759 "sub/inner.txt",
5760 &BlameOptions {
5761 start: Some(tip),
5762 ..Default::default()
5763 },
5764 )?;
5765 assert_eq!(result.lines.len(), 1);
5766 assert_eq!(result.lines[0].content, "hello");
5767 Ok(())
5768 }
5769
5770 fn write_file_commit(
5771 repo: &Repository,
5772 path: &str,
5773 lines: &[String],
5774 parents: &[ObjectId],
5775 time: i64,
5776 ) -> Result<ObjectId> {
5777 use crate::objects::ObjectKind;
5778 let content: String = lines.iter().map(|l| format!("{l}\n")).collect();
5779 let blob = repo.odb().write(ObjectKind::Blob, content.as_bytes())?;
5780 let mut tree_body = format!("100644 {path}\0").into_bytes();
5781 tree_body.extend_from_slice(blob.as_bytes());
5782 let tree = repo.odb().write(ObjectKind::Tree, &tree_body)?;
5783 let mut body = format!("tree {tree}\n");
5784 for parent in parents {
5785 body.push_str(&format!("parent {parent}\n"));
5786 }
5787 body.push_str(&format!(
5788 "author T <t@e.com> {time} +0000\ncommitter T <t@e.com> {time} +0000\n\nc{time}\n"
5789 ));
5790 repo.odb().write(ObjectKind::Commit, body.as_bytes())
5791 }
5792
5793 #[test]
5797 fn merge_ladder_diffs_each_parent_pair_once() -> Result<()> {
5798 use crate::repo::init_repository;
5799 use crate::RefStorageFormat;
5800
5801 let dir = tempfile::tempdir().map_err(crate::error::Error::Io)?;
5802 let repo = init_repository(dir.path(), false, "main", None, RefStorageFormat::Files)?;
5803 let path = "ladder.txt";
5804 let base: Vec<String> = (1..=40).map(|i| format!("base {i}")).collect();
5805 let root = write_file_commit(&repo, path, &base, &[], 1000)?;
5806 let mut tip = root;
5807 let mut tip_lines = base.clone();
5808 let mut main_commits = Vec::new();
5809 let mut side_commits = Vec::new();
5810 for k in 1..=8_usize {
5811 let t = 1000 + 10 * k as i64;
5812 let mut main_lines = tip_lines.clone();
5813 main_lines[k - 1] = format!("main {k}");
5814 main_lines[9 + k] = format!("kept main {k}");
5815 let main = write_file_commit(&repo, path, &main_lines, &[tip], t)?;
5816 let mut side_lines = tip_lines.clone();
5817 side_lines[19 + k] = format!("side {k}");
5818 let side = write_file_commit(&repo, path, &side_lines, &[tip], t + 1)?;
5819 let mut merged = side_lines.clone();
5821 merged[9 + k] = main_lines[9 + k].clone();
5822 tip = write_file_commit(&repo, path, &merged, &[main, side], t + 2)?;
5823 tip_lines = merged;
5824 main_commits.push(main);
5825 side_commits.push(side);
5826 }
5827
5828 let thread_policy = repo.thread_policy();
5829 let mut session = BlameSession::new(repo.odb(), None, false, &thread_policy);
5830 let lines = compute_blame_with_session(
5831 repo.odb(),
5832 tip,
5833 "tip",
5834 path,
5835 &HashSet::new(),
5836 BlameDiffAlgorithm::Myers,
5837 None,
5838 false,
5839 0,
5840 false,
5841 &HashMap::new(),
5842 false,
5843 None,
5844 None,
5845 &mut session,
5846 )?;
5847
5848 assert_eq!(lines.len(), 40);
5849 for line in &lines {
5850 let expected = match line.final_lineno {
5851 n @ 11..=18 => main_commits[n - 11],
5852 n @ 21..=28 => side_commits[n - 21],
5853 _ => root,
5854 };
5855 assert_eq!(line.oid, expected, "line {}", line.final_lineno);
5856 }
5857 let unique: HashSet<_> = session.diffed_pairs.iter().collect();
5858 assert_eq!(
5859 unique.len(),
5860 session.diffed_pairs.len(),
5861 "a parent/child pair was diffed more than once"
5862 );
5863 Ok(())
5864 }
5865
5866 #[test]
5867 fn copy_detection_from_count_maps_git_levels() -> Result<()> {
5868 assert_eq!(CopyDetection::from_count(0)?, CopyDetection::Off);
5869 assert_eq!(
5870 CopyDetection::from_count(1)?,
5871 CopyDetection::FromModifiedFiles
5872 );
5873 assert_eq!(
5874 CopyDetection::from_count(2)?,
5875 CopyDetection::FromCreatingCommit
5876 );
5877 assert_eq!(CopyDetection::from_count(3)?, CopyDetection::FromAnyCommit);
5878 assert!(CopyDetection::from_count(4).is_err());
5879 Ok(())
5880 }
5881
5882 #[test]
5883 fn blame_file_walks_linear_history() -> Result<()> {
5884 use crate::objects::ObjectKind;
5885 use crate::repo::init_repository;
5886 use crate::RefStorageFormat;
5887 use std::fs;
5888 use tempfile::tempdir;
5889
5890 let dir = tempdir().map_err(crate::error::Error::Io)?;
5891 let repo = init_repository(dir.path(), false, "main", None, RefStorageFormat::Files)?;
5892 let path = "trace.txt";
5893 let mut parent: Option<ObjectId> = None;
5894 let mut tip = repo.odb().write(ObjectKind::Blob, b"line0\n")?;
5895 for i in 1..=48_i32 {
5896 let content = format!("line{i}\n");
5897 fs::write(dir.path().join(path), &content).map_err(crate::error::Error::Io)?;
5898 tip = repo.odb().write(ObjectKind::Blob, content.as_bytes())?;
5899 let mut body = format!("100644 {path}\0").into_bytes();
5900 body.extend_from_slice(tip.as_bytes());
5901 let tree = repo.odb().write(ObjectKind::Tree, &body)?;
5902 let mut commit_body = format!("tree {tree}\n");
5903 if let Some(p) = parent {
5904 commit_body.push_str(&format!("parent {p}\n"));
5905 }
5906 commit_body.push_str(&format!(
5907 "author T <t@e.com> {} +0000\ncommitter T <t@e.com> {} +0000\n\nc{i}\n",
5908 100 + i,
5909 100 + i
5910 ));
5911 tip = repo
5912 .odb()
5913 .write(ObjectKind::Commit, commit_body.as_bytes())?;
5914 parent = Some(tip);
5915 }
5916 fs::write(repo.git_dir().join("refs/heads/main"), format!("{tip}\n"))
5917 .map_err(crate::error::Error::Io)?;
5918 for name in ["myers", "histogram", "minimal", "patience"] {
5919 let _ = parse_diff_algorithm_name(name);
5920 }
5921 let result = blame_file(
5922 &repo,
5923 path,
5924 &BlameOptions {
5925 start: Some(tip),
5926 line_range: Some(1..=10),
5927 ..Default::default()
5928 },
5929 )?;
5930 assert!(!result.lines.is_empty());
5931 Ok(())
5932 }
5933
5934 #[test]
5935 fn stored_path_lookup_preserves_raw_names_and_cache_keys() -> Result<()> {
5936 use crate::objects::{serialize_tree, TreeEntry};
5937 use crate::repo::init_repository;
5938 use crate::RefStorageFormat;
5939
5940 let dir = tempfile::tempdir()?;
5941 let repo = init_repository(dir.path(), false, "main", None, RefStorageFormat::Files)?;
5942 let mut entries = Vec::new();
5943 let names: &[&[u8]] = &[
5944 b"ab",
5945 b"grit+pathhex:6162",
5946 b"raw\xfe",
5947 b"raw\xff",
5948 "raw\u{fffd}".as_bytes(),
5949 ];
5950 for name in names {
5951 let oid = repo.odb().write(ObjectKind::Blob, name)?;
5952 entries.push(TreeEntry {
5953 mode: 0o100644,
5954 name: name.to_vec(),
5955 oid,
5956 });
5957 }
5958 entries.sort_by(|a, b| a.name.cmp(&b.name));
5959 let subtree = repo
5960 .odb()
5961 .write(ObjectKind::Tree, &serialize_tree(&entries))?;
5962 let root = repo.odb().write(
5963 ObjectKind::Tree,
5964 &serialize_tree(&[TreeEntry {
5965 mode: 0o040000,
5966 name: b"dir\xff".to_vec(),
5967 oid: subtree,
5968 }]),
5969 )?;
5970 let policy = ThreadPolicy::default();
5971 let mut session = BlameSession::new(repo.odb(), None, false, &policy);
5972 for entry in entries {
5973 let raw = crate::repo_relpath::join_rel(b"dir\xff", &entry.name);
5974 let stored = storage_string(&raw);
5975 let expected = Some((entry.oid, entry.mode));
5976 assert_eq!(session.resolve_path_in_tree(root, &stored)?, expected);
5977 assert_eq!(
5978 resolve_path_in_tree_entry(repo.odb(), &root, &stored)?,
5979 expected
5980 );
5981 let count = session.path_entries.len();
5982 let uppercase = format!(
5983 "grit+pathhex:{}",
5984 stored
5985 .strip_prefix("grit+pathhex:")
5986 .unwrap_or_default()
5987 .to_ascii_uppercase()
5988 );
5989 assert_eq!(session.resolve_path_in_tree(root, &uppercase)?, expected);
5990 assert_eq!(session.path_entries.len(), count);
5991 }
5992 Ok(())
5993 }
5994
5995 #[test]
5996 fn stored_path_bloom_keys_hash_raw_directory_prefixes() -> Result<()> {
5997 use crate::bloom::{bloom_key_hashes, BloomFilterSettings};
5998 use crate::repo::init_repository;
5999 use crate::RefStorageFormat;
6000
6001 let dir = tempfile::tempdir()?;
6002 let repo = init_repository(dir.path(), false, "main", None, RefStorageFormat::Files)?;
6003 let policy = ThreadPolicy::default();
6004 let mut session = BlameSession::new(repo.odb(), None, false, &policy);
6005 for version in [1, 2] {
6006 let settings = BloomFilterSettings {
6007 hash_version: version,
6008 ..Default::default()
6009 };
6010 for path in [
6011 b"dir\xff/raw\xfe.txt".as_slice(),
6012 b"grit+pathhex:6162/file",
6013 b"utf8/file",
6014 ] {
6015 let slash = path.iter().rposition(|&b| b == b'/').unwrap_or_default();
6016 let expected = vec![
6017 bloom_key_hashes(path, &settings),
6018 bloom_key_hashes(&path[..slash], &settings),
6019 ];
6020 let keys = session.bloom_keys_for_path(&storage_string(path), &settings);
6021 assert_eq!(keys.as_ref(), &expected);
6022 }
6023 }
6024 Ok(())
6025 }
6026}