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>
This commit is contained in:

committed by
Junio C Hamano

parent
2a22c1b35d
commit
35d2fffdb8
@ -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
|
||||||
|
@ -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
Executable file
313
t/t7609-merge-abort.sh
Executable file
@ -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
|
Reference in New Issue
Block a user