Skip to content

Commit e5f6635

Browse files
committed
Merge branch 'hn/git-checkout-m-with-stash' into seen
* hn/git-checkout-m-with-stash: checkout: add --autostash option for branch switching
2 parents 2fd41cd + e616795 commit e5f6635

7 files changed

Lines changed: 293 additions & 129 deletions

File tree

Documentation/git-checkout.adoc

Lines changed: 22 additions & 28 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,36 @@ $ 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+
Created autostash: 7a9afa3
584+
Applied autostash.
587585
------------
588586
589-
After this three-way merge, the local modifications are _not_
587+
After the switch, the local modifications are reapplied and are _not_
590588
registered in your index file, so `git diff` would show you what
591589
changes you made since the tip of the new branch.
592590
593591
=== 3. Merge conflict
594592
595-
When a merge conflict happens during switching branches with
596-
the `-m` option, you would see something like this:
593+
When the locally modified files overlap with files that need to be
594+
updated by the branch switch, the changes are stashed and reapplied
595+
after the switch. If the stash application results in conflicts,
596+
they are not 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
600+
Created autostash: 7a9afa3
601+
Applying autostash resulted in conflicts.
602+
Your changes are safe in the stash.
603+
You can run "git stash pop" or "git stash drop" at any time.
603604
------------
604605
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
613-
------------
606+
At this point, `git stash pop` can be used to recover and resolve
607+
the conflicts, and `git stash drop` to discard the stash when done.
614608
615609
CONFIGURATION
616610
-------------

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: 79 additions & 82 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 "strvec.h"
3535
#include "submodule.h"
@@ -852,84 +852,8 @@ static int merge_working_tree(const struct checkout_opts *opts,
852852

853853
ret = unpack_trees(2, trees, &topts);
854854
clear_unpack_trees_porcelain(&topts);
855-
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-
return 1;
869-
870-
/*
871-
* Without old_branch_info->commit, the below is the same as
872-
* the two-tree unpack we already tried and failed.
873-
*/
874-
if (!old_branch_info->commit)
875-
return 1;
876-
old_tree = repo_get_commit_tree(the_repository,
877-
old_branch_info->commit);
878-
879-
if (repo_index_has_changes(the_repository, old_tree, &sb))
880-
die(_("cannot continue with staged changes in "
881-
"the following files:\n%s"), sb.buf);
882-
strbuf_release(&sb);
883-
884-
/* Do more real merge */
885-
886-
/*
887-
* We update the index fully, then write the
888-
* tree from the index, then merge the new
889-
* branch with the current tree, with the old
890-
* branch as the base. Then we reset the index
891-
* (but not the working tree) to the new
892-
* branch, leaving the working tree as the
893-
* merged version, but skipping unmerged
894-
* entries in the index.
895-
*/
896-
897-
add_files_to_cache(the_repository, NULL, NULL, NULL, 0,
898-
0, 0);
899-
init_ui_merge_options(&o, the_repository);
900-
o.verbosity = 0;
901-
work = write_in_core_index_as_tree(the_repository,
902-
the_repository->index);
903-
904-
ret = reset_tree(new_tree,
905-
opts, 1,
906-
writeout_error, new_branch_info);
907-
if (ret)
908-
return ret;
909-
o.ancestor = old_branch_info->name;
910-
if (!old_branch_info->name) {
911-
strbuf_add_unique_abbrev(&old_commit_shortname,
912-
&old_branch_info->commit->object.oid,
913-
DEFAULT_ABBREV);
914-
o.ancestor = old_commit_shortname.buf;
915-
}
916-
o.branch1 = new_branch_info->name;
917-
o.branch2 = "local";
918-
o.conflict_style = opts->conflict_style;
919-
ret = merge_ort_nonrecursive(&o,
920-
new_tree,
921-
work,
922-
old_tree);
923-
if (ret < 0)
924-
die(NULL);
925-
ret = reset_tree(new_tree,
926-
opts, 0,
927-
writeout_error, new_branch_info);
928-
strbuf_release(&o.obuf);
929-
strbuf_release(&old_commit_shortname);
930-
if (ret)
931-
return ret;
932-
}
855+
if (ret == -1)
856+
return 1;
933857
}
934858

935859
if (!cache_tree_fully_valid(the_repository->index->cache_tree))
@@ -938,9 +862,6 @@ static int merge_working_tree(const struct checkout_opts *opts,
938862
if (write_locked_index(the_repository->index, &lock_file, COMMIT_LOCK))
939863
die(_("unable to write new index file"));
940864

941-
if (!opts->discard_changes && !opts->quiet && new_branch_info->commit)
942-
show_local_changes(&new_branch_info->commit->object, &opts->diff_options);
943-
944865
return 0;
945866
}
946867

@@ -1165,6 +1086,55 @@ static void orphaned_commit_warning(struct commit *old_commit, struct commit *ne
11651086
release_revisions(&revs);
11661087
}
11671088

1089+
static int checkout_would_clobber_changes(struct branch_info *old_branch_info,
1090+
struct branch_info *new_branch_info)
1091+
{
1092+
struct tree_desc trees[2];
1093+
struct tree *old_tree, *new_tree;
1094+
struct unpack_trees_options topts;
1095+
struct index_state tmp_index = INDEX_STATE_INIT(the_repository);
1096+
const struct object_id *old_commit_oid;
1097+
int ret;
1098+
1099+
if (!new_branch_info->commit)
1100+
return 0;
1101+
1102+
old_commit_oid = old_branch_info->commit ?
1103+
&old_branch_info->commit->object.oid :
1104+
the_hash_algo->empty_tree;
1105+
old_tree = repo_parse_tree_indirect(the_repository, old_commit_oid);
1106+
if (!old_tree)
1107+
return 0;
1108+
1109+
new_tree = repo_get_commit_tree(the_repository,
1110+
new_branch_info->commit);
1111+
if (!new_tree)
1112+
return 0;
1113+
if (repo_parse_tree(the_repository, new_tree) < 0)
1114+
return 0;
1115+
1116+
memset(&topts, 0, sizeof(topts));
1117+
topts.head_idx = -1;
1118+
topts.src_index = the_repository->index;
1119+
topts.dst_index = &tmp_index;
1120+
topts.initial_checkout = is_index_unborn(the_repository->index);
1121+
topts.merge = 1;
1122+
topts.update = 1;
1123+
topts.dry_run = 1;
1124+
topts.quiet = 1;
1125+
topts.fn = twoway_merge;
1126+
1127+
init_tree_desc(&trees[0], &old_tree->object.oid,
1128+
old_tree->buffer, old_tree->size);
1129+
init_tree_desc(&trees[1], &new_tree->object.oid,
1130+
new_tree->buffer, new_tree->size);
1131+
1132+
ret = unpack_trees(2, trees, &topts);
1133+
discard_index(&tmp_index);
1134+
1135+
return ret != 0;
1136+
}
1137+
11681138
static int switch_branches(const struct checkout_opts *opts,
11691139
struct branch_info *new_branch_info)
11701140
{
@@ -1210,9 +1180,19 @@ static int switch_branches(const struct checkout_opts *opts,
12101180
do_merge = 0;
12111181
}
12121182

1183+
if (opts->merge) {
1184+
if (repo_read_index(the_repository) < 0)
1185+
die(_("index file corrupt"));
1186+
if (checkout_would_clobber_changes(&old_branch_info,
1187+
new_branch_info))
1188+
create_autostash_ref(the_repository,
1189+
"CHECKOUT_AUTOSTASH");
1190+
}
1191+
12131192
if (do_merge) {
12141193
ret = merge_working_tree(opts, &old_branch_info, new_branch_info, &writeout_error);
12151194
if (ret) {
1195+
apply_autostash_ref(the_repository, "CHECKOUT_AUTOSTASH");
12161196
branch_info_release(&old_branch_info);
12171197
return ret;
12181198
}
@@ -1223,6 +1203,23 @@ static int switch_branches(const struct checkout_opts *opts,
12231203

12241204
update_refs_for_switch(opts, &old_branch_info, new_branch_info);
12251205

1206+
if (opts->conflict_style >= 0) {
1207+
struct strbuf cfg = STRBUF_INIT;
1208+
strbuf_addf(&cfg, "merge.conflictStyle=%s",
1209+
conflict_style_name(opts->conflict_style));
1210+
git_config_push_parameter(cfg.buf);
1211+
strbuf_release(&cfg);
1212+
}
1213+
apply_autostash_ref(the_repository, "CHECKOUT_AUTOSTASH");
1214+
1215+
discard_index(the_repository->index);
1216+
if (repo_read_index(the_repository) < 0)
1217+
die(_("index file corrupt"));
1218+
1219+
if (!opts->discard_changes && !opts->quiet && new_branch_info->commit)
1220+
show_local_changes(&new_branch_info->commit->object,
1221+
&opts->diff_options);
1222+
12261223
ret = post_checkout_hook(old_branch_info.commit, new_branch_info->commit, 1);
12271224
branch_info_release(&old_branch_info);
12281225

sequencer.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4702,7 +4702,7 @@ static void create_autostash_internal(struct repository *r,
47024702
&oid, null_oid(the_hash_algo), 0, UPDATE_REFS_DIE_ON_ERR);
47034703
}
47044704

4705-
printf(_("Created autostash: %s\n"), buf.buf);
4705+
fprintf(stderr, _("Created autostash: %s\n"), buf.buf);
47064706
if (reset_head(r, &ropts) < 0)
47074707
die(_("could not reset --hard"));
47084708
discard_index(r->index);

0 commit comments

Comments
 (0)