diff --git a/merge-ort.c b/merge-ort.c index 9e85a5e60ae69f..2b837a58c3a6f8 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 f14c0fb30e1bf2..e18d5a227d54f7 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: +# 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