Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion merge-ort.c
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
86 changes: 86 additions & 0 deletions t/t6422-merge-rename-corner-cases.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading