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:
parent
2a22c1b35d
commit
35d2fffdb8
@ -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
|
||||
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[]
|
||||
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:
|
||||
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:
|
||||
|
||||
* 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
|
||||
|
@ -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[] = {
|
||||
"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)
|
||||
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
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
|
Loading…
Reference in New Issue
Block a user