Skip to content

Commit bf6ab08

Browse files
phillipwoodgitster
authored andcommitted
rebase: apply and cleanup autostash when rebase fails to start
If "git rebase" fails to start after stashing the user's uncommitted changes then it forgets to restore the stashed changes and remove the state directory. To make matters worse, running "git rebase --abort" to apply the stashed changes and cleanup the state directory fails because the state directory only contains the "autostash" file and is missing the "head-name" and "onto" files required by read_basic_state(). Fix this by applying the autostash and removing the state directory if the pre-rebase hook or initial checkout fail. This matches what finish_rebase() does at the end of a successful rebase. If the user modifies any files after the autostash is created it is possible there will be conflicts when the autostash is applied. In that case apply_autostash() saves the stash in a new entry under refs/stash and so it is safe to remove the state directory containing the autostash file. New tests are added to check the autostash is applied and the state directory is removed if the rebase fails to start. Checks are also added to some existing tests in order to ensure there is no state directory left behind when a rebase fails to start and no autostash has been created. Reported-by: Brian Lyles <[email protected]> Signed-off-by: Phillip Wood <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 39bf06a commit bf6ab08

File tree

4 files changed

+81
-10
lines changed

4 files changed

+81
-10
lines changed

builtin/rebase.c

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,23 @@ static int rebase_write_basic_state(struct rebase_options *opts)
526526
return 0;
527527
}
528528

529+
static int cleanup_autostash(struct rebase_options *opts)
530+
{
531+
int ret;
532+
struct strbuf dir = STRBUF_INIT;
533+
const char *path = state_dir_path("autostash", opts);
534+
535+
if (!file_exists(path))
536+
return 0;
537+
ret = apply_autostash(path);
538+
strbuf_addstr(&dir, opts->state_dir);
539+
if (remove_dir_recursively(&dir, 0))
540+
ret = error_errno(_("could not remove '%s'"), opts->state_dir);
541+
strbuf_release(&dir);
542+
543+
return ret;
544+
}
545+
529546
static int finish_rebase(struct rebase_options *opts)
530547
{
531548
struct strbuf dir = STRBUF_INIT;
@@ -1726,7 +1743,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
17261743
if (require_clean_work_tree(the_repository, "rebase",
17271744
_("Please commit or stash them."), 1, 1)) {
17281745
ret = -1;
1729-
goto cleanup;
1746+
goto cleanup_autostash;
17301747
}
17311748

17321749
/*
@@ -1749,7 +1766,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
17491766
if (options.switch_to) {
17501767
ret = checkout_up_to_date(&options);
17511768
if (ret)
1752-
goto cleanup;
1769+
goto cleanup_autostash;
17531770
}
17541771

17551772
if (!(options.flags & REBASE_NO_QUIET))
@@ -1775,8 +1792,10 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
17751792
/* If a hook exists, give it a chance to interrupt*/
17761793
if (!ok_to_skip_pre_rebase &&
17771794
run_hooks_l("pre-rebase", options.upstream_arg,
1778-
argc ? argv[0] : NULL, NULL))
1779-
die(_("The pre-rebase hook refused to rebase."));
1795+
argc ? argv[0] : NULL, NULL)) {
1796+
ret = error(_("The pre-rebase hook refused to rebase."));
1797+
goto cleanup_autostash;
1798+
}
17801799

17811800
if (options.flags & REBASE_DIFFSTAT) {
17821801
struct diff_options opts;
@@ -1821,9 +1840,10 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
18211840
RESET_HEAD_RUN_POST_CHECKOUT_HOOK;
18221841
ropts.head_msg = msg.buf;
18231842
ropts.default_reflog_action = options.reflog_action;
1824-
if (reset_head(the_repository, &ropts))
1825-
die(_("Could not detach HEAD"));
1826-
strbuf_release(&msg);
1843+
if (reset_head(the_repository, &ropts)) {
1844+
ret = error(_("Could not detach HEAD"));
1845+
goto cleanup_autostash;
1846+
}
18271847

18281848
/*
18291849
* If the onto is a proper descendant of the tip of the branch, then
@@ -1851,9 +1871,14 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
18511871

18521872
cleanup:
18531873
strbuf_release(&buf);
1874+
strbuf_release(&msg);
18541875
strbuf_release(&revisions);
18551876
rebase_options_release(&options);
18561877
free(squash_onto_name);
18571878
free(keep_base_onto_name);
18581879
return !!ret;
1880+
1881+
cleanup_autostash:
1882+
ret |= !!cleanup_autostash(&options);
1883+
goto cleanup;
18591884
}

t/t3400-rebase.sh

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,9 @@ test_expect_success 'Show verbose error when HEAD could not be detached' '
145145
test_when_finished "rm -f B" &&
146146
test_must_fail git rebase topic 2>output.err >output.out &&
147147
test_grep "The following untracked working tree files would be overwritten by checkout:" output.err &&
148-
test_grep B output.err
148+
test_grep B output.err &&
149+
test_must_fail git rebase --quit 2>err &&
150+
test_grep "no rebase in progress" err
149151
'
150152

151153
test_expect_success 'fail when upstream arg is missing and not on branch' '
@@ -422,7 +424,9 @@ test_expect_success 'refuse to switch to branch checked out elsewhere' '
422424
git checkout main &&
423425
git worktree add wt &&
424426
test_must_fail git -C wt rebase main main 2>err &&
425-
test_grep "already used by worktree at" err
427+
test_grep "already used by worktree at" err &&
428+
test_must_fail git -C wt rebase --quit 2>err &&
429+
test_grep "no rebase in progress" err
426430
'
427431

428432
test_expect_success 'rebase when inside worktree subdirectory' '

t/t3413-rebase-hook.sh

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,9 @@ test_expect_success 'pre-rebase hook stops rebase (1)' '
110110
git reset --hard side &&
111111
test_must_fail git rebase main &&
112112
test "z$(git symbolic-ref HEAD)" = zrefs/heads/test &&
113-
test 0 = $(git rev-list HEAD...side | wc -l)
113+
test 0 = $(git rev-list HEAD...side | wc -l) &&
114+
test_must_fail git rebase --quit 2>err &&
115+
test_grep "no rebase in progress" err
114116
'
115117

116118
test_expect_success 'pre-rebase hook stops rebase (2)' '

t/t3420-rebase-autostash.sh

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,46 @@ testrebase () {
8282
type=$1
8383
dotest=$2
8484

85+
test_expect_success "rebase$type: restore autostash when pre-rebase hook fails" '
86+
git checkout -f feature-branch &&
87+
test_hook pre-rebase <<-\EOF &&
88+
exit 1
89+
EOF
90+
91+
echo changed >file0 &&
92+
test_must_fail git rebase $type --autostash -f HEAD^ &&
93+
test_must_fail git rebase --quit 2>err &&
94+
test_grep "no rebase in progress" err &&
95+
echo changed >expect &&
96+
test_cmp expect file0
97+
'
98+
99+
test_expect_success "rebase$type: restore autostash when checkout onto fails" '
100+
git checkout -f --detach feature-branch &&
101+
echo uncommitted-content >file0 &&
102+
echo untracked >file4 &&
103+
test_when_finished "rm file4" &&
104+
test_must_fail git rebase $type --autostash \
105+
unrelated-onto-branch &&
106+
test_must_fail git rebase --quit 2>err &&
107+
test_grep "no rebase in progress" err &&
108+
echo uncommitted-content >expect &&
109+
test_cmp expect file0
110+
'
111+
112+
test_expect_success "rebase$type: restore autostash when branch checkout fails" '
113+
git checkout -f unrelated-onto-branch^ &&
114+
echo uncommitted-content >file0 &&
115+
echo untracked >file4 &&
116+
test_when_finished "rm file4" &&
117+
test_must_fail git rebase $type --autostash HEAD \
118+
unrelated-onto-branch &&
119+
test_must_fail git rebase --quit 2>err &&
120+
test_grep "no rebase in progress" err &&
121+
echo uncommitted-content >expect &&
122+
test_cmp expect file0
123+
'
124+
85125
test_expect_success "rebase$type: dirty worktree, --no-autostash" '
86126
test_config rebase.autostash true &&
87127
git reset --hard &&

0 commit comments

Comments
 (0)