@@ -72,19 +72,21 @@ pub fn file(
7272) -> Result < Outcome , Error > {
7373 let _span = gix_trace:: coarse!( "gix_blame::file()" , ?file_path, ?suspect) ;
7474
75+ let mut current_file_path: BString = file_path. into ( ) ;
76+
7577 let mut stats = Statistics :: default ( ) ;
7678 let ( mut buf, mut buf2, mut buf3) = ( Vec :: new ( ) , Vec :: new ( ) , Vec :: new ( ) ) ;
7779 let blamed_file_entry_id = find_path_entry_in_commit (
7880 & odb,
7981 & suspect,
80- file_path ,
82+ current_file_path . as_ref ( ) ,
8183 cache. as_ref ( ) ,
8284 & mut buf,
8385 & mut buf2,
8486 & mut stats,
8587 ) ?
8688 . ok_or_else ( || Error :: FileMissing {
87- file_path : file_path . to_owned ( ) ,
89+ file_path : current_file_path . to_owned ( ) ,
8890 commit_id : suspect,
8991 } ) ?;
9092 let blamed_file_blob = odb. find_blob ( & blamed_file_entry_id, & mut buf) ?. data . to_vec ( ) ;
@@ -102,6 +104,7 @@ pub fn file(
102104 hunks_to_blame. push ( UnblamedHunk {
103105 range_in_blamed_file : range. clone ( ) ,
104106 suspects : [ ( suspect, range) ] . into ( ) ,
107+ source_file_name : None ,
105108 } ) ;
106109 }
107110
@@ -165,7 +168,7 @@ pub fn file(
165168 entry = find_path_entry_in_commit (
166169 & odb,
167170 & suspect,
168- file_path ,
171+ current_file_path . as_ref ( ) ,
169172 cache. as_ref ( ) ,
170173 & mut buf,
171174 & mut buf2,
@@ -216,7 +219,7 @@ pub fn file(
216219 if let Some ( parent_entry_id) = find_path_entry_in_commit (
217220 & odb,
218221 parent_id,
219- file_path ,
222+ current_file_path . as_ref ( ) ,
220223 cache. as_ref ( ) ,
221224 & mut buf,
222225 & mut buf2,
@@ -239,12 +242,13 @@ pub fn file(
239242 queue. insert ( parent_commit_time, parent_id) ;
240243 let changes_for_file_path = tree_diff_at_file_path (
241244 & odb,
242- file_path ,
245+ current_file_path . as_ref ( ) ,
243246 suspect,
244247 parent_id,
245248 cache. as_ref ( ) ,
246249 & mut stats,
247250 & mut diff_state,
251+ resource_cache,
248252 & mut buf,
249253 & mut buf2,
250254 & mut buf3,
@@ -263,7 +267,7 @@ pub fn file(
263267 } ;
264268
265269 match modification {
266- gix_diff :: tree :: recorder :: Change :: Addition { .. } => {
270+ TreeDiffChange :: Addition => {
267271 if more_than_one_parent {
268272 // Do nothing under the assumption that this always (or almost always)
269273 // implies that the file comes from a different parent, compared to which
@@ -272,20 +276,44 @@ pub fn file(
272276 break ' outer;
273277 }
274278 }
275- gix_diff :: tree :: recorder :: Change :: Deletion { .. } => {
279+ TreeDiffChange :: Deletion => {
276280 unreachable ! ( "We already found file_path in suspect^{{tree}}, so it can't be deleted" )
277281 }
278- gix_diff :: tree :: recorder :: Change :: Modification { previous_oid , oid , .. } => {
282+ TreeDiffChange :: Modification { previous_id , id } => {
279283 let changes = blob_changes (
280284 & odb,
281285 resource_cache,
282- oid,
283- previous_oid,
284- file_path,
286+ id,
287+ previous_id,
288+ current_file_path. as_ref ( ) ,
289+ options. diff_algorithm ,
290+ & mut stats,
291+ ) ?;
292+ hunks_to_blame = process_changes ( hunks_to_blame, changes, suspect, parent_id) ;
293+ }
294+ TreeDiffChange :: Rewrite {
295+ source_location,
296+ source_id,
297+ id,
298+ } => {
299+ let changes = blob_changes (
300+ & odb,
301+ resource_cache,
302+ id,
303+ source_id,
304+ current_file_path. as_ref ( ) ,
285305 options. diff_algorithm ,
286306 & mut stats,
287307 ) ?;
288308 hunks_to_blame = process_changes ( hunks_to_blame, changes, suspect, parent_id) ;
309+
310+ for hunk in hunks_to_blame. iter_mut ( ) {
311+ if hunk. has_suspect ( & parent_id) {
312+ hunk. source_file_name = Some ( source_location. clone ( ) ) ;
313+ }
314+ }
315+
316+ current_file_path = source_location;
289317 }
290318 }
291319 }
@@ -382,6 +410,7 @@ fn coalesce_blame_entries(lines_blamed: Vec<BlameEntry>) -> Vec<BlameEntry> {
382410 len : NonZeroU32 :: new ( ( current_source_range. end - previous_source_range. start ) as u32 )
383411 . expect ( "BUG: hunks are never zero-sized" ) ,
384412 commit_id : previous_entry. commit_id ,
413+ source_file_name : previous_entry. source_file_name . clone ( ) ,
385414 } ;
386415
387416 acc. pop ( ) ;
@@ -399,6 +428,57 @@ fn coalesce_blame_entries(lines_blamed: Vec<BlameEntry>) -> Vec<BlameEntry> {
399428 } )
400429}
401430
431+ enum TreeDiffChange {
432+ Addition ,
433+ Deletion ,
434+ Modification {
435+ previous_id : ObjectId ,
436+ id : ObjectId ,
437+ } ,
438+ Rewrite {
439+ source_location : BString ,
440+ source_id : ObjectId ,
441+ id : ObjectId ,
442+ } ,
443+ }
444+
445+ impl From < gix_diff:: tree:: recorder:: Change > for TreeDiffChange {
446+ fn from ( value : gix_diff:: tree:: recorder:: Change ) -> Self {
447+ use gix_diff:: tree:: recorder:: Change ;
448+
449+ match value {
450+ Change :: Addition { .. } => Self :: Addition ,
451+ Change :: Deletion { .. } => Self :: Deletion ,
452+ Change :: Modification { previous_oid, oid, .. } => Self :: Modification {
453+ previous_id : previous_oid,
454+ id : oid,
455+ } ,
456+ }
457+ }
458+ }
459+
460+ impl From < gix_diff:: tree_with_rewrites:: Change > for TreeDiffChange {
461+ fn from ( value : gix_diff:: tree_with_rewrites:: Change ) -> Self {
462+ use gix_diff:: tree_with_rewrites:: Change ;
463+
464+ match value {
465+ Change :: Addition { .. } => Self :: Addition ,
466+ Change :: Deletion { .. } => Self :: Deletion ,
467+ Change :: Modification { previous_id, id, .. } => Self :: Modification { previous_id, id } ,
468+ Change :: Rewrite {
469+ source_location,
470+ source_id,
471+ id,
472+ ..
473+ } => Self :: Rewrite {
474+ source_location,
475+ source_id,
476+ id,
477+ } ,
478+ }
479+ }
480+ }
481+
402482#[ allow( clippy:: too_many_arguments) ]
403483fn tree_diff_at_file_path (
404484 odb : impl gix_object:: Find + gix_object:: FindHeader ,
@@ -408,10 +488,11 @@ fn tree_diff_at_file_path(
408488 cache : Option < & gix_commitgraph:: Graph > ,
409489 stats : & mut Statistics ,
410490 state : & mut gix_diff:: tree:: State ,
491+ resource_cache : & mut gix_diff:: blob:: Platform ,
411492 commit_buf : & mut Vec < u8 > ,
412493 lhs_tree_buf : & mut Vec < u8 > ,
413494 rhs_tree_buf : & mut Vec < u8 > ,
414- ) -> Result < Option < gix_diff :: tree :: recorder :: Change > , Error > {
495+ ) -> Result < Option < TreeDiffChange > , Error > {
415496 let parent_tree_id = find_commit ( cache, & odb, & parent_id, commit_buf) ?. tree_id ( ) ?;
416497
417498 let parent_tree_iter = odb. find_tree_iter ( & parent_tree_id, lhs_tree_buf) ?;
@@ -422,6 +503,37 @@ fn tree_diff_at_file_path(
422503 let tree_iter = odb. find_tree_iter ( & tree_id, rhs_tree_buf) ?;
423504 stats. trees_decoded += 1 ;
424505
506+ let result = tree_diff_without_rewrites_at_file_path ( & odb, file_path, stats, state, parent_tree_iter, tree_iter) ?;
507+
508+ // Here, we follow git’s behaviour. We return when we’ve found a `Modification`. We try a
509+ // second time with rename tracking when the change is either an `Addition` or a `Deletion`
510+ // because those can turn out to have been a `Rewrite`.
511+ if matches ! ( result, Some ( TreeDiffChange :: Modification { .. } ) ) {
512+ return Ok ( result) ;
513+ }
514+
515+ let result = tree_diff_with_rewrites_at_file_path (
516+ & odb,
517+ file_path,
518+ stats,
519+ state,
520+ resource_cache,
521+ parent_tree_iter,
522+ tree_iter,
523+ ) ?;
524+
525+ Ok ( result)
526+ }
527+
528+ #[ allow( clippy:: too_many_arguments) ]
529+ fn tree_diff_without_rewrites_at_file_path (
530+ odb : impl gix_object:: Find + gix_object:: FindHeader ,
531+ file_path : & BStr ,
532+ stats : & mut Statistics ,
533+ state : & mut gix_diff:: tree:: State ,
534+ parent_tree_iter : gix_object:: TreeRefIter < ' _ > ,
535+ tree_iter : gix_object:: TreeRefIter < ' _ > ,
536+ ) -> Result < Option < TreeDiffChange > , Error > {
425537 struct FindChangeToPath {
426538 inner : gix_diff:: tree:: Recorder ,
427539 interesting_path : BString ,
@@ -509,11 +621,53 @@ fn tree_diff_at_file_path(
509621 stats. trees_diffed += 1 ;
510622
511623 match result {
512- Ok ( _) | Err ( gix_diff:: tree:: Error :: Cancelled ) => Ok ( recorder. change ) ,
624+ Ok ( _) | Err ( gix_diff:: tree:: Error :: Cancelled ) => Ok ( recorder. change . map ( std :: convert :: Into :: into ) ) ,
513625 Err ( error) => Err ( Error :: DiffTree ( error) ) ,
514626 }
515627}
516628
629+ #[ allow( clippy:: too_many_arguments) ]
630+ fn tree_diff_with_rewrites_at_file_path (
631+ odb : impl gix_object:: Find + gix_object:: FindHeader ,
632+ file_path : & BStr ,
633+ stats : & mut Statistics ,
634+ state : & mut gix_diff:: tree:: State ,
635+ resource_cache : & mut gix_diff:: blob:: Platform ,
636+ parent_tree_iter : gix_object:: TreeRefIter < ' _ > ,
637+ tree_iter : gix_object:: TreeRefIter < ' _ > ,
638+ ) -> Result < Option < TreeDiffChange > , Error > {
639+ let mut change: Option < gix_diff:: tree_with_rewrites:: Change > = None ;
640+
641+ let options: gix_diff:: tree_with_rewrites:: Options = gix_diff:: tree_with_rewrites:: Options {
642+ location : Some ( gix_diff:: tree:: recorder:: Location :: Path ) ,
643+ rewrites : Some ( gix_diff:: Rewrites :: default ( ) ) ,
644+ } ;
645+ let result = gix_diff:: tree_with_rewrites (
646+ parent_tree_iter,
647+ tree_iter,
648+ resource_cache,
649+ state,
650+ & odb,
651+ |change_ref| -> Result < _ , std:: convert:: Infallible > {
652+ if change_ref. location ( ) == file_path {
653+ change = Some ( change_ref. into_owned ( ) ) ;
654+ Ok ( gix_diff:: tree_with_rewrites:: Action :: Cancel )
655+ } else {
656+ Ok ( gix_diff:: tree_with_rewrites:: Action :: Continue )
657+ }
658+ } ,
659+ options,
660+ ) ;
661+ stats. trees_diffed_with_rewrites += 1 ;
662+
663+ match result {
664+ Ok ( _) | Err ( gix_diff:: tree_with_rewrites:: Error :: Diff ( gix_diff:: tree:: Error :: Cancelled ) ) => {
665+ Ok ( change. map ( std:: convert:: Into :: into) )
666+ }
667+ Err ( error) => Err ( Error :: DiffTreeWithRewrites ( error) ) ,
668+ }
669+ }
670+
517671fn blob_changes (
518672 odb : impl gix_object:: Find + gix_object:: FindHeader ,
519673 resource_cache : & mut gix_diff:: blob:: Platform ,
0 commit comments