aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJunio C Hamano <gitster@pobox.com>2026-01-08 16:40:12 +0900
committerJunio C Hamano <gitster@pobox.com>2026-01-08 16:40:12 +0900
commit2db806d817e2a0cfbd1c79cb67a84642f5544939 (patch)
treecf870cfc73ebb194410410db37084e2cf97bf68c
parent512351f2a84388d223ee6bb763ce5e6c5965f1b8 (diff)
parent979ee83e8a908f920d097c10cea5f8857e10898f (diff)
downloadgit-2db806d817e2a0cfbd1c79cb67a84642f5544939.tar.xz
Merge branch 'en/ort-recursive-d-f-conflict-fix'
The ort merge machinery hit an assertion failure in a history with criss-cross merges renamed a directory and a non-directory, which has been corrected. * en/ort-recursive-d-f-conflict-fix: merge-ort: fix corner case recursive submodule/directory conflict handling
-rw-r--r--merge-ort.c35
-rwxr-xr-xt/t6422-merge-rename-corner-cases.sh86
2 files changed, 120 insertions, 1 deletions
diff --git a/merge-ort.c b/merge-ort.c
index 9e85a5e60a..2b837a58c3 100644
--- a/merge-ort.c
+++ b/merge-ort.c
@@ -1502,11 +1502,44 @@ static void resolve_trivial_directory_merge(struct conflict_info *ci, int side)
VERIFY_CI(ci);
assert((side == 1 && ci->match_mask == 5) ||
(side == 2 && ci->match_mask == 3));
+
+ /*
+ * Since ci->stages[0] matches ci->stages[3-side], resolve merge in
+ * favor of ci->stages[side].
+ */
oidcpy(&ci->merged.result.oid, &ci->stages[side].oid);
ci->merged.result.mode = ci->stages[side].mode;
ci->merged.is_null = is_null_oid(&ci->stages[side].oid);
+
+ /*
+ * Because we resolved in favor of "side", we are no longer
+ * considering the paths which matched (i.e. had the same hash) any
+ * more. Strip the matching paths from both dirmask & filemask.
+ * Another consequence of merging in favor of side is that we can no
+ * longer have a directory/file conflict either..but there's a slight
+ * nuance we consider before clearing it.
+ *
+ * In most cases, resolving in favor of the other side means there's
+ * no conflict at all, but if we had a directory/file conflict to
+ * start, and the directory is resolved away, the remaining file could
+ * still be part of a rename. If the remaining file is part of a
+ * rename, then it may also be part of a rename conflict (e.g.
+ * rename/delete or rename/rename(1to2)), so we can't
+ * mark it as a clean merge if we started with a directory/file
+ * conflict and still have a file left.
+ *
+ * In contrast, if we started with a directory/file conflict and
+ * still have a directory left, no file under that directory can be
+ * part of a rename, otherwise we would have had to recurse into the
+ * directory and would have never ended up within
+ * resolve_trivial_directory_merge() for that directory.
+ */
+ ci->dirmask &= (~ci->match_mask);
+ ci->filemask &= (~ci->match_mask);
+ assert(!ci->filemask || !ci->dirmask);
ci->match_mask = 0;
- ci->merged.clean = 1; /* (ci->filemask == 0); */
+ ci->merged.clean = !ci->df_conflict || ci->dirmask;
+ ci->df_conflict = 0;
}
static int handle_deferred_entries(struct merge_options *opt,
diff --git a/t/t6422-merge-rename-corner-cases.sh b/t/t6422-merge-rename-corner-cases.sh
index f14c0fb30e..e18d5a227d 100755
--- a/t/t6422-merge-rename-corner-cases.sh
+++ b/t/t6422-merge-rename-corner-cases.sh
@@ -1439,4 +1439,90 @@ test_expect_success 'rename/rename(1to2) with a binary file' '
)
'
+# Testcase preliminary submodule/directory conflict and submodule rename
+# Commit O: <empty, or additional irrelevant stuff>
+# Commit A1: introduce "folder" (as a tree)
+# Commit B1: introduce "folder" (as a submodule)
+# Commit A2: merge B1 into A1, but keep folder as a tree
+# Commit B2: merge A1 into B1, but keep folder as a submodule
+# Merge A2 & B2
+test_setup_submodule_directory_preliminary_conflict () {
+ git init submodule_directory_preliminary_conflict &&
+ (
+ cd submodule_directory_preliminary_conflict &&
+
+ # Trying to do the A2 and B2 merges above is slightly more
+ # challenging with a local submodule (because checking out
+ # another commit has the submodule in the way). Instead,
+ # first create the commits with the wrong parents but right
+ # trees, in the order A1, A2, B1, B2...
+ #
+ # Then go back and create new A2 & B2 with the correct
+ # parents and the same trees.
+
+ git commit --allow-empty -m orig &&
+
+ git branch A &&
+ git branch B &&
+
+ git checkout B &&
+ mkdir folder &&
+ echo A>folder/A &&
+ echo B>folder/B &&
+ echo C>folder/C &&
+ echo D>folder/D &&
+ echo E>folder/E &&
+ git add folder &&
+ git commit -m B1 &&
+
+ git commit --allow-empty -m B2 &&
+
+ git checkout A &&
+ git init folder &&
+ (
+ cd folder &&
+ >Z &&
+ >Y &&
+ git add Z Y &&
+ git commit -m "original submodule commit"
+ ) &&
+ git add folder &&
+ git commit -m A1 &&
+
+ git commit --allow-empty -m A2 &&
+
+ NewA2=$(git commit-tree -p A^ -p B^ -m "Merge B into A" A^{tree}) &&
+ NewB2=$(git commit-tree -p B^ -p A^ -m "Merge A into B" B^{tree}) &&
+ git update-ref refs/heads/A $NewA2 &&
+ git update-ref refs/heads/B $NewB2
+ )
+}
+
+test_expect_success 'submodule/directory preliminary conflict' '
+ test_setup_submodule_directory_preliminary_conflict &&
+ (
+ cd submodule_directory_preliminary_conflict &&
+
+ git checkout A^0 &&
+
+ test_expect_code 1 git merge B^0 &&
+
+ # Make sure the index has the right number of entries
+ git ls-files -s >actual &&
+ test_line_count = 2 actual &&
+
+ # The "folder" as directory should have been resolved away
+ # as part of the merge. The "folder" as submodule got
+ # renamed to "folder~Temporary merge branch 2" in the
+ # virtual merge base, resulting in a
+ # "folder~Temporary merge branch 2" -> "folder"
+ # rename in the outermerge for the submodule, which then
+ # becomes part of a rename/delete conflict (because "folder"
+ # as a submodule was deleted in A2).
+ submod=$(git rev-parse A:folder) &&
+ printf "160000 $submod 1\tfolder\n160000 $submod 2\tfolder\n" >expect &&
+ test_cmp expect actual
+ )
+'
+
test_done