mirror of
https://github.com/git/git.git
synced 2026-01-11 13:23:12 +09:00
At GitHub, a few repositories were triggering errors of the form:
git: merge-ort.c:3037: process_renames: Assertion `newinfo && !newinfo->merged.clean' failed.
Aborted (core dumped)
While these may look similar to both
a562d90a350d (merge-ort: fix failing merges in special corner case,
2025-11-03)
and
f6ecb603ff8a (merge-ort: fix directory rename on top of source of other
rename/delete, 2025-08-06)
the cause is different and in this case the problem is not an
over-conservative assertion, but a bug before the assertion where we did
not update all relevant state appropriately.
It sadly took me a really long time to figure out how to get a simple
reproducer for this one. It doesn't really have that many moving parts,
but there are multiple pieces of background information needed to
understand it.
First of all, when we have two files added at the same path, merge-ort
does a two-way merge of those files. If we have two directories added
at the same path, we basically do the same thing (taking the union of
files, and two-way merging files with the same name). But two-way
merging requires components of the same type. We can't merge the
contents of a regular file with a directory, or with a symlink, or with
a submodule. Nor can any of those other types be merged with each
other, e.g. merging a submodule with a directory is a bad idea. When
two paths have the same name but their types do not match, merge-ort is
forced to move one of them to an alternate filename (using the
unique_path() function).
Second, if two commits being merged have more than one merge-base,
merge-ort will merge the merge-bases to create a virtual merge-base, and
use that as the base commit.
Third, one of the really important optimizations in merge-ort is trivial
tree-level resolution (roughly meaning merging trees without recursing
into them). This optimization has some nuance to it that is important
to the current bug, and to understand it, it helps to first look at the
high-level overview of how merge-ort runs; there are basically three
high-level functions that the work is divided between:
collect_merge_info() - walks the top-level trees getting individual
paths of interest
detect_renames() - detect renames between paths in order to match up
paths for three-way merging
process_entries() - does a few things of interest:
* three-way merging of files,
* other special handling (e.g. adjusting paths with conflicting
types to avoid path collisions)
* as it finishes handling all the files within a subdirectory,
writes out a new tree object for that directory
If it were not for renames, we could just always do tree-level merging
whenever the tree on at least one side was unmodified. Unfortunately,
we need to recurse into trees to determine whether there are renames.
However, we can also do tree-level merging so long as there aren't any
*relevant* renames (another merge-ort optimization), which we can
determine without recursing into trees.
We would also be able to do tree-level merging if we somehow apriori
knew what renames existed, by only recursing into the trees which we
could otherwise trivially merge if they contained files involved in
renames. That might not seem useful, because we need to find out the
renames and we have to recurse into trees to do so, but when you find
out that the process_entries() step is more computationally expensive
than the collect_merge_info() step, it yields an interesting strategy:
* run collect_merge_info()
* run detect_renames()
* cache the renames()
* restart -- rerun collect_merge_info(), using the cached renames to
only recurse into the needed trees
* we already have the renames cached so no need to re-detect
* run process_entries() on the reduced list of paths
which was implemented back in 7bee6c100431 (merge-ort: avoid recursing
into directories when we don't need to, 2021-07-16) Crucially, this
restarting only occurs if the number of paths we could skip recursing
into exceeds the number we still need to recurse into by some safety
factor (wanted_factor in handle_deferred_entries()); forgetting this
fact is a great way to repeatedly fail to create a minimal testcase for
several days and go down alternate wrong paths).
Now, I earlier summarized this optimization as "merging trees without
recursing into them", but this optimization does not require that all
three sides of history has a directory at a given path. So long as the
tree on one side matches the tree in the base version, we can decide to
resolve in favor of whatever the other side of history has at that path
-- be it a directory, a file, a submodule, or a symlink. Unfortunately,
the code in question didn't fully realize this, and was written assuming
the base version and both sides would have a directory at the given
path, as can be seen by the "ci->filemask == 0" comment in
resolve_trivial_directory_merge() that was added as part of 7bee6c100431
(merge-ort: avoid recursing into directories when we don't need to,
2021-07-16). A few additional lines of code are needed to handle cases
where we have something other than a directory on the other side of
history.
But, knowing that resolve_trivial_directory_merge() doesn't have
sufficient state updating logic doesn't show us how to trigger a bug
without combining with the other bits of information we provided above.
Here's a relevant testcase:
* branches A & B
* commit A1: adds "folder" as a directory with files tracked under it
* commit B1: adds "folder" as a submodule
* commit A2: merges B1 into A1, keeping "folder" as a directory
(and in fact, with no changes to "folder" since A1), discarding the
submodule
* commit B2: merges A1 into B1, keeping "folder" as a submodule
(and in fact, with no changes to "folder" since B1), discarding the
directory
Here, if we try to merge A2 & B2, the logic proceeds as follows:
* we have multiple merge-bases: A1 & B1. So we have to merge those
to get a virtual merge base.
* due to "folder" as a directory and "folder" as a submodule, the
path collision logic triggers and renames "folder" as a submodule
to "folder~Temporary merge branch 2" so we can keep it alongside
"folder" as a directory.
* we now have a virtual merge base (containing both "folder"
directory and a "folder~Temporary merge branch 2" submodule) and
can now do the outer merge
* in the first step of the outer merge, we attempt to defer recursing
into folder/ as a directory, but find we need to for rename
detection.
* in rename detection, we note that "folder~Temporary merge branch 2"
has the same hash as "folder" as a submodule in B2, which means we
have an exact rename.
* after rename detection, we discover no path in folder/ is needed
for renames, and so we can cache renames and restart.
* after restarting, we avoid recursing into "folder/" and realize we
can resolve it trivially since it hasn't been modified. The
resolution removes "folder/", leaving us only "folder" as a
submodule from commit B2.
* After this point, we should have a rename/delete conflict on
"folder~Temporary merge branch 2" -> "folder", but our marking of
the merge of "folder" as clean broke our ability to handle that and
in fact triggers an assertion in process_renames().
When there was a df_conflict (directory/"file" conflict, where "file"
could be submodule or regular file or symlink), ensure
resolve_trivial_directory_merge() handles it properly. In particular:
* do not pre-emptively mark the path as cleanly merged if the
remaining path is a file; allow it to be processed in
process_entries() later to determine if it was clean
* clear the parts of dirmask or filemask corresponding to the matching
sides of history, since we are resolving those away
* clear the df_conflict bit afterwards; since we cleared away the two
matching sides and only have one side left, that one side can't
have a directory/file conflict with itself.
Also add the above minimal testcase showcasing this bug to t6422, **with
a sufficient number of paths under the folder/ directory to actually
trigger it**. (I wish I could have all those days back from all the
wrong paths I went down due to not having enough files under that
directory...)
I know this commit has a very high ratio of lines in the commit message
to lines of comments, and a relatively high ratio of comments to actual
code, but given how long it took me to track down, on the off chance
that we ever need to further modify this logic, I wanted it thoroughly
documented for future me and for whatever other poor soul might end up
needing to read this commit message.
Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
1529 lines
35 KiB
Bash
Executable File
1529 lines
35 KiB
Bash
Executable File
#!/bin/sh
|
|
|
|
test_description="recursive merge corner cases w/ renames but not criss-crosses"
|
|
# t6036 has corner cases that involve both criss-cross merges and renames
|
|
|
|
GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
|
|
export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
|
|
|
|
. ./test-lib.sh
|
|
|
|
test_setup_rename_delete_untracked () {
|
|
git init rename-delete-untracked &&
|
|
(
|
|
cd rename-delete-untracked &&
|
|
|
|
echo "A pretty inscription" >ring &&
|
|
git add ring &&
|
|
test_tick &&
|
|
git commit -m beginning &&
|
|
|
|
git branch people &&
|
|
git checkout -b rename-the-ring &&
|
|
git mv ring one-ring-to-rule-them-all &&
|
|
test_tick &&
|
|
git commit -m fullname &&
|
|
|
|
git checkout people &&
|
|
git rm ring &&
|
|
echo gollum >owner &&
|
|
git add owner &&
|
|
test_tick &&
|
|
git commit -m track-people-instead-of-objects &&
|
|
echo "Myyy PRECIOUSSS" >ring
|
|
)
|
|
}
|
|
|
|
test_expect_success "Does git preserve Gollum's precious artifact?" '
|
|
test_setup_rename_delete_untracked &&
|
|
(
|
|
cd rename-delete-untracked &&
|
|
|
|
test_must_fail git merge -s recursive rename-the-ring &&
|
|
|
|
# Make sure git did not delete an untracked file
|
|
test_path_is_file ring
|
|
)
|
|
'
|
|
|
|
# Testcase setup for rename/modify/add-source:
|
|
# Commit A: new file: a
|
|
# Commit B: modify a slightly
|
|
# Commit C: rename a->b, add completely different a
|
|
#
|
|
# We should be able to merge B & C cleanly
|
|
|
|
test_setup_rename_modify_add_source () {
|
|
git init rename-modify-add-source &&
|
|
(
|
|
cd rename-modify-add-source &&
|
|
|
|
printf "1\n2\n3\n4\n5\n6\n7\n" >a &&
|
|
git add a &&
|
|
git commit -m A &&
|
|
git tag A &&
|
|
|
|
git checkout -b B A &&
|
|
echo 8 >>a &&
|
|
git add a &&
|
|
git commit -m B &&
|
|
|
|
git checkout -b C A &&
|
|
git mv a b &&
|
|
echo something completely different >a &&
|
|
git add a &&
|
|
git commit -m C
|
|
)
|
|
}
|
|
|
|
test_expect_failure 'rename/modify/add-source conflict resolvable' '
|
|
test_setup_rename_modify_add_source &&
|
|
(
|
|
cd rename-modify-add-source &&
|
|
|
|
git checkout B^0 &&
|
|
|
|
git merge -s recursive C^0 &&
|
|
|
|
git rev-parse >expect \
|
|
B:a C:a &&
|
|
git rev-parse >actual \
|
|
b c &&
|
|
test_cmp expect actual
|
|
)
|
|
'
|
|
|
|
test_setup_break_detection_1 () {
|
|
git init break-detection-1 &&
|
|
(
|
|
cd break-detection-1 &&
|
|
|
|
printf "1\n2\n3\n4\n5\n" >a &&
|
|
echo foo >b &&
|
|
git add a b &&
|
|
git commit -m A &&
|
|
git tag A &&
|
|
|
|
git checkout -b B A &&
|
|
git mv a c &&
|
|
echo "Completely different content" >a &&
|
|
git add a &&
|
|
git commit -m B &&
|
|
|
|
git checkout -b C A &&
|
|
echo 6 >>a &&
|
|
git add a &&
|
|
git commit -m C
|
|
)
|
|
}
|
|
|
|
test_expect_failure 'conflict caused if rename not detected' '
|
|
test_setup_break_detection_1 &&
|
|
(
|
|
cd break-detection-1 &&
|
|
|
|
git checkout -q C^0 &&
|
|
git merge -s recursive B^0 &&
|
|
|
|
git ls-files -s >out &&
|
|
test_line_count = 3 out &&
|
|
git ls-files -u >out &&
|
|
test_line_count = 0 out &&
|
|
git ls-files -o >out &&
|
|
test_line_count = 1 out &&
|
|
|
|
test_line_count = 6 c &&
|
|
git rev-parse >expect \
|
|
B:a A:b &&
|
|
git rev-parse >actual \
|
|
:0:a :0:b &&
|
|
test_cmp expect actual
|
|
)
|
|
'
|
|
|
|
test_setup_break_detection_2 () {
|
|
git init break-detection-2 &&
|
|
(
|
|
cd break-detection-2 &&
|
|
|
|
printf "1\n2\n3\n4\n5\n" >a &&
|
|
echo foo >b &&
|
|
git add a b &&
|
|
git commit -m A &&
|
|
git tag A &&
|
|
|
|
git checkout -b D A &&
|
|
echo 7 >>a &&
|
|
git add a &&
|
|
git mv a c &&
|
|
echo "Completely different content" >a &&
|
|
git add a &&
|
|
git commit -m D &&
|
|
|
|
git checkout -b E A &&
|
|
git rm a &&
|
|
echo "Completely different content" >>a &&
|
|
git add a &&
|
|
git commit -m E
|
|
)
|
|
}
|
|
|
|
test_expect_failure 'missed conflict if rename not detected' '
|
|
test_setup_break_detection_2 &&
|
|
(
|
|
cd break-detection-2 &&
|
|
|
|
git checkout -q E^0 &&
|
|
test_must_fail git merge -s recursive D^0
|
|
)
|
|
'
|
|
|
|
# Tests for undetected rename/add-source causing a file to erroneously be
|
|
# deleted (and for mishandled rename/rename(1to1) causing the same issue).
|
|
#
|
|
# This test uses a rename/rename(1to1)+add-source conflict (1to1 means the
|
|
# same file is renamed on both sides to the same thing; it should trigger
|
|
# the 1to2 logic, which it would do if the add-source didn't cause issues
|
|
# for git's rename detection):
|
|
# Commit A: new file: a
|
|
# Commit B: rename a->b
|
|
# Commit C: rename a->b, add unrelated a
|
|
|
|
test_setup_break_detection_3 () {
|
|
git init break-detection-3 &&
|
|
(
|
|
cd break-detection-3 &&
|
|
|
|
printf "1\n2\n3\n4\n5\n" >a &&
|
|
git add a &&
|
|
git commit -m A &&
|
|
git tag A &&
|
|
|
|
git checkout -b B A &&
|
|
git mv a b &&
|
|
git commit -m B &&
|
|
|
|
git checkout -b C A &&
|
|
git mv a b &&
|
|
echo foobar >a &&
|
|
git add a &&
|
|
git commit -m C
|
|
)
|
|
}
|
|
|
|
test_expect_failure 'detect rename/add-source and preserve all data' '
|
|
test_setup_break_detection_3 &&
|
|
(
|
|
cd break-detection-3 &&
|
|
|
|
git checkout B^0 &&
|
|
|
|
git merge -s recursive C^0 &&
|
|
|
|
git ls-files -s >out &&
|
|
test_line_count = 2 out &&
|
|
git ls-files -u >out &&
|
|
test_line_count = 2 out &&
|
|
git ls-files -o >out &&
|
|
test_line_count = 1 out &&
|
|
|
|
test_path_is_file a &&
|
|
test_path_is_file b &&
|
|
|
|
git rev-parse >expect \
|
|
A:a C:a &&
|
|
git rev-parse >actual \
|
|
:0:b :0:a &&
|
|
test_cmp expect actual
|
|
)
|
|
'
|
|
|
|
test_expect_failure 'detect rename/add-source and preserve all data, merge other way' '
|
|
test_setup_break_detection_3 &&
|
|
(
|
|
cd break-detection-3 &&
|
|
|
|
git checkout C^0 &&
|
|
|
|
git merge -s recursive B^0 &&
|
|
|
|
git ls-files -s >out &&
|
|
test_line_count = 2 out &&
|
|
git ls-files -u >out &&
|
|
test_line_count = 2 out &&
|
|
git ls-files -o >out &&
|
|
test_line_count = 1 out &&
|
|
|
|
test_path_is_file a &&
|
|
test_path_is_file b &&
|
|
|
|
git rev-parse >expect \
|
|
A:a C:a &&
|
|
git rev-parse >actual \
|
|
:0:b :0:a &&
|
|
test_cmp expect actual
|
|
)
|
|
'
|
|
|
|
test_setup_rename_directory () {
|
|
git init rename-directory-$1 &&
|
|
(
|
|
cd rename-directory-$1 &&
|
|
|
|
printf "1\n2\n3\n4\n5\n6\n" >file &&
|
|
git add file &&
|
|
test_tick &&
|
|
git commit -m base &&
|
|
git tag base &&
|
|
|
|
git checkout -b right &&
|
|
echo 7 >>file &&
|
|
mkdir newfile &&
|
|
echo junk >newfile/realfile &&
|
|
git add file newfile/realfile &&
|
|
test_tick &&
|
|
git commit -m right &&
|
|
|
|
git checkout -b left-conflict base &&
|
|
echo 8 >>file &&
|
|
git add file &&
|
|
git mv file newfile &&
|
|
test_tick &&
|
|
git commit -m left &&
|
|
|
|
git checkout -b left-clean base &&
|
|
echo 0 >newfile &&
|
|
cat file >>newfile &&
|
|
git add newfile &&
|
|
git rm file &&
|
|
test_tick &&
|
|
git commit -m left
|
|
)
|
|
}
|
|
|
|
test_expect_success 'rename/directory conflict + clean content merge' '
|
|
test_setup_rename_directory 1a &&
|
|
(
|
|
cd rename-directory-1a &&
|
|
|
|
git checkout left-clean^0 &&
|
|
|
|
test_must_fail git merge -s recursive right^0 &&
|
|
|
|
git ls-files -s >out &&
|
|
test_line_count = 2 out &&
|
|
git ls-files -u >out &&
|
|
test_line_count = 1 out &&
|
|
git ls-files -o >out &&
|
|
test_line_count = 1 out &&
|
|
|
|
echo 0 >expect &&
|
|
git cat-file -p base:file >>expect &&
|
|
echo 7 >>expect &&
|
|
test_cmp expect newfile~HEAD &&
|
|
|
|
test_path_is_file newfile/realfile &&
|
|
test_path_is_file newfile~HEAD
|
|
)
|
|
'
|
|
|
|
test_expect_success 'rename/directory conflict + content merge conflict' '
|
|
test_setup_rename_directory 1b &&
|
|
(
|
|
cd rename-directory-1b &&
|
|
|
|
git reset --hard &&
|
|
git clean -fdqx &&
|
|
|
|
git checkout left-conflict^0 &&
|
|
|
|
test_must_fail git merge -s recursive right^0 &&
|
|
|
|
git ls-files -s >out &&
|
|
test_line_count = 4 out &&
|
|
git ls-files -u >out &&
|
|
test_line_count = 3 out &&
|
|
git ls-files -o >out &&
|
|
test_line_count = 1 out &&
|
|
|
|
git cat-file -p left-conflict:newfile >left &&
|
|
git cat-file -p base:file >base &&
|
|
git cat-file -p right:file >right &&
|
|
test_must_fail git merge-file \
|
|
-L "HEAD:newfile" \
|
|
-L "" \
|
|
-L "right^0:file" \
|
|
left base right &&
|
|
test_cmp left newfile~HEAD &&
|
|
|
|
git rev-parse >expect \
|
|
base:file left-conflict:newfile right:file &&
|
|
git rev-parse >actual \
|
|
:1:newfile~HEAD :2:newfile~HEAD :3:newfile~HEAD &&
|
|
test_cmp expect actual &&
|
|
|
|
test_path_is_file newfile/realfile &&
|
|
test_path_is_file newfile~HEAD
|
|
)
|
|
'
|
|
|
|
test_setup_rename_directory_2 () {
|
|
git init rename-directory-2 &&
|
|
(
|
|
cd rename-directory-2 &&
|
|
|
|
mkdir sub &&
|
|
printf "1\n2\n3\n4\n5\n6\n" >sub/file &&
|
|
git add sub/file &&
|
|
test_tick &&
|
|
git commit -m base &&
|
|
git tag base &&
|
|
|
|
git checkout -b right &&
|
|
echo 7 >>sub/file &&
|
|
git add sub/file &&
|
|
test_tick &&
|
|
git commit -m right &&
|
|
|
|
git checkout -b left base &&
|
|
echo 0 >newfile &&
|
|
cat sub/file >>newfile &&
|
|
git rm sub/file &&
|
|
mv newfile sub &&
|
|
git add sub &&
|
|
test_tick &&
|
|
git commit -m left
|
|
)
|
|
}
|
|
|
|
test_expect_success 'disappearing dir in rename/directory conflict handled' '
|
|
test_setup_rename_directory_2 &&
|
|
(
|
|
cd rename-directory-2 &&
|
|
|
|
git checkout left^0 &&
|
|
|
|
git merge -s recursive right^0 &&
|
|
|
|
git ls-files -s >out &&
|
|
test_line_count = 1 out &&
|
|
git ls-files -u >out &&
|
|
test_line_count = 0 out &&
|
|
git ls-files -o >out &&
|
|
test_line_count = 1 out &&
|
|
|
|
echo 0 >expect &&
|
|
git cat-file -p base:sub/file >>expect &&
|
|
echo 7 >>expect &&
|
|
test_cmp expect sub &&
|
|
|
|
test_path_is_file sub
|
|
)
|
|
'
|
|
|
|
# Test for basic rename/add-dest conflict, with rename needing content merge:
|
|
# Commit O: a
|
|
# Commit A: rename a->b, modifying b too
|
|
# Commit B: modify a, add different b
|
|
|
|
test_setup_rename_with_content_merge_and_add () {
|
|
git init rename-with-content-merge-and-add-$1 &&
|
|
(
|
|
cd rename-with-content-merge-and-add-$1 &&
|
|
|
|
test_seq 1 5 >a &&
|
|
git add a &&
|
|
git commit -m O &&
|
|
git tag O &&
|
|
|
|
git checkout -b A O &&
|
|
git mv a b &&
|
|
test_seq 0 5 >b &&
|
|
git add b &&
|
|
git commit -m A &&
|
|
|
|
git checkout -b B O &&
|
|
echo 6 >>a &&
|
|
echo hello world >b &&
|
|
git add a b &&
|
|
git commit -m B
|
|
)
|
|
}
|
|
|
|
test_expect_success 'handle rename-with-content-merge vs. add' '
|
|
test_setup_rename_with_content_merge_and_add AB &&
|
|
(
|
|
cd rename-with-content-merge-and-add-AB &&
|
|
|
|
git checkout A^0 &&
|
|
|
|
test_must_fail git merge -s recursive B^0 >out &&
|
|
test_grep "CONFLICT (.*/add)" out &&
|
|
|
|
git ls-files -s >out &&
|
|
test_line_count = 2 out &&
|
|
git ls-files -u >out &&
|
|
test_line_count = 2 out &&
|
|
# Also, make sure both unmerged entries are for "b"
|
|
git ls-files -u b >out &&
|
|
test_line_count = 2 out &&
|
|
git ls-files -o >out &&
|
|
test_line_count = 1 out &&
|
|
|
|
test_path_is_missing a &&
|
|
test_path_is_file b &&
|
|
|
|
test_seq 0 6 >tmp &&
|
|
git hash-object tmp >expect &&
|
|
git rev-parse B:b >>expect &&
|
|
git rev-parse >actual \
|
|
:2:b :3:b &&
|
|
test_cmp expect actual &&
|
|
|
|
# Test that the two-way merge in b is as expected
|
|
git cat-file -p :2:b >>ours &&
|
|
git cat-file -p :3:b >>theirs &&
|
|
>empty &&
|
|
test_must_fail git merge-file \
|
|
-L "HEAD" \
|
|
-L "" \
|
|
-L "B^0" \
|
|
ours empty theirs &&
|
|
test_cmp ours b
|
|
)
|
|
'
|
|
|
|
test_expect_success 'handle rename-with-content-merge vs. add, merge other way' '
|
|
test_setup_rename_with_content_merge_and_add BA &&
|
|
(
|
|
cd rename-with-content-merge-and-add-BA &&
|
|
|
|
git reset --hard &&
|
|
git clean -fdx &&
|
|
|
|
git checkout B^0 &&
|
|
|
|
test_must_fail git merge -s recursive A^0 >out &&
|
|
test_grep "CONFLICT (.*/add)" out &&
|
|
|
|
git ls-files -s >out &&
|
|
test_line_count = 2 out &&
|
|
git ls-files -u >out &&
|
|
test_line_count = 2 out &&
|
|
# Also, make sure both unmerged entries are for "b"
|
|
git ls-files -u b >out &&
|
|
test_line_count = 2 out &&
|
|
git ls-files -o >out &&
|
|
test_line_count = 1 out &&
|
|
|
|
test_path_is_missing a &&
|
|
test_path_is_file b &&
|
|
|
|
test_seq 0 6 >tmp &&
|
|
git rev-parse B:b >expect &&
|
|
git hash-object tmp >>expect &&
|
|
git rev-parse >actual \
|
|
:2:b :3:b &&
|
|
test_cmp expect actual &&
|
|
|
|
# Test that the two-way merge in b is as expected
|
|
git cat-file -p :2:b >>ours &&
|
|
git cat-file -p :3:b >>theirs &&
|
|
>empty &&
|
|
test_must_fail git merge-file \
|
|
-L "HEAD" \
|
|
-L "" \
|
|
-L "A^0" \
|
|
ours empty theirs &&
|
|
test_cmp ours b
|
|
)
|
|
'
|
|
|
|
# Test for all kinds of things that can go wrong with rename/rename (2to1):
|
|
# Commit A: new files: a & b
|
|
# Commit B: rename a->c, modify b
|
|
# Commit C: rename b->c, modify a
|
|
#
|
|
# Merging of B & C should NOT be clean. Questions:
|
|
# * Both a & b should be removed by the merge; are they?
|
|
# * The two c's should contain modifications to a & b; do they?
|
|
# * The index should contain two files, both for c; does it?
|
|
# * The working copy should have two files, both of form c~<unique>; does it?
|
|
# * Nothing else should be present. Is anything?
|
|
|
|
test_setup_rename_rename_2to1 () {
|
|
git init rename-rename-2to1 &&
|
|
(
|
|
cd rename-rename-2to1 &&
|
|
|
|
printf "1\n2\n3\n4\n5\n" >a &&
|
|
printf "5\n4\n3\n2\n1\n" >b &&
|
|
git add a b &&
|
|
git commit -m A &&
|
|
git tag A &&
|
|
|
|
git checkout -b B A &&
|
|
git mv a c &&
|
|
echo 0 >>b &&
|
|
git add b &&
|
|
git commit -m B &&
|
|
|
|
git checkout -b C A &&
|
|
git mv b c &&
|
|
echo 6 >>a &&
|
|
git add a &&
|
|
git commit -m C
|
|
)
|
|
}
|
|
|
|
test_expect_success 'handle rename/rename (2to1) conflict correctly' '
|
|
test_setup_rename_rename_2to1 &&
|
|
(
|
|
cd rename-rename-2to1 &&
|
|
|
|
git checkout B^0 &&
|
|
|
|
test_must_fail git merge -s recursive C^0 >out &&
|
|
test_grep "CONFLICT (\(.*\)/\1)" out &&
|
|
|
|
git ls-files -s >out &&
|
|
test_line_count = 2 out &&
|
|
git ls-files -u >out &&
|
|
test_line_count = 2 out &&
|
|
git ls-files -u c >out &&
|
|
test_line_count = 2 out &&
|
|
git ls-files -o >out &&
|
|
test_line_count = 1 out &&
|
|
|
|
test_path_is_missing a &&
|
|
test_path_is_missing b &&
|
|
|
|
git rev-parse >expect \
|
|
C:a B:b &&
|
|
git rev-parse >actual \
|
|
:2:c :3:c &&
|
|
test_cmp expect actual &&
|
|
|
|
# Test that the two-way merge in new_a is as expected
|
|
git cat-file -p :2:c >>ours &&
|
|
git cat-file -p :3:c >>theirs &&
|
|
>empty &&
|
|
test_must_fail git merge-file \
|
|
-L "HEAD" \
|
|
-L "" \
|
|
-L "C^0" \
|
|
ours empty theirs &&
|
|
git hash-object c >actual &&
|
|
git hash-object ours >expect &&
|
|
test_cmp expect actual
|
|
)
|
|
'
|
|
|
|
# Testcase setup for simple rename/rename (1to2) conflict:
|
|
# Commit A: new file: a
|
|
# Commit B: rename a->b
|
|
# Commit C: rename a->c
|
|
test_setup_rename_rename_1to2 () {
|
|
git init rename-rename-1to2 &&
|
|
(
|
|
cd rename-rename-1to2 &&
|
|
|
|
echo stuff >a &&
|
|
git add a &&
|
|
test_tick &&
|
|
git commit -m A &&
|
|
git tag A &&
|
|
|
|
git checkout -b B A &&
|
|
git mv a b &&
|
|
test_tick &&
|
|
git commit -m B &&
|
|
|
|
git checkout -b C A &&
|
|
git mv a c &&
|
|
test_tick &&
|
|
git commit -m C
|
|
)
|
|
}
|
|
|
|
test_expect_success 'merge has correct working tree contents' '
|
|
test_setup_rename_rename_1to2 &&
|
|
(
|
|
cd rename-rename-1to2 &&
|
|
|
|
git checkout C^0 &&
|
|
|
|
test_must_fail git merge -s recursive B^0 &&
|
|
|
|
git ls-files -s >out &&
|
|
test_line_count = 3 out &&
|
|
git ls-files -u >out &&
|
|
test_line_count = 3 out &&
|
|
git ls-files -o >out &&
|
|
test_line_count = 1 out &&
|
|
|
|
test_path_is_missing a &&
|
|
git rev-parse >expect \
|
|
A:a A:a A:a \
|
|
A:a A:a &&
|
|
git rev-parse >actual \
|
|
:1:a :3:b :2:c &&
|
|
git hash-object >>actual \
|
|
b c &&
|
|
test_cmp expect actual
|
|
)
|
|
'
|
|
|
|
# Testcase setup for rename/rename(1to2)/add-source conflict:
|
|
# Commit A: new file: a
|
|
# Commit B: rename a->b
|
|
# Commit C: rename a->c, add completely different a
|
|
#
|
|
# Merging of B & C should NOT be clean; there's a rename/rename conflict
|
|
|
|
test_setup_rename_rename_1to2_add_source_1 () {
|
|
git init rename-rename-1to2-add-source-1 &&
|
|
(
|
|
cd rename-rename-1to2-add-source-1 &&
|
|
|
|
printf "1\n2\n3\n4\n5\n6\n7\n" >a &&
|
|
git add a &&
|
|
git commit -m A &&
|
|
git tag A &&
|
|
|
|
git checkout -b B A &&
|
|
git mv a b &&
|
|
git commit -m B &&
|
|
|
|
git checkout -b C A &&
|
|
git mv a c &&
|
|
echo something completely different >a &&
|
|
git add a &&
|
|
git commit -m C
|
|
)
|
|
}
|
|
|
|
test_expect_failure 'detect conflict with rename/rename(1to2)/add-source merge' '
|
|
test_setup_rename_rename_1to2_add_source_1 &&
|
|
(
|
|
cd rename-rename-1to2-add-source-1 &&
|
|
|
|
git checkout B^0 &&
|
|
|
|
test_must_fail git merge -s recursive C^0 &&
|
|
|
|
git ls-files -s >out &&
|
|
test_line_count = 4 out &&
|
|
git ls-files -o >out &&
|
|
test_line_count = 1 out &&
|
|
|
|
git rev-parse >expect \
|
|
C:a A:a B:b C:C &&
|
|
git rev-parse >actual \
|
|
:3:a :1:a :2:b :3:c &&
|
|
test_cmp expect actual &&
|
|
|
|
test_path_is_file a &&
|
|
test_path_is_file b &&
|
|
test_path_is_file c
|
|
)
|
|
'
|
|
|
|
test_setup_rename_rename_1to2_add_source_2 () {
|
|
git init rename-rename-1to2-add-source-2 &&
|
|
(
|
|
cd rename-rename-1to2-add-source-2 &&
|
|
|
|
>a &&
|
|
git add a &&
|
|
test_tick &&
|
|
git commit -m base &&
|
|
git tag A &&
|
|
|
|
git checkout -b B A &&
|
|
git mv a b &&
|
|
test_tick &&
|
|
git commit -m one &&
|
|
|
|
git checkout -b C A &&
|
|
git mv a b &&
|
|
echo important-info >a &&
|
|
git add a &&
|
|
test_tick &&
|
|
git commit -m two
|
|
)
|
|
}
|
|
|
|
test_expect_failure 'rename/rename/add-source still tracks new a file' '
|
|
test_setup_rename_rename_1to2_add_source_2 &&
|
|
(
|
|
cd rename-rename-1to2-add-source-2 &&
|
|
|
|
git checkout C^0 &&
|
|
git merge -s recursive B^0 &&
|
|
|
|
git ls-files -s >out &&
|
|
test_line_count = 2 out &&
|
|
git ls-files -o >out &&
|
|
test_line_count = 1 out &&
|
|
|
|
git rev-parse >expect \
|
|
C:a A:a &&
|
|
git rev-parse >actual \
|
|
:0:a :0:b &&
|
|
test_cmp expect actual
|
|
)
|
|
'
|
|
|
|
test_setup_rename_rename_1to2_add_dest () {
|
|
git init rename-rename-1to2-add-dest &&
|
|
(
|
|
cd rename-rename-1to2-add-dest &&
|
|
|
|
echo stuff >a &&
|
|
git add a &&
|
|
test_tick &&
|
|
git commit -m base &&
|
|
git tag A &&
|
|
|
|
git checkout -b B A &&
|
|
git mv a b &&
|
|
echo precious-data >c &&
|
|
git add c &&
|
|
test_tick &&
|
|
git commit -m one &&
|
|
|
|
git checkout -b C A &&
|
|
git mv a c &&
|
|
echo important-info >b &&
|
|
git add b &&
|
|
test_tick &&
|
|
git commit -m two
|
|
)
|
|
}
|
|
|
|
test_expect_success 'rename/rename/add-dest merge still knows about conflicting file versions' '
|
|
test_setup_rename_rename_1to2_add_dest &&
|
|
(
|
|
cd rename-rename-1to2-add-dest &&
|
|
|
|
git checkout C^0 &&
|
|
test_must_fail git merge -s recursive B^0 &&
|
|
|
|
git ls-files -s >out &&
|
|
test_line_count = 5 out &&
|
|
git ls-files -u b >out &&
|
|
test_line_count = 2 out &&
|
|
git ls-files -u c >out &&
|
|
test_line_count = 2 out &&
|
|
git ls-files -o >out &&
|
|
test_line_count = 1 out &&
|
|
|
|
git rev-parse >expect \
|
|
A:a C:b B:b C:c B:c &&
|
|
git rev-parse >actual \
|
|
:1:a :2:b :3:b :2:c :3:c &&
|
|
test_cmp expect actual &&
|
|
|
|
# Record some contents for re-doing merges
|
|
git cat-file -p A:a >stuff &&
|
|
git cat-file -p C:b >important_info &&
|
|
git cat-file -p B:c >precious_data &&
|
|
>empty &&
|
|
|
|
# Test the merge in b
|
|
test_must_fail git merge-file \
|
|
-L "HEAD" \
|
|
-L "" \
|
|
-L "B^0" \
|
|
important_info empty stuff &&
|
|
test_cmp important_info b &&
|
|
|
|
# Test the merge in c
|
|
test_must_fail git merge-file \
|
|
-L "HEAD" \
|
|
-L "" \
|
|
-L "B^0" \
|
|
stuff empty precious_data &&
|
|
test_cmp stuff c
|
|
)
|
|
'
|
|
|
|
# Testcase rad, rename/add/delete
|
|
# Commit O: foo
|
|
# Commit A: rm foo, add different bar
|
|
# Commit B: rename foo->bar
|
|
# Expected: CONFLICT (rename/add/delete), two-way merged bar
|
|
|
|
test_setup_rad () {
|
|
git init rad &&
|
|
(
|
|
cd rad &&
|
|
echo "original file" >foo &&
|
|
git add foo &&
|
|
git commit -m "original" &&
|
|
|
|
git branch O &&
|
|
git branch A &&
|
|
git branch B &&
|
|
|
|
git checkout A &&
|
|
git rm foo &&
|
|
echo "different file" >bar &&
|
|
git add bar &&
|
|
git commit -m "Remove foo, add bar" &&
|
|
|
|
git checkout B &&
|
|
git mv foo bar &&
|
|
git commit -m "rename foo to bar"
|
|
)
|
|
}
|
|
|
|
test_expect_success 'rad-check: rename/add/delete conflict' '
|
|
test_setup_rad &&
|
|
(
|
|
cd rad &&
|
|
|
|
git checkout B^0 &&
|
|
test_must_fail git merge -s recursive A^0 >out 2>err &&
|
|
|
|
# Instead of requiring the output to contain one combined line
|
|
# CONFLICT (rename/add/delete)
|
|
# or perhaps two lines:
|
|
# CONFLICT (rename/add): new file collides with rename target
|
|
# CONFLICT (rename/delete): rename source removed on other side
|
|
# and instead of requiring "rename/add" instead of "add/add",
|
|
# be flexible in the type of console output message(s) reported
|
|
# for this particular case; we will be more stringent about the
|
|
# contents of the index and working directory.
|
|
test_grep "CONFLICT (.*/add)" out &&
|
|
test_grep "CONFLICT (rename.*/delete)" out &&
|
|
test_must_be_empty err &&
|
|
|
|
git ls-files -s >file_count &&
|
|
test_line_count = 2 file_count &&
|
|
git ls-files -u >file_count &&
|
|
test_line_count = 2 file_count &&
|
|
git ls-files -o >file_count &&
|
|
test_line_count = 3 file_count &&
|
|
|
|
git rev-parse >actual \
|
|
:2:bar :3:bar &&
|
|
git rev-parse >expect \
|
|
B:bar A:bar &&
|
|
|
|
test_path_is_missing foo &&
|
|
# bar should have two-way merged contents of the different
|
|
# versions of bar; check that content from both sides is
|
|
# present.
|
|
grep original bar &&
|
|
grep different bar
|
|
)
|
|
'
|
|
|
|
# Testcase rrdd, rename/rename(2to1)/delete/delete
|
|
# Commit O: foo, bar
|
|
# Commit A: rename foo->baz, rm bar
|
|
# Commit B: rename bar->baz, rm foo
|
|
# Expected: CONFLICT (rename/rename/delete/delete), two-way merged baz
|
|
|
|
test_setup_rrdd () {
|
|
git init rrdd &&
|
|
(
|
|
cd rrdd &&
|
|
echo foo >foo &&
|
|
echo bar >bar &&
|
|
git add foo bar &&
|
|
git commit -m O &&
|
|
|
|
git branch O &&
|
|
git branch A &&
|
|
git branch B &&
|
|
|
|
git checkout A &&
|
|
git mv foo baz &&
|
|
git rm bar &&
|
|
git commit -m "Rename foo, remove bar" &&
|
|
|
|
git checkout B &&
|
|
git mv bar baz &&
|
|
git rm foo &&
|
|
git commit -m "Rename bar, remove foo"
|
|
)
|
|
}
|
|
|
|
test_expect_success 'rrdd-check: rename/rename(2to1)/delete/delete conflict' '
|
|
test_setup_rrdd &&
|
|
(
|
|
cd rrdd &&
|
|
|
|
git checkout A^0 &&
|
|
test_must_fail git merge -s recursive B^0 >out 2>err &&
|
|
|
|
# Instead of requiring the output to contain one combined line
|
|
# CONFLICT (rename/rename/delete/delete)
|
|
# or perhaps two lines:
|
|
# CONFLICT (rename/rename): ...
|
|
# CONFLICT (rename/delete): info about pair 1
|
|
# CONFLICT (rename/delete): info about pair 2
|
|
# and instead of requiring "rename/rename" instead of "add/add",
|
|
# be flexible in the type of console output message(s) reported
|
|
# for this particular case; we will be more stringent about the
|
|
# contents of the index and working directory.
|
|
test_grep "CONFLICT (\(.*\)/\1)" out &&
|
|
test_grep "CONFLICT (rename.*delete)" out &&
|
|
test_must_be_empty err &&
|
|
|
|
git ls-files -s >file_count &&
|
|
test_line_count = 2 file_count &&
|
|
git ls-files -u >file_count &&
|
|
test_line_count = 2 file_count &&
|
|
git ls-files -o >file_count &&
|
|
test_line_count = 3 file_count &&
|
|
|
|
git rev-parse >actual \
|
|
:2:baz :3:baz &&
|
|
git rev-parse >expect \
|
|
O:foo O:bar &&
|
|
|
|
test_path_is_missing foo &&
|
|
test_path_is_missing bar &&
|
|
# baz should have two-way merged contents of the original
|
|
# contents of foo and bar; check that content from both sides
|
|
# is present.
|
|
grep foo baz &&
|
|
grep bar baz
|
|
)
|
|
'
|
|
|
|
# Testcase mod6, chains of rename/rename(1to2) and rename/rename(2to1)
|
|
# Commit O: one, three, five
|
|
# Commit A: one->two, three->four, five->six
|
|
# Commit B: one->six, three->two, five->four
|
|
# Expected: six CONFLICT(rename/rename) messages, each path in two of the
|
|
# multi-way merged contents found in two, four, six
|
|
|
|
test_setup_mod6 () {
|
|
git init mod6 &&
|
|
(
|
|
cd mod6 &&
|
|
test_seq 11 19 >one &&
|
|
test_seq 31 39 >three &&
|
|
test_seq 51 59 >five &&
|
|
git add . &&
|
|
test_tick &&
|
|
git commit -m "O" &&
|
|
|
|
git branch O &&
|
|
git branch A &&
|
|
git branch B &&
|
|
|
|
git checkout A &&
|
|
test_seq 10 19 >one &&
|
|
echo 40 >>three &&
|
|
git add one three &&
|
|
git mv one two &&
|
|
git mv three four &&
|
|
git mv five six &&
|
|
test_tick &&
|
|
git commit -m "A" &&
|
|
|
|
git checkout B &&
|
|
echo 20 >>one &&
|
|
echo forty >>three &&
|
|
echo 60 >>five &&
|
|
git add one three five &&
|
|
git mv one six &&
|
|
git mv three two &&
|
|
git mv five four &&
|
|
test_tick &&
|
|
git commit -m "B"
|
|
)
|
|
}
|
|
|
|
test_expect_success 'mod6-check: chains of rename/rename(1to2) and rename/rename(2to1)' '
|
|
test_setup_mod6 &&
|
|
(
|
|
cd mod6 &&
|
|
|
|
git checkout A^0 &&
|
|
|
|
test_must_fail git merge -s recursive B^0 >out 2>err &&
|
|
|
|
test_grep "CONFLICT (rename/rename)" out &&
|
|
test_must_be_empty err &&
|
|
|
|
git ls-files -s >file_count &&
|
|
test_line_count = 9 file_count &&
|
|
git ls-files -u >file_count &&
|
|
test_line_count = 9 file_count &&
|
|
git ls-files -o >file_count &&
|
|
test_line_count = 3 file_count &&
|
|
|
|
test_seq 10 20 >merged-one &&
|
|
test_seq 51 60 >merged-five &&
|
|
# Determine what the merge of three would give us.
|
|
test_seq 31 39 >three-base &&
|
|
test_seq 31 40 >three-side-A &&
|
|
test_seq 31 39 >three-side-B &&
|
|
echo forty >>three-side-B &&
|
|
test_must_fail git merge-file \
|
|
-L "HEAD:four" \
|
|
-L "" \
|
|
-L "B^0:two" \
|
|
three-side-A three-base three-side-B &&
|
|
sed -e "s/^\([<=>]\)/\1\1/" three-side-A >merged-three &&
|
|
|
|
# Verify the index is as expected
|
|
git rev-parse >actual \
|
|
:2:two :3:two \
|
|
:2:four :3:four \
|
|
:2:six :3:six &&
|
|
git hash-object >expect \
|
|
merged-one merged-three \
|
|
merged-three merged-five \
|
|
merged-five merged-one &&
|
|
test_cmp expect actual &&
|
|
|
|
git cat-file -p :2:two >expect &&
|
|
git cat-file -p :3:two >other &&
|
|
>empty &&
|
|
test_must_fail git merge-file \
|
|
-L "HEAD" -L "" -L "B^0" \
|
|
expect empty other &&
|
|
test_cmp expect two &&
|
|
|
|
git cat-file -p :2:four >expect &&
|
|
git cat-file -p :3:four >other &&
|
|
test_must_fail git merge-file \
|
|
-L "HEAD" -L "" -L "B^0" \
|
|
expect empty other &&
|
|
test_cmp expect four &&
|
|
|
|
git cat-file -p :2:six >expect &&
|
|
git cat-file -p :3:six >other &&
|
|
test_must_fail git merge-file \
|
|
-L "HEAD" -L "" -L "B^0" \
|
|
expect empty other &&
|
|
test_cmp expect six
|
|
)
|
|
'
|
|
|
|
test_conflicts_with_adds_and_renames() {
|
|
sideL=$1
|
|
sideR=$2
|
|
|
|
# Setup:
|
|
# L
|
|
# / \
|
|
# main ?
|
|
# \ /
|
|
# R
|
|
#
|
|
# Where:
|
|
# Both L and R have files named 'three' which collide. Each of
|
|
# the colliding files could have been involved in a rename, in
|
|
# which case there was a file named 'one' or 'two' that was
|
|
# modified on the opposite side of history and renamed into the
|
|
# collision on this side of history.
|
|
#
|
|
# Questions:
|
|
# 1) The index should contain both a stage 2 and stage 3 entry
|
|
# for the colliding file. Does it?
|
|
# 2) When renames are involved, the content merges are clean, so
|
|
# the index should reflect the content merges, not merely the
|
|
# version of the colliding file from the prior commit. Does
|
|
# it?
|
|
# 3) There should be a file in the worktree named 'three'
|
|
# containing the two-way merged contents of the content-merged
|
|
# versions of 'three' from each of the two colliding
|
|
# files. Is it present?
|
|
# 4) There should not be any three~* files in the working
|
|
# tree
|
|
test_setup_collision_conflict () {
|
|
git init simple_${sideL}_${sideR} &&
|
|
(
|
|
cd simple_${sideL}_${sideR} &&
|
|
|
|
# Create some related files now
|
|
test_seq -f "Random base content line %d" 1 10 >file_v1 &&
|
|
cp file_v1 file_v2 &&
|
|
echo modification >>file_v2 &&
|
|
|
|
cp file_v1 file_v3 &&
|
|
echo more stuff >>file_v3 &&
|
|
cp file_v3 file_v4 &&
|
|
echo yet more stuff >>file_v4 &&
|
|
|
|
# Use a tag to record both these files for simple
|
|
# access, and clean out these untracked files
|
|
git tag file_v1 $(git hash-object -w file_v1) &&
|
|
git tag file_v2 $(git hash-object -w file_v2) &&
|
|
git tag file_v3 $(git hash-object -w file_v3) &&
|
|
git tag file_v4 $(git hash-object -w file_v4) &&
|
|
git clean -f &&
|
|
|
|
# Setup original commit (or merge-base), consisting of
|
|
# files named "one" and "two" if renames were involved.
|
|
touch irrelevant_file &&
|
|
git add irrelevant_file &&
|
|
if [ $sideL = "rename" ]
|
|
then
|
|
git show file_v1 >one &&
|
|
git add one
|
|
fi &&
|
|
if [ $sideR = "rename" ]
|
|
then
|
|
git show file_v3 >two &&
|
|
git add two
|
|
fi &&
|
|
test_tick && git commit -m initial &&
|
|
|
|
git branch L &&
|
|
git branch R &&
|
|
|
|
# Handle the left side
|
|
git checkout L &&
|
|
if [ $sideL = "rename" ]
|
|
then
|
|
git mv one three
|
|
else
|
|
git show file_v2 >three &&
|
|
git add three
|
|
fi &&
|
|
if [ $sideR = "rename" ]
|
|
then
|
|
git show file_v4 >two &&
|
|
git add two
|
|
fi &&
|
|
test_tick && git commit -m L &&
|
|
|
|
# Handle the right side
|
|
git checkout R &&
|
|
if [ $sideL = "rename" ]
|
|
then
|
|
git show file_v2 >one &&
|
|
git add one
|
|
fi &&
|
|
if [ $sideR = "rename" ]
|
|
then
|
|
git mv two three
|
|
else
|
|
git show file_v4 >three &&
|
|
git add three
|
|
fi &&
|
|
test_tick && git commit -m R
|
|
)
|
|
}
|
|
|
|
test_expect_success "check simple $sideL/$sideR conflict" '
|
|
test_setup_collision_conflict &&
|
|
(
|
|
cd simple_${sideL}_${sideR} &&
|
|
|
|
git checkout L^0 &&
|
|
|
|
# Merge must fail; there is a conflict
|
|
test_must_fail git merge -s recursive R^0 &&
|
|
|
|
# Make sure the index has the right number of entries
|
|
git ls-files -s >out &&
|
|
test_line_count = 3 out &&
|
|
git ls-files -u >out &&
|
|
test_line_count = 2 out &&
|
|
# Ensure we have the correct number of untracked files
|
|
git ls-files -o >out &&
|
|
test_line_count = 1 out &&
|
|
|
|
# Nothing should have touched irrelevant_file
|
|
git rev-parse >actual \
|
|
:0:irrelevant_file \
|
|
:2:three \
|
|
:3:three &&
|
|
git rev-parse >expected \
|
|
main:irrelevant_file \
|
|
file_v2 \
|
|
file_v4 &&
|
|
test_cmp expected actual &&
|
|
|
|
# Make sure we have the correct merged contents for
|
|
# three
|
|
git show file_v1 >expected &&
|
|
cat <<-\EOF >>expected &&
|
|
<<<<<<< HEAD
|
|
modification
|
|
=======
|
|
more stuff
|
|
yet more stuff
|
|
>>>>>>> R^0
|
|
EOF
|
|
|
|
test_cmp expected three
|
|
)
|
|
'
|
|
}
|
|
|
|
test_conflicts_with_adds_and_renames rename rename
|
|
test_conflicts_with_adds_and_renames rename add
|
|
test_conflicts_with_adds_and_renames add rename
|
|
test_conflicts_with_adds_and_renames add add
|
|
|
|
# Setup:
|
|
# L
|
|
# / \
|
|
# main ?
|
|
# \ /
|
|
# R
|
|
#
|
|
# Where:
|
|
# main has two files, named 'one' and 'two'.
|
|
# branches L and R both modify 'one', in conflicting ways.
|
|
# branches L and R both modify 'two', in conflicting ways.
|
|
# branch L also renames 'one' to 'three'.
|
|
# branch R also renames 'two' to 'three'.
|
|
#
|
|
# So, we have four different conflicting files that all end up at path
|
|
# 'three'.
|
|
test_setup_nested_conflicts_from_rename_rename () {
|
|
git init nested_conflicts_from_rename_rename &&
|
|
(
|
|
cd nested_conflicts_from_rename_rename &&
|
|
|
|
# Create some related files now
|
|
test_seq -f "Random base content line %d" 1 10 >file_v1 &&
|
|
|
|
cp file_v1 file_v2 &&
|
|
cp file_v1 file_v3 &&
|
|
cp file_v1 file_v4 &&
|
|
cp file_v1 file_v5 &&
|
|
cp file_v1 file_v6 &&
|
|
|
|
echo one >>file_v1 &&
|
|
echo uno >>file_v2 &&
|
|
echo eins >>file_v3 &&
|
|
|
|
echo two >>file_v4 &&
|
|
echo dos >>file_v5 &&
|
|
echo zwei >>file_v6 &&
|
|
|
|
# Setup original commit (or merge-base), consisting of
|
|
# files named "one" and "two".
|
|
mv file_v1 one &&
|
|
mv file_v4 two &&
|
|
git add one two &&
|
|
test_tick && git commit -m english &&
|
|
|
|
git branch L &&
|
|
git branch R &&
|
|
|
|
# Handle the left side
|
|
git checkout L &&
|
|
git rm one two &&
|
|
mv -f file_v2 three &&
|
|
mv -f file_v5 two &&
|
|
git add two three &&
|
|
test_tick && git commit -m spanish &&
|
|
|
|
# Handle the right side
|
|
git checkout R &&
|
|
git rm one two &&
|
|
mv -f file_v3 one &&
|
|
mv -f file_v6 three &&
|
|
git add one three &&
|
|
test_tick && git commit -m german
|
|
)
|
|
}
|
|
|
|
test_expect_success 'check nested conflicts from rename/rename(2to1)' '
|
|
test_setup_nested_conflicts_from_rename_rename &&
|
|
(
|
|
cd nested_conflicts_from_rename_rename &&
|
|
|
|
git checkout L^0 &&
|
|
|
|
# Merge must fail; there is a conflict
|
|
test_must_fail git merge -s recursive R^0 &&
|
|
|
|
# Make sure the index has the right number of entries
|
|
git ls-files -s >out &&
|
|
test_line_count = 2 out &&
|
|
git ls-files -u >out &&
|
|
test_line_count = 2 out &&
|
|
# Ensure we have the correct number of untracked files
|
|
git ls-files -o >out &&
|
|
test_line_count = 1 out &&
|
|
|
|
# Compare :2:three to expected values
|
|
git cat-file -p main:one >base &&
|
|
git cat-file -p L:three >ours &&
|
|
git cat-file -p R:one >theirs &&
|
|
test_must_fail git merge-file \
|
|
-L "HEAD:three" -L "" -L "R^0:one" \
|
|
ours base theirs &&
|
|
sed -e "s/^\([<=>]\)/\1\1/" ours >L-three &&
|
|
git cat-file -p :2:three >expect &&
|
|
test_cmp expect L-three &&
|
|
|
|
# Compare :2:three to expected values
|
|
git cat-file -p main:two >base &&
|
|
git cat-file -p L:two >ours &&
|
|
git cat-file -p R:three >theirs &&
|
|
test_must_fail git merge-file \
|
|
-L "HEAD:two" -L "" -L "R^0:three" \
|
|
ours base theirs &&
|
|
sed -e "s/^\([<=>]\)/\1\1/" ours >R-three &&
|
|
git cat-file -p :3:three >expect &&
|
|
test_cmp expect R-three &&
|
|
|
|
# Compare three to expected contents
|
|
>empty &&
|
|
test_must_fail git merge-file \
|
|
-L "HEAD" -L "" -L "R^0" \
|
|
L-three empty R-three &&
|
|
test_cmp three L-three
|
|
)
|
|
'
|
|
|
|
# Testcase rename/rename(1to2) of a binary file
|
|
# Commit O: orig
|
|
# Commit A: orig-A
|
|
# Commit B: orig-B
|
|
# Expected: CONFLICT(rename/rename) message, three unstaged entries in the
|
|
# index, and contents of orig-[AB] at path orig-[AB]
|
|
test_setup_rename_rename_1_to_2_binary () {
|
|
git init rename_rename_1_to_2_binary &&
|
|
(
|
|
cd rename_rename_1_to_2_binary &&
|
|
|
|
echo '* binary' >.gitattributes &&
|
|
git add .gitattributes &&
|
|
|
|
test_seq 1 10 >orig &&
|
|
git add orig &&
|
|
git commit -m orig &&
|
|
|
|
git branch A &&
|
|
git branch B &&
|
|
|
|
git checkout A &&
|
|
git mv orig orig-A &&
|
|
test_seq 1 11 >orig-A &&
|
|
git add orig-A &&
|
|
git commit -m orig-A &&
|
|
|
|
git checkout B &&
|
|
git mv orig orig-B &&
|
|
test_seq 0 10 >orig-B &&
|
|
git add orig-B &&
|
|
git commit -m orig-B
|
|
|
|
)
|
|
}
|
|
|
|
test_expect_success 'rename/rename(1to2) with a binary file' '
|
|
test_setup_rename_rename_1_to_2_binary &&
|
|
(
|
|
cd rename_rename_1_to_2_binary &&
|
|
|
|
git checkout A^0 &&
|
|
|
|
test_must_fail git merge -s recursive B^0 &&
|
|
|
|
# Make sure the index has the right number of entries
|
|
git ls-files -s >actual &&
|
|
test_line_count = 4 actual &&
|
|
|
|
git rev-parse A:orig-A B:orig-B >expect &&
|
|
git hash-object orig-A orig-B >actual &&
|
|
test_cmp expect actual
|
|
)
|
|
'
|
|
|
|
# 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
|