Skip to content

Commit 966ff64

Browse files
committed
Merge branch 'en/merge-restore-to-pristine'
When "git merge" finds that it cannot perform a merge, it should restore the working tree to the state before the command was initiated, but in some corner cases it didn't. * en/merge-restore-to-pristine: merge: do not exit restore_state() prematurely merge: ensure we can actually restore pre-merge state merge: make restore_state() restore staged state too merge: fix save_state() to work when there are stat-dirty files merge: do not abort early if one strategy fails to handle the merge merge: abort if index does not match HEAD for trivial merges merge-resolve: abort if index does not match HEAD merge-ort-wrappers: make printed message match the one from recursive
2 parents 4e0d160 + c23fc07 commit 966ff64

7 files changed

+154
-17
lines changed

builtin/merge.c

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -313,8 +313,16 @@ static int save_state(struct object_id *stash)
313313
int len;
314314
struct child_process cp = CHILD_PROCESS_INIT;
315315
struct strbuf buffer = STRBUF_INIT;
316+
struct lock_file lock_file = LOCK_INIT;
317+
int fd;
316318
int rc = -1;
317319

320+
fd = repo_hold_locked_index(the_repository, &lock_file, 0);
321+
refresh_cache(REFRESH_QUIET);
322+
if (0 <= fd)
323+
repo_update_index_if_able(the_repository, &lock_file);
324+
rollback_lock_file(&lock_file);
325+
318326
strvec_pushl(&cp.args, "stash", "create", NULL);
319327
cp.out = -1;
320328
cp.git_cmd = 1;
@@ -375,22 +383,26 @@ static void reset_hard(const struct object_id *oid, int verbose)
375383
static void restore_state(const struct object_id *head,
376384
const struct object_id *stash)
377385
{
378-
const char *args[] = { "stash", "apply", NULL, NULL };
379-
380-
if (is_null_oid(stash))
381-
return;
386+
struct strvec args = STRVEC_INIT;
382387

383388
reset_hard(head, 1);
384389

385-
args[2] = oid_to_hex(stash);
390+
if (is_null_oid(stash))
391+
goto refresh_cache;
392+
393+
strvec_pushl(&args, "stash", "apply", "--index", "--quiet", NULL);
394+
strvec_push(&args, oid_to_hex(stash));
386395

387396
/*
388397
* It is OK to ignore error here, for example when there was
389398
* nothing to restore.
390399
*/
391-
run_command_v_opt(args, RUN_GIT_CMD);
400+
run_command_v_opt(args.v, RUN_GIT_CMD);
401+
strvec_clear(&args);
392402

393-
refresh_cache(REFRESH_QUIET);
403+
refresh_cache:
404+
if (discard_cache() < 0 || read_cache() < 0)
405+
die(_("could not read index"));
394406
}
395407

396408
/* This is called when no merge was necessary. */
@@ -754,8 +766,10 @@ static int try_merge_strategy(const char *strategy, struct commit_list *common,
754766
else
755767
clean = merge_recursive(&o, head, remoteheads->item,
756768
reversed, &result);
757-
if (clean < 0)
758-
exit(128);
769+
if (clean < 0) {
770+
rollback_lock_file(&lock);
771+
return 2;
772+
}
759773
if (write_locked_index(&the_index, &lock,
760774
COMMIT_LOCK | SKIP_IF_UNCHANGED))
761775
die(_("unable to write %s"), get_index_file());
@@ -1599,6 +1613,21 @@ int cmd_merge(int argc, const char **argv, const char *prefix)
15991613
*/
16001614
refresh_cache(REFRESH_QUIET);
16011615
if (allow_trivial && fast_forward != FF_ONLY) {
1616+
/*
1617+
* Must first ensure that index matches HEAD before
1618+
* attempting a trivial merge.
1619+
*/
1620+
struct tree *head_tree = get_commit_tree(head_commit);
1621+
struct strbuf sb = STRBUF_INIT;
1622+
1623+
if (repo_index_has_changes(the_repository, head_tree,
1624+
&sb)) {
1625+
error(_("Your local changes to the following files would be overwritten by merge:\n %s"),
1626+
sb.buf);
1627+
strbuf_release(&sb);
1628+
return 2;
1629+
}
1630+
16021631
/* See if it is really trivial. */
16031632
git_committer_info(IDENT_STRICT);
16041633
printf(_("Trying really trivial in-index merge...\n"));
@@ -1655,12 +1684,12 @@ int cmd_merge(int argc, const char **argv, const char *prefix)
16551684
* tree in the index -- this means that the index must be in
16561685
* sync with the head commit. The strategies are responsible
16571686
* to ensure this.
1687+
*
1688+
* Stash away the local changes so that we can try more than one
1689+
* and/or recover from merge strategies bailing while leaving the
1690+
* index and working tree polluted.
16581691
*/
1659-
if (use_strategies_nr == 1 ||
1660-
/*
1661-
* Stash away the local changes so that we can try more than one.
1662-
*/
1663-
save_state(&stash))
1692+
if (save_state(&stash))
16641693
oidclr(&stash);
16651694

16661695
for (i = 0; !merge_was_ok && i < use_strategies_nr; i++) {

git-merge-resolve.sh

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@
55
#
66
# Resolve two trees, using enhanced multi-base read-tree.
77

8+
. git-sh-setup
9+
10+
# Abort if index does not match HEAD
11+
if ! git diff-index --quiet --cached HEAD --
12+
then
13+
gettextln "Error: Your local changes to the following files would be overwritten by merge"
14+
git diff-index --cached --name-only HEAD -- | sed -e 's/^/ /'
15+
exit 2
16+
fi
17+
818
# The first parameters up to -- are merge bases; the rest are heads.
919
bases= head= remotes= sep_seen=
1020
for arg

merge-ort-wrappers.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ static int unclean(struct merge_options *opt, struct tree *head)
1010
struct strbuf sb = STRBUF_INIT;
1111

1212
if (head && repo_index_has_changes(opt->repo, head, &sb)) {
13-
fprintf(stderr, _("Your local changes to the following files would be overwritten by merge:\n %s"),
14-
sb.buf);
13+
error(_("Your local changes to the following files would be overwritten by merge:\n %s"),
14+
sb.buf);
1515
strbuf_release(&sb);
1616
return -1;
1717
}

t/t6402-merge-rename.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ test_expect_success 'updated working tree file should prevent the merge' '
210210
echo >>M one line addition &&
211211
cat M >M.saved &&
212212
git update-index M &&
213-
test_expect_code 128 git pull --no-rebase . yellow &&
213+
test_expect_code 2 git pull --no-rebase . yellow &&
214214
test_cmp M M.saved &&
215215
rm -f M.saved
216216
'

t/t6424-merge-unrelated-index-changes.sh

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,39 @@ test_expect_success 'resolve, non-trivial' '
114114
test_path_is_missing .git/MERGE_HEAD
115115
'
116116

117+
test_expect_success 'resolve, trivial, related file removed' '
118+
git reset --hard &&
119+
git checkout B^0 &&
120+
121+
git rm a &&
122+
test_path_is_missing a &&
123+
124+
test_must_fail git merge -s resolve C^0 &&
125+
126+
test_path_is_missing a &&
127+
test_path_is_missing .git/MERGE_HEAD
128+
'
129+
130+
test_expect_success 'resolve, non-trivial, related file removed' '
131+
git reset --hard &&
132+
git checkout B^0 &&
133+
134+
git rm a &&
135+
test_path_is_missing a &&
136+
137+
# We also ask for recursive in order to turn off the "allow_trivial"
138+
# setting in builtin/merge.c, and ensure that resolve really does
139+
# correctly fail the merge (I guess this also tests that recursive
140+
# correctly fails the merge, but the main thing we are attempting
141+
# to test here is resolve and are just using the side effect of
142+
# adding recursive to ensure that resolve is actually tested rather
143+
# than the trivial merge codepath)
144+
test_must_fail git merge -s resolve -s recursive D^0 &&
145+
146+
test_path_is_missing a &&
147+
test_path_is_missing .git/MERGE_HEAD
148+
'
149+
117150
test_expect_success 'recursive' '
118151
git reset --hard &&
119152
git checkout B^0 &&
@@ -242,4 +275,36 @@ test_expect_success 'subtree' '
242275
test_path_is_missing .git/MERGE_HEAD
243276
'
244277

278+
test_expect_success 'avoid failure due to stat-dirty files' '
279+
git reset --hard &&
280+
git checkout B^0 &&
281+
282+
# Make "a" be stat-dirty
283+
test-tool chmtime =+1 a &&
284+
285+
# stat-dirty file should not prevent stash creation in builtin/merge.c
286+
git merge -s resolve -s recursive D^0
287+
'
288+
289+
test_expect_success 'with multiple strategies, recursive or ort failure do not early abort' '
290+
git reset --hard &&
291+
git checkout B^0 &&
292+
293+
test_seq 0 10 >a &&
294+
git add a &&
295+
git rev-parse :a >expect &&
296+
297+
sane_unset GIT_TEST_MERGE_ALGORITHM &&
298+
test_must_fail git merge -s recursive -s ort -s octopus C^0 >output 2>&1 &&
299+
300+
grep "Trying merge strategy recursive..." output &&
301+
grep "Trying merge strategy ort..." output &&
302+
grep "Trying merge strategy octopus..." output &&
303+
grep "No merge strategy handled the merge." output &&
304+
305+
# Changes to "a" should remain staged
306+
git rev-parse :a >actual &&
307+
test_cmp expect actual
308+
'
309+
245310
test_done

t/t6439-merge-co-error-msgs.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ test_expect_success 'untracked files overwritten by merge (fast and non-fast for
4747
export GIT_MERGE_VERBOSITY &&
4848
test_must_fail git merge branch 2>out2
4949
) &&
50+
echo "Merge with strategy ${GIT_TEST_MERGE_ALGORITHM:-ort} failed." >>expect &&
5051
test_cmp out2 expect &&
5152
git reset --hard HEAD^
5253
'

t/t7607-merge-state.sh

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#!/bin/sh
2+
3+
test_description="Test that merge state is as expected after failed merge"
4+
5+
GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
6+
export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
7+
. ./test-lib.sh
8+
9+
test_expect_success 'Ensure we restore original state if no merge strategy handles it' '
10+
test_commit --no-tag "Initial" base base &&
11+
12+
for b in branch1 branch2 branch3
13+
do
14+
git checkout -b $b main &&
15+
test_commit --no-tag "Change on $b" base $b || return 1
16+
done &&
17+
18+
git checkout branch1 &&
19+
# This is a merge that octopus cannot handle. Note, that it does not
20+
# just hit conflicts, it completely fails and says that it cannot
21+
# handle this type of merge.
22+
test_expect_code 2 git merge branch2 branch3 >output 2>&1 &&
23+
grep "fatal: merge program failed" output &&
24+
grep "Should not be doing an octopus" output &&
25+
26+
# Make sure we did not leave stray changes around when no appropriate
27+
# merge strategy was found
28+
git diff --exit-code --name-status &&
29+
test_path_is_missing .git/MERGE_HEAD
30+
'
31+
32+
test_done

0 commit comments

Comments
 (0)