Skip to content

Commit bce3857

Browse files
committed
merge-ort: handle cached rename & trivial resolution interaction better
Back in commit a562d90 (merge-ort: fix failing merges in special corner case, 2025-11-03), we hit a rename assertion due to a trivial directory resolution affecting the parent of a cached rename. Since the path didn't need to be considered, we side-stepped it with if (!newinfo) continue; in process_renames(). We have since run into a case in production where a trivial resolution of a file affects the direct target of a cached rename rather than a parent directory of it. Add a testcase demonstrating this additional case. Now, if we were to follow the lead of commit a562d90, we could resolve this alternate case with an extra condition on the above if: if (!newinfo || newinfo->merged.clean) continue; However, if we had done that earlier, we would have made 979ee83 (merge-ort: fix corner case recursive submodule/directory conflict handling, 2025-12-29) harder to find and fix, and this particular position for this condition isn't actually at the root of the issue but downstream from it. Instead, let's rip out this if-check from a562d90 and put in an alternative that more directly addresses trivially resolved paths that happen to be cached renames or parent directories thereof, which is a better fix for the original testcase and which also solves the newly added testcase as well. Signed-off-by: Elijah Newren <newren@gmail.com>
1 parent e895506 commit bce3857

2 files changed

Lines changed: 82 additions & 26 deletions

File tree

merge-ort.c

Lines changed: 22 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2953,32 +2953,6 @@ static int process_renames(struct merge_options *opt,
29532953
if (!oldinfo || oldinfo->merged.clean)
29542954
continue;
29552955

2956-
/*
2957-
* Rename caching from a previous commit might give us an
2958-
* irrelevant rename for the current commit.
2959-
*
2960-
* Imagine:
2961-
* foo/A -> bar/A
2962-
* was a cached rename for the upstream side from the
2963-
* previous commit (without the directories being renamed),
2964-
* but the next commit being replayed
2965-
* * does NOT add or delete files
2966-
* * does NOT have directory renames
2967-
* * does NOT modify any files under bar/
2968-
* * does NOT modify foo/A
2969-
* * DOES modify other files under foo/ (otherwise the
2970-
* !oldinfo check above would have already exited for
2971-
* us)
2972-
* In such a case, our trivial directory resolution will
2973-
* have already merged bar/, and our attempt to process
2974-
* the cached
2975-
* foo/A -> bar/A
2976-
* would be counterproductive, and lack the necessary
2977-
* information anyway. Skip such renames.
2978-
*/
2979-
if (!newinfo)
2980-
continue;
2981-
29822956
/*
29832957
* diff_filepairs have copies of pathnames, thus we have to
29842958
* use standard 'strcmp()' (negated) instead of '=='.
@@ -3329,6 +3303,28 @@ static void use_cached_pairs(struct merge_options *opt,
33293303
if (!new_name)
33303304
new_name = old_name;
33313305

3306+
/*
3307+
* If this is a rename and the target path is either
3308+
* absent from opt->priv->paths (because a parent
3309+
* directory was trivially resolved) or already cleanly
3310+
* resolved (e.g. all three sides agree on its content),
3311+
* the cached rename is irrelevant for this commit.
3312+
* Skip it here rather than in process_renames() to
3313+
* preserve VERIFY_CI(newinfo)'s ability to catch bugs
3314+
* for non-cached renames (see 979ee83e8a90 (merge-ort:
3315+
* fix corner case recursive submodule/directory conflict
3316+
* handling, 2025-12-29) for an example of a bug that
3317+
* assertion caught). The rename remains in cached_pairs
3318+
* for use in subsequent commits.
3319+
*/
3320+
if (entry->value) {
3321+
struct merged_info *mi;
3322+
3323+
mi = strmap_get(&opt->priv->paths, new_name);
3324+
if (!mi || mi->clean)
3325+
continue;
3326+
}
3327+
33323328
/*
33333329
* cached_pairs has *copies* of old_name and new_name,
33343330
* because it has to persist across merges. Since

t/t6429-merge-sequence-rename-caching.sh

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -846,4 +846,64 @@ test_expect_success 'rename a file, use it on first pick, but irrelevant on seco
846846
)
847847
'
848848

849+
#
850+
# In the following testcase:
851+
# Base: subdir/file_1
852+
# Upstream: file_1 (renamed from subdir/file)
853+
# Topic_1: subdir/file_2 (modified subdir/file)
854+
# Topic_2: subdir/file_2, file_2 (added another "file" with same contents)
855+
# Topic_3: file_2 (deleted subdir/file)
856+
#
857+
#
858+
# This testcase presents no problems for git traditionally, but the fact that
859+
# subdir/file -> file
860+
# gets cached after the first pick presents a problem for the third commit
861+
# to be replayed, because file has contents file_2 on all three sides and
862+
# is thus trivially resolved early. The point of renames is to allow us to
863+
# three-way merge contents across multiple filenames, but if the target is
864+
# already resolved, we risk throwing an assertion. Verify that the code
865+
# correctly drops the irrelevant rename in order to avoid hitting that
866+
# assertion.
867+
#
868+
test_expect_success 'cached rename does not assert on trivially clean target' '
869+
git init cached-rename-trivially-clean-target &&
870+
(
871+
cd cached-rename-trivially-clean-target &&
872+
873+
mkdir subdir &&
874+
printf "%s\n" 1 2 3 >subdir/file &&
875+
git add subdir/file &&
876+
git commit -m orig &&
877+
878+
git branch upstream &&
879+
git branch topic &&
880+
881+
git switch upstream &&
882+
git mv subdir/file file &&
883+
git commit -m "rename subdir/file to file" &&
884+
885+
git switch topic &&
886+
887+
echo 4 >>subdir/file &&
888+
git add subdir/file &&
889+
git commit -m "modify subdir/file" &&
890+
891+
cp subdir/file file &&
892+
git add file &&
893+
git commit -m "copy subdir/file to file" &&
894+
895+
git rm subdir/file &&
896+
git commit -m "delete subdir/file" &&
897+
898+
git switch upstream &&
899+
git replay --onto HEAD upstream..topic &&
900+
git checkout topic &&
901+
902+
git ls-files >tracked-files &&
903+
test_line_count = 1 tracked-files &&
904+
printf "%s\n" 1 2 3 4 >expect &&
905+
test_cmp expect file
906+
)
907+
'
908+
849909
test_done

0 commit comments

Comments
 (0)