Skip to content

Commit 5d49c00

Browse files
checkout: -m (--merge) uses autostash when switching branches
When switching branches with "git checkout -m", local modifications can block the switch. Teach the -m flow to create a temporary stash before switching and reapply it after. On success, only "Applied autostash." is shown. 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>
1 parent dc6ecd5 commit 5d49c00

File tree

11 files changed

+412
-151
lines changed

11 files changed

+412
-151
lines changed

Documentation/git-checkout.adoc

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -251,20 +251,17 @@ working tree, by copying them from elsewhere, extracting a tarball, etc.
251251
are different between the current branch and the branch to
252252
which you are switching, the command refuses to switch
253253
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 so you can use `git stash
260+
pop` to recover and `git stash drop` when done.
262261
+
263262
When checking out paths from the index, this option lets you recreate
264263
the conflicted merge in the specified paths. This option cannot be
265264
used when checking out paths from a tree-ish.
266-
+
267-
When switching branches with `--merge`, staged changes may be lost.
268265
269266
`--conflict=<style>`::
270267
The same as `--merge` option above, but changes the way the
@@ -578,39 +575,44 @@ $ git checkout mytopic
578575
error: You have local changes to 'frotz'; not switching branches.
579576
------------
580577
581-
You can give the `-m` flag to the command, which would try a
582-
three-way merge:
578+
You can give the `-m` flag to the command, which would save the local
579+
changes in a stash entry and reset the working tree to allow switching:
583580
584581
------------
585582
$ git checkout -m mytopic
586-
Auto-merging frotz
583+
Applied autostash.
587584
------------
588585
589-
After this three-way merge, the local modifications are _not_
586+
After the switch, the local modifications are reapplied and are _not_
590587
registered in your index file, so `git diff` would show you what
591588
changes you made since the tip of the new branch.
592589
593590
=== 3. Merge conflict
594591
595-
When a merge conflict happens during switching branches with
596-
the `-m` option, you would see something like this:
592+
When the `--merge` (`-m`) option is in effect and the locally
593+
modified files overlap with files that need to be updated by the
594+
branch switch, the changes are stashed and reapplied after the
595+
switch. If the stash application results in conflicts, they are not
596+
resolved and the stash is saved to the stash list:
597597
598598
------------
599599
$ git checkout -m mytopic
600-
Auto-merging frotz
601-
ERROR: Merge conflict in frotz
602-
fatal: merge program failed
603-
------------
600+
Your local changes are stashed, however, applying it to carry
601+
forward your local changes resulted in conflicts:
604602
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:
603+
- You can try resolving them now. If you resolved them
604+
successfully, discard the stash entry with "git stash drop".
609605
606+
- Alternatively you can "git reset --hard" if you do not want
607+
to deal with them right now, and later "git stash pop" to
608+
recover your local changes.
610609
------------
611-
$ edit frotz
612-
$ git add frotz
613-
------------
610+
611+
You can try resolving the conflicts now. Edit the conflicting files
612+
and mark them resolved with `git add` as usual, then run `git stash
613+
drop` to discard the stash entry. Alternatively, you can clear the
614+
working tree with `git reset --hard` and recover your local changes
615+
later with `git stash pop`.
614616
615617
CONFIGURATION
616618
-------------

Documentation/git-switch.adoc

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -126,15 +126,14 @@ variable.
126126
If you have local modifications to one or more files that are
127127
different between the current branch and the branch to which
128128
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).
129+
order to preserve your modifications in context. With this
130+
option, the conflicting local changes are automatically
131+
stashed before the switch and reapplied afterwards. If the
132+
local changes do not overlap with the differences between
133+
branches, the switch proceeds without stashing. If
134+
reapplying the stash results in conflicts, the entry is
135+
saved to the stash list so you can use `git stash pop` to
136+
recover and `git stash drop` when done.
138137

139138
`--conflict=<style>`::
140139
The same as `--merge` option above, but changes the way the
@@ -217,15 +216,16 @@ $ git switch mytopic
217216
error: You have local changes to 'frotz'; not switching branches.
218217
------------
219218
220-
You can give the `-m` flag to the command, which would try a three-way
221-
merge:
219+
You can give the `-m` flag to the command, which would save the local
220+
changes in a stash entry and reset the working tree to allow switching:
222221
223222
------------
224223
$ git switch -m mytopic
225-
Auto-merging frotz
224+
Created autostash: 7a9afa3
225+
Applied autostash.
226226
------------
227227
228-
After this three-way merge, the local modifications are _not_
228+
After the switch, the local modifications are reapplied and are _not_
229229
registered in your index file, so `git diff` would show you what
230230
changes you made since the tip of the new branch.
231231

builtin/checkout.c

Lines changed: 98 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
#include "merge-ll.h"
1818
#include "lockfile.h"
1919
#include "mem-pool.h"
20-
#include "merge-ort-wrappers.h"
2120
#include "object-file.h"
2221
#include "object-name.h"
2322
#include "odb.h"
@@ -30,6 +29,7 @@
3029
#include "repo-settings.h"
3130
#include "resolve-undo.h"
3231
#include "revision.h"
32+
#include "sequencer.h"
3333
#include "setup.h"
3434
#include "submodule.h"
3535
#include "symlinks.h"
@@ -845,83 +845,8 @@ static int merge_working_tree(const struct checkout_opts *opts,
845845

846846
ret = unpack_trees(2, trees, &topts);
847847
clear_unpack_trees_porcelain(&topts);
848-
if (ret == -1) {
849-
/*
850-
* Unpack couldn't do a trivial merge; either
851-
* give up or do a real merge, depending on
852-
* whether the merge flag was used.
853-
*/
854-
struct tree *work;
855-
struct tree *old_tree;
856-
struct merge_options o;
857-
struct strbuf sb = STRBUF_INIT;
858-
struct strbuf old_commit_shortname = STRBUF_INIT;
859-
860-
if (!opts->merge)
861-
return 1;
862-
863-
/*
864-
* Without old_branch_info->commit, the below is the same as
865-
* the two-tree unpack we already tried and failed.
866-
*/
867-
if (!old_branch_info->commit)
868-
return 1;
869-
old_tree = repo_get_commit_tree(the_repository,
870-
old_branch_info->commit);
871-
872-
if (repo_index_has_changes(the_repository, old_tree, &sb))
873-
die(_("cannot continue with staged changes in "
874-
"the following files:\n%s"), sb.buf);
875-
strbuf_release(&sb);
876-
877-
/* Do more real merge */
878-
879-
/*
880-
* We update the index fully, then write the
881-
* tree from the index, then merge the new
882-
* branch with the current tree, with the old
883-
* branch as the base. Then we reset the index
884-
* (but not the working tree) to the new
885-
* branch, leaving the working tree as the
886-
* merged version, but skipping unmerged
887-
* entries in the index.
888-
*/
889-
890-
add_files_to_cache(the_repository, NULL, NULL, NULL, 0,
891-
0, 0);
892-
init_ui_merge_options(&o, the_repository);
893-
o.verbosity = 0;
894-
work = write_in_core_index_as_tree(the_repository);
895-
896-
ret = reset_tree(new_tree,
897-
opts, 1,
898-
writeout_error, new_branch_info);
899-
if (ret)
900-
return ret;
901-
o.ancestor = old_branch_info->name;
902-
if (!old_branch_info->name) {
903-
strbuf_add_unique_abbrev(&old_commit_shortname,
904-
&old_branch_info->commit->object.oid,
905-
DEFAULT_ABBREV);
906-
o.ancestor = old_commit_shortname.buf;
907-
}
908-
o.branch1 = new_branch_info->name;
909-
o.branch2 = "local";
910-
o.conflict_style = opts->conflict_style;
911-
ret = merge_ort_nonrecursive(&o,
912-
new_tree,
913-
work,
914-
old_tree);
915-
if (ret < 0)
916-
die(NULL);
917-
ret = reset_tree(new_tree,
918-
opts, 0,
919-
writeout_error, new_branch_info);
920-
strbuf_release(&o.obuf);
921-
strbuf_release(&old_commit_shortname);
922-
if (ret)
923-
return ret;
924-
}
848+
if (ret == -1)
849+
return 1;
925850
}
926851

927852
if (!cache_tree_fully_valid(the_repository->index->cache_tree))
@@ -930,9 +855,6 @@ static int merge_working_tree(const struct checkout_opts *opts,
930855
if (write_locked_index(the_repository->index, &lock_file, COMMIT_LOCK))
931856
die(_("unable to write new index file"));
932857

933-
if (!opts->discard_changes && !opts->quiet && new_branch_info->commit)
934-
show_local_changes(&new_branch_info->commit->object, &opts->diff_options);
935-
936858
return 0;
937859
}
938860

@@ -1157,6 +1079,55 @@ static void orphaned_commit_warning(struct commit *old_commit, struct commit *ne
11571079
release_revisions(&revs);
11581080
}
11591081

1082+
static int checkout_would_clobber_changes(struct branch_info *old_branch_info,
1083+
struct branch_info *new_branch_info)
1084+
{
1085+
struct tree_desc trees[2];
1086+
struct tree *old_tree, *new_tree;
1087+
struct unpack_trees_options topts;
1088+
struct index_state tmp_index = INDEX_STATE_INIT(the_repository);
1089+
const struct object_id *old_commit_oid;
1090+
int ret;
1091+
1092+
if (!new_branch_info->commit)
1093+
return 0;
1094+
1095+
old_commit_oid = old_branch_info->commit ?
1096+
&old_branch_info->commit->object.oid :
1097+
the_hash_algo->empty_tree;
1098+
old_tree = repo_parse_tree_indirect(the_repository, old_commit_oid);
1099+
if (!old_tree)
1100+
return 0;
1101+
1102+
new_tree = repo_get_commit_tree(the_repository,
1103+
new_branch_info->commit);
1104+
if (!new_tree)
1105+
return 0;
1106+
if (repo_parse_tree(the_repository, new_tree) < 0)
1107+
return 0;
1108+
1109+
memset(&topts, 0, sizeof(topts));
1110+
topts.head_idx = -1;
1111+
topts.src_index = the_repository->index;
1112+
topts.dst_index = &tmp_index;
1113+
topts.initial_checkout = is_index_unborn(the_repository->index);
1114+
topts.merge = 1;
1115+
topts.update = 1;
1116+
topts.dry_run = 1;
1117+
topts.quiet = 1;
1118+
topts.fn = twoway_merge;
1119+
1120+
init_tree_desc(&trees[0], &old_tree->object.oid,
1121+
old_tree->buffer, old_tree->size);
1122+
init_tree_desc(&trees[1], &new_tree->object.oid,
1123+
new_tree->buffer, new_tree->size);
1124+
1125+
ret = unpack_trees(2, trees, &topts);
1126+
discard_index(&tmp_index);
1127+
1128+
return ret != 0;
1129+
}
1130+
11601131
static int switch_branches(const struct checkout_opts *opts,
11611132
struct branch_info *new_branch_info)
11621133
{
@@ -1165,6 +1136,8 @@ static int switch_branches(const struct checkout_opts *opts,
11651136
struct object_id rev;
11661137
int flag, writeout_error = 0;
11671138
int do_merge = 1;
1139+
struct strbuf old_commit_shortname = STRBUF_INIT;
1140+
const char *stash_label_ancestor = NULL;
11681141

11691142
trace2_cmd_mode("branch");
11701143

@@ -1202,10 +1175,34 @@ static int switch_branches(const struct checkout_opts *opts,
12021175
do_merge = 0;
12031176
}
12041177

1178+
if (old_branch_info.name)
1179+
stash_label_ancestor = old_branch_info.name;
1180+
else if (old_branch_info.commit) {
1181+
strbuf_add_unique_abbrev(&old_commit_shortname,
1182+
&old_branch_info.commit->object.oid,
1183+
DEFAULT_ABBREV);
1184+
stash_label_ancestor = old_commit_shortname.buf;
1185+
}
1186+
1187+
if (opts->merge) {
1188+
if (repo_read_index(the_repository) < 0)
1189+
die(_("index file corrupt"));
1190+
if (checkout_would_clobber_changes(&old_branch_info,
1191+
new_branch_info))
1192+
create_autostash_ref_silent(the_repository,
1193+
"CHECKOUT_AUTOSTASH");
1194+
}
1195+
12051196
if (do_merge) {
12061197
ret = merge_working_tree(opts, &old_branch_info, new_branch_info, &writeout_error);
12071198
if (ret) {
1199+
apply_autostash_ref_with_labels(the_repository,
1200+
"CHECKOUT_AUTOSTASH",
1201+
new_branch_info->name,
1202+
"local",
1203+
stash_label_ancestor);
12081204
branch_info_release(&old_branch_info);
1205+
strbuf_release(&old_commit_shortname);
12091206
return ret;
12101207
}
12111208
}
@@ -1215,8 +1212,28 @@ static int switch_branches(const struct checkout_opts *opts,
12151212

12161213
update_refs_for_switch(opts, &old_branch_info, new_branch_info);
12171214

1215+
if (opts->conflict_style >= 0) {
1216+
struct strbuf cfg = STRBUF_INIT;
1217+
strbuf_addf(&cfg, "merge.conflictStyle=%s",
1218+
conflict_style_name(opts->conflict_style));
1219+
git_config_push_parameter(cfg.buf);
1220+
strbuf_release(&cfg);
1221+
}
1222+
apply_autostash_ref_with_labels(the_repository, "CHECKOUT_AUTOSTASH",
1223+
new_branch_info->name, "local",
1224+
stash_label_ancestor);
1225+
1226+
discard_index(the_repository->index);
1227+
if (repo_read_index(the_repository) < 0)
1228+
die(_("index file corrupt"));
1229+
1230+
if (!opts->discard_changes && !opts->quiet && new_branch_info->commit)
1231+
show_local_changes(&new_branch_info->commit->object,
1232+
&opts->diff_options);
1233+
12181234
ret = post_checkout_hook(old_branch_info.commit, new_branch_info->commit, 1);
12191235
branch_info_release(&old_branch_info);
1236+
strbuf_release(&old_commit_shortname);
12201237

12211238
return ret || writeout_error;
12221239
}

0 commit comments

Comments
 (0)