Skip to content

Commit d69d7a5

Browse files
committed
Merge branch 'hn/git-checkout-m-with-stash' into seen
"git checkout -m another-branch" was invented to deal with local changes to paths that are different between the current and the new branch, but it gave only one chance to resolve conflict. The command was taught to create a stash to save the local changes. Reroll exists. cf. <pull.2234.v5.git.git.1773573553.gitgitgadget@gmail.com> * hn/git-checkout-m-with-stash: checkout: -m (--merge) uses autostash when switching branches sequencer: teach autostash apply to take optional conflict marker labels sequencer: allow create_autostash to run silently stash: add --ours-label, --theirs-label, --base-label for apply
2 parents 34f843f + 1b0152b commit d69d7a5

13 files changed

Lines changed: 447 additions & 150 deletions

Documentation/git-checkout.adoc

Lines changed: 31 additions & 27 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,39 +577,44 @@ $ 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 would carry your local
581+
changes to the new branch:
583582
584583
------------
585584
$ git checkout -m mytopic
586-
Auto-merging frotz
585+
Switched to branch 'mytopic'
587586
------------
588587
589-
After this three-way merge, the local modifications are _not_
588+
After the switch, the local modifications are reapplied and are _not_
590589
registered in your index file, so `git diff` would show you what
591590
changes you made since the tip of the new branch.
592591
593592
=== 3. Merge conflict
594593
595-
When a merge conflict happens during switching branches with
596-
the `-m` option, you would see something like this:
594+
When the `--merge` (`-m`) option is in effect and the locally
595+
modified files overlap with files that need to be updated by the
596+
branch switch, the changes are stashed and reapplied after the
597+
switch. If the stash application results in conflicts, they are not
598+
resolved and the stash is saved to the stash list:
597599
598600
------------
599601
$ git checkout -m mytopic
600-
Auto-merging frotz
601-
ERROR: Merge conflict in frotz
602-
fatal: merge program failed
603-
------------
602+
Your local changes are stashed, however, applying it to carry
603+
forward your local changes resulted in conflicts:
604604
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:
605+
- You can try resolving them now. If you resolved them
606+
successfully, discard the stash entry with "git stash drop".
609607
608+
- Alternatively you can "git reset --hard" if you do not want
609+
to deal with them right now, and later "git stash pop" to
610+
recover your local changes.
610611
------------
611-
$ edit frotz
612-
$ git add frotz
613-
------------
612+
613+
You can try resolving the conflicts now. Edit the conflicting files
614+
and mark them resolved with `git add` as usual, then run `git stash
615+
drop` to discard the stash entry. Alternatively, you can clear the
616+
working tree with `git reset --hard` and recover your local changes
617+
later with `git stash pop`.
614618
615619
CONFIGURATION
616620
-------------

Documentation/git-stash.adoc

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ git stash list [<log-options>]
1212
git stash show [-u | --include-untracked | --only-untracked] [<diff-options>] [<stash>]
1313
git stash drop [-q | --quiet] [<stash>]
1414
git stash pop [--index] [-q | --quiet] [<stash>]
15-
git stash apply [--index] [-q | --quiet] [<stash>]
15+
git stash apply [--index] [-q | --quiet] [--ours-label=<label>] [--theirs-label=<label>] [--base-label=<label>] [<stash>]
1616
git stash branch <branchname> [<stash>]
1717
git stash [push [-p | --patch] [-S | --staged] [-k | --[no-]keep-index] [-q | --quiet]
1818
[-u | --include-untracked] [-a | --all] [(-m | --message) <message>]
@@ -197,6 +197,15 @@ the index's ones. However, this can fail, when you have conflicts
197197
(which are stored in the index, where you therefore can no longer
198198
apply the changes as they were originally).
199199
200+
`--ours-label=<label>`::
201+
`--theirs-label=<label>`::
202+
`--base-label=<label>`::
203+
These options are only valid for the `apply` command.
204+
+
205+
Use the given labels in conflict markers instead of the default
206+
"Updated upstream", "Stashed changes", and "Stash base".
207+
`--base-label` only has an effect with merge.conflictStyle=diff3.
208+
200209
`-k`::
201210
`--keep-index`::
202211
`--no-keep-index`::

Documentation/git-switch.adoc

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -126,15 +126,16 @@ 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. Resolve the conflicts and run
136+
`git stash drop` when done, or clear the working tree
137+
(e.g. with `git reset --hard`) before running `git stash pop`
138+
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,15 @@ $ 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 would carry your local
222+
changes to the new branch:
222223
223224
------------
224225
$ git switch -m mytopic
225-
Auto-merging frotz
226+
Switched to branch 'mytopic'
226227
------------
227228
228-
After this three-way merge, the local modifications are _not_
229+
After the switch, the local modifications are reapplied and are _not_
229230
registered in your index file, so `git diff` would show you what
230231
changes you made since the tip of the new branch.
231232

builtin/checkout.c

Lines changed: 102 additions & 79 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))
@@ -1165,6 +1089,55 @@ static void orphaned_commit_warning(struct commit *old_commit, struct commit *ne
11651089
release_revisions(&revs);
11661090
}
11671091

1092+
static int checkout_would_clobber_changes(struct branch_info *old_branch_info,
1093+
struct branch_info *new_branch_info)
1094+
{
1095+
struct tree_desc trees[2];
1096+
struct tree *old_tree, *new_tree;
1097+
struct unpack_trees_options topts;
1098+
struct index_state tmp_index = INDEX_STATE_INIT(the_repository);
1099+
const struct object_id *old_commit_oid;
1100+
int ret;
1101+
1102+
if (!new_branch_info->commit)
1103+
return 0;
1104+
1105+
old_commit_oid = old_branch_info->commit ?
1106+
&old_branch_info->commit->object.oid :
1107+
the_hash_algo->empty_tree;
1108+
old_tree = repo_parse_tree_indirect(the_repository, old_commit_oid);
1109+
if (!old_tree)
1110+
return 0;
1111+
1112+
new_tree = repo_get_commit_tree(the_repository,
1113+
new_branch_info->commit);
1114+
if (!new_tree)
1115+
return 0;
1116+
if (repo_parse_tree(the_repository, new_tree) < 0)
1117+
return 0;
1118+
1119+
memset(&topts, 0, sizeof(topts));
1120+
topts.head_idx = -1;
1121+
topts.src_index = the_repository->index;
1122+
topts.dst_index = &tmp_index;
1123+
topts.initial_checkout = is_index_unborn(the_repository->index);
1124+
topts.merge = 1;
1125+
topts.update = 1;
1126+
topts.dry_run = 1;
1127+
topts.quiet = 1;
1128+
topts.fn = twoway_merge;
1129+
1130+
init_tree_desc(&trees[0], &old_tree->object.oid,
1131+
old_tree->buffer, old_tree->size);
1132+
init_tree_desc(&trees[1], &new_tree->object.oid,
1133+
new_tree->buffer, new_tree->size);
1134+
1135+
ret = unpack_trees(2, trees, &topts);
1136+
discard_index(&tmp_index);
1137+
1138+
return ret != 0;
1139+
}
1140+
11681141
static int switch_branches(const struct checkout_opts *opts,
11691142
struct branch_info *new_branch_info)
11701143
{
@@ -1173,6 +1146,9 @@ static int switch_branches(const struct checkout_opts *opts,
11731146
struct object_id rev;
11741147
int flag, writeout_error = 0;
11751148
int do_merge = 1;
1149+
int created_autostash = 0;
1150+
struct strbuf old_commit_shortname = STRBUF_INIT;
1151+
const char *stash_label_ancestor = NULL;
11761152

11771153
trace2_cmd_mode("branch");
11781154

@@ -1210,10 +1186,36 @@ static int switch_branches(const struct checkout_opts *opts,
12101186
do_merge = 0;
12111187
}
12121188

1189+
if (old_branch_info.name)
1190+
stash_label_ancestor = old_branch_info.name;
1191+
else if (old_branch_info.commit) {
1192+
strbuf_add_unique_abbrev(&old_commit_shortname,
1193+
&old_branch_info.commit->object.oid,
1194+
DEFAULT_ABBREV);
1195+
stash_label_ancestor = old_commit_shortname.buf;
1196+
}
1197+
1198+
if (opts->merge) {
1199+
if (repo_read_index(the_repository) < 0)
1200+
die(_("index file corrupt"));
1201+
if (checkout_would_clobber_changes(&old_branch_info,
1202+
new_branch_info)) {
1203+
create_autostash_ref_silent(the_repository,
1204+
"CHECKOUT_AUTOSTASH");
1205+
created_autostash = 1;
1206+
}
1207+
}
1208+
12131209
if (do_merge) {
12141210
ret = merge_working_tree(opts, &old_branch_info, new_branch_info, &writeout_error);
12151211
if (ret) {
1212+
apply_autostash_ref_with_labels(the_repository,
1213+
"CHECKOUT_AUTOSTASH",
1214+
new_branch_info->name,
1215+
"local",
1216+
stash_label_ancestor);
12161217
branch_info_release(&old_branch_info);
1218+
strbuf_release(&old_commit_shortname);
12171219
return ret;
12181220
}
12191221
}
@@ -1223,8 +1225,29 @@ static int switch_branches(const struct checkout_opts *opts,
12231225

12241226
update_refs_for_switch(opts, &old_branch_info, new_branch_info);
12251227

1228+
if (opts->conflict_style >= 0) {
1229+
struct strbuf cfg = STRBUF_INIT;
1230+
strbuf_addf(&cfg, "merge.conflictStyle=%s",
1231+
conflict_style_name(opts->conflict_style));
1232+
git_config_push_parameter(cfg.buf);
1233+
strbuf_release(&cfg);
1234+
}
1235+
apply_autostash_ref_with_labels(the_repository, "CHECKOUT_AUTOSTASH",
1236+
new_branch_info->name, "local",
1237+
stash_label_ancestor);
1238+
1239+
discard_index(the_repository->index);
1240+
if (repo_read_index(the_repository) < 0)
1241+
die(_("index file corrupt"));
1242+
1243+
if (created_autostash && !opts->discard_changes && !opts->quiet &&
1244+
new_branch_info->commit)
1245+
show_local_changes(&new_branch_info->commit->object,
1246+
&opts->diff_options);
1247+
12261248
ret = post_checkout_hook(old_branch_info.commit, new_branch_info->commit, 1);
12271249
branch_info_release(&old_branch_info);
1250+
strbuf_release(&old_commit_shortname);
12281251

12291252
return ret || writeout_error;
12301253
}

0 commit comments

Comments
 (0)