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 15 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
[-s <strategy>] [-X <strategy-option>] [-s <strategy>] [-X <strategy-option>]
[--[no-]rerere-autoupdate] [-m <msg>] <commit>... [--[no-]rerere-autoupdate] [-m <msg>] <commit>...
'git merge' <msg> HEAD <commit>... 'git merge' <msg> HEAD <commit>...
'git merge' --abort


DESCRIPTION DESCRIPTION
----------- -----------
@ -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 historical reasons. Do not use it from the command line or in
new scripts. It is the same as `git merge -m <msg> <commit>...`. 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 *Warning*: Running 'git merge' with uncommitted changes is
discouraged: while possible, it leaves you in a state that is hard to discouraged: while possible, it leaves you in a state that is hard to
back out of in the case of a conflict. back out of in the case of a conflict.
@ -72,6 +81,18 @@ include::merge-options.txt[]
Allow the rerere mechanism to update the index with the Allow the rerere mechanism to update the index with the
result of auto-conflict resolution if possible. 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>...:: <commit>...::
Commits, usually other branch heads, to merge into our branch. Commits, usually other branch heads, to merge into our branch.
You need at least one <commit>. Specifying more than one You need at least one <commit>. Specifying more than one
@ -142,7 +163,7 @@ happens:
i.e. matching `HEAD`. i.e. matching `HEAD`.


If you tried a merge which resulted in complex conflicts and 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 HOW CONFLICTS ARE PRESENTED
--------------------------- ---------------------------
@ -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 * 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 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 up working tree changes made by 2. and 3.; `git merge --abort`
be used for this. can be used for this.


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

14
builtin/merge.c

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


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


@ -914,6 +917,17 @@ int cmd_merge(int argc, const char **argv, const char *prefix)
argc = parse_options(argc, argv, prefix, builtin_merge_options, argc = parse_options(argc, argv, prefix, builtin_merge_options,
builtin_merge_usage, 0); 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()) if (read_cache_unmerged())
die_resolve_conflict("merge"); die_resolve_conflict("merge");



313
t/t7609-merge-abort.sh

@ -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