checkout -m: autostash when switching branches

When switching branches with "git checkout -m", the attempted merge of local modifications may cause conflicts with the changes made on the other branch, which the user may not want to (or may not be able to) resolve right now. Because there is no easy way to recover from this situation, we discouraged users from using "checkout -m" unless they are certain their changes are trivial and within their ability to resolve conflicts. Teach the -m flow to create a temporary stash before switching and reapply it after. On success, the stash is silently applied and the list of locally modified paths is shown, same as a successful "git checkout" without "-m". If reapplying causes conflicts, the stash is kept and the user is told they can resolve and run "git stash drop", or run "git reset --hard" and later "git stash pop" to recover their changes. Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Harald Nordgren committed Apr 28, 2026 at 18:39 UTC c07039ebc4bbf2eb6c852fb1280891a448d1bf48
9 files changed +219 -150
Documentation/git-checkout.adoc
+26 -29
@@ -251,20 +251,19 @@ working tree, by copying them from elsewhere, extracting a tarball, etc.
251 are different between the current branch and the branch to
252 which you are switching, the command refuses to switch
253 branches in order to preserve your modifications in context.
254 - However, with this option, a three-way merge between the current
255 - branch, your working tree contents, and the new branch
256 - is done, and you will be on the new branch.
257 -+
258 -When a merge conflict happens, the index entries for conflicting
259 -paths are left unmerged, and you need to resolve the conflicts
260 -and mark the resolved paths with `git add` (or `git rm` if the merge
261 -should result in deletion of the path).
254 + With this option, the conflicting local changes are
255 + automatically stashed before the switch and reapplied
256 + afterwards. If the local changes do not overlap with the
257 + differences between branches, the switch proceeds without
258 + stashing. If reapplying the stash results in conflicts, the
259 + entry is saved to the stash list. Resolve the conflicts
260 + and run `git stash drop` when done, or clear the working
261 + tree (e.g. with `git reset --hard`) before running `git stash
262 + pop` later to re-apply your changes.
263 +
264 When checking out paths from the index, this option lets you recreate
265 the conflicted merge in the specified paths. This option cannot be
266 used when checking out paths from a tree-ish.
266 -+
267 -When switching branches with `--merge`, staged changes may be lost.
267
268 `--conflict=<style>`::
269 The same as `--merge` option above, but changes the way the
@@ -578,38 +577,36 @@ $ git checkout mytopic
577 error: You have local changes to 'frotz'; not switching branches.
578 ------------
579
581 -You can give the `-m` flag to the command, which would try a
582 -three-way merge:
580 +You can give the `-m` flag to the command, which will carry your local
581 +changes to the new branch:
582
583 ------------
584 $ git checkout -m mytopic
586 -Auto-merging frotz
585 +Applied autostash.
586 +Switched to branch 'mytopic'
587 +The following paths have local changes:
588 +M frotz
589 ------------
590
589 -After this three-way merge, the local modifications are _not_
591 +After the switch, the local modifications are reapplied and are _not_
592 registered in your index file, so `git diff` would show you what
593 changes you made since the tip of the new branch.
594
595 === 3. Merge conflict
596
595 -When a merge conflict happens during switching branches with
596 -the `-m` option, you would see something like this:
597 +When the `--merge` (`-m`) option is given and the local changes
598 +overlap with the changes in the branch we're switching to, the
599 +changes are stashed and reapplied after the switch. If this
600 +process results in conflicts, the stash entry is saved and a
601 +message is printed:
602
603 ------------
604 $ git checkout -m mytopic
600 -Auto-merging frotz
601 -ERROR: Merge conflict in frotz
602 -fatal: merge program failed
603 -------------
604 -
605 -At this point, `git diff` shows the changes cleanly merged as in
606 -the previous example, as well as the changes in the conflicted
607 -files. Edit and resolve the conflict and mark it resolved with
608 -`git add` as usual:
609 -
610 -------------
611 -$ edit frotz
612 -$ git add frotz
605 +Your local changes are stashed, however applying them
606 +resulted in conflicts. You can either resolve the conflicts
607 +and then discard the stash with "git stash drop", or, if you
608 +do not want to resolve them now, run "git reset --hard" and
609 +apply the local changes later by running "git stash pop".
610 ------------
611
612 CONFIGURATION
Documentation/git-switch.adoc
+20 -16
@@ -123,18 +123,19 @@ variable.
123
124 `-m`::
125 `--merge`::
126 - If you have local modifications to one or more files that are
127 - different between the current branch and the branch to which
128 - you are switching, the command refuses to switch branches in
129 - order to preserve your modifications in context. However,
130 - with this option, a three-way merge between the current
131 - branch, your working tree contents, and the new branch is
132 - done, and you will be on the new branch.
133 -+
134 -When a merge conflict happens, the index entries for conflicting
135 -paths are left unmerged, and you need to resolve the conflicts
136 -and mark the resolved paths with `git add` (or `git rm` if the merge
137 -should result in deletion of the path).
126 + If you have local modifications to one or more files that
127 + are different between the current branch and the branch to
128 + which you are switching, the command normally refuses to
129 + switch branches in order to preserve your modifications in
130 + context. However, with this option, the conflicting local
131 + changes are automatically stashed before the switch and
132 + reapplied afterwards. If the local changes do not overlap
133 + with the differences between branches, the switch proceeds
134 + without stashing. If reapplying the stash results in
135 + conflicts, the entry is saved to the stash list. Resolve
136 + the conflicts and run `git stash drop` when done, or clear
137 + the working tree (e.g. with `git reset --hard`) before
138 + running `git stash pop` later to re-apply your changes.
139
140 `--conflict=<style>`::
141 The same as `--merge` option above, but changes the way the
@@ -217,15 +218,18 @@ $ git switch mytopic
218 error: You have local changes to 'frotz'; not switching branches.
219 ------------
220
220 -You can give the `-m` flag to the command, which would try a three-way
221 -merge:
221 +You can give the `-m` flag to the command, which will carry your local
222 +changes to the new branch:
223
224 ------------
225 $ git switch -m mytopic
225 -Auto-merging frotz
226 +Applied autostash.
227 +Switched to branch 'mytopic'
228 +The following paths have local changes:
229 +M frotz
230 ------------
231
228 -After this three-way merge, the local modifications are _not_
232 +After the switch, the local modifications are reapplied and are _not_
233 registered in your index file, so `git diff` would show you what
234 changes you made since the tip of the new branch.
235
builtin/checkout.c
+69 -92
@@ -17,7 +17,6 @@
17 #include "merge-ll.h"
18 #include "lockfile.h"
19 #include "mem-pool.h"
20 -#include "merge-ort-wrappers.h"
20 #include "object-file.h"
21 #include "object-name.h"
22 #include "odb.h"
@@ -30,6 +29,7 @@
29 #include "repo-settings.h"
30 #include "resolve-undo.h"
31 #include "revision.h"
32 +#include "sequencer.h"
33 #include "setup.h"
34 #include "submodule.h"
35 #include "symlinks.h"
@@ -99,6 +99,8 @@ struct checkout_opts {
99 .auto_advance = 1, \
100 }
101
102 +#define MERGE_WORKING_TREE_UNPACK_FAILED (-2)
103 +
104 struct branch_info {
105 char *name; /* The short name used */
106 char *path; /* The full name of a real branch */
@@ -753,9 +755,9 @@ static void setup_branch_path(struct branch_info *branch)
755 branch->path = strbuf_detach(&buf, NULL);
756 }
757
756 -static void init_topts(struct unpack_trees_options *topts, int merge,
758 +static void init_topts(struct unpack_trees_options *topts,
759 int show_progress, int overwrite_ignore,
758 - struct commit *old_commit)
760 + bool quiet)
761 {
762 memset(topts, 0, sizeof(*topts));
763 topts->head_idx = -1;
@@ -767,7 +769,7 @@ static void init_topts(struct unpack_trees_options *topts, int merge,
769 topts->initial_checkout = is_index_unborn(the_repository->index);
770 topts->update = 1;
771 topts->merge = 1;
770 - topts->quiet = merge && old_commit;
772 + topts->quiet = quiet;
773 topts->verbose_update = show_progress;
774 topts->fn = twoway_merge;
775 topts->preserve_ignored = !overwrite_ignore;
@@ -776,6 +778,7 @@ static void init_topts(struct unpack_trees_options *topts, int merge,
778 static int merge_working_tree(const struct checkout_opts *opts,
779 struct branch_info *old_branch_info,
780 struct branch_info *new_branch_info,
781 + bool quiet,
782 int *writeout_error)
783 {
784 int ret;
@@ -826,8 +829,8 @@ static int merge_working_tree(const struct checkout_opts *opts,
829 }
830
831 /* 2-way merge to the new branch */
829 - init_topts(&topts, opts->merge, opts->show_progress,
830 - opts->overwrite_ignore, old_branch_info->commit);
832 + init_topts(&topts, opts->show_progress,
833 + opts->overwrite_ignore, quiet);
834 init_checkout_metadata(&topts.meta, new_branch_info->refname,
835 new_branch_info->commit ?
836 &new_branch_info->commit->object.oid :
@@ -853,90 +856,8 @@ static int merge_working_tree(const struct checkout_opts *opts,
856 ret = unpack_trees(2, trees, &topts);
857 clear_unpack_trees_porcelain(&topts);
858 if (ret == -1) {
856 - /*
857 - * Unpack couldn't do a trivial merge; either
858 - * give up or do a real merge, depending on
859 - * whether the merge flag was used.
860 - */
861 - struct tree *work;
862 - struct tree *old_tree;
863 - struct merge_options o;
864 - struct strbuf sb = STRBUF_INIT;
865 - struct strbuf old_commit_shortname = STRBUF_INIT;
866 -
867 - if (!opts->merge) {
868 - rollback_lock_file(&lock_file);
869 - return 1;
870 - }
871 -
872 - /*
873 - * Without old_branch_info->commit, the below is the same as
874 - * the two-tree unpack we already tried and failed.
875 - */
876 - if (!old_branch_info->commit) {
877 - rollback_lock_file(&lock_file);
878 - return 1;
879 - }
880 - old_tree = repo_get_commit_tree(the_repository,
881 - old_branch_info->commit);
882 -
883 - if (repo_index_has_changes(the_repository, old_tree, &sb))
884 - die(_("cannot continue with staged changes in "
885 - "the following files:\n%s"), sb.buf);
886 - strbuf_release(&sb);
887 -
888 - /* Do more real merge */
889 -
890 - /*
891 - * We update the index fully, then write the
892 - * tree from the index, then merge the new
893 - * branch with the current tree, with the old
894 - * branch as the base. Then we reset the index
895 - * (but not the working tree) to the new
896 - * branch, leaving the working tree as the
897 - * merged version, but skipping unmerged
898 - * entries in the index.
899 - */
900 -
901 - add_files_to_cache(the_repository, NULL, NULL, NULL, 0,
902 - 0, 0);
903 - init_ui_merge_options(&o, the_repository);
904 - o.verbosity = 0;
905 - work = write_in_core_index_as_tree(the_repository,
906 - the_repository->index);
907 -
908 - ret = reset_tree(new_tree,
909 - opts, 1,
910 - writeout_error, new_branch_info);
911 - if (ret) {
912 - rollback_lock_file(&lock_file);
913 - return ret;
914 - }
915 - o.ancestor = old_branch_info->name;
916 - if (!old_branch_info->name) {
917 - strbuf_add_unique_abbrev(&old_commit_shortname,
918 - &old_branch_info->commit->object.oid,
919 - DEFAULT_ABBREV);
920 - o.ancestor = old_commit_shortname.buf;
921 - }
922 - o.branch1 = new_branch_info->name;
923 - o.branch2 = "local";
924 - o.conflict_style = opts->conflict_style;
925 - ret = merge_ort_nonrecursive(&o,
926 - new_tree,
927 - work,
928 - old_tree);
929 - if (ret < 0)
930 - die(NULL);
931 - ret = reset_tree(new_tree,
932 - opts, 0,
933 - writeout_error, new_branch_info);
934 - strbuf_release(&o.obuf);
935 - strbuf_release(&old_commit_shortname);
936 - if (ret) {
937 - rollback_lock_file(&lock_file);
938 - return ret;
939 - }
859 + rollback_lock_file(&lock_file);
860 + return MERGE_WORKING_TREE_UNPACK_FAILED;
861 }
862 }
863
@@ -1181,6 +1102,10 @@ static int switch_branches(const struct checkout_opts *opts,
1102 struct object_id rev;
1103 int flag, writeout_error = 0;
1104 int do_merge = 1;
1105 + int created_autostash = 0;
1106 + struct strbuf old_commit_shortname = STRBUF_INIT;
1107 + struct strbuf autostash_msg = STRBUF_INIT;
1108 + const char *stash_label_base = NULL;
1109
1110 trace2_cmd_mode("branch");
1111
@@ -1218,11 +1143,49 @@ static int switch_branches(const struct checkout_opts *opts,
1143 do_merge = 0;
1144 }
1145
1146 + if (old_branch_info.name) {
1147 + stash_label_base = old_branch_info.name;
1148 + } else if (old_branch_info.commit) {
1149 + strbuf_add_unique_abbrev(&old_commit_shortname,
1150 + &old_branch_info.commit->object.oid,
1151 + DEFAULT_ABBREV);
1152 + stash_label_base = old_commit_shortname.buf;
1153 + }
1154 +
1155 if (do_merge) {
1222 - ret = merge_working_tree(opts, &old_branch_info, new_branch_info, &writeout_error);
1156 + ret = merge_working_tree(opts, &old_branch_info, new_branch_info,
1157 + opts->merge, &writeout_error);
1158 + if (ret == MERGE_WORKING_TREE_UNPACK_FAILED && opts->merge) {
1159 + strbuf_addf(&autostash_msg,
1160 + "autostash while switching to '%s'",
1161 + new_branch_info->name);
1162 + create_autostash_ref(the_repository,
1163 + "CHECKOUT_AUTOSTASH_HEAD",
1164 + autostash_msg.buf, true);
1165 + created_autostash = 1;
1166 + ret = merge_working_tree(opts, &old_branch_info, new_branch_info,
1167 + false, &writeout_error);
1168 + }
1169 + if (created_autostash) {
1170 + if (opts->conflict_style >= 0) {
1171 + struct strbuf cfg = STRBUF_INIT;
1172 + strbuf_addf(&cfg, "merge.conflictStyle=%s",
1173 + conflict_style_name(opts->conflict_style));
1174 + git_config_push_parameter(cfg.buf);
1175 + strbuf_release(&cfg);
1176 + }
1177 + apply_autostash_ref(the_repository,
1178 + "CHECKOUT_AUTOSTASH_HEAD",
1179 + new_branch_info->name,
1180 + "local",
1181 + stash_label_base,
1182 + autostash_msg.buf);
1183 + }
1184 if (ret) {
1185 branch_info_release(&old_branch_info);
1225 - return ret;
1186 + strbuf_release(&old_commit_shortname);
1187 + strbuf_release(&autostash_msg);
1188 + return ret < 0 ? 1 : ret;
1189 }
1190 }
1191
@@ -1231,8 +1194,22 @@ static int switch_branches(const struct checkout_opts *opts,
1194
1195 update_refs_for_switch(opts, &old_branch_info, new_branch_info);
1196
1197 + if (created_autostash) {
1198 + discard_index(the_repository->index);
1199 + if (repo_read_index(the_repository) < 0)
1200 + die(_("index file corrupt"));
1201 +
1202 + if (!opts->quiet && new_branch_info->commit) {
1203 + printf(_("The following paths have local changes:\n"));
1204 + show_local_changes(&new_branch_info->commit->object,
1205 + &opts->diff_options);
1206 + }
1207 + }
1208 +
1209 ret = post_checkout_hook(old_branch_info.commit, new_branch_info->commit, 1);
1210 branch_info_release(&old_branch_info);
1211 + strbuf_release(&old_commit_shortname);
1212 + strbuf_release(&autostash_msg);
1213
1214 return ret || writeout_error;
1215 }
sequencer.c
+9 -5
@@ -4765,15 +4765,19 @@ static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply,
4765 strvec_push(&store.args, stash_oid);
4766 if (run_command(&store))
4767 ret = error(_("cannot store %s"), stash_oid);
4768 + else if (attempt_apply)
4769 + fprintf(stderr,
4770 + _("Your local changes are stashed, however applying them\n"
4771 + "resulted in conflicts. You can either resolve the conflicts\n"
4772 + "and then discard the stash with \"git stash drop\", or, if you\n"
4773 + "do not want to resolve them now, run \"git reset --hard\" and\n"
4774 + "apply the local changes later by running \"git stash pop\".\n"));
4775 else
4776 fprintf(stderr,
4770 - _("%s\n"
4777 + _("Autostash exists; creating a new stash entry.\n"
4778 "Your changes are safe in the stash.\n"
4779 "You can run \"git stash pop\" or"
4773 - " \"git stash drop\" at any time.\n"),
4774 - attempt_apply ?
4775 - _("Applying autostash resulted in conflicts.") :
4776 - _("Autostash exists; creating a new stash entry."));
4780 + " \"git stash drop\" at any time.\n"));
4781 }
4782
4783 return ret;
t/t3420-rebase-autostash.sh
+10 -6
@@ -61,18 +61,22 @@ create_expected_failure_apply () {
61 First, rewinding head to replay your work on top of it...
62 Applying: second commit
63 Applying: third commit
64 - Applying autostash resulted in conflicts.
65 - Your changes are safe in the stash.
66 - You can run "git stash pop" or "git stash drop" at any time.
64 + Your local changes are stashed, however applying them
65 + resulted in conflicts. You can either resolve the conflicts
66 + and then discard the stash with "git stash drop", or, if you
67 + do not want to resolve them now, run "git reset --hard" and
68 + apply the local changes later by running "git stash pop".
69 EOF
70 }
71
72 create_expected_failure_merge () {
73 cat >expected <<-EOF
74 $(grep "^Created autostash: [0-9a-f][0-9a-f]*\$" actual)
73 - Applying autostash resulted in conflicts.
74 - Your changes are safe in the stash.
75 - You can run "git stash pop" or "git stash drop" at any time.
75 + Your local changes are stashed, however applying them
76 + resulted in conflicts. You can either resolve the conflicts
77 + and then discard the stash with "git stash drop", or, if you
78 + do not want to resolve them now, run "git reset --hard" and
79 + apply the local changes later by running "git stash pop".
80 Successfully rebased and updated refs/heads/rebased-feature-branch.
81 EOF
82 }
t/t7201-co.sh
+70 -1
@@ -102,7 +102,10 @@ test_expect_success 'checkout -m with dirty tree' '
102
103 test "$(git symbolic-ref HEAD)" = "refs/heads/side" &&
104
105 - printf "M\t%s\n" one >expect.messages &&
105 + cat >expect.messages <<-\EOF &&
106 + The following paths have local changes:
107 + M one
108 + EOF
109 test_cmp expect.messages messages &&
110
111 fill "M one" "A three" "D two" >expect.main &&
@@ -210,6 +213,72 @@ test_expect_success 'checkout --merge --conflict=diff3 <branch>' '
213 test_cmp expect two
214 '
215
216 +test_expect_success 'checkout -m with mixed staged and unstaged changes' '
217 + git checkout -f main &&
218 + git clean -f &&
219 +
220 + fill 0 x y z >same &&
221 + git add same &&
222 + fill 1 2 3 4 5 6 7 >one &&
223 + git checkout -m side >actual 2>&1 &&
224 + test_grep "Applied autostash" actual &&
225 + fill 0 x y z >expect &&
226 + test_cmp expect same &&
227 + fill 1 2 3 4 5 6 7 >expect &&
228 + test_cmp expect one
229 +'
230 +
231 +test_expect_success 'checkout -m creates a recoverable stash on conflict' '
232 + git checkout -f main &&
233 + git clean -f &&
234 +
235 + fill 1 2 3 4 5 >one &&
236 + test_must_fail git checkout side 2>stderr &&
237 + test_grep "Your local changes" stderr &&
238 + git checkout -m side >actual 2>&1 &&
239 + test_grep "resulted in conflicts" actual &&
240 + test_grep "git stash drop" actual &&
241 + test_grep "git stash pop" actual &&
242 + test_grep "The following paths have local changes" actual &&
243 + git log -p -1 --format="%gs%n%B" -g --diff-merges=1 refs/stash >actual &&
244 + sed /^index/d actual >actual.trimmed &&
245 + cat >expect <<-EOF &&
246 + autostash while switching to ${SQ}side${SQ}
247 + On main: autostash while switching to ${SQ}side${SQ}
248 +
249 + diff --git a/one b/one
250 + --- a/one
251 + +++ b/one
252 + @@ -3,6 +3,3 @@
253 + 3
254 + 4
255 + 5
256 + -6
257 + -7
258 + -8
259 + EOF
260 + test_cmp expect actual.trimmed &&
261 + git stash drop &&
262 + git reset --hard
263 +'
264 +
265 +test_expect_success 'checkout -m which would overwrite untracked file' '
266 + git checkout -f --detach main &&
267 + test_commit another-file &&
268 + git checkout HEAD^ &&
269 + >another-file.t &&
270 + fill 1 2 3 4 5 >one &&
271 + test_must_fail git checkout -m @{-1} 2>err &&
272 + q_to_tab >expect <<-\EOF &&
273 + error: The following untracked working tree files would be overwritten by checkout:
274 + Qanother-file.t
275 + Please move or remove them before you switch branches.
276 + Aborting
277 + Applied autostash.
278 + EOF
279 + test_cmp expect err
280 +'
281 +
282 test_expect_success 'switch to another branch while carrying a deletion' '
283 git checkout -f main &&
284 git reset --hard &&
t/t7600-merge.sh
+2 -1
@@ -914,7 +914,8 @@ test_expect_success 'merge with conflicted --autostash changes' '
914 git diff >expect &&
915 test_when_finished "test_might_fail git stash drop" &&
916 git merge --autostash c3 2>err &&
917 - test_grep "Applying autostash resulted in conflicts." err &&
917 + test_grep "applying them" err &&
918 + test_grep "resulted in conflicts" err &&
919 git show HEAD:file >merge-result &&
920 test_cmp result.1-9 merge-result &&
921 git stash show -p >actual &&
xdiff-interface.c
+12
@@ -325,6 +325,18 @@ int parse_conflict_style_name(const char *value)
325 return -1;
326 }
327
328 +const char *conflict_style_name(int style)
329 +{
330 + switch (style) {
331 + case XDL_MERGE_DIFF3:
332 + return "diff3";
333 + case XDL_MERGE_ZEALOUS_DIFF3:
334 + return "zdiff3";
335 + default:
336 + return "merge";
337 + }
338 +}
339 +
340 int git_xmerge_style = -1;
341
342 int git_xmerge_config(const char *var, const char *value,
xdiff-interface.h
+1
@@ -55,6 +55,7 @@ void xdiff_set_find_func(xdemitconf_t *xecfg, const char *line, int cflags);
55 void xdiff_clear_find_func(xdemitconf_t *xecfg);
56 struct config_context;
57 int parse_conflict_style_name(const char *value);
58 +const char *conflict_style_name(int style);
59 int git_xmerge_config(const char *var, const char *value,
60 const struct config_context *ctx, void *cb);
61 extern int git_xmerge_style;