Skip to content

Commit c07039e

Browse files
HaraldNordgrengitster
authored andcommitted
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>
1 parent 26e4e50 commit c07039e

9 files changed

Lines changed: 219 additions & 150 deletions

File tree

Documentation/git-checkout.adoc

Lines changed: 26 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -251,20 +251,19 @@ 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. 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.
262263
+
263264
When checking out paths from the index, this option lets you recreate
264265
the conflicted merge in the specified paths. This option cannot be
265266
used when checking out paths from a tree-ish.
266-
+
267-
When switching branches with `--merge`, staged changes may be lost.
268267
269268
`--conflict=<style>`::
270269
The same as `--merge` option above, but changes the way the
@@ -578,38 +577,36 @@ $ git checkout mytopic
578577
error: You have local changes to 'frotz'; not switching branches.
579578
------------
580579
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:
583582
584583
------------
585584
$ 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
587589
------------
588590
589-
After this three-way merge, the local modifications are _not_
591+
After the switch, the local modifications are reapplied and are _not_
590592
registered in your index file, so `git diff` would show you what
591593
changes you made since the tip of the new branch.
592594
593595
=== 3. Merge conflict
594596
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:
597602
598603
------------
599604
$ 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".
613610
------------
614611
615612
CONFIGURATION

Documentation/git-switch.adoc

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -123,18 +123,19 @@ variable.
123123

124124
`-m`::
125125
`--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.
138139

139140
`--conflict=<style>`::
140141
The same as `--merge` option above, but changes the way the
@@ -217,15 +218,18 @@ $ git switch mytopic
217218
error: You have local changes to 'frotz'; not switching branches.
218219
------------
219220
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:
222223
223224
------------
224225
$ 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
226230
------------
227231
228-
After this three-way merge, the local modifications are _not_
232+
After the switch, the local modifications are reapplied and are _not_
229233
registered in your index file, so `git diff` would show you what
230234
changes you made since the tip of the new branch.
231235

builtin/checkout.c

Lines changed: 69 additions & 92 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"
@@ -99,6 +99,8 @@ struct checkout_opts {
9999
.auto_advance = 1, \
100100
}
101101

102+
#define MERGE_WORKING_TREE_UNPACK_FAILED (-2)
103+
102104
struct branch_info {
103105
char *name; /* The short name used */
104106
char *path; /* The full name of a real branch */
@@ -753,9 +755,9 @@ static void setup_branch_path(struct branch_info *branch)
753755
branch->path = strbuf_detach(&buf, NULL);
754756
}
755757

756-
static void init_topts(struct unpack_trees_options *topts, int merge,
758+
static void init_topts(struct unpack_trees_options *topts,
757759
int show_progress, int overwrite_ignore,
758-
struct commit *old_commit)
760+
bool quiet)
759761
{
760762
memset(topts, 0, sizeof(*topts));
761763
topts->head_idx = -1;
@@ -767,7 +769,7 @@ static void init_topts(struct unpack_trees_options *topts, int merge,
767769
topts->initial_checkout = is_index_unborn(the_repository->index);
768770
topts->update = 1;
769771
topts->merge = 1;
770-
topts->quiet = merge && old_commit;
772+
topts->quiet = quiet;
771773
topts->verbose_update = show_progress;
772774
topts->fn = twoway_merge;
773775
topts->preserve_ignored = !overwrite_ignore;
@@ -776,6 +778,7 @@ static void init_topts(struct unpack_trees_options *topts, int merge,
776778
static int merge_working_tree(const struct checkout_opts *opts,
777779
struct branch_info *old_branch_info,
778780
struct branch_info *new_branch_info,
781+
bool quiet,
779782
int *writeout_error)
780783
{
781784
int ret;
@@ -826,8 +829,8 @@ static int merge_working_tree(const struct checkout_opts *opts,
826829
}
827830

828831
/* 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);
831834
init_checkout_metadata(&topts.meta, new_branch_info->refname,
832835
new_branch_info->commit ?
833836
&new_branch_info->commit->object.oid :
@@ -853,90 +856,8 @@ static int merge_working_tree(const struct checkout_opts *opts,
853856
ret = unpack_trees(2, trees, &topts);
854857
clear_unpack_trees_porcelain(&topts);
855858
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;
940861
}
941862
}
942863

@@ -1181,6 +1102,10 @@ static int switch_branches(const struct checkout_opts *opts,
11811102
struct object_id rev;
11821103
int flag, writeout_error = 0;
11831104
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;
11841109

11851110
trace2_cmd_mode("branch");
11861111

@@ -1218,11 +1143,49 @@ static int switch_branches(const struct checkout_opts *opts,
12181143
do_merge = 0;
12191144
}
12201145

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+
12211155
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+
}
12231184
if (ret) {
12241185
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;
12261189
}
12271190
}
12281191

@@ -1231,8 +1194,22 @@ static int switch_branches(const struct checkout_opts *opts,
12311194

12321195
update_refs_for_switch(opts, &old_branch_info, new_branch_info);
12331196

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+
12341209
ret = post_checkout_hook(old_branch_info.commit, new_branch_info->commit, 1);
12351210
branch_info_release(&old_branch_info);
1211+
strbuf_release(&old_commit_shortname);
1212+
strbuf_release(&autostash_msg);
12361213

12371214
return ret || writeout_error;
12381215
}

0 commit comments

Comments
 (0)