Browse Source

Provide 'git merge --abort' as a synonym to 'git reset --merge'

Teach 'git merge' the --abort option, which verifies the existence of
MERGE_HEAD and then invokes 'git reset --merge' to abort the current
in-progress merge and attempt to reconstruct the pre-merge state.

The reason for adding this option is to provide a user interface for
aborting an in-progress merge that is consistent with the interface
for aborting a rebase ('git rebase --abort'), aborting the application
of a patch series ('git am --abort'), and aborting an in-progress notes
merge ('git notes merge --abort').

The patch includes documentation and testcases that explain and verify
the various scenarios in which 'git merge --abort' can run. The
testcases also document the cases in which 'git merge --abort' is
unable to correctly restore the pre-merge state (look for the '###'
comments towards the bottom of t/t7609-merge-abort.sh).

This patch has been improved by the following contributions:
- Jonathan Nieder: Move test documentation into test_description

Thanks-to: Jonathan Nieder <jrnieder@gmail.com>
Signed-off-by: Johan Herland <johan@herland.net>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
maint
Johan Herland 14 years ago committed by Junio C Hamano
parent
commit
35d2fffdb8
  1. 27
      Documentation/git-merge.txt
  2. 14
      builtin/merge.c
  3. 313
      t/t7609-merge-abort.sh

27
Documentation/git-merge.txt

@ -13,6 +13,7 @@ SYNOPSIS @@ -13,6 +13,7 @@ SYNOPSIS
[-s <strategy>] [-X <strategy-option>]
[--[no-]rerere-autoupdate] [-m <msg>] <commit>...
'git merge' <msg> HEAD <commit>...
'git merge' --abort

DESCRIPTION
-----------
@ -47,6 +48,14 @@ The second syntax (<msg> `HEAD` <commit>...) is supported for @@ -47,6 +48,14 @@ The second syntax (<msg> `HEAD` <commit>...) is supported for
historical reasons. Do not use it from the command line or in
new scripts. It is the same as `git merge -m <msg> <commit>...`.

The third syntax ("`git merge --abort`") can only be run after the
merge has resulted in conflicts. 'git merge --abort' will abort the
merge process and try to reconstruct the pre-merge state. However,
if there were uncommitted changes when the merge started (and
especially if those changes were further modified after the merge
was started), 'git merge --abort' will in some cases be unable to
reconstruct the original (pre-merge) changes. Therefore:

*Warning*: Running 'git merge' with uncommitted changes is
discouraged: while possible, it leaves you in a state that is hard to
back out of in the case of a conflict.
@ -72,6 +81,18 @@ include::merge-options.txt[] @@ -72,6 +81,18 @@ include::merge-options.txt[]
Allow the rerere mechanism to update the index with the
result of auto-conflict resolution if possible.

--abort::
Abort the current conflict resolution process, and
try to reconstruct the pre-merge state.
+
If there were uncommitted worktree changes present when the merge
started, 'git merge --abort' will in some cases be unable to
reconstruct these changes. It is therefore recommended to always
commit or stash your changes before running 'git merge'.
+
'git merge --abort' is equivalent to 'git reset --merge' when
`MERGE_HEAD` is present.

<commit>...::
Commits, usually other branch heads, to merge into our branch.
You need at least one <commit>. Specifying more than one
@ -142,7 +163,7 @@ happens: @@ -142,7 +163,7 @@ happens:
i.e. matching `HEAD`.

If you tried a merge which resulted in complex conflicts and
want to start over, you can recover with `git reset --merge`.
want to start over, you can recover with `git merge --abort`.

HOW CONFLICTS ARE PRESENTED
---------------------------
@ -213,8 +234,8 @@ After seeing a conflict, you can do two things: @@ -213,8 +234,8 @@ After seeing a conflict, you can do two things:

* Decide not to merge. The only clean-ups you need are to reset
the index file to the `HEAD` commit to reverse 2. and to clean
up working tree changes made by 2. and 3.; `git-reset --hard` can
be used for this.
up working tree changes made by 2. and 3.; `git merge --abort`
can be used for this.

* Resolve the conflicts. Git will mark the conflicts in
the working tree. Edit the files into shape and

14
builtin/merge.c

@ -56,6 +56,7 @@ static size_t xopts_nr, xopts_alloc; @@ -56,6 +56,7 @@ static size_t xopts_nr, xopts_alloc;
static const char *branch;
static int verbosity;
static int allow_rerere_auto;
static int abort_current_merge;

static struct strategy all_strategy[] = {
{ "recursive", DEFAULT_TWOHEAD | NO_TRIVIAL },
@ -194,6 +195,8 @@ static struct option builtin_merge_options[] = { @@ -194,6 +195,8 @@ static struct option builtin_merge_options[] = {
"message to be used for the merge commit (if any)",
option_parse_message),
OPT__VERBOSITY(&verbosity),
OPT_BOOLEAN(0, "abort", &abort_current_merge,
"abort the current in-progress merge"),
OPT_END()
};

@ -914,6 +917,17 @@ int cmd_merge(int argc, const char **argv, const char *prefix) @@ -914,6 +917,17 @@ int cmd_merge(int argc, const char **argv, const char *prefix)
argc = parse_options(argc, argv, prefix, builtin_merge_options,
builtin_merge_usage, 0);

if (abort_current_merge) {
int nargc = 2;
const char *nargv[] = {"reset", "--merge", NULL};

if (!file_exists(git_path("MERGE_HEAD")))
die("There is no merge to abort (MERGE_HEAD missing).");

/* Invoke 'git reset --merge' */
return cmd_reset(nargc, nargv, prefix);
}

if (read_cache_unmerged())
die_resolve_conflict("merge");


313
t/t7609-merge-abort.sh

@ -0,0 +1,313 @@ @@ -0,0 +1,313 @@
#!/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 &&
grep -q MERGE_HEAD output &&
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 &&
grep -q MERGE_HEAD output &&
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 'Abort clean merge with matching dirty index' '
git add bar &&
git diff --staged > expect &&
git merge --no-commit clean_branch &&
test -f .git/MERGE_HEAD &&
### When aborting the merge, git will discard all staged changes,
### including those that were staged pre-merge. In other words,
### --abort will LOSE any staged changes (the staged changes that
### are lost must match the merge result, or the merge would not
### have been allowed to start). Change expectations accordingly:
rm expect &&
touch expect &&
# Abort merge
git merge --abort &&
test ! -f .git/MERGE_HEAD &&
test "$pre_merge_head" = "$(git rev-parse HEAD)" &&
git diff --staged > actual &&
test_cmp expect actual &&
test -z "$(git diff)"
'

test_expect_success 'Reset worktree changes' '
git reset --hard "$pre_merge_head"
'

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_expect_success 'Abort conflicting merge with matching dirty index' '
git add bar &&
git diff --staged > expect &&
test_must_fail git merge conflict_branch &&
test -f .git/MERGE_HEAD &&
### When aborting the merge, git will discard all staged changes,
### including those that were staged pre-merge. In other words,
### --abort will LOSE any staged changes (the staged changes that
### are lost must match the merge result, or the merge would not
### have been allowed to start). Change expectations accordingly:
rm expect &&
touch expect &&
# Abort merge
git merge --abort &&
test ! -f .git/MERGE_HEAD &&
test "$pre_merge_head" = "$(git rev-parse HEAD)" &&
git diff --staged > actual &&
test_cmp expect actual &&
test -z "$(git diff)"
'

test_expect_success 'Reset worktree changes' '
git reset --hard "$pre_merge_head"
'

test_expect_success 'Abort merge with pre- and post-merge worktree changes' '
# Pre-merge worktree changes
echo xyzzy > foo &&
echo barf > bar &&
git add bar &&
git diff > expect &&
git diff --staged > expect-staged &&
# Perform merge
test_must_fail git merge conflict_branch &&
test -f .git/MERGE_HEAD &&
# Post-merge worktree changes
echo yzxxz > foo &&
echo blech > baz &&
### When aborting the merge, git will discard staged changes (bar)
### and unmerged changes (baz). Other changes that are neither
### staged nor marked as unmerged (foo), will be preserved. For
### these changed, git cannot tell pre-merge changes apart from
### post-merge changes, so the post-merge changes will be
### preserved. Change expectations accordingly:
git diff -- foo > expect &&
rm expect-staged &&
touch expect-staged &&
# Abort merge
git merge --abort &&
test ! -f .git/MERGE_HEAD &&
test "$pre_merge_head" = "$(git rev-parse HEAD)" &&
git diff > actual &&
test_cmp expect actual &&
git diff --staged > actual-staged &&
test_cmp expect-staged actual-staged
'

test_expect_success 'Reset worktree changes' '
git reset --hard "$pre_merge_head"
'

test_expect_success 'Abort merge with pre- and post-merge index changes' '
# Pre-merge worktree changes
echo xyzzy > foo &&
echo barf > bar &&
git add bar &&
git diff > expect &&
git diff --staged > expect-staged &&
# Perform merge
test_must_fail git merge conflict_branch &&
test -f .git/MERGE_HEAD &&
# Post-merge worktree changes
echo yzxxz > foo &&
echo blech > baz &&
git add foo bar &&
### When aborting the merge, git will discard all staged changes
### (foo, bar and baz), and no changes will be preserved. Whether
### the changes were staged pre- or post-merge does not matter
### (except for not preventing starting the merge).
### Change expectations accordingly:
rm expect expect-staged &&
touch expect &&
touch expect-staged &&
# Abort merge
git merge --abort &&
test ! -f .git/MERGE_HEAD &&
test "$pre_merge_head" = "$(git rev-parse HEAD)" &&
git diff > actual &&
test_cmp expect actual &&
git diff --staged > actual-staged &&
test_cmp expect-staged actual-staged
'

test_done
Loading…
Cancel
Save