Skip to content

Commit f3083fa

Browse files
checkout: add --autostash option for branch switching
When switching branches, local modifications in the working tree can prevent the checkout from succeeding. While "git rebase" and "git merge" already support --autostash to handle this case automatically, "git checkout" and "git switch" require users to manually stash and unstash their changes. Teach "git checkout" and "git switch" to accept --autostash and --no-autostash options that automatically create a temporary stash entry before the branch switch begins and apply it after the switch completes. If the stash application results in conflicts, the stash entry is saved to the stash list so the user can resolve them later. Also add a checkout.autoStash configuration option that enables this behavior by default, which can be overridden with --no-autostash on the command line. Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
1 parent 7f19e4e commit f3083fa

File tree

7 files changed

+229
-0
lines changed

7 files changed

+229
-0
lines changed

Documentation/config/checkout.adoc

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,18 @@ with a small number of cores, the default sequential checkout often performs
3636
better. The size and compression level of a repository might also influence how
3737
well the parallel version performs.
3838

39+
`checkout.autoStash`::
40+
When set to true, automatically create a temporary stash entry
41+
before the operation begins, and apply it after the operation
42+
ends. This means that you can run `git checkout` or `git switch`
43+
on a dirty worktree. However, use with care: the final stash
44+
application after a successful branch switch might result in
45+
non-trivial conflicts.
46+
This option can be overridden by the `--no-autostash` and
47+
`--autostash` options of linkgit:git-checkout[1] and
48+
linkgit:git-switch[1].
49+
Defaults to false.
50+
3951
`checkout.thresholdForParallelism`::
4052
When running parallel checkout with a small number of files, the cost
4153
of subprocess spawning and inter-process communication might outweigh

Documentation/git-checkout.adoc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,15 @@ When switching branches with `--merge`, staged changes may be lost.
272272
`merge.conflictStyle` configuration variable. Possible values are
273273
`merge` (default), `diff3`, and `zdiff3`.
274274
275+
`--autostash`::
276+
`--no-autostash`::
277+
When switching branches, automatically create a temporary stash
278+
entry before the operation begins, and apply it after the
279+
operation ends. This means that you can switch branches on a
280+
dirty worktree. However, use with care: the final stash
281+
application after a successful branch switch might result in
282+
non-trivial conflicts.
283+
275284
`-p`::
276285
`--patch`::
277286
Interactively select hunks in the difference between the

Documentation/git-switch.adoc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,15 @@ should result in deletion of the path).
142142
`merge.conflictStyle` configuration variable. Possible values are
143143
`merge` (default), `diff3`, and `zdiff3`.
144144

145+
`--autostash`::
146+
`--no-autostash`::
147+
Automatically create a temporary stash entry before the
148+
operation begins, and apply it after the operation ends.
149+
This means that you can switch branches on a dirty worktree.
150+
However, use with care: the final stash application after a
151+
successful branch switch might result in non-trivial
152+
conflicts.
153+
145154
`-q`::
146155
`--quiet`::
147156
Quiet, suppress feedback messages.

builtin/checkout.c

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
#include "repo-settings.h"
3131
#include "resolve-undo.h"
3232
#include "revision.h"
33+
#include "sequencer.h"
3334
#include "setup.h"
3435
#include "submodule.h"
3536
#include "symlinks.h"
@@ -68,6 +69,7 @@ struct checkout_opts {
6869
int only_merge_on_switching_branches;
6970
int can_switch_when_in_progress;
7071
int orphan_from_empty_tree;
72+
int autostash;
7173
int empty_pathspec_ok;
7274
int checkout_index;
7375
int checkout_worktree;
@@ -1202,9 +1204,16 @@ static int switch_branches(const struct checkout_opts *opts,
12021204
do_merge = 0;
12031205
}
12041206

1207+
if (opts->autostash) {
1208+
if (repo_read_index(the_repository) < 0)
1209+
die(_("index file corrupt"));
1210+
create_autostash_ref(the_repository, "CHECKOUT_AUTOSTASH");
1211+
}
1212+
12051213
if (do_merge) {
12061214
ret = merge_working_tree(opts, &old_branch_info, new_branch_info, &writeout_error);
12071215
if (ret) {
1216+
apply_autostash_ref(the_repository, "CHECKOUT_AUTOSTASH");
12081217
branch_info_release(&old_branch_info);
12091218
return ret;
12101219
}
@@ -1215,6 +1224,8 @@ static int switch_branches(const struct checkout_opts *opts,
12151224

12161225
update_refs_for_switch(opts, &old_branch_info, new_branch_info);
12171226

1227+
apply_autostash_ref(the_repository, "CHECKOUT_AUTOSTASH");
1228+
12181229
ret = post_checkout_hook(old_branch_info.commit, new_branch_info->commit, 1);
12191230
branch_info_release(&old_branch_info);
12201231

@@ -1236,6 +1247,10 @@ static int git_checkout_config(const char *var, const char *value,
12361247
opts->dwim_new_local_branch = git_config_bool(var, value);
12371248
return 0;
12381249
}
1250+
if (!strcmp(var, "checkout.autostash")) {
1251+
opts->autostash = git_config_bool(var, value);
1252+
return 0;
1253+
}
12391254

12401255
if (starts_with(var, "submodule."))
12411256
return git_default_submodule_config(var, value, NULL);
@@ -1745,6 +1760,7 @@ static struct option *add_common_switch_branch_options(
17451760
PARSE_OPT_NOCOMPLETE),
17461761
OPT_BOOL(0, "ignore-other-worktrees", &opts->ignore_other_worktrees,
17471762
N_("do not check if another worktree is using this branch")),
1763+
OPT_AUTOSTASH(&opts->autostash),
17481764
OPT_END()
17491765
};
17501766
struct option *newopts = parse_options_concat(prevopts, options);

t/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ integration_tests = [
275275
't2030-unresolve-info.sh',
276276
't2050-git-dir-relative.sh',
277277
't2060-switch.sh',
278+
't2061-switch-autostash.sh',
278279
't2070-restore.sh',
279280
't2071-restore-patch.sh',
280281
't2072-restore-pathspec-file.sh',

t/t2061-switch-autostash.sh

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
#!/bin/sh
2+
3+
test_description='checkout/switch --autostash tests'
4+
5+
GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
6+
export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
7+
8+
. ./test-lib.sh
9+
10+
test_expect_success 'setup' '
11+
echo file0content >file0 &&
12+
echo file1content >file1 &&
13+
git add . &&
14+
test_tick &&
15+
git commit -m "initial commit" &&
16+
git branch other-branch &&
17+
echo file1main >file1 &&
18+
git add . &&
19+
test_tick &&
20+
git commit -m "modify file1 on main" &&
21+
git checkout other-branch &&
22+
echo file1other >file1 &&
23+
git add . &&
24+
test_tick &&
25+
git commit -m "modify file1 on other-branch" &&
26+
echo file2content >file2 &&
27+
git add . &&
28+
test_tick &&
29+
git commit -m "add file2 on other-branch" &&
30+
git checkout main
31+
'
32+
33+
test_expect_success 'switch --autostash on dirty worktree' '
34+
git branch branch1 other-branch &&
35+
echo dirty >file0 &&
36+
git switch --autostash branch1 >actual 2>&1 &&
37+
test_grep "Created autostash" actual &&
38+
test_grep "Applied autostash" actual &&
39+
echo dirty >expected &&
40+
test_cmp expected file0 &&
41+
git switch main
42+
'
43+
44+
test_expect_success 'checkout --autostash on dirty worktree' '
45+
git branch branch2 other-branch &&
46+
echo dirty >file0 &&
47+
git checkout --autostash branch2 >actual 2>&1 &&
48+
test_grep "Created autostash" actual &&
49+
test_grep "Applied autostash" actual &&
50+
echo dirty >expected &&
51+
test_cmp expected file0 &&
52+
git checkout main
53+
'
54+
55+
test_expect_success 'switch: checkout.autostash config' '
56+
git branch branch3 other-branch &&
57+
echo dirty >file0 &&
58+
test_config checkout.autostash true &&
59+
git switch branch3 >actual 2>&1 &&
60+
test_grep "Created autostash" actual &&
61+
test_grep "Applied autostash" actual &&
62+
echo dirty >expected &&
63+
test_cmp expected file0 &&
64+
git switch main
65+
'
66+
67+
test_expect_success 'checkout: checkout.autostash config' '
68+
git branch branch4 other-branch &&
69+
echo dirty >file0 &&
70+
test_config checkout.autostash true &&
71+
git checkout branch4 >actual 2>&1 &&
72+
test_grep "Created autostash" actual &&
73+
test_grep "Applied autostash" actual &&
74+
echo dirty >expected &&
75+
test_cmp expected file0 &&
76+
git checkout main
77+
'
78+
79+
test_expect_success '--no-autostash overrides checkout.autostash' '
80+
git branch branch5 other-branch &&
81+
echo dirty >file1 &&
82+
test_config checkout.autostash true &&
83+
test_must_fail git switch --no-autostash branch5 2>stderr &&
84+
test_grep ! "Created autostash" stderr &&
85+
git checkout -- file1
86+
'
87+
88+
test_expect_success '--autostash overrides checkout.autostash=false' '
89+
git branch branch6 other-branch &&
90+
echo dirty >file0 &&
91+
test_config checkout.autostash false &&
92+
git switch --autostash branch6 >actual 2>&1 &&
93+
test_grep "Created autostash" actual &&
94+
test_grep "Applied autostash" actual &&
95+
echo dirty >expected &&
96+
test_cmp expected file0 &&
97+
git switch main
98+
'
99+
100+
test_expect_success 'autostash with dirty index' '
101+
git branch branch7 other-branch &&
102+
echo dirty-index >file0 &&
103+
git add file0 &&
104+
git switch --autostash branch7 >actual 2>&1 &&
105+
test_grep "Created autostash" actual &&
106+
test_grep "Applied autostash" actual &&
107+
echo dirty-index >expected &&
108+
test_cmp expected file0 &&
109+
git checkout -- file0 &&
110+
git switch main
111+
'
112+
113+
test_expect_success 'autostash bypasses conflicting local changes' '
114+
git branch branch8 other-branch &&
115+
echo dirty >file1 &&
116+
test_must_fail git switch branch8 2>stderr &&
117+
test_grep "Your local changes" stderr &&
118+
git switch --autostash branch8 >actual 2>&1 &&
119+
test_grep "Created autostash" actual &&
120+
test_grep "Applying autostash resulted in conflicts" actual &&
121+
test_grep "Your changes are safe in the stash" actual &&
122+
git stash drop &&
123+
git reset --hard &&
124+
git switch main
125+
'
126+
127+
test_expect_success 'autostash is a no-op with clean worktree' '
128+
git branch branch9 other-branch &&
129+
git switch --autostash branch9 >actual 2>&1 &&
130+
test_grep ! "Created autostash" actual &&
131+
git switch main
132+
'
133+
134+
test_expect_success '--autostash with --merge stashes and switches' '
135+
git branch branch10 other-branch &&
136+
echo dirty >file0 &&
137+
git switch --autostash --merge branch10 >actual 2>&1 &&
138+
test_grep "Created autostash" actual &&
139+
test_grep "Applied autostash" actual &&
140+
echo dirty >expected &&
141+
test_cmp expected file0 &&
142+
git switch main
143+
'
144+
145+
test_expect_success 'autostash with staged conflicting changes' '
146+
git branch branch11 other-branch &&
147+
echo staged-change >file1 &&
148+
git add file1 &&
149+
git switch --autostash branch11 >actual 2>&1 &&
150+
test_grep "Created autostash" actual &&
151+
test_grep "Applying autostash resulted in conflicts" actual &&
152+
test_grep "Your changes are safe in the stash" actual &&
153+
git stash drop &&
154+
git reset --hard &&
155+
git switch main
156+
'
157+
158+
test_expect_success '--autostash with --force preserves dirty changes' '
159+
git branch branch12 other-branch &&
160+
echo dirty-force >file1 &&
161+
git switch --autostash --force branch12 >actual 2>&1 &&
162+
test_grep "Created autostash" actual &&
163+
test_grep "Applying autostash resulted in conflicts" actual &&
164+
test_grep "Your changes are safe in the stash" actual &&
165+
git stash drop &&
166+
git reset --hard &&
167+
git switch main
168+
'
169+
170+
test_expect_success '--autostash with new branch creation' '
171+
echo dirty >file0 &&
172+
git switch --autostash -c branch13 >actual 2>&1 &&
173+
test_grep "Created autostash" actual &&
174+
test_grep "Applied autostash" actual &&
175+
echo dirty >expected &&
176+
test_cmp expected file0 &&
177+
git switch main &&
178+
git branch -D branch13
179+
'
180+
181+
test_done

t/t9902-completion.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2602,6 +2602,7 @@ test_expect_success 'double dash "git checkout"' '
26022602
--ignore-other-worktrees Z
26032603
--recurse-submodules Z
26042604
--auto-advance Z
2605+
--autostash Z
26052606
--progress Z
26062607
--guess Z
26072608
--no-guess Z

0 commit comments

Comments
 (0)