@@ -102,6 +102,7 @@ pub fn file(
102102 hunks_to_blame. push ( UnblamedHunk {
103103 range_in_blamed_file : range. clone ( ) ,
104104 suspects : [ ( suspect, range) ] . into ( ) ,
105+ source_file_name : None ,
105106 } ) ;
106107 }
107108
@@ -120,12 +121,19 @@ pub fn file(
120121 break ;
121122 }
122123
123- let is_still_suspect = hunks_to_blame. iter ( ) . any ( |hunk| hunk. has_suspect ( & suspect) ) ;
124- if !is_still_suspect {
124+ let first_hunk_for_suspect = hunks_to_blame. iter ( ) . find ( |hunk| hunk. has_suspect ( & suspect) ) ;
125+ let Some ( first_hunk_for_suspect ) = first_hunk_for_suspect else {
125126 // There are no `UnblamedHunk`s associated with this `suspect`, so we can continue with
126127 // the next one.
127128 continue ' outer;
128- }
129+ } ;
130+
131+ // We know `first_hunk_for_suspect` can’t be `None` here because we check `is_some()`
132+ // above.
133+ let current_file_path = first_hunk_for_suspect
134+ . source_file_name
135+ . clone ( )
136+ . unwrap_or_else ( || file_path. to_owned ( ) ) ;
129137
130138 let commit = find_commit ( cache. as_ref ( ) , & odb, & suspect, & mut buf) ?;
131139 let commit_time = commit. commit_time ( ) ?;
@@ -165,7 +173,7 @@ pub fn file(
165173 entry = find_path_entry_in_commit (
166174 & odb,
167175 & suspect,
168- file_path ,
176+ current_file_path . as_ref ( ) ,
169177 cache. as_ref ( ) ,
170178 & mut buf,
171179 & mut buf2,
@@ -216,7 +224,7 @@ pub fn file(
216224 if let Some ( parent_entry_id) = find_path_entry_in_commit (
217225 & odb,
218226 parent_id,
219- file_path ,
227+ current_file_path . as_ref ( ) ,
220228 cache. as_ref ( ) ,
221229 & mut buf,
222230 & mut buf2,
@@ -239,15 +247,17 @@ pub fn file(
239247 queue. insert ( parent_commit_time, parent_id) ;
240248 let changes_for_file_path = tree_diff_at_file_path (
241249 & odb,
242- file_path ,
250+ current_file_path . as_ref ( ) ,
243251 suspect,
244252 parent_id,
245253 cache. as_ref ( ) ,
246254 & mut stats,
247255 & mut diff_state,
256+ resource_cache,
248257 & mut buf,
249258 & mut buf2,
250259 & mut buf3,
260+ options. rewrites ,
251261 ) ?;
252262 let Some ( modification) = changes_for_file_path else {
253263 if more_than_one_parent {
@@ -263,7 +273,7 @@ pub fn file(
263273 } ;
264274
265275 match modification {
266- gix_diff :: tree :: recorder :: Change :: Addition { .. } => {
276+ TreeDiffChange :: Addition => {
267277 if more_than_one_parent {
268278 // Do nothing under the assumption that this always (or almost always)
269279 // implies that the file comes from a different parent, compared to which
@@ -272,20 +282,44 @@ pub fn file(
272282 break ' outer;
273283 }
274284 }
275- gix_diff :: tree :: recorder :: Change :: Deletion { .. } => {
285+ TreeDiffChange :: Deletion => {
276286 unreachable ! ( "We already found file_path in suspect^{{tree}}, so it can't be deleted" )
277287 }
278- gix_diff :: tree :: recorder :: Change :: Modification { previous_oid , oid , .. } => {
288+ TreeDiffChange :: Modification { previous_id , id } => {
279289 let changes = blob_changes (
280290 & odb,
281291 resource_cache,
282- oid,
283- previous_oid,
292+ id,
293+ previous_id,
294+ file_path,
295+ file_path,
296+ options. diff_algorithm ,
297+ & mut stats,
298+ ) ?;
299+ hunks_to_blame = process_changes ( hunks_to_blame, changes, suspect, parent_id) ;
300+ }
301+ TreeDiffChange :: Rewrite {
302+ source_location,
303+ source_id,
304+ id,
305+ } => {
306+ let changes = blob_changes (
307+ & odb,
308+ resource_cache,
309+ id,
310+ source_id,
284311 file_path,
312+ source_location. as_ref ( ) ,
285313 options. diff_algorithm ,
286314 & mut stats,
287315 ) ?;
288316 hunks_to_blame = process_changes ( hunks_to_blame, changes, suspect, parent_id) ;
317+
318+ for hunk in hunks_to_blame. iter_mut ( ) {
319+ if hunk. has_suspect ( & parent_id) {
320+ hunk. source_file_name = Some ( source_location. clone ( ) ) ;
321+ }
322+ }
289323 }
290324 }
291325 }
@@ -382,6 +416,7 @@ fn coalesce_blame_entries(lines_blamed: Vec<BlameEntry>) -> Vec<BlameEntry> {
382416 len : NonZeroU32 :: new ( ( current_source_range. end - previous_source_range. start ) as u32 )
383417 . expect ( "BUG: hunks are never zero-sized" ) ,
384418 commit_id : previous_entry. commit_id ,
419+ source_file_name : previous_entry. source_file_name . clone ( ) ,
385420 } ;
386421
387422 acc. pop ( ) ;
@@ -399,6 +434,59 @@ fn coalesce_blame_entries(lines_blamed: Vec<BlameEntry>) -> Vec<BlameEntry> {
399434 } )
400435}
401436
437+ /// The union of [`gix_diff::tree::recorder::Change`] and [`gix_diff::tree_with_rewrites::Change`],
438+ /// keeping only the blame-relevant information.
439+ enum TreeDiffChange {
440+ Addition ,
441+ Deletion ,
442+ Modification {
443+ previous_id : ObjectId ,
444+ id : ObjectId ,
445+ } ,
446+ Rewrite {
447+ source_location : BString ,
448+ source_id : ObjectId ,
449+ id : ObjectId ,
450+ } ,
451+ }
452+
453+ impl From < gix_diff:: tree:: recorder:: Change > for TreeDiffChange {
454+ fn from ( value : gix_diff:: tree:: recorder:: Change ) -> Self {
455+ use gix_diff:: tree:: recorder:: Change ;
456+
457+ match value {
458+ Change :: Addition { .. } => Self :: Addition ,
459+ Change :: Deletion { .. } => Self :: Deletion ,
460+ Change :: Modification { previous_oid, oid, .. } => Self :: Modification {
461+ previous_id : previous_oid,
462+ id : oid,
463+ } ,
464+ }
465+ }
466+ }
467+
468+ impl From < gix_diff:: tree_with_rewrites:: Change > for TreeDiffChange {
469+ fn from ( value : gix_diff:: tree_with_rewrites:: Change ) -> Self {
470+ use gix_diff:: tree_with_rewrites:: Change ;
471+
472+ match value {
473+ Change :: Addition { .. } => Self :: Addition ,
474+ Change :: Deletion { .. } => Self :: Deletion ,
475+ Change :: Modification { previous_id, id, .. } => Self :: Modification { previous_id, id } ,
476+ Change :: Rewrite {
477+ source_location,
478+ source_id,
479+ id,
480+ ..
481+ } => Self :: Rewrite {
482+ source_location,
483+ source_id,
484+ id,
485+ } ,
486+ }
487+ }
488+ }
489+
402490#[ allow( clippy:: too_many_arguments) ]
403491fn tree_diff_at_file_path (
404492 odb : impl gix_object:: Find + gix_object:: FindHeader ,
@@ -408,10 +496,12 @@ fn tree_diff_at_file_path(
408496 cache : Option < & gix_commitgraph:: Graph > ,
409497 stats : & mut Statistics ,
410498 state : & mut gix_diff:: tree:: State ,
499+ resource_cache : & mut gix_diff:: blob:: Platform ,
411500 commit_buf : & mut Vec < u8 > ,
412501 lhs_tree_buf : & mut Vec < u8 > ,
413502 rhs_tree_buf : & mut Vec < u8 > ,
414- ) -> Result < Option < gix_diff:: tree:: recorder:: Change > , Error > {
503+ rewrites : Option < gix_diff:: Rewrites > ,
504+ ) -> Result < Option < TreeDiffChange > , Error > {
415505 let parent_tree_id = find_commit ( cache, & odb, & parent_id, commit_buf) ?. tree_id ( ) ?;
416506
417507 let parent_tree_iter = odb. find_tree_iter ( & parent_tree_id, lhs_tree_buf) ?;
@@ -422,6 +512,44 @@ fn tree_diff_at_file_path(
422512 let tree_iter = odb. find_tree_iter ( & tree_id, rhs_tree_buf) ?;
423513 stats. trees_decoded += 1 ;
424514
515+ let result = tree_diff_without_rewrites_at_file_path ( & odb, file_path, stats, state, parent_tree_iter, tree_iter) ?;
516+
517+ // Here, we follow git’s behaviour. We return when we’ve found a `Modification`. We try a
518+ // second time with rename tracking when the change is either an `Addition` or a `Deletion`
519+ // because those can turn out to have been a `Rewrite`.
520+ // TODO(perf): renames are usually rare enough to not care about the work duplication done here.
521+ // But in theory, a rename tracker could be used by us, on demand, and we could stuff the
522+ // changes in there and have it find renames, without repeating the diff.
523+ if matches ! ( result, Some ( TreeDiffChange :: Modification { .. } ) ) {
524+ return Ok ( result) ;
525+ }
526+ let Some ( rewrites) = rewrites else {
527+ return Ok ( result) ;
528+ } ;
529+
530+ let result = tree_diff_with_rewrites_at_file_path (
531+ & odb,
532+ file_path,
533+ stats,
534+ state,
535+ resource_cache,
536+ parent_tree_iter,
537+ tree_iter,
538+ rewrites,
539+ ) ?;
540+
541+ Ok ( result)
542+ }
543+
544+ #[ allow( clippy:: too_many_arguments) ]
545+ fn tree_diff_without_rewrites_at_file_path (
546+ odb : impl gix_object:: Find + gix_object:: FindHeader ,
547+ file_path : & BStr ,
548+ stats : & mut Statistics ,
549+ state : & mut gix_diff:: tree:: State ,
550+ parent_tree_iter : gix_object:: TreeRefIter < ' _ > ,
551+ tree_iter : gix_object:: TreeRefIter < ' _ > ,
552+ ) -> Result < Option < TreeDiffChange > , Error > {
425553 struct FindChangeToPath {
426554 inner : gix_diff:: tree:: Recorder ,
427555 interesting_path : BString ,
@@ -509,17 +637,62 @@ fn tree_diff_at_file_path(
509637 stats. trees_diffed += 1 ;
510638
511639 match result {
512- Ok ( _) | Err ( gix_diff:: tree:: Error :: Cancelled ) => Ok ( recorder. change ) ,
640+ Ok ( _) | Err ( gix_diff:: tree:: Error :: Cancelled ) => Ok ( recorder. change . map ( Into :: into ) ) ,
513641 Err ( error) => Err ( Error :: DiffTree ( error) ) ,
514642 }
515643}
516644
645+ #[ allow( clippy:: too_many_arguments) ]
646+ fn tree_diff_with_rewrites_at_file_path (
647+ odb : impl gix_object:: Find + gix_object:: FindHeader ,
648+ file_path : & BStr ,
649+ stats : & mut Statistics ,
650+ state : & mut gix_diff:: tree:: State ,
651+ resource_cache : & mut gix_diff:: blob:: Platform ,
652+ parent_tree_iter : gix_object:: TreeRefIter < ' _ > ,
653+ tree_iter : gix_object:: TreeRefIter < ' _ > ,
654+ rewrites : gix_diff:: Rewrites ,
655+ ) -> Result < Option < TreeDiffChange > , Error > {
656+ let mut change: Option < gix_diff:: tree_with_rewrites:: Change > = None ;
657+
658+ let options: gix_diff:: tree_with_rewrites:: Options = gix_diff:: tree_with_rewrites:: Options {
659+ location : Some ( gix_diff:: tree:: recorder:: Location :: Path ) ,
660+ rewrites : Some ( rewrites) ,
661+ } ;
662+ let result = gix_diff:: tree_with_rewrites (
663+ parent_tree_iter,
664+ tree_iter,
665+ resource_cache,
666+ state,
667+ & odb,
668+ |change_ref| -> Result < _ , std:: convert:: Infallible > {
669+ if change_ref. location ( ) == file_path {
670+ change = Some ( change_ref. into_owned ( ) ) ;
671+ Ok ( gix_diff:: tree_with_rewrites:: Action :: Cancel )
672+ } else {
673+ Ok ( gix_diff:: tree_with_rewrites:: Action :: Continue )
674+ }
675+ } ,
676+ options,
677+ ) ;
678+ stats. trees_diffed_with_rewrites += 1 ;
679+
680+ match result {
681+ Ok ( _) | Err ( gix_diff:: tree_with_rewrites:: Error :: Diff ( gix_diff:: tree:: Error :: Cancelled ) ) => {
682+ Ok ( change. map ( Into :: into) )
683+ }
684+ Err ( error) => Err ( Error :: DiffTreeWithRewrites ( error) ) ,
685+ }
686+ }
687+
688+ #[ allow( clippy:: too_many_arguments) ]
517689fn blob_changes (
518690 odb : impl gix_object:: Find + gix_object:: FindHeader ,
519691 resource_cache : & mut gix_diff:: blob:: Platform ,
520692 oid : ObjectId ,
521693 previous_oid : ObjectId ,
522694 file_path : & BStr ,
695+ previous_file_path : & BStr ,
523696 diff_algorithm : gix_diff:: blob:: Algorithm ,
524697 stats : & mut Statistics ,
525698) -> Result < Vec < Change > , Error > {
@@ -579,7 +752,7 @@ fn blob_changes(
579752 resource_cache. set_resource (
580753 previous_oid,
581754 gix_object:: tree:: EntryKind :: Blob ,
582- file_path ,
755+ previous_file_path ,
583756 gix_diff:: blob:: ResourceKind :: OldOrSource ,
584757 & odb,
585758 ) ?;
0 commit comments