git/t/t7611-merge-abort.sh
Elijah Newren eddd1a411d merge-recursive: enforce rule that index matches head before merging
builtin/merge.c says that when we are about to perform a merge:

    ...the index must be in sync with the head commit.  The strategies are
    responsible to ensure this.

merge-recursive has always relied on unpack_trees() to enforce this
requirement, except in the case of an "Already up to date!" merge.
unpack-trees.c does not actually enforce this requirement, though.  It
allows for a pair of exceptions, in cases which it refers to as #14(ALT)
and #2ALT.  Documentation/technical/trivial-merge.txt can be consulted for
the precise meanings of the various case numbers and their meanings for
unpack-trees.c, but we have a high-level description of the intent behind
these two exceptions in a combined and summarized form in
Documentation/git-merge.txt:

    ...[merge will] abort if there are any changes registered in the index
    relative to the `HEAD` commit.  (One exception is when the changed index
    entries are in the state that would result from the merge already.)

While this high-level description does describe conditions under which it
would be safe to allow the index to diverge from HEAD, it does not match
what is actually implemented.  In particular, unpack-trees.c has no
knowledge of renames, and these two exceptions were written assuming that
no renames take place.  Once renames get into the mix, it is no longer
safe to allow the index to not match for #2ALT.  We could modify
unpack-trees to only allow #14(ALT) as an exception, but that would be
more strict than required for the resolve strategy (since the resolve
strategy doesn't handle renames at all).  Therefore, unpack_trees.c seems
like the wrong place to fix this.

Further, if someone fixes the combination of break and rename detection
and modifies merge-recursive to take advantage of the combination, then it
will also no longer be safe to allow the index to not match for #14(ALT)
when the recursive strategy is in use.  Therefore, leaving one of the
exceptions in place with the recursive merge strategy feels like we are
just leaving a latent bug in the code for folks in the future to stumble
across.

It may be possible to fix both unpack-trees and merge-recursive in a way
that implements the exception as stated in Documentation/git-merge.txt,
but it would be somewhat complex, possibly also buggy at first, and
ultimately, not all that valuable.  Instead, just enforce the requirement
stated in builtin/merge.c; error out if the index does not match the HEAD
commit, just like the 'ours' and 'octopus' strategies do.

Some testcase fixups were in order:
  t7611: had many tests designed to show that `git merge --abort` could
	 not always restore the index and working tree to the state they
	 were in before the merge started.  The tests that were associated
	 with having changes in the index before the merge started are no
         longer applicable, so they have been removed.
  t7504: had a few tests that had stray staged changes that were not
         actually part of the test under consideration
  t6044: We no longer expect stray staged changes to sometimes result
         in the merge continuing.  Also, fix a case where a merge
         didn't abort but should have.

Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
2018-07-11 09:38:36 -07:00

202 lines
5.7 KiB
Bash
Executable File

#!/bin/sh
test_description='test aborting in-progress merges
Set up repo with conflicting and non-conflicting branches:
There are three files foo/bar/baz, and the following graph illustrates the
content of these files in each commit:
# foo/bar/baz --- foo/bar/bazz <-- master
# \
# --- foo/barf/bazf <-- conflict_branch
# \
# --- foo/bart/baz <-- clean_branch
Next, test git merge --abort with the following variables:
- before/after successful merge (should fail when not in merge context)
- with/without conflicts
- clean/dirty index before merge
- clean/dirty worktree before merge
- dirty index before merge matches contents on remote branch
- changed/unchanged worktree after merge
- changed/unchanged index after merge
'
. ./test-lib.sh
test_expect_success 'setup' '
# Create the above repo
echo foo > foo &&
echo bar > bar &&
echo baz > baz &&
git add foo bar baz &&
git commit -m initial &&
echo bazz > baz &&
git commit -a -m "second" &&
git checkout -b conflict_branch HEAD^ &&
echo barf > bar &&
echo bazf > baz &&
git commit -a -m "conflict" &&
git checkout -b clean_branch HEAD^ &&
echo bart > bar &&
git commit -a -m "clean" &&
git checkout master
'
pre_merge_head="$(git rev-parse HEAD)"
test_expect_success 'fails without MERGE_HEAD (unstarted merge)' '
test_must_fail git merge --abort 2>output &&
test_i18ngrep MERGE_HEAD output
'
test_expect_success 'fails without MERGE_HEAD (unstarted merge): .git/MERGE_HEAD sanity' '
test ! -f .git/MERGE_HEAD &&
test "$pre_merge_head" = "$(git rev-parse HEAD)"
'
test_expect_success 'fails without MERGE_HEAD (completed merge)' '
git merge clean_branch &&
test ! -f .git/MERGE_HEAD &&
# Merge successfully completed
post_merge_head="$(git rev-parse HEAD)" &&
test_must_fail git merge --abort 2>output &&
test_i18ngrep MERGE_HEAD output
'
test_expect_success 'fails without MERGE_HEAD (completed merge): .git/MERGE_HEAD sanity' '
test ! -f .git/MERGE_HEAD &&
test "$post_merge_head" = "$(git rev-parse HEAD)"
'
test_expect_success 'Forget previous merge' '
git reset --hard "$pre_merge_head"
'
test_expect_success 'Abort after --no-commit' '
# Redo merge, but stop before creating merge commit
git merge --no-commit clean_branch &&
test -f .git/MERGE_HEAD &&
# Abort non-conflicting merge
git merge --abort &&
test ! -f .git/MERGE_HEAD &&
test "$pre_merge_head" = "$(git rev-parse HEAD)" &&
test -z "$(git diff)" &&
test -z "$(git diff --staged)"
'
test_expect_success 'Abort after conflicts' '
# Create conflicting merge
test_must_fail git merge conflict_branch &&
test -f .git/MERGE_HEAD &&
# Abort conflicting merge
git merge --abort &&
test ! -f .git/MERGE_HEAD &&
test "$pre_merge_head" = "$(git rev-parse HEAD)" &&
test -z "$(git diff)" &&
test -z "$(git diff --staged)"
'
test_expect_success 'Clean merge with dirty index fails' '
echo xyzzy >> foo &&
git add foo &&
git diff --staged > expect &&
test_must_fail git merge clean_branch &&
test ! -f .git/MERGE_HEAD &&
test "$pre_merge_head" = "$(git rev-parse HEAD)" &&
test -z "$(git diff)" &&
git diff --staged > actual &&
test_cmp expect actual
'
test_expect_success 'Conflicting merge with dirty index fails' '
test_must_fail git merge conflict_branch &&
test ! -f .git/MERGE_HEAD &&
test "$pre_merge_head" = "$(git rev-parse HEAD)" &&
test -z "$(git diff)" &&
git diff --staged > actual &&
test_cmp expect actual
'
test_expect_success 'Reset index (but preserve worktree changes)' '
git reset "$pre_merge_head" &&
git diff > actual &&
test_cmp expect actual
'
test_expect_success 'Abort clean merge with non-conflicting dirty worktree' '
git merge --no-commit clean_branch &&
test -f .git/MERGE_HEAD &&
# Abort merge
git merge --abort &&
test ! -f .git/MERGE_HEAD &&
test "$pre_merge_head" = "$(git rev-parse HEAD)" &&
test -z "$(git diff --staged)" &&
git diff > actual &&
test_cmp expect actual
'
test_expect_success 'Abort conflicting merge with non-conflicting dirty worktree' '
test_must_fail git merge conflict_branch &&
test -f .git/MERGE_HEAD &&
# Abort merge
git merge --abort &&
test ! -f .git/MERGE_HEAD &&
test "$pre_merge_head" = "$(git rev-parse HEAD)" &&
test -z "$(git diff --staged)" &&
git diff > actual &&
test_cmp expect actual
'
test_expect_success 'Reset worktree changes' '
git reset --hard "$pre_merge_head"
'
test_expect_success 'Fail clean merge with conflicting dirty worktree' '
echo xyzzy >> bar &&
git diff > expect &&
test_must_fail git merge --no-commit clean_branch &&
test ! -f .git/MERGE_HEAD &&
test "$pre_merge_head" = "$(git rev-parse HEAD)" &&
test -z "$(git diff --staged)" &&
git diff > actual &&
test_cmp expect actual
'
test_expect_success 'Fail conflicting merge with conflicting dirty worktree' '
test_must_fail git merge conflict_branch &&
test ! -f .git/MERGE_HEAD &&
test "$pre_merge_head" = "$(git rev-parse HEAD)" &&
test -z "$(git diff --staged)" &&
git diff > actual &&
test_cmp expect actual
'
test_expect_success 'Reset worktree changes' '
git reset --hard "$pre_merge_head"
'
test_expect_success 'Fail clean merge with matching dirty worktree' '
echo bart > bar &&
git diff > expect &&
test_must_fail git merge --no-commit clean_branch &&
test ! -f .git/MERGE_HEAD &&
test "$pre_merge_head" = "$(git rev-parse HEAD)" &&
test -z "$(git diff --staged)" &&
git diff > actual &&
test_cmp expect actual
'
test_expect_success 'Fail conflicting merge with matching dirty worktree' '
echo barf > bar &&
git diff > expect &&
test_must_fail git merge conflict_branch &&
test ! -f .git/MERGE_HEAD &&
test "$pre_merge_head" = "$(git rev-parse HEAD)" &&
test -z "$(git diff --staged)" &&
git diff > actual &&
test_cmp expect actual
'
test_done