@@ -28,8 +28,8 @@ use lib::{
2828 } ,
2929 git:: {
3030 CherryPickFastOptions , FileMode , GitRunInfo , MaybeZeroOid , NonZeroOid , Repo ,
31- ResolvedReferenceInfo , hydrate_tree , make_empty_tree , process_diff_for_record ,
32- summarize_diff_for_temporary_commit,
31+ ResolvedReferenceInfo , get_changed_paths_between_trees , hydrate_tree , make_empty_tree ,
32+ process_diff_for_record , summarize_diff_for_temporary_commit,
3333 } ,
3434 try_exit_code,
3535 util:: { ExitCode , EyreExitOr } ,
@@ -298,6 +298,41 @@ pub fn split(
298298 }
299299 } ;
300300
301+ // Pre-compute a map of exact renames in the target commit: new_path -> old_path.
302+ // A rename is detected when a path is absent from parent_tree but present in
303+ // target_tree, and another path with the SAME blob OID is absent from target_tree
304+ // but present in parent_tree (same content = exact rename, no content change).
305+ //
306+ // This map is used when extracting a renamed file to correctly restore the
307+ // original path in the remainder tree, preventing cherry-pick conflicts.
308+ let rename_map: HashMap < PathBuf , PathBuf > = {
309+ let changed_paths =
310+ get_changed_paths_between_trees ( & repo, Some ( & parent_tree) , Some ( & target_tree) ) ?;
311+ let mut deleted_by_oid: HashMap < NonZeroOid , PathBuf > = HashMap :: new ( ) ;
312+ let mut added_entries: Vec < ( PathBuf , NonZeroOid ) > = Vec :: new ( ) ;
313+ for path in changed_paths {
314+ let parent_oid = parent_tree. get_oid_for_path ( & path) ?;
315+ let target_oid = target_tree. get_oid_for_path ( & path) ?;
316+ match ( parent_oid, target_oid) {
317+ ( Some ( MaybeZeroOid :: NonZero ( p_oid) ) , None | Some ( MaybeZeroOid :: Zero ) ) => {
318+ deleted_by_oid. insert ( p_oid, path) ;
319+ }
320+ ( None | Some ( MaybeZeroOid :: Zero ) , Some ( MaybeZeroOid :: NonZero ( t_oid) ) ) => {
321+ added_entries. push ( ( path, t_oid) ) ;
322+ }
323+ _ => { }
324+ }
325+ }
326+ added_entries
327+ . into_iter ( )
328+ . filter_map ( |( new_path, blob_oid) | {
329+ deleted_by_oid
330+ . get ( & blob_oid)
331+ . map ( |old_path| ( new_path, old_path. clone ( ) ) )
332+ } )
333+ . collect ( )
334+ } ;
335+
301336 for ( spec, path) in resolved_specs. iter ( ) {
302337 let path = path. as_path ( ) ;
303338
@@ -326,21 +361,50 @@ pub fn split(
326361
327362 let target_entry = target_tree. get_path ( path) ?;
328363 let temp_tree_oid = match ( parent_entry, target_entry, & split_mode) {
329- // added or modified & InsertBefore => add to extracted commit
330- ( None , Some ( commit_entry) , SplitMode :: InsertBefore )
331- | ( Some ( _) , Some ( commit_entry) , SplitMode :: InsertBefore ) => {
364+ // modified & InsertBefore => add to extracted commit (no rename handling needed)
365+ ( Some ( _) , Some ( commit_entry) , SplitMode :: InsertBefore ) => {
332366 remainder_tree. add_or_replace ( & repo, path, & commit_entry) ?
333367 }
334368
369+ // added & InsertBefore => add to extracted commit; for renames, also remove
370+ // the original path (which is present in the parent-based remainder tree)
371+ ( None , Some ( commit_entry) , SplitMode :: InsertBefore ) => {
372+ let oid = remainder_tree. add_or_replace ( & repo, path, & commit_entry) ?;
373+ if let Some ( old_path) = rename_map. get ( path) {
374+ let interim = repo
375+ . find_tree ( oid) ?
376+ . expect ( "hydrated tree should always be findable" ) ;
377+ interim. remove ( & repo, old_path) ?
378+ } else {
379+ oid
380+ }
381+ }
382+
335383 // removed & InsertBefore => remove from remainder commit
336384 ( Some ( _) , None , SplitMode :: InsertBefore ) => {
337385 remainder_tree. remove ( & repo, path) ?
338386 }
339387
340- // added => remove from remainder commit
388+ // added => remove from remainder commit; for renames, also restore the
389+ // original path from the parent tree so the remainder accurately reflects
390+ // only the non-extracted changes
341391 ( None , Some ( _) , SplitMode :: InsertAfter )
342392 | ( None , Some ( _) , SplitMode :: DetachAfter )
343- | ( None , Some ( _) , SplitMode :: Discard ) => remainder_tree. remove ( & repo, path) ?,
393+ | ( None , Some ( _) , SplitMode :: Discard ) => {
394+ let oid = remainder_tree. remove ( & repo, path) ?;
395+ if let Some ( old_path) = rename_map. get ( path) {
396+ if let Some ( old_entry) = parent_tree. get_path ( old_path) ? {
397+ let interim = repo
398+ . find_tree ( oid) ?
399+ . expect ( "hydrated tree should always be findable" ) ;
400+ interim. add_or_replace ( & repo, old_path, & old_entry) ?
401+ } else {
402+ oid
403+ }
404+ } else {
405+ oid
406+ }
407+ }
344408
345409 // deleted or modified => replace w/ parent content in split commit
346410 ( Some ( parent_entry) , _, _) => {
@@ -489,12 +553,16 @@ pub fn split(
489553 } else {
490554 ( & remainder_tree, & target_tree)
491555 } ;
492- let diff = repo. get_diff_between_trees (
556+ let mut diff = repo. get_diff_between_trees (
493557 effects,
494558 Some ( old_tree) ,
495559 new_tree,
496560 0 , // we don't care about the context here
497561 ) ?;
562+ // Enable rename detection so that a pure rename shows as one file
563+ // changed with 0 insertions/deletions rather than two separate
564+ // add/delete entries.
565+ diff. find_similar ( ) ?;
498566
499567 summarize_diff_for_temporary_commit ( & diff) ?
500568 } ;
0 commit comments