checkout: add --autostash option for branch switching
Subsystems (2)
Quoted 13 lines
builtin/checkout.c static int switch_branches(const struct checkout_opts *opts, · line 1154
Quoted 12 lines
builtin/checkout.c +10
-0+if (old_branch_info.name) + stash_label_base = old_branch_info.name; +else if (old_branch_info.commit) { +strbuf_add_unique_abbrev (&old_commit_shortname, + &old_branch_info.commit->object.oid, +DEFAULT_ABBREV ); + stash_label_base = old_commit_shortname.buf; + } +if (do_merge) { ret =merge_working_tree (opts, &old_branch_info, new_branch_info, &writeout_error); +if (ret && opts->merge) {
Quoted 7 lines
As we saw above
merge_working_tree()can return non-zero for a variety of reasons. We only want to try stashing if the call tounpack_trees()failed. Even then if you look at the list of errors inunpack-trees.hyou'll see that only a few of them relate to problems that can be solved by stashing. The old code just tried merging wheneverunpack_trees()failed so it probably not so bad to do the same here but we should not be stashing ifmerge_working_tree()returns before callingunpack_trees().
What you are saying makes a lot of sense.
I gave this a shot now, trying to return an error code that only attempts the stashing when it has a chance of improving the outcome. Not at all sure if it's correct though!
Quoted 10 lines
builtin/checkout.c static int switch_branches(const struct checkout_opts *opts, · line 1164
Quoted 5 lines
builtin/checkout.c +4
-0+ autostash_msg.buf); + created_autostash =1 ; + ret =merge_working_tree (opts, &old_branch_info, new_branch_info, &writeout_error); + }if (ret) {I'm confused by this - if we stash then don't we expect the call to
unpack_trees()inmerge_working_tree()to succeed and therefore return 0? Ifopts->mergeis false then we should not be trying to apply the stash whenmerge_working_tree()fails.
I'm attempting to fix this by making call to apply_autostash_ref conditional on whether or not the autostash was actually created. Makes sense?
Harald
Quoted 21 lines · Harald Nordgren
Quoted 13 lines
builtin/checkout.c static int switch_branches(const struct checkout_opts *opts, · line 1154
Quoted 12 lines
builtin/checkout.c +10
-0+if (old_branch_info.name) + stash_label_base = old_branch_info.name; +else if (old_branch_info.commit) { +strbuf_add_unique_abbrev (&old_commit_shortname, + &old_branch_info.commit->object.oid, +DEFAULT_ABBREV ); + stash_label_base = old_commit_shortname.buf; + } +if (do_merge) { ret =merge_working_tree (opts, &old_branch_info, new_branch_info, &writeout_error); +if (ret && opts->merge) {Quoted 7 lines
As we saw above
merge_working_tree()can return non-zero for a variety of reasons. We only want to try stashing if the call tounpack_trees()failed. Even then if you look at the list of errors inunpack-trees.hyou'll see that only a few of them relate to problems that can be solved by stashing. The old code just tried merging wheneverunpack_trees()failed so it probably not so bad to do the same here but we should not be stashing ifmerge_working_tree()returns before callingunpack_trees().
Quoted 5 lines
What you are saying makes a lot of sense.
I gave this a shot now, trying to return an error code that only attempts the stashing when it has a chance of improving the outcome. Not at all sure if it's correct though!
That sounds like the right approach
Quoted 11 lines
Quoted 10 lines
builtin/checkout.c static int switch_branches(const struct checkout_opts *opts, · line 1164
Quoted 5 lines
builtin/checkout.c +4
-0+ autostash_msg.buf); + created_autostash =1 ; + ret =merge_working_tree (opts, &old_branch_info, new_branch_info, &writeout_error); + }if (ret) {I'm confused by this - if we stash then don't we expect the call to
unpack_trees()inmerge_working_tree()to succeed and therefore return 0? Ifopts->mergeis false then we should not be trying to apply the stash whenmerge_working_tree()fails.
Quoted 3 lines
I'm attempting to fix this by making call to
apply_autostash_refconditional on whether or not the autostash was actually created. Makes sense?
Yes, exactly
Thanks
Phillip
Re: [PATCH v12 0/4] checkout: 'autostash' for branch switching
In reply to Harald Nordgren via GitGitGadgetQuoted 7 lines · Harald Nordgren via GitGitGadget
Harald Nordgren (4):
stash: add --label-ours, --label-theirs, --label-base for apply
sequencer: allowcreate_autostashto run silently
sequencer: teach autostash apply to take optional conflict marker
labels
checkout: -m (--merge) uses autostash when switching branches
Quoted 15 lines
Documentation/git-checkout.adoc | 58 ++++----- Documentation/git-stash.adoc | 11 +- Documentation/git-switch.adoc | 33 ++--- builtin/checkout.c | 147 ++++++++++------------ builtin/stash.c | 32 +++-- sequencer.c | 81 ++++++++++--- sequencer.h | 6 + t/t3420-rebase-autostash.sh | 24 +++- t/t3903-stash.sh | 29 +++++ t/t7201-co.sh | 208 ++++++++++++++++++++++++++++++++ t/t7600-merge.sh | 2 +- xdiff-interface.c | 12 ++ xdiff-interface.h | 1 + xdiff/xmerge.c | 6 +- 14 files changed, 491 insertions(+), 159 deletions(-)
I am not taking v11 and v12 today, as v10 is already in 'next', but for future reference, I have to say that this cover letter is not very helpful.
It shows 350+ lines of range-diff to show mostly irrelevant noise, when the true difference between v11 and v12 is only that two helper functions create_autostash_ref_silent{,_with_msg}() are merged into one create_autostash_ref_with_msg_silent() helper function.
It is much easier to read that read from the diff between the results of applying v11 and v12 on the same base commit, which is a mere 55 lines (shown at the end).
I would not expect you to teach GGG to produce a better range-diff or add an option to instead show an interdiff, but doesn't GGG already have a way to add some human-written comment, e.g.,
Changes since v11:
Two helper functions create_autostash_ref_silent{,_with_msg}() are merged into one create_autostash_ref_with_msg_silent().
to the cover letter? Such a comment, even a very short one like I showed above, would be a lot more effective way to help human readers to decide what is the point of this iteration is, especially when you are sending v11 and v12 almost back-to-back, and to decide if it is easier to understand to read both or if v11 can be skipped and reading v12 alone (with the understanding of the difference explained in the comment) should be enough to tell if v12 is an improvement over v11 or not.
https://github.com/gitgitgadget/gitgitgadget/blob/main/DESIGN.md#patch-submissions
says that
The description of the Pull Request will be used as cover letter, ...
so perhaps your pull-request comment should have something more than just the list of CC: recipients?
Thanks.
builtin/checkout.c +3 -3
@@ -1155,9 +1155,9 @@ static int switch_branches(const struct checkout_opts *opts,strbuf_addf (&autostash_msg,"autostash while switching to '%s'" , new_branch_info->name); -create_autostash_ref_silent_with_msg (the_repository, -"CHECKOUT_AUTOSTASH_HEAD" , - autostash_msg.buf); +create_autostash_ref_with_msg_silent (the_repository, +"CHECKOUT_AUTOSTASH_HEAD" , + autostash_msg.buf); created_autostash =1 ; ret =merge_working_tree (opts, &old_branch_info, new_branch_info, &writeout_error); }
sequencer.c +2 -8
@@ -4726,14 +4726,8 @@ void create_autostash_ref(struct repository *r, const char *refname)create_autostash_internal (r,NULL , refname,NULL , false); } -void create_autostash_ref_silent (struct repository *r,const char *refname) -{ -create_autostash_internal (r,NULL , refname,NULL , true); -} - -void create_autostash_ref_silent_with_msg (struct repository *r, -const char *refname, -const char *message) +void create_autostash_ref_with_msg_silent (struct repository *r,const char *refname, +const char *message) {create_autostash_internal (r,NULL , refname, message, true); }
sequencer.h +2 -4
@@ -230,10 +230,8 @@ void commit_post_rewrite(struct repository *r,void create_autostash (struct repository *r,const char *path);void create_autostash_ref (struct repository *r,const char *refname); -void create_autostash_ref_silent (struct repository *r,const char *refname); -void create_autostash_ref_silent_with_msg (struct repository *r, -const char *refname, -const char *message); +void create_autostash_ref_with_msg_silent (struct repository *r,const char *refname, +const char *message);int save_autostash (const char *path);int save_autostash_ref (struct repository *r,const char *refname);int apply_autostash (const char *path);
Quoted 352 lines
base-commit:
9e8f4e9c04e3efa494e78b710e0c5f6cc77a0a5e
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2234%2FHaraldNordgren%2Fcheckout_autostash-v12
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2234/HaraldNordgren/checkout_autostash-v12
Pull-Request: https://github.com/git/git/pull/2234Range-diff vs v11:
1: 9ab5431b47 = 1: 9ab5431b47 stash: add --label-ours, --label-theirs, --label-base for apply 2: e7f8328e3c ! 2: e11a622bdf sequencer: allow create_autostash to run silently @@ sequencer.c: static enum todo_command peek_command(struct todo_list *todo_list, const char *path, - const char *refname) + const char *refname, ++ const char *message, + bool silent) { struct strbuf buf = STRBUF_INIT; struct lock_file lock_file = LOCK_INIT; +@@ sequencer.c: static void create_autostash_internal(struct repository *r, + struct object_id oid; + + strvec_pushl(&stash.args, +- "stash", "create", "autostash", NULL); ++ "stash", "create", ++ message ? message : "autostash", NULL); + stash.git_cmd = 1; + stash.no_stdin = 1; + strbuf_reset(&buf); @@ sequencer.c: static void create_autostash_internal(struct repository *r, &oid, null_oid(the_hash_algo), 0, UPDATE_REFS_DIE_ON_ERR); } @@ sequencer.c: static void create_autostash_internal(struct repository *r, void create_autostash(struct repository *r, const char *path) { - create_autostash_internal(r, path, NULL); -+ create_autostash_internal(r, path, NULL, false); ++ create_autostash_internal(r, path, NULL, NULL, false); } void create_autostash_ref(struct repository *r, const char *refname) { - create_autostash_internal(r, NULL, refname); -+ create_autostash_internal(r, NULL, refname, false); ++ create_autostash_internal(r, NULL, refname, NULL, false); +} + -+void create_autostash_ref_silent(struct repository *r, const char *refname) ++void create_autostash_ref_with_msg_silent(struct repository *r, const char *refname, ++ const char *message) +{ -+ create_autostash_internal(r, NULL, refname, true); ++ create_autostash_internal(r, NULL, refname, message, true); } static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply) @@ sequencer.h: void commit_post_rewrite(struct repository *r, void create_autostash(struct repository *r, const char *path); void create_autostash_ref(struct repository *r, const char *refname); -+void create_autostash_ref_silent(struct repository *r, const char *refname); ++void create_autostash_ref_with_msg_silent(struct repository *r, const char *refname, ++ const char *message); int save_autostash(const char *path); int save_autostash_ref(struct repository *r, const char *refname); int apply_autostash(const char *path); 3: 3242fd3261 ! 3: 4593745e90 sequencer: teach autostash apply to take optional conflict marker labels @@ Commit message Signed-off-by: Harald Nordgren <[email protected]> ## sequencer.c ## -@@ sequencer.c: void create_autostash_ref_silent(struct repository *r, const char *refname) - create_autostash_internal(r, NULL, refname, true); +@@ sequencer.c: void create_autostash_ref_with_msg_silent(struct repository *r, const char *refn + create_autostash_internal(r, NULL, refname, message, true); } -static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply) +static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply, + const char *label_ours, const char *label_theirs, -+ const char *label_base) ++ const char *label_base, ++ const char *stash_msg) { struct child_process child = CHILD_PROCESS_INIT; int ret = 0; @@ sequencer.c: static int apply_save_autostash_oid(const char *stash_oid, int atte strvec_push(&child.args, stash_oid); ret = run_command(&child); } +@@ sequencer.c: static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply) + strvec_push(&store.args, "stash"); + strvec_push(&store.args, "store"); + strvec_push(&store.args, "-m"); +- strvec_push(&store.args, "autostash"); ++ strvec_push(&store.args, stash_msg ? stash_msg : "autostash"); + strvec_push(&store.args, "-q"); + strvec_push(&store.args, stash_oid); + if (run_command(&store)) @@ sequencer.c: static int apply_save_autostash(const char *path, int attempt_apply) } strbuf_trim(&stash_oid); - ret = apply_save_autostash_oid(stash_oid.buf, attempt_apply); + ret = apply_save_autostash_oid(stash_oid.buf, attempt_apply, -+ NULL, NULL, NULL); ++ NULL, NULL, NULL, NULL); unlink(path); strbuf_release(&stash_oid); @@ sequencer.c: int apply_autostash(const char *path) int apply_autostash_oid(const char *stash_oid) { - return apply_save_autostash_oid(stash_oid, 1); -+ return apply_save_autostash_oid(stash_oid, 1, NULL, NULL, NULL); ++ return apply_save_autostash_oid(stash_oid, 1, NULL, NULL, NULL, NULL); } static int apply_save_autostash_ref(struct repository *r, const char *refname, - int attempt_apply) + int attempt_apply, + const char *label_ours, const char *label_theirs, -+ const char *label_base) ++ const char *label_base, ++ const char *stash_msg) { struct object_id stash_oid; char stash_oid_hex[GIT_MAX_HEXSZ + 1]; @@ sequencer.c: static int apply_save_autostash_ref(struct repository *r, const cha oid_to_hex_r(stash_oid_hex, &stash_oid); - ret = apply_save_autostash_oid(stash_oid_hex, attempt_apply); + ret = apply_save_autostash_oid(stash_oid_hex, attempt_apply, -+ label_ours, label_theirs, label_base); ++ label_ours, label_theirs, label_base, ++ stash_msg); refs_delete_ref(get_main_ref_store(r), "", refname, &stash_oid, REF_NO_DEREF); @@ sequencer.c: static int apply_save_autostash_ref(struct repository *r, const cha int save_autostash_ref(struct repository *r, const char *refname) { - return apply_save_autostash_ref(r, refname, 0); -+ return apply_save_autostash_ref(r, refname, 0, NULL, NULL, NULL); ++ return apply_save_autostash_ref(r, refname, 0, ++ NULL, NULL, NULL, NULL); } int apply_autostash_ref(struct repository *r, const char *refname) { - return apply_save_autostash_ref(r, refname, 1); -+ return apply_save_autostash_ref(r, refname, 1, NULL, NULL, NULL); ++ return apply_save_autostash_ref(r, refname, 1, ++ NULL, NULL, NULL, NULL); +} + +int apply_autostash_ref_with_labels(struct repository *r, const char *refname, + const char *label_ours, const char *label_theirs, -+ const char *label_base) ++ const char *label_base, ++ const char *stash_msg) +{ + return apply_save_autostash_ref(r, refname, 1, -+ label_ours, label_theirs, label_base); ++ label_ours, label_theirs, label_base, ++ stash_msg); } static int checkout_onto(struct repository *r, struct replay_opts *opts, @@ sequencer.h: int save_autostash_ref(struct repository *r, const char *refname); int apply_autostash_ref(struct repository *r, const char *refname); +int apply_autostash_ref_with_labels(struct repository *r, const char *refname, + const char *label_ours, const char *label_theirs, -+ const char *label_base); ++ const char *label_base, ++ const char *stash_msg); #define SUMMARY_INITIAL_COMMIT (1 << 0) #define SUMMARY_SHOW_AUTHOR_DATE (1 << 1)4:
97a5d87c81! 4:911e520431checkout: -m (--merge) uses autostash when switching branches
@@builtin/checkout.c: static int switch_branches(const structcheckout_opts*opts,
+ strbuf_addf(&autostash_msg,
+ "autostash while switching to '%s'",
+new_branch_info->name);
-+ create_autostash_ref_silent_with_msg(the_repository,
-+ "CHECKOUT_AUTOSTASH_HEAD",
-+ autostash_msg.buf);
++ create_autostash_ref_with_msg_silent(the_repository,
++ "CHECKOUT_AUTOSTASH_HEAD",
++ autostash_msg.buf);
+created_autostash= 1;
+ ret =merge_working_tree(opts, &old_branch_info, new_branch_info, &writeout_error);
+ }
@@builtin/checkout.c: static int switch_branches(const structcheckout_opts*opts,
}##
sequencer.c##
-@@sequencer.c: static enumtodo_commandpeek_command(struct todo_list *todo_list, int offset)
- static void create_autostash_internal(struct repository *r,
- const char *path,
- const char *refname,
-+ const char *message,
- bool silent)
- {
- struct strbuf buf =STRBUF_INIT;
-@@sequencer.c: static void create_autostash_internal(struct repository *r,
- structobject_idoid;
-
- strvec_pushl(&stash.args,
-- "stash", "create", "autostash", NULL);
-+ "stash", "create",
-+ message ? message : "autostash", NULL);
- stash.git_cmd = 1;
- stash.no_stdin = 1;
-strbuf_reset(&buf);
-@@sequencer.c: static void create_autostash_internal(struct repository *r,
-
- voidcreate_autostash(struct repository *r, const char *path)
- {
--create_autostash_internal(r, path, NULL, false);
-+create_autostash_internal(r, path, NULL, NULL, false);
- }
-
- voidcreate_autostash_ref(struct repository *r, const char *refname)
- {
--create_autostash_internal(r, NULL, refname, false);
-+create_autostash_internal(r, NULL, refname, NULL, false);
- }
-
- voidcreate_autostash_ref_silent(struct repository *r, const char *refname)
- {
--create_autostash_internal(r, NULL, refname, true);
-+create_autostash_internal(r, NULL, refname, NULL, true);
-+}
-+
-+void create_autostash_ref_silent_with_msg(struct repository *r,
-+ const char *refname,
-+ const char *message)
-+{
-+create_autostash_internal(r, NULL, refname, message, true);
- }
-
- static int apply_save_autostash_oid(const char*stash_oid, intattempt_apply,
- const char*label_ours, const char*label_theirs,
-- const char*label_base)
-+ const char*label_base,
-+ const char*stash_msg)
- {
- structchild_processchild =CHILD_PROCESS_INIT;
- int ret = 0;
@@sequencer.c: static int apply_save_autostash_oid(const char*stash_oid, intattempt_apply,
-strvec_push(&store.args, "stash");
-strvec_push(&store.args, "store");
-strvec_push(&store.args, "-m");
--strvec_push(&store.args, "autostash");
-+strvec_push(&store.args, stash_msg ? stash_msg : "autostash");
-strvec_push(&store.args, "-q");
strvec_push(&store.args, stash_oid);
if (run_command(&store))
ret =error(_("cannot store %s"), stash_oid);
@@sequencer.c: static int apply_save_autostash_oid(const char*stash_oid, int atte
}return ret;
-@@sequencer.c: static intapply_save_autostash(const char *path, int attempt_apply)
-strbuf_trim(&stash_oid);
-
- ret = apply_save_autostash_oid(stash_oid.buf,attempt_apply,
-- NULL, NULL, NULL);
-+ NULL, NULL, NULL, NULL);
-
- unlink(path);
-strbuf_release(&stash_oid);
-@@sequencer.c: intapply_autostash(const char *path)
-
- intapply_autostash_oid(const char *stash_oid)
- {
-- returnapply_save_autostash_oid(stash_oid, 1, NULL, NULL, NULL);
-+ returnapply_save_autostash_oid(stash_oid, 1, NULL, NULL, NULL, NULL);
- }
-
- static int apply_save_autostash_ref(struct repository *r, const char *refname,
- intattempt_apply,
- const char*label_ours, const char*label_theirs,
-- const char*label_base)
-+ const char*label_base,
-+ const char*stash_msg)
- {
- structobject_idstash_oid;
- char stash_oid_hex[GIT_MAX_HEXSZ + 1];
-@@sequencer.c: static int apply_save_autostash_ref(struct repository *r, const char *refname, -
-oid_to_hex_r(stash_oid_hex, &stash_oid);
- ret = apply_save_autostash_oid(stash_oid_hex,attempt_apply,
--label_ours,label_theirs,label_base);
-+label_ours,label_theirs,label_base,
-+stash_msg);
-
- refs_delete_ref(get_main_ref_store(r), "", refname,
-&stash_oid,REF_NO_DEREF);
-@@sequencer.c: static int apply_save_autostash_ref(struct repository *r, const char *refname, -
- intsave_autostash_ref(struct repository *r, const char *refname)
- {
-- returnapply_save_autostash_ref(r, refname, 0, NULL, NULL, NULL);
-+ return apply_save_autostash_ref(r, refname, 0,
-+ NULL, NULL, NULL, NULL);
- }
-
- intapply_autostash_ref(struct repository *r, const char *refname)
- {
-- returnapply_save_autostash_ref(r, refname, 1, NULL, NULL, NULL);
-+ return apply_save_autostash_ref(r, refname, 1,
-+ NULL, NULL, NULL, NULL);
- }
-
- int apply_autostash_ref_with_labels(struct repository *r, const char *refname,
- const char*label_ours, const char*label_theirs,
-- const char*label_base)
-+ const char*label_base,
-+ const char*stash_msg)
- {
- return apply_save_autostash_ref(r, refname, 1,
--label_ours,label_theirs,label_base);
-+label_ours,label_theirs,label_base,
-+stash_msg);
- }
-
- static int checkout_onto(struct repository *r, structreplay_opts*opts,
-
- ##sequencer.h##
-@@sequencer.h: void commit_post_rewrite(struct repository *r,
- voidcreate_autostash(struct repository *r, const char *path);
- voidcreate_autostash_ref(struct repository *r, const char *refname);
- voidcreate_autostash_ref_silent(struct repository *r, const char *refname); -+void create_autostash_ref_silent_with_msg(struct repository *r,
-+ const char *refname,
-+ const char *message);
- intsave_autostash(const char *path);
- intsave_autostash_ref(struct repository *r, const char *refname);
- intapply_autostash(const char *path);
-@@sequencer.h: intapply_autostash_oid(const char *stash_oid);
- intapply_autostash_ref(struct repository *r, const char *refname);
- int apply_autostash_ref_with_labels(struct repository *r, const char *refname,
- const char*label_ours, const char*label_theirs,
-- const char*label_base);
-+ const char*label_base,
-+ const char*stash_msg);
-
- #defineSUMMARY_INITIAL_COMMIT(1 << 0)
- #defineSUMMARY_SHOW_AUTHOR_DATE(1 << 1)##
t/t3420-rebase-autostash.sh##
@@t/t3420-rebase-autostash.sh:create_expected_failure_apply() {
Quoted 5 lines
The description of the Pull Request will be used as cover letter, ...
so perhaps your pull-request comment should have something more than just the list of CC: recipients?
I'll give it a try!
Harald
Quoted 7 lines · Harald Nordgren
Quoted 5 lines
The description of the Pull Request will be used as cover letter, ...
so perhaps your pull-request comment should have something more than just the list of CC: recipients?
I'll give it a try!
;-)
I find that many topics by Patrick Steinhardt and Jeff King with multiple iterations often come with good cover letters that outline updates between iterations.
Thanks.
Quoted 5 lines
It shows 350+ lines of range-diff to show mostly irrelevant noise, when the true difference between v11 and v12 is only that two helper functions create_autostash_ref_silent{,_with_msg}() are merged into one
create_autostash_ref_with_msg_silent()helper function.
Quoted 7 lines
It is much easier to read that read from the diff between the results of applying v11 and v12 on the same base commit, which is a mere 55 lines (shown at the end).
I would not expect you to teach GGG to produce a better range-diff or add an option to instead show an interdiff, but doesn't GGG already have a way to add some human-written comment
I will work on my cover letters, that's a very fair point.
I do think there is some possibility to handle this via maybe a new option 'git range-diff --rebase', or directly via GitGitGadget. This would automatically create a diff with only the files actually changed, which saves both author's and reviewer's time.
Perhaps this: https://github.com/gitgitgadget/gitgitgadget/pull/2212
Harald
Quoted 4 lines · Harald Nordgren
I do think there is some possibility to handle this via maybe a new option 'git range-diff --rebase', or directly via GitGitGadget. This would automatically create a diff with only the files actually changed, which saves both author's and reviewer's time.
I am not sure. Have you actually tried to apply two iterations (I think it was between v11 and v12 but please double check) on the same base and ran range-diff, and compared the result with what I complained about? You added one helper in the new iteration, that replaces two helpers you added to the old iteration, and the part of the range-diff that I called "less interesting" noise were the change to the callers to the original two helpers to make them call the unified helper, inevitably with different arguments. I am not sure a mechanical textual comparison tool can tell them from the more interesting change that shows that two old helpers did not get added and instead one new unified helper got added. I do not expect this to change if two versions compared were built on the same base.
And that is why I kept saying that the cover letter needs some comments written by the author to guide readers which parts of the changes are notable.
Quoted 4 lines
I am not sure. Have you actually tried to apply two iterations (I think it was between v11 and v12 but please double check) on the same base and ran range-diff, and compared the result with what I complained about?
Fair enough, it's not great!
Quoted 3 lines
And that is why I kept saying that the cover letter needs some comments written by the author to guide readers which parts of the changes are notable.
I hear you loud and clear! Next patch will have a better cover letter if or when it comes!
Harald
v13 cover checkout: 'autostash' for branch switching
In reply to Harald Nordgren via GitGitGadgetHandling the review comments from Phillip Wood and JCH.
The ones I struggled to fix I commented on on the mailing list instead, wuld be nice to get some feedback on my proposed fixes 🤗
Harald Nordgren (5):
stash: add --label-ours, --label-theirs, --label-base for apply
sequencer: allow create_autostash to run silently
sequencer: teach autostash apply to take optional conflict marker
labels
checkout: rollback lock on early returns in merge_working_tree checkout -m: autostash when switching branches
Documentation/git-checkout.adoc | 58 ++++----- Documentation/git-stash.adoc | 11 +- Documentation/git-switch.adoc | 33 ++--- builtin/checkout.c | 149 +++++++++++------------ builtin/commit.c | 3 +- builtin/merge.c | 15 ++- builtin/stash.c | 28 +++-- sequencer.c | 73 ++++++++--- sequencer.h | 7 +- t/t3420-rebase-autostash.sh | 24 +++- t/t3903-stash.sh | 25 ++++ t/t7201-co.sh | 208 ++++++++++++++++++++++++++++++++ t/t7600-merge.sh | 2 +- xdiff-interface.c | 12 ++ xdiff-interface.h | 1 + xdiff/xmerge.c | 6 +- 16 files changed, 484 insertions(+), 171 deletions(-)
base-commit: 9e8f4e9c04e3efa494e78b710e0c5f6cc77a0a5e
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2234%2FHaraldNordgren%2Fcheckout_autostash-v13
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2234/HaraldNordgren/checkout_autostash-v13
Pull-Request: https://github.com/git/git/pull/2234
Range-diff vs v12:
1: 9ab5431b47 ! 1: 43bfdf2136 stash: add --label-ours, --label-theirs, --label-base for apply
@@ builtin/stash.c
N_("git stash branch <branchname> [<stash>]")
#define BUILTIN_STASH_STORE_USAGE \
@@ builtin/stash.c: static void unstage_changes_unless_new(struct object_id *orig_tree)
- die(_("could not write index"));
}
--static int do_apply_stash(const char *prefix, struct stash_info *info,
+ static int do_apply_stash(const char *prefix, struct stash_info *info,
- int index, int quiet)
-+static int do_apply_stash_with_labels(const char *prefix,
-+ struct stash_info *info,
-+ int index, int quiet,
-+ const char *label_ours, const char *label_theirs,
-+ const char *label_base)
++ int index, int quiet,
++ const char *label_ours, const char *label_theirs,
++ const char *label_base)
{
int clean, ret;
int has_index = index;
@@ builtin/stash.c: static int do_apply_stash(const char *prefix, struct stash_info
if (oideq(&info->b_tree, &c_tree))
o.branch1 = "Version stash was based on";
-@@ builtin/stash.c: restore_untracked:
- return ret;
- }
-
-+static int do_apply_stash(const char *prefix, struct stash_info *info,
-+ int index, int quiet)
-+{
-+ return do_apply_stash_with_labels(prefix, info, index, quiet,
-+ NULL, NULL, NULL);
-+}
-+
- static int apply_stash(int argc, const char **argv, const char *prefix,
- struct repository *repo UNUSED)
- {
+@@ builtin/stash.c: static int apply_stash(int argc, const char **argv, const char *prefix,
int ret = -1;
int quiet = 0;
int index = use_index;
@@ builtin/stash.c: static int apply_stash(int argc, const char **argv, const char
goto cleanup;
- ret = do_apply_stash(prefix, &info, index, quiet);
-+ ret = do_apply_stash_with_labels(prefix, &info, index, quiet,
-+ label_ours, label_theirs, label_base);
++ ret = do_apply_stash(prefix, &info, index, quiet,
++ label_ours, label_theirs, label_base);
cleanup:
free_stash_info(&info);
return ret;
+@@ builtin/stash.c: static int pop_stash(int argc, const char **argv, const char *prefix,
+ if (get_stash_info_assert(&info, argc, argv))
+ goto cleanup;
+
+- if ((ret = do_apply_stash(prefix, &info, index, quiet)))
++ if ((ret = do_apply_stash(prefix, &info, index, quiet,
++ NULL, NULL, NULL)))
+ printf_ln(_("The stash entry is kept in case "
+ "you need it again."));
+ else
+@@ builtin/stash.c: static int branch_stash(int argc, const char **argv, const char *prefix,
+ strvec_push(&cp.args, oid_to_hex(&info.b_commit));
+ ret = run_command(&cp);
+ if (!ret)
+- ret = do_apply_stash(prefix, &info, 1, 0);
++ ret = do_apply_stash(prefix, &info, 1, 0,
++ NULL, NULL, NULL);
+ if (!ret && info.is_stash_ref)
+ ret = do_drop_stash(&info, 0);
+
## t/t3903-stash.sh ##
@@ t/t3903-stash.sh: test_expect_success 'restore untracked files even when we hit conflicts' '
@@ t/t3903-stash.sh: test_expect_success 'restore untracked files even when we hit
'
+test_expect_success 'apply with custom conflict labels' '
-+ git init conflict_labels &&
-+ (
-+ cd conflict_labels &&
-+ test_commit base file &&
-+ echo stashed >file &&
-+ git stash push -m "stashed" &&
-+ test_commit upstream file &&
-+ test_must_fail git -c merge.conflictStyle=diff3 stash apply --label-ours=UP --label-theirs=STASH &&
-+ test_grep "^<<<<<<< UP" file &&
-+ test_grep "^||||||| Stash base" file &&
-+ test_grep "^>>>>>>> STASH" file
-+ )
++ test_when_finished "git reset --hard && git stash drop" &&
++ git reset --hard &&
++ test_commit label-base conflict-file base-content &&
++ echo stashed >conflict-file &&
++ git stash push -m "stashed" &&
++ test_commit label-upstream conflict-file upstream-content &&
++ test_must_fail git -c merge.conflictStyle=diff3 stash apply --label-ours=UP --label-theirs=STASH &&
++ test_grep "^<<<<<<< UP" conflict-file &&
++ test_grep "^||||||| Stash base" conflict-file &&
++ test_grep "^>>>>>>> STASH" conflict-file
+'
+
+test_expect_success 'apply with empty conflict labels' '
-+ git init empty_labels &&
-+ (
-+ cd empty_labels &&
-+ test_commit base file &&
-+ echo stashed >file &&
-+ git stash push -m "stashed" &&
-+ test_commit upstream file &&
-+ test_must_fail git stash apply --label-ours= --label-theirs= &&
-+ test_grep "^<<<<<<<$" file &&
-+ test_grep "^>>>>>>>$" file
-+ )
++ test_when_finished "git reset --hard && git stash drop" &&
++ git reset --hard &&
++ test_commit empty-label-base conflict-file base-content &&
++ echo stashed >conflict-file &&
++ git stash push -m "stashed" &&
++ test_commit empty-label-upstream conflict-file upstream-content &&
++ test_must_fail git stash apply --label-ours= --label-theirs= &&
++ test_grep "^<<<<<<<$" conflict-file &&
++ test_grep "^>>>>>>>$" conflict-file
+'
+
test_expect_success 'stash create reports a locked index' ' 2: e11a622bdf ! 2: 7f3c32f5e9 sequencer: allow create_autostash to run silently
@@ Commit message
+ ## builtin/merge.c ##
+@@ builtin/merge.c: int cmd_merge(int argc,
+ }
+
+ if (autostash)
+- create_autostash_ref(the_repository, "MERGE_AUTOSTASH");
++ create_autostash_ref(the_repository, "MERGE_AUTOSTASH",
++ NULL, false);
+ if (checkout_fast_forward(the_repository,
+ &head_commit->object.oid,
+ &commit->object.oid,
+@@ builtin/merge.c: int cmd_merge(int argc,
+ die_ff_impossible();
+
+ if (autostash)
+- create_autostash_ref(the_repository, "MERGE_AUTOSTASH");
++ create_autostash_ref(the_repository, "MERGE_AUTOSTASH",
++ NULL, false);
+
+ /* We are going to make a new commit. */
+ git_committer_info(IDENT_STRICT);
+
## sequencer.c ##
@@ sequencer.c: static enum todo_command peek_command(struct todo_list *todo_list, int offset)
@@ sequencer.c: static void create_autostash_internal(struct repository *r,
+ create_autostash_internal(r, path, NULL, NULL, false);
}
- void create_autostash_ref(struct repository *r, const char *refname) +-void create_autostash_ref(struct repository *r, const char *refname) ++void create_autostash_ref(struct repository *r, const char *refname,
++ const char *message, bool silent)
{
- create_autostash_internal(r, NULL, refname);
-+ create_autostash_internal(r, NULL, refname, NULL, false);
-+}
-+
-+void create_autostash_ref_with_msg_silent(struct repository *r, const char *refname,
-+ const char *message)
-+{
-+ create_autostash_internal(r, NULL, refname, message, true);
++ create_autostash_internal(r, NULL, refname, message, silent);
}
static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply)
## sequencer.h ##
@@ sequencer.h: void commit_post_rewrite(struct repository *r,
+ const struct object_id *new_head);
void create_autostash(struct repository *r, const char *path);
- void create_autostash_ref(struct repository *r, const char *refname);
-+void create_autostash_ref_with_msg_silent(struct repository *r, const char *refname,
-+ const char *message);
+-void create_autostash_ref(struct repository *r, const char *refname);
++void create_autostash_ref(struct repository *r, const char *refname,
++ const char *message, bool silent);
int save_autostash(const char *path);
int save_autostash_ref(struct repository *r, const char *refname);
int apply_autostash(const char *path);
3: 4593745e90 ! 3: b279d1dac8 sequencer: teach autostash apply to take optional conflict marker labels
@@ Metadata
## Commit message ##
sequencer: teach autostash apply to take optional conflict marker labels
- Add label_ours, label_theirs, and label_base parameters to the autostash
- apply machinery so callers can pass custom conflict marker labels
- through to "git stash apply --label-ours/--label-theirs/--label-base".
- Introduce apply_autostash_ref_with_labels() for callers that want
- to pass labels.
+ Add label_ours, label_theirs, label_base, and stash_msg parameters to
+ apply_autostash_ref() and the autostash apply machinery so callers can
+ pass custom conflict marker labels through to
+ "git stash apply --label-ours/--label-theirs/--label-base", as well as
+ a custom stash message for "git stash store -m".
+ ## builtin/commit.c ##
+@@ builtin/commit.c: int cmd_commit(int argc,
+ &oid, flags);
+ }
+
+- apply_autostash_ref(the_repository, "MERGE_AUTOSTASH");
++ apply_autostash_ref(the_repository, "MERGE_AUTOSTASH",
++ NULL, NULL, NULL, NULL);
+
+ cleanup:
+ free_commit_extra_headers(extra);
+
+ ## builtin/merge.c ##
+@@ builtin/merge.c: static void finish(struct commit *head_commit,
+ run_hooks_l(the_repository, "post-merge", squash ? "1" : "0", NULL);
+
+ if (new_head)
+- apply_autostash_ref(the_repository, "MERGE_AUTOSTASH");
++ apply_autostash_ref(the_repository, "MERGE_AUTOSTASH",
++ NULL, NULL, NULL, NULL);
+ strbuf_release(&reflog_message);
+ }
+
+@@ builtin/merge.c: int cmd_merge(int argc,
+ &head_commit->object.oid,
+ &commit->object.oid,
+ overwrite_ignore)) {
+- apply_autostash_ref(the_repository, "MERGE_AUTOSTASH");
++ apply_autostash_ref(the_repository, "MERGE_AUTOSTASH",
++ NULL, NULL, NULL, NULL);
+ ret = 1;
+ goto done;
+ }
+@@ builtin/merge.c: int cmd_merge(int argc,
+ else
+ fprintf(stderr, _("Merge with strategy %s failed.\n"),
+ use_strategies[0]->name);
+- apply_autostash_ref(the_repository, "MERGE_AUTOSTASH");
++ apply_autostash_ref(the_repository, "MERGE_AUTOSTASH",
++ NULL, NULL, NULL, NULL);
+ ret = 2;
+ goto done;
+ } else if (best_strategy == wt_strategy)
+
## sequencer.c ##
-@@ sequencer.c: void create_autostash_ref_with_msg_silent(struct repository *r, const char *refn
- create_autostash_internal(r, NULL, refname, message, true);
+@@ sequencer.c: void create_autostash_ref(struct repository *r, const char *refname,
+ create_autostash_internal(r, NULL, refname, message, silent);
}
-static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply)
@@ sequencer.c: static int apply_save_autostash_ref(struct repository *r, const cha
+ NULL, NULL, NULL, NULL);
}
- int apply_autostash_ref(struct repository *r, const char *refname) +-int apply_autostash_ref(struct repository *r, const char *refname) ++int apply_autostash_ref(struct repository *r, const char *refname,
++ const char *label_ours, const char *label_theirs,
++ const char *label_base, const char *stash_msg)
{
- return apply_save_autostash_ref(r, refname, 1);
+ return apply_save_autostash_ref(r, refname, 1,
-+ NULL, NULL, NULL, NULL);
-+}
-+
-+int apply_autostash_ref_with_labels(struct repository *r, const char *refname,
-+ const char *label_ours, const char *label_theirs,
-+ const char *label_base,
-+ const char *stash_msg)
-+{
-+ return apply_save_autostash_ref(r, refname, 1,
+ label_ours, label_theirs, label_base,
+ stash_msg);
}
@@ sequencer.c: static int apply_save_autostash_ref(struct repository *r, const cha
static int checkout_onto(struct repository *r, struct replay_opts *opts,
## sequencer.h ##
-@@ sequencer.h: int save_autostash_ref(struct repository *r, const char *refname); +@@ sequencer.h: int save_autostash(const char *path);
+ int save_autostash_ref(struct repository *r, const char *refname); int apply_autostash(const char *path);
int apply_autostash_oid(const char *stash_oid);
- int apply_autostash_ref(struct repository *r, const char *refname); -+int apply_autostash_ref_with_labels(struct repository *r, const char *refname,
-+ const char *label_ours, const char *label_theirs,
-+ const char *label_base,
-+ const char *stash_msg);
+-int apply_autostash_ref(struct repository *r, const char *refname); ++int apply_autostash_ref(struct repository *r, const char *refname,
++ const char *label_ours, const char *label_theirs,
++ const char *label_base, const char *stash_msg);
#define SUMMARY_INITIAL_COMMIT (1 << 0)
#define SUMMARY_SHOW_AUTHOR_DATE (1 << 1)
-: ---------- > 4: 04869314ec checkout: rollback lock on early returns in merge_working_tree
4: 911e520431 ! 5: 4b3c6025ac checkout: -m (--merge) uses autostash when switching branches
@@ Metadata
Author: Harald Nordgren <[email protected]>
## Commit message ##
- checkout: -m (--merge) uses autostash when switching branches
+ checkout -m: 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.
+ 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 <[email protected]>
@@ builtin/checkout.c
#include "setup.h"
#include "submodule.h"
#include "symlinks.h"
-@@ builtin/checkout.c: static int merge_working_tree(const struct checkout_opts *opts,
- struct tree *new_tree;
-
- repo_hold_locked_index(the_repository, &lock_file, LOCK_DIE_ON_ERROR);
-- if (repo_read_index_preload(the_repository, NULL, 0) < 0)
-+ if (repo_read_index_preload(the_repository, NULL, 0) < 0) {
-+ rollback_lock_file(&lock_file);
- return error(_("index file corrupt"));
-+ }
-
- resolve_undo_clear_index(the_repository->index);
- if (opts->new_orphan_branch && opts->orphan_from_empty_tree) {
-@@ builtin/checkout.c: static int merge_working_tree(const struct checkout_opts *opts,
- } else {
- new_tree = repo_get_commit_tree(the_repository,
- new_branch_info->commit);
-- if (!new_tree)
-+ if (!new_tree) {
-+ rollback_lock_file(&lock_file);
- return error(_("unable to read tree (%s)"),
- oid_to_hex(&new_branch_info->commit->object.oid));
-+ }
- }
- if (opts->discard_changes) {
- ret = reset_tree(new_tree, opts, 1, writeout_error, new_branch_info);
-- if (ret)
-+ if (ret) {
-+ rollback_lock_file(&lock_file);
- return ret;
-+ }
- } else {
- struct tree_desc trees[2];
- struct tree *tree;
-@@ builtin/checkout.c: static int merge_working_tree(const struct checkout_opts *opts,
- refresh_index(the_repository->index, REFRESH_QUIET, NULL, NULL, NULL);
-
- if (unmerged_index(the_repository->index)) {
-+ rollback_lock_file(&lock_file);
- error(_("you need to resolve your current index first"));
- return 1;
- }
@@ builtin/checkout.c: static int merge_working_tree(const struct checkout_opts *opts,
ret = unpack_trees(2, trees, &topts);
clear_unpack_trees_porcelain(&topts);
@@ builtin/checkout.c: static int merge_working_tree(const struct checkout_opts *op
- struct strbuf sb = STRBUF_INIT;
- struct strbuf old_commit_shortname = STRBUF_INIT;
-
-- if (!opts->merge)
+- if (!opts->merge) {
+- rollback_lock_file(&lock_file);
- return 1;
+- }
-
- /*
- * Without old_branch_info->commit, the below is the same as
- * the two-tree unpack we already tried and failed.
- */
-- if (!old_branch_info->commit)
+- if (!old_branch_info->commit) {
+- rollback_lock_file(&lock_file);
- return 1;
+- }
- old_tree = repo_get_commit_tree(the_repository,
- old_branch_info->commit);
-
@@ builtin/checkout.c: static int merge_working_tree(const struct checkout_opts *op
- ret = reset_tree(new_tree,
- opts, 1,
- writeout_error, new_branch_info);
-- if (ret)
+- if (ret) {
+- rollback_lock_file(&lock_file);
- return ret;
+- }
- o.ancestor = old_branch_info->name;
- if (!old_branch_info->name) {
- strbuf_add_unique_abbrev(&old_commit_shortname,
@@ builtin/checkout.c: static int merge_working_tree(const struct checkout_opts *op
- writeout_error, new_branch_info);
- strbuf_release(&o.obuf);
- strbuf_release(&old_commit_shortname);
-- if (ret)
+- if (ret) {
+- rollback_lock_file(&lock_file);
- return ret;
+- }
+ rollback_lock_file(&lock_file);
-+ return 1;
++ return ret;
}
}
@@ builtin/checkout.c: static int switch_branches(const struct checkout_opts *opts,
+
if (do_merge) {
ret = merge_working_tree(opts, &old_branch_info, new_branch_info, &writeout_error);
-+ if (ret && opts->merge) {
++ if (ret == -1 && opts->merge) {
+ strbuf_addf(&autostash_msg,
+ "autostash while switching to '%s'",
+ new_branch_info->name);
-+ create_autostash_ref_with_msg_silent(the_repository,
-+ "CHECKOUT_AUTOSTASH_HEAD",
-+ autostash_msg.buf);
++ create_autostash_ref(the_repository,
++ "CHECKOUT_AUTOSTASH_HEAD",
++ autostash_msg.buf, true);
+ created_autostash = 1;
+ ret = merge_working_tree(opts, &old_branch_info, new_branch_info, &writeout_error);
+ }
if (ret) {
-+ apply_autostash_ref_with_labels(the_repository,
-+ "CHECKOUT_AUTOSTASH_HEAD",
-+ new_branch_info->name,
-+ "local",
-+ stash_label_base,
-+ autostash_msg.len ? autostash_msg.buf : NULL);
++ if (created_autostash)
++ apply_autostash_ref(the_repository,
++ "CHECKOUT_AUTOSTASH_HEAD",
++ new_branch_info->name,
++ "local",
++ stash_label_base,
++ autostash_msg.buf);
branch_info_release(&old_branch_info);
+- return ret;
+ strbuf_release(&old_commit_shortname);
+ strbuf_release(&autostash_msg);
- return ret;
++ return ret < 0 ? 1 : ret;
}
}
+
@@ builtin/checkout.c: static int switch_branches(const struct checkout_opts *opts,
update_refs_for_switch(opts, &old_branch_info, new_branch_info);
@@ builtin/checkout.c: static int switch_branches(const struct checkout_opts *opts,
+ git_config_push_parameter(cfg.buf);
+ strbuf_release(&cfg);
+ }
-+ apply_autostash_ref_with_labels(the_repository, "CHECKOUT_AUTOSTASH_HEAD",
-+ new_branch_info->name, "local",
-+ stash_label_base,
-+ autostash_msg.len ? autostash_msg.buf : NULL);
++ apply_autostash_ref(the_repository, "CHECKOUT_AUTOSTASH_HEAD",
++ new_branch_info->name, "local",
++ stash_label_base,
++ autostash_msg.buf);
+
+ discard_index(the_repository->index);
+ if (repo_read_index(the_repository) < 0)
+ die(_("index file corrupt"));
+
-+ if (created_autostash && !opts->discard_changes && !opts->quiet &&
-+ new_branch_info->commit)
++ if (created_autostash && !opts->quiet && new_branch_info->commit)
+ show_local_changes(&new_branch_info->commit->object,
+ &opts->diff_options);
+Signature · 1 line
gitgitgadget
PATCH v13 1/5 stash: add --label-ours, --label-theirs, --label-base for apply
From: Harald Nordgren <[email protected]>
Allow callers of "git stash apply" to pass custom labels for conflict markers instead of the default "Updated upstream" and "Stashed changes". Document the new options and add a test.
--- Documentation/git-stash.adoc | 11 ++++++++++- builtin/stash.c | 28 ++++++++++++++++++++-------- t/t3903-stash.sh | 25 +++++++++++++++++++++++++ xdiff/xmerge.c | 6 +++--- 4 files changed, 58 insertions(+), 12 deletions(-)
Documentation/git-stash.adoc +10 -1
@@ -12,7 +12,7 @@ git stash list [<log-options>] git stash show [-u | --include-untracked | --only-untracked] [<diff-options>] [<stash>] git stash drop [-q | --quiet] [<stash>] git stash pop [--index] [-q | --quiet] [<stash>] -git stash apply [--index] [-q | --quiet] [<stash>] +git stash apply [--index] [-q | --quiet] [--label-ours=<label>] [--label-theirs=<label>] [--label-base=<label>] [<stash>] git stash branch <branchname> [<stash>] git stash [push] [-p | --patch] [-S | --staged] [-k | --[no-]keep-index] [-q | --quiet] [-u | --include-untracked] [-a | --all] [(-m | --message) <message>]
@@ -195,6 +195,15 @@ the index's ones. However, this can fail, when you have conflicts (which are stored in the index, where you therefore can no longer apply the changes as they were originally). +`--label-ours=<label>`:: +`--label-theirs=<label>`:: +`--label-base=<label>`:: + These options are only valid for the `apply` command. ++ +Use the given labels in conflict markers instead of the default +"Updated upstream", "Stashed changes", and "Stash base". +`--label-base` only has an effect with merge.conflictStyle=diff3. + `-k`:: `--keep-index`:: `--no-keep-index`::
builtin/stash.c +20 -8
@@ -44,7 +44,7 @@#define BUILTIN_STASH_POP_USAGE \ N_("git stash pop [--index] [-q | --quiet] [<stash>]")#define BUILTIN_STASH_APPLY_USAGE \ - N_("git stash apply [--index] [-q | --quiet] [<stash>]") + N_("git stash apply [--index] [-q | --quiet] [--label-ours=<label>] [--label-theirs=<label>] [--label-base=<label>] [<stash>]")#define BUILTIN_STASH_BRANCH_USAGE \ N_("git stash branch <branchname> [<stash>]")#define BUILTIN_STASH_STORE_USAGE \
@@ -591,7 +591,9 @@ static void unstage_changes_unless_new(struct object_id *orig_tree) }static int do_apply_stash (const char *prefix,struct stash_info *info, -int index,int quiet) +int index,int quiet, +const char *label_ours,const char *label_theirs, +const char *label_base) {int clean, ret;int has_index = index;
@@ -643,9 +645,9 @@ static int do_apply_stash(const char *prefix, struct stash_info *info,init_ui_merge_options (&o, the_repository); - o.branch1 ="Updated upstream" ; - o.branch2 ="Stashed changes" ; - o.ancestor ="Stash base" ; + o.branch1 = label_ours ? label_ours :"Updated upstream" ; + o.branch2 = label_theirs ? label_theirs :"Stashed changes" ; + o.ancestor = label_base ? label_base :"Stash base" ;if (oideq (&info->b_tree, &c_tree)) o.branch1 ="Version stash was based on" ;
@@ -723,11 +725,18 @@ static int apply_stash(int argc, const char **argv, const char *prefix,int ret =-1 ;int quiet =0 ;int index = use_index; +const char *label_ours =NULL , *label_theirs =NULL , *label_base =NULL ;struct stash_info info =STASH_INFO_INIT ;struct option options[] = {OPT__QUIET (&quiet,N_ ("be quiet, only report errors" )),OPT_BOOL (0 ,"index" , &index,N_ ("attempt to recreate the index" )), +OPT_STRING (0 ,"label-ours" , &label_ours,N_ ("label" ), +N_ ("label for the upstream side in conflict markers" )), +OPT_STRING (0 ,"label-theirs" , &label_theirs,N_ ("label" ), +N_ ("label for the stashed side in conflict markers" )), +OPT_STRING (0 ,"label-base" , &label_base,N_ ("label" ), +N_ ("label for the base in diff3 conflict markers" )),OPT_END () };
@@ -737,7 +746,8 @@ static int apply_stash(int argc, const char **argv, const char *prefix,if (get_stash_info (&info, argc, argv)) goto cleanup; - ret =do_apply_stash (prefix, &info, index, quiet); + ret =do_apply_stash (prefix, &info, index, quiet, + label_ours, label_theirs, label_base); cleanup:free_stash_info (&info);return ret;
@@ -836,7 +846,8 @@ static int pop_stash(int argc, const char **argv, const char *prefix,if (get_stash_info_assert (&info, argc, argv)) goto cleanup; -if ((ret =do_apply_stash (prefix, &info, index, quiet))) +if ((ret =do_apply_stash (prefix, &info, index, quiet, +NULL ,NULL ,NULL )))printf_ln (_ ("The stash entry is kept in case " "you need it again." ));else
@@ -877,7 +888,8 @@ static int branch_stash(int argc, const char **argv, const char *prefix,strvec_push (&cp.args,oid_to_hex (&info.b_commit)); ret =run_command (&cp);if (!ret) - ret =do_apply_stash (prefix, &info,1 ,0 ); + ret =do_apply_stash (prefix, &info,1 ,0 , +NULL ,NULL ,NULL );if (!ret && info.is_stash_ref) ret =do_drop_stash (&info,0 );
t/t3903-stash.sh +25 -0
@@ -1666,6 +1666,31 @@ test_expect_success 'restore untracked files even when we hit conflicts' ' ) ' +test_expect_success 'apply with custom conflict labels' ' + test_when_finished "git reset --hard && git stash drop" && + git reset --hard && + test_commit label-base conflict-file base-content && + echo stashed >conflict-file && + git stash push -m "stashed" && + test_commit label-upstream conflict-file upstream-content && + test_must_fail git -c merge.conflictStyle=diff3 stash apply --label-ours=UP --label-theirs=STASH && + test_grep "^<<<<<<< UP" conflict-file && + test_grep "^||||||| Stash base" conflict-file && + test_grep "^>>>>>>> STASH" conflict-file +' + +test_expect_success 'apply with empty conflict labels' ' + test_when_finished "git reset --hard && git stash drop" && + git reset --hard && + test_commit empty-label-base conflict-file base-content && + echo stashed >conflict-file && + git stash push -m "stashed" && + test_commit empty-label-upstream conflict-file upstream-content && + test_must_fail git stash apply --label-ours= --label-theirs= && + test_grep "^<<<<<<<$" conflict-file && + test_grep "^>>>>>>>$" conflict-file +' + test_expect_success 'stash create reports a locked index' ' test_when_finished "rm -rf repo" && git init repo &&
xdiff/xmerge.c +3 -3
@@ -199,9 +199,9 @@ static int fill_conflict_hunk(xdfenv_t *xe1, const char *name1, int size, int i, int style, xdmerge_t *m, char *dest, int marker_size) { - int marker1_size = (name1 ? strlen(name1) + 1 : 0); -int marker2_size = (name2 ?strlen (name2) +1 :0 ); -int marker3_size = (name3 ?strlen (name3) +1 :0 ); + int marker1_size = (name1 && *name1 ? strlen(name1) + 1 : 0); +int marker2_size = (name2 && *name2 ?strlen (name2) +1 :0 ); +int marker3_size = (name3 && *name3 ?strlen (name3) +1 :0 );int needs_cr =is_cr_needed (xe1, xe2, m);if (marker_size <=0 )
Signature · 1 line
gitgitgadget
PATCH v13 2/5 sequencer: allow create_autostash to run silently
In reply to Harald Nordgren via GitGitGadgetFrom: Harald Nordgren <[email protected]>
Add a silent parameter to create_autostash_internal and introduce create_autostash_ref_silent so that callers can create an autostash without printing the "Created autostash" message.
--- builtin/merge.c | 6 ++++-- sequencer.c | 17 +++++++++++------ sequencer.h | 3 ++- 3 files changed, 17 insertions(+), 9 deletions(-)
builtin/merge.c +4 -2
@@ -1672,7 +1672,8 @@ int cmd_merge(int argc, }if (autostash) -create_autostash_ref (the_repository,"MERGE_AUTOSTASH" ); +create_autostash_ref (the_repository,"MERGE_AUTOSTASH" , +NULL , false); if (checkout_fast_forward(the_repository, &head_commit->object.oid, &commit->object.oid,
@@ -1764,7 +1765,8 @@ int cmd_merge(int argc,die_ff_impossible ();if (autostash) -create_autostash_ref (the_repository,"MERGE_AUTOSTASH" ); +create_autostash_ref (the_repository,"MERGE_AUTOSTASH" , +NULL , false);/* We are going to make a new commit. */ git_committer_info (IDENT_STRICT );
sequencer.c +11 -6
@@ -4657,7 +4657,9 @@ static enum todo_command peek_command(struct todo_list *todo_list, int offset)static void create_autostash_internal (struct repository *r,const char *path, -const char *refname) +const char *refname, +const char *message, +bool silent) {struct strbuf buf =STRBUF_INIT ;struct lock_file lock_file =LOCK_INIT ;
@@ -4679,7 +4681,8 @@ static void create_autostash_internal(struct repository *r,struct object_id oid;strvec_pushl (&stash.args, -"stash" ,"create" ,"autostash" ,NULL ); +"stash" ,"create" , + message ? message :"autostash" ,NULL ); stash.git_cmd =1 ; stash.no_stdin =1 ;strbuf_reset (&buf);
@@ -4702,7 +4705,8 @@ static void create_autostash_internal(struct repository *r, &oid, null_oid(the_hash_algo), 0, UPDATE_REFS_DIE_ON_ERR); } -printf (_ ("Created autostash: %s\n" ), buf.buf); +if (!silent) +printf (_ ("Created autostash: %s\n" ), buf.buf);if (reset_head (r, &ropts) <0 )die (_ ("could not reset --hard" ));discard_index (r->index);
@@ -4714,12 +4718,13 @@ static void create_autostash_internal(struct repository *r,void create_autostash (struct repository *r,const char *path) { -create_autostash_internal (r, path,NULL ); +create_autostash_internal (r, path,NULL ,NULL , false); } -void create_autostash_ref (struct repository *r,const char *refname) +void create_autostash_ref (struct repository *r,const char *refname, +const char *message,bool silent) { -create_autostash_internal (r,NULL , refname); +create_autostash_internal (r,NULL , refname, message, silent); }static int apply_save_autostash_oid (const char *stash_oid,int attempt_apply)
sequencer.h +2 -1
@@ -229,7 +229,8 @@ void commit_post_rewrite(struct repository *r, const struct object_id *new_head);void create_autostash (struct repository *r,const char *path); -void create_autostash_ref (struct repository *r,const char *refname); +void create_autostash_ref (struct repository *r,const char *refname, +const char *message,bool silent);int save_autostash (const char *path);int save_autostash_ref (struct repository *r,const char *refname);int apply_autostash (const char *path);
Signature · 1 line
gitgitgadget
PATCH v13 3/5 sequencer: teach autostash apply to take optional conflict marker labels
In reply to Harald Nordgren via GitGitGadgetFrom: Harald Nordgren <[email protected]>
Add label_ours, label_theirs, label_base, and stash_msg parameters to apply_autostash_ref() and the autostash apply machinery so callers can pass custom conflict marker labels through to
"git stash apply --label-ours/--label-theirs/--label-base", as well as a custom stash message for "git stash store -m".
--- builtin/commit.c | 3 ++- builtin/merge.c | 9 ++++++--- sequencer.c | 38 +++++++++++++++++++++++++++++--------- sequencer.h | 4 +++- 4 files changed, 40 insertions(+), 14 deletions(-)
builtin/commit.c +2 -1
@@ -1979,7 +1979,8 @@ int cmd_commit(int argc, &oid, flags); } -apply_autostash_ref (the_repository,"MERGE_AUTOSTASH" ); +apply_autostash_ref (the_repository,"MERGE_AUTOSTASH" , +NULL ,NULL ,NULL ,NULL ); cleanup:free_commit_extra_headers (extra);
builtin/merge.c +6 -3
@@ -537,7 +537,8 @@ static void finish(struct commit *head_commit,run_hooks_l (the_repository,"post-merge" , squash ?"1" :"0" ,NULL );if (new_head) -apply_autostash_ref (the_repository,"MERGE_AUTOSTASH" ); +apply_autostash_ref (the_repository,"MERGE_AUTOSTASH" , +NULL ,NULL ,NULL ,NULL );strbuf_release (&reflog_message); }
@@ -1678,7 +1679,8 @@ int cmd_merge(int argc, &head_commit->object.oid, &commit->object.oid, overwrite_ignore)) { -apply_autostash_ref (the_repository,"MERGE_AUTOSTASH" ); +apply_autostash_ref (the_repository,"MERGE_AUTOSTASH" , +NULL ,NULL ,NULL ,NULL ); ret =1 ; goto done; }
@@ -1851,7 +1853,8 @@ int cmd_merge(int argc,else fprintf (stderr,_ ("Merge with strategy %s failed.\n" ), use_strategies[0 ]->name); -apply_autostash_ref (the_repository,"MERGE_AUTOSTASH" ); +apply_autostash_ref (the_repository,"MERGE_AUTOSTASH" , +NULL ,NULL ,NULL ,NULL ); ret =2 ; goto done; }else if (best_strategy == wt_strategy)
sequencer.c +29 -9
@@ -4727,7 +4727,10 @@ void create_autostash_ref(struct repository *r, const char *refname,create_autostash_internal (r,NULL , refname, message, silent); } -static int apply_save_autostash_oid (const char *stash_oid,int attempt_apply) +static int apply_save_autostash_oid (const char *stash_oid,int attempt_apply, +const char *label_ours,const char *label_theirs, +const char *label_base, +const char *stash_msg) {struct child_process child =CHILD_PROCESS_INIT ;int ret =0 ;
@@ -4738,6 +4741,12 @@ static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply) child.no_stderr =1 ;strvec_push (&child.args,"stash" );strvec_push (&child.args,"apply" ); +if (label_ours) +strvec_pushf (&child.args,"--label-ours=%s" , label_ours); +if (label_theirs) +strvec_pushf (&child.args,"--label-theirs=%s" , label_theirs); +if (label_base) +strvec_pushf (&child.args,"--label-base=%s" , label_base);strvec_push (&child.args, stash_oid); ret =run_command (&child); }
@@ -4751,7 +4760,7 @@ static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply)strvec_push (&store.args,"stash" );strvec_push (&store.args,"store" );strvec_push (&store.args,"-m" ); -strvec_push (&store.args,"autostash" ); +strvec_push (&store.args, stash_msg ? stash_msg :"autostash" );strvec_push (&store.args,"-q" );strvec_push (&store.args, stash_oid);if (run_command (&store))
@@ -4782,7 +4791,8 @@ static int apply_save_autostash(const char *path, int attempt_apply) }strbuf_trim (&stash_oid); - ret =apply_save_autostash_oid (stash_oid.buf, attempt_apply); + ret =apply_save_autostash_oid (stash_oid.buf, attempt_apply, +NULL ,NULL ,NULL ,NULL );unlink (path);strbuf_release (&stash_oid);
@@ -4801,11 +4811,14 @@ int apply_autostash(const char *path)int apply_autostash_oid (const char *stash_oid) { -return apply_save_autostash_oid (stash_oid,1 ); +return apply_save_autostash_oid (stash_oid,1 ,NULL ,NULL ,NULL ,NULL ); }static int apply_save_autostash_ref (struct repository *r,const char *refname, -int attempt_apply) +int attempt_apply, +const char *label_ours,const char *label_theirs, +const char *label_base, +const char *stash_msg) {struct object_id stash_oid;char stash_oid_hex[GIT_MAX_HEXSZ +1 ];
@@ -4821,7 +4834,9 @@ static int apply_save_autostash_ref(struct repository *r, const char *refname,return error (_ ("autostash reference is a symref" ));oid_to_hex_r (stash_oid_hex, &stash_oid); - ret =apply_save_autostash_oid (stash_oid_hex, attempt_apply); + ret =apply_save_autostash_oid (stash_oid_hex, attempt_apply, + label_ours, label_theirs, label_base, + stash_msg);refs_delete_ref (get_main_ref_store (r),"" , refname, &stash_oid,REF_NO_DEREF );
@@ -4831,12 +4846,17 @@ static int apply_save_autostash_ref(struct repository *r, const char *refname,int save_autostash_ref (struct repository *r,const char *refname) { -return apply_save_autostash_ref (r, refname,0 ); +return apply_save_autostash_ref (r, refname,0 , +NULL ,NULL ,NULL ,NULL ); } -int apply_autostash_ref (struct repository *r,const char *refname) +int apply_autostash_ref (struct repository *r,const char *refname, +const char *label_ours,const char *label_theirs, +const char *label_base,const char *stash_msg) { -return apply_save_autostash_ref (r, refname,1 ); +return apply_save_autostash_ref (r, refname,1 , + label_ours, label_theirs, label_base, + stash_msg); } static int checkout_onto(struct repository *r, struct replay_opts *opts,
sequencer.h +3 -1
@@ -235,7 +235,9 @@ int save_autostash(const char *path);int save_autostash_ref (struct repository *r,const char *refname);int apply_autostash (const char *path);int apply_autostash_oid (const char *stash_oid); -int apply_autostash_ref (struct repository *r,const char *refname); +int apply_autostash_ref (struct repository *r,const char *refname, +const char *label_ours,const char *label_theirs, +const char *label_base,const char *stash_msg);#define SUMMARY_INITIAL_COMMIT (1 << 0)#define SUMMARY_SHOW_AUTHOR_DATE (1 << 1)
Signature · 1 line
gitgitgadget
PATCH v13 4/5 checkout: rollback lock on early returns in merge_working_tree
In reply to Harald Nordgren via GitGitGadgetFrom: Harald Nordgren <[email protected]>
merge_working_tree() acquires the index lock via repo_hold_locked_index() but several early return paths exit without calling rollback_lock_file(), leaving the lock held. While this is currently harmless because the process exits soon after, it becomes a problem if the function is ever called more than once in the same process.
Add rollback_lock_file() calls to all early return paths.
--- builtin/checkout.c | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-)
builtin/checkout.c +22 -7
@@ -783,8 +783,10 @@ static int merge_working_tree(const struct checkout_opts *opts,struct tree *new_tree;repo_hold_locked_index (the_repository, &lock_file,LOCK_DIE_ON_ERROR ); -if (repo_read_index_preload (the_repository,NULL ,0 ) <0 ) +if (repo_read_index_preload (the_repository,NULL ,0 ) <0 ) { +rollback_lock_file (&lock_file);return error (_ ("index file corrupt" )); + }resolve_undo_clear_index (the_repository->index);if (opts->new_orphan_branch && opts->orphan_from_empty_tree) {
@@ -797,14 +799,18 @@ static int merge_working_tree(const struct checkout_opts *opts, }else { new_tree =repo_get_commit_tree (the_repository, new_branch_info->commit); -if (!new_tree) +if (!new_tree) { +rollback_lock_file (&lock_file);return error (_ ("unable to read tree (%s)" ),oid_to_hex (&new_branch_info->commit->object.oid)); + } }if (opts->discard_changes) { ret =reset_tree (new_tree, opts,1 , writeout_error, new_branch_info); -if (ret) +if (ret) { +rollback_lock_file (&lock_file);return ret; + } }else {struct tree_desc trees[2 ];struct tree *tree;
@@ -814,6 +820,7 @@ static int merge_working_tree(const struct checkout_opts *opts,refresh_index (the_repository->index,REFRESH_QUIET ,NULL ,NULL ,NULL );if (unmerged_index (the_repository->index)) { +rollback_lock_file (&lock_file);error (_ ("you need to resolve your current index first" ));return 1 ; }
@@ -857,15 +864,19 @@ static int merge_working_tree(const struct checkout_opts *opts,struct strbuf sb =STRBUF_INIT ;struct strbuf old_commit_shortname =STRBUF_INIT ; -if (!opts->merge) +if (!opts->merge) { +rollback_lock_file (&lock_file);return 1 ; + }/* * Without old_branch_info->commit, the below is the same as * the two-tree unpack we already tried and failed. */ -if (!old_branch_info->commit) +if (!old_branch_info->commit) { +rollback_lock_file (&lock_file);return 1 ; + } old_tree =repo_get_commit_tree (the_repository, old_branch_info->commit);
@@ -897,8 +908,10 @@ static int merge_working_tree(const struct checkout_opts *opts, ret =reset_tree (new_tree, opts,1 , writeout_error, new_branch_info); -if (ret) +if (ret) { +rollback_lock_file (&lock_file);return ret; + } o.ancestor = old_branch_info->name;if (!old_branch_info->name) { strbuf_add_unique_abbrev(&old_commit_shortname,
@@ -920,8 +933,10 @@ static int merge_working_tree(const struct checkout_opts *opts, writeout_error, new_branch_info);strbuf_release (&o.obuf);strbuf_release (&old_commit_shortname); -if (ret) +if (ret) { +rollback_lock_file (&lock_file);return ret; + } } }
Signature · 1 line
gitgitgadget
PATCH v13 5/5 checkout -m: autostash when switching branches
In reply to Harald Nordgren via GitGitGadgetFrom: Harald Nordgren <[email protected]>
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.
--- Documentation/git-checkout.adoc | 58 ++++----- Documentation/git-switch.adoc | 33 ++--- builtin/checkout.c | 144 +++++++++------------- sequencer.c | 18 ++- t/t3420-rebase-autostash.sh | 24 +++- t/t7201-co.sh | 208 ++++++++++++++++++++++++++++++++ t/t7600-merge.sh | 2 +- xdiff-interface.c | 12 ++ xdiff-interface.h | 1 + 9 files changed, 359 insertions(+), 141 deletions(-)
Documentation/git-checkout.adoc +31 -27
@@ -251,20 +251,19 @@ working tree, by copying them from elsewhere, extracting a tarball, etc. are different between the current branch and the branch to which you are switching, the command refuses to switch branches in order to preserve your modifications in context. - However, with this option, a three-way merge between the current - branch, your working tree contents, and the new branch - is done, and you will be on the new branch. -+ -When a merge conflict happens, the index entries for conflicting -paths are left unmerged, and you need to resolve the conflicts -and mark the resolved paths with `git add` (or `git rm` if the merge -should result in deletion of the path). + With this option, the conflicting local changes are + automatically stashed before the switch and reapplied + afterwards. If the local changes do not overlap with the + differences between branches, the switch proceeds without + stashing. If reapplying the stash results in conflicts, the + entry is saved to the stash list. Resolve the conflicts + and run `git stash drop` when done, or clear the working + tree (e.g. with `git reset --hard`) before running `git stash + pop` later to re-apply your changes. + When checking out paths from the index, this option lets you recreate the conflicted merge in the specified paths. This option cannot be used when checking out paths from a tree-ish. -+ -When switching branches with `--merge`, staged changes may be lost. `--conflict=<style>`:: The same as `--merge` option above, but changes the way the
@@ -578,39 +577,44 @@ $ git checkout mytopic error: You have local changes to 'frotz'; not switching branches. ------------ -You can give the `-m` flag to the command, which would try a -three-way merge: +You can give the `-m` flag to the command, which would carry your local +changes to the new branch: ------------ $ git checkout -m mytopic -Auto-merging frotz +Switched to branch 'mytopic' ------------ -After this three-way merge, the local modifications are _not_ +After the switch, the local modifications are reapplied and are _not_ registered in your index file, so `git diff` would show you what changes you made since the tip of the new branch. === 3. Merge conflict -When a merge conflict happens during switching branches with -the `-m` option, you would see something like this: +When the `--merge` (`-m`) option is in effect and the locally +modified files overlap with files that need to be updated by the +branch switch, the changes are stashed and reapplied after the +switch. If this process results in conflicts, a stash entry is saved +and made available in `git stash list`: ------------ $ git checkout -m mytopic -Auto-merging frotz -ERROR: Merge conflict in frotz -fatal: merge program failed ------------- +Your local changes are stashed, however, applying it to carry +forward your local changes resulted in conflicts: -At this point, `git diff` shows the changes cleanly merged as in -the previous example, as well as the changes in the conflicted -files. Edit and resolve the conflict and mark it resolved with -`git add` as usual: + - You can try resolving them now. If you resolved them + successfully, discard the stash entry with "git stash drop". + - Alternatively you can "git reset --hard" if you do not want + to deal with them right now, and later "git stash pop" to + recover your local changes. ------------ -$ edit frotz -$ git add frotz ------------- + +You can try resolving the conflicts now. Edit the conflicting files +and mark them resolved with `git add` as usual, then run `git stash +drop` to discard the stash entry. Alternatively, you can clear the +working tree with `git reset --hard` and recover your local changes +later with `git stash pop`. CONFIGURATION -------------
Documentation/git-switch.adoc +17 -16
@@ -123,18 +123,19 @@ variable. `-m`:: `--merge`:: - If you have local modifications to one or more files that are - different between the current branch and the branch to which - you are switching, the command refuses to switch branches in - order to preserve your modifications in context. However, - with this option, a three-way merge between the current - branch, your working tree contents, and the new branch is - done, and you will be on the new branch. -+ -When a merge conflict happens, the index entries for conflicting -paths are left unmerged, and you need to resolve the conflicts -and mark the resolved paths with `git add` (or `git rm` if the merge -should result in deletion of the path). + If you have local modifications to one or more files that + are different between the current branch and the branch to + which you are switching, the command normally refuses to + switch branches in order to preserve your modifications in + context. However, with this option, the conflicting local + changes are automatically stashed before the switch and + reapplied afterwards. If the local changes do not overlap + with the differences between branches, the switch proceeds + without stashing. If reapplying the stash results in + conflicts, the entry is saved to the stash list. Resolve + the conflicts and run `git stash drop` when done, or clear + the working tree (e.g. with `git reset --hard`) before + running `git stash pop` later to re-apply your changes. `--conflict=<style>`:: The same as `--merge` option above, but changes the way the
@@ -217,15 +218,15 @@ $ git switch mytopic error: You have local changes to 'frotz'; not switching branches. ------------ -You can give the `-m` flag to the command, which would try a three-way -merge: +You can give the `-m` flag to the command, which would carry your local +changes to the new branch: ------------ $ git switch -m mytopic -Auto-merging frotz +Switched to branch 'mytopic' ------------ -After this three-way merge, the local modifications are _not_ +After the switch, the local modifications are reapplied and are _not_ registered in your index file, so `git diff` would show you what changes you made since the tip of the new branch.
builtin/checkout.c +58 -86
186 lines not on this page · raw message
sequencer.c +13 -5
@@ -4765,15 +4765,23 @@ static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply,strvec_push (&store.args, stash_oid);if (run_command (&store)) ret =error (_ ("cannot store %s" ), stash_oid); +else if (attempt_apply) +fprintf (stderr, +_ ("Your local changes are stashed, however, applying it to carry\n" +"forward your local changes resulted in conflicts:\n" +"\n" +" - You can try resolving them now. If you resolved them\n" +" successfully, discard the stash entry with \"git stash drop\".\n" +"\n" +" - Alternatively you can \"git reset --hard\" if you do not want\n" +" to deal with them right now, and later \"git stash pop\" to\n" +" recover your local changes.\n" ));else fprintf (stderr, -_ ("%s\n" +_ ("Autostash exists; creating a new stash entry.\n" "Your changes are safe in the stash.\n" "You can run \"git stash pop\" or" -" \"git stash drop\" at any time.\n" ), - attempt_apply ? -_ ("Applying autostash resulted in conflicts." ) : -_ ("Autostash exists; creating a new stash entry." )); +" \"git stash drop\" at any time.\n" )); }return ret;
t/t3420-rebase-autostash.sh +18 -6
@@ -61,18 +61,30 @@ create_expected_failure_apply () { First, rewinding head to replay your work on top of it... Applying: second commit Applying: third commit - Applying autostash resulted in conflicts. - Your changes are safe in the stash. - You can run "git stash pop" or "git stash drop" at any time. + Your local changes are stashed, however, applying it to carry + forward your local changes resulted in conflicts: + + - You can try resolving them now. If you resolved them + successfully, discard the stash entry with "git stash drop". + + - Alternatively you can "git reset --hard" if you do not want + to deal with them right now, and later "git stash pop" to + recover your local changes. EOF } create_expected_failure_merge () { cat >expected <<-EOF $(grep "^Created autostash: [0-9a-f][0-9a-f]*\$" actual) - Applying autostash resulted in conflicts. - Your changes are safe in the stash. - You can run "git stash pop" or "git stash drop" at any time. + Your local changes are stashed, however, applying it to carry + forward your local changes resulted in conflicts: + + - You can try resolving them now. If you resolved them + successfully, discard the stash entry with "git stash drop". + + - Alternatively you can "git reset --hard" if you do not want + to deal with them right now, and later "git stash pop" to + recover your local changes. Successfully rebased and updated refs/heads/rebased-feature-branch. EOF }
t/t7201-co.sh +208 -0
214 lines not on this page · raw message
t/t7600-merge.sh +1 -1
@@ -914,7 +914,7 @@ test_expect_success 'merge with conflicted --autostash changes' ' git diff >expect && test_when_finished "test_might_fail git stash drop" && git merge --autostash c3 2>err && - test_grep "Applying autostash resulted in conflicts." err && + test_grep "your local changes resulted in conflicts" err && git show HEAD:file >merge-result && test_cmp result.1-9 merge-result && git stash show -p >actual &&
xdiff-interface.c +12 -0
@@ -325,6 +325,18 @@ int parse_conflict_style_name(const char *value)return -1 ; } +const char *conflict_style_name (int style) +{ +switch (style) { +case XDL_MERGE_DIFF3 : +return "diff3" ; +case XDL_MERGE_ZEALOUS_DIFF3 : +return "zdiff3" ; +default : +return "merge" ; + } +} +int git_xmerge_style =-1 ; int git_xmerge_config(const char *var, const char *value,
xdiff-interface.h +1 -0
@@ -55,6 +55,7 @@ void xdiff_set_find_func(xdemitconf_t *xecfg, const char *line, int cflags);void xdiff_clear_find_func (xdemitconf_t *xecfg);struct config_context ;int parse_conflict_style_name (const char *value); +const char *conflict_style_name (int style);int git_xmerge_config (const char *var,const char *value,const struct config_context *ctx,void *cb);extern int git_xmerge_style;
Signature · 1 line
gitgitgadget
v14 cover checkout: 'autostash' for branch switching
In reply to Harald Nordgren via GitGitGadgetSimplifying the tests in t/t3903-stash.sh according to Phillip Wood's comment. I believe everything sound be fixed now or responded to.
Also rebasing against upstream.
Harald Nordgren (5):
stash: add --label-ours, --label-theirs, --label-base for apply
sequencer: allow create_autostash to run silently
sequencer: teach autostash apply to take optional conflict marker
labels
checkout: rollback lock on early returns in merge_working_tree checkout -m: autostash when switching branches
Documentation/git-checkout.adoc | 58 ++++----- Documentation/git-stash.adoc | 11 +- Documentation/git-switch.adoc | 33 ++--- builtin/checkout.c | 149 +++++++++++------------ builtin/commit.c | 3 +- builtin/merge.c | 15 ++- builtin/stash.c | 28 +++-- sequencer.c | 73 ++++++++--- sequencer.h | 7 +- t/t3420-rebase-autostash.sh | 24 +++- t/t3903-stash.sh | 24 ++++ t/t7201-co.sh | 208 ++++++++++++++++++++++++++++++++ t/t7600-merge.sh | 2 +- xdiff-interface.c | 12 ++ xdiff-interface.h | 1 + xdiff/xmerge.c | 6 +- 16 files changed, 483 insertions(+), 171 deletions(-)
base-commit: 9f223ef1c026d91c7ac68cc0211bde255dda6199
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2234%2FHaraldNordgren%2Fcheckout_autostash-v14
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2234/HaraldNordgren/checkout_autostash-v14
Pull-Request: https://github.com/git/git/pull/2234
Range-diff vs v13:
1: 43bfdf2136 ! 1: e18c25599a stash: add --label-ours, --label-theirs, --label-base for apply
@@ builtin/stash.c: static int branch_stash(int argc, const char **argv, const char
## t/t3903-stash.sh ##
-@@ t/t3903-stash.sh: test_expect_success 'restore untracked files even when we hit conflicts' '
- )
+@@ t/t3903-stash.sh: setup_stash() {
+ git add other-file &&
+ test_tick &&
+ git commit -m initial &&
++ git tag initial &&
+ echo 2 >file &&
+ git add file &&
+ echo 3 >file &&
+@@ t/t3903-stash.sh: test_expect_success 'stash.index=false overridden by --index' '
+ test_cmp expect file
'
+test_expect_success 'apply with custom conflict labels' '
-+ test_when_finished "git reset --hard && git stash drop" &&
-+ git reset --hard &&
++ git reset --hard initial &&
+ test_commit label-base conflict-file base-content &&
+ echo stashed >conflict-file &&
+ git stash push -m "stashed" &&
@@ t/t3903-stash.sh: test_expect_success 'restore untracked files even when we hit
+'
+
+test_expect_success 'apply with empty conflict labels' '
-+ test_when_finished "git reset --hard && git stash drop" &&
-+ git reset --hard &&
++ git reset --hard initial &&
+ test_commit empty-label-base conflict-file base-content &&
+ echo stashed >conflict-file &&
+ git stash push -m "stashed" &&
@@ t/t3903-stash.sh: test_expect_success 'restore untracked files even when we hit
+ test_grep "^>>>>>>>$" conflict-file
+'
+
- test_expect_success 'stash create reports a locked index' '
- test_when_finished "rm -rf repo" &&
- git init repo &&
+ test_done
## xdiff/xmerge.c ##
@@ xdiff/xmerge.c: static int fill_conflict_hunk(xdfenv_t *xe1, const char *name1,
2: 7f3c32f5e9 = 2: ce29b10264 sequencer: allow create_autostash to run silently
3: b279d1dac8 = 3: 73051d1762 sequencer: teach autostash apply to take optional conflict marker labels
4: 04869314ec = 4: 191058d8e3 checkout: rollback lock on early returns in merge_working_tree
5: 4b3c6025ac = 5: 86f33df1eb checkout -m: autostash when switching branchesSignature · 1 line
gitgitgadget
PATCH v14 1/5 stash: add --label-ours, --label-theirs, --label-base for apply
From: Harald Nordgren <[email protected]>
Allow callers of "git stash apply" to pass custom labels for conflict markers instead of the default "Updated upstream" and "Stashed changes". Document the new options and add a test.
--- Documentation/git-stash.adoc | 11 ++++++++++- builtin/stash.c | 28 ++++++++++++++++++++-------- t/t3903-stash.sh | 24 ++++++++++++++++++++++++ xdiff/xmerge.c | 6 +++--- 4 files changed, 57 insertions(+), 12 deletions(-)
Documentation/git-stash.adoc +10 -1
@@ -12,7 +12,7 @@ git stash list [<log-options>] git stash show [-u | --include-untracked | --only-untracked] [<diff-options>] [<stash>] git stash drop [-q | --quiet] [<stash>] git stash pop [--index] [-q | --quiet] [<stash>] -git stash apply [--index] [-q | --quiet] [<stash>] +git stash apply [--index] [-q | --quiet] [--label-ours=<label>] [--label-theirs=<label>] [--label-base=<label>] [<stash>] git stash branch <branchname> [<stash>] git stash [push] [-p | --patch] [-S | --staged] [-k | --[no-]keep-index] [-q | --quiet] [-u | --include-untracked] [-a | --all] [(-m | --message) <message>]
@@ -195,6 +195,15 @@ the index's ones. However, this can fail, when you have conflicts (which are stored in the index, where you therefore can no longer apply the changes as they were originally). +`--label-ours=<label>`:: +`--label-theirs=<label>`:: +`--label-base=<label>`:: + These options are only valid for the `apply` command. ++ +Use the given labels in conflict markers instead of the default +"Updated upstream", "Stashed changes", and "Stash base". +`--label-base` only has an effect with merge.conflictStyle=diff3. + `-k`:: `--keep-index`:: `--no-keep-index`::
builtin/stash.c +20 -8
@@ -44,7 +44,7 @@#define BUILTIN_STASH_POP_USAGE \ N_("git stash pop [--index] [-q | --quiet] [<stash>]")#define BUILTIN_STASH_APPLY_USAGE \ - N_("git stash apply [--index] [-q | --quiet] [<stash>]") + N_("git stash apply [--index] [-q | --quiet] [--label-ours=<label>] [--label-theirs=<label>] [--label-base=<label>] [<stash>]")#define BUILTIN_STASH_BRANCH_USAGE \ N_("git stash branch <branchname> [<stash>]")#define BUILTIN_STASH_STORE_USAGE \
@@ -591,7 +591,9 @@ static void unstage_changes_unless_new(struct object_id *orig_tree) }static int do_apply_stash (const char *prefix,struct stash_info *info, -int index,int quiet) +int index,int quiet, +const char *label_ours,const char *label_theirs, +const char *label_base) {int clean, ret;int has_index = index;
@@ -643,9 +645,9 @@ static int do_apply_stash(const char *prefix, struct stash_info *info,init_ui_merge_options (&o, the_repository); - o.branch1 ="Updated upstream" ; - o.branch2 ="Stashed changes" ; - o.ancestor ="Stash base" ; + o.branch1 = label_ours ? label_ours :"Updated upstream" ; + o.branch2 = label_theirs ? label_theirs :"Stashed changes" ; + o.ancestor = label_base ? label_base :"Stash base" ;if (oideq (&info->b_tree, &c_tree)) o.branch1 ="Version stash was based on" ;
@@ -723,11 +725,18 @@ static int apply_stash(int argc, const char **argv, const char *prefix,int ret =-1 ;int quiet =0 ;int index = use_index; +const char *label_ours =NULL , *label_theirs =NULL , *label_base =NULL ;struct stash_info info =STASH_INFO_INIT ;struct option options[] = {OPT__QUIET (&quiet,N_ ("be quiet, only report errors" )),OPT_BOOL (0 ,"index" , &index,N_ ("attempt to recreate the index" )), +OPT_STRING (0 ,"label-ours" , &label_ours,N_ ("label" ), +N_ ("label for the upstream side in conflict markers" )), +OPT_STRING (0 ,"label-theirs" , &label_theirs,N_ ("label" ), +N_ ("label for the stashed side in conflict markers" )), +OPT_STRING (0 ,"label-base" , &label_base,N_ ("label" ), +N_ ("label for the base in diff3 conflict markers" )),OPT_END () };
@@ -737,7 +746,8 @@ static int apply_stash(int argc, const char **argv, const char *prefix,if (get_stash_info (&info, argc, argv)) goto cleanup; - ret =do_apply_stash (prefix, &info, index, quiet); + ret =do_apply_stash (prefix, &info, index, quiet, + label_ours, label_theirs, label_base); cleanup:free_stash_info (&info);return ret;
@@ -836,7 +846,8 @@ static int pop_stash(int argc, const char **argv, const char *prefix,if (get_stash_info_assert (&info, argc, argv)) goto cleanup; -if ((ret =do_apply_stash (prefix, &info, index, quiet))) +if ((ret =do_apply_stash (prefix, &info, index, quiet, +NULL ,NULL ,NULL )))printf_ln (_ ("The stash entry is kept in case " "you need it again." ));else
@@ -877,7 +888,8 @@ static int branch_stash(int argc, const char **argv, const char *prefix,strvec_push (&cp.args,oid_to_hex (&info.b_commit)); ret =run_command (&cp);if (!ret) - ret =do_apply_stash (prefix, &info,1 ,0 ); + ret =do_apply_stash (prefix, &info,1 ,0 , +NULL ,NULL ,NULL );if (!ret && info.is_stash_ref) ret =do_drop_stash (&info,0 );
t/t3903-stash.sh +24 -0
@@ -56,6 +56,7 @@ setup_stash() { git add other-file && test_tick && git commit -m initial && + git tag initial && echo 2 >file && git add file && echo 3 >file &&
@@ -1790,4 +1791,27 @@ test_expect_success 'stash.index=false overridden by --index' ' test_cmp expect file ' +test_expect_success 'apply with custom conflict labels' ' + git reset --hard initial && + test_commit label-base conflict-file base-content && + echo stashed >conflict-file && + git stash push -m "stashed" && + test_commit label-upstream conflict-file upstream-content && + test_must_fail git -c merge.conflictStyle=diff3 stash apply --label-ours=UP --label-theirs=STASH && + test_grep "^<<<<<<< UP" conflict-file && + test_grep "^||||||| Stash base" conflict-file && + test_grep "^>>>>>>> STASH" conflict-file +' + +test_expect_success 'apply with empty conflict labels' ' + git reset --hard initial && + test_commit empty-label-base conflict-file base-content && + echo stashed >conflict-file && + git stash push -m "stashed" && + test_commit empty-label-upstream conflict-file upstream-content && + test_must_fail git stash apply --label-ours= --label-theirs= && + test_grep "^<<<<<<<$" conflict-file && + test_grep "^>>>>>>>$" conflict-file +' + test_done
xdiff/xmerge.c +3 -3
@@ -199,9 +199,9 @@ static int fill_conflict_hunk(xdfenv_t *xe1, const char *name1, int size, int i, int style, xdmerge_t *m, char *dest, int marker_size) { - int marker1_size = (name1 ? strlen(name1) + 1 : 0); -int marker2_size = (name2 ?strlen (name2) +1 :0 ); -int marker3_size = (name3 ?strlen (name3) +1 :0 ); + int marker1_size = (name1 && *name1 ? strlen(name1) + 1 : 0); +int marker2_size = (name2 && *name2 ?strlen (name2) +1 :0 ); +int marker3_size = (name3 && *name3 ?strlen (name3) +1 :0 );int needs_cr =is_cr_needed (xe1, xe2, m);if (marker_size <=0 )
Signature · 1 line
gitgitgadget
PATCH v14 2/5 sequencer: allow create_autostash to run silently
In reply to Harald Nordgren via GitGitGadgetFrom: Harald Nordgren <[email protected]>
Add a silent parameter to create_autostash_internal and introduce create_autostash_ref_silent so that callers can create an autostash without printing the "Created autostash" message.
--- builtin/merge.c | 6 ++++-- sequencer.c | 17 +++++++++++------ sequencer.h | 3 ++- 3 files changed, 17 insertions(+), 9 deletions(-)
builtin/merge.c +4 -2
@@ -1672,7 +1672,8 @@ int cmd_merge(int argc, }if (autostash) -create_autostash_ref (the_repository,"MERGE_AUTOSTASH" ); +create_autostash_ref (the_repository,"MERGE_AUTOSTASH" , +NULL , false); if (checkout_fast_forward(the_repository, &head_commit->object.oid, &commit->object.oid,
@@ -1764,7 +1765,8 @@ int cmd_merge(int argc,die_ff_impossible ();if (autostash) -create_autostash_ref (the_repository,"MERGE_AUTOSTASH" ); +create_autostash_ref (the_repository,"MERGE_AUTOSTASH" , +NULL , false);/* We are going to make a new commit. */ git_committer_info (IDENT_STRICT );
sequencer.c +11 -6
@@ -4657,7 +4657,9 @@ static enum todo_command peek_command(struct todo_list *todo_list, int offset)static void create_autostash_internal (struct repository *r,const char *path, -const char *refname) +const char *refname, +const char *message, +bool silent) {struct strbuf buf =STRBUF_INIT ;struct lock_file lock_file =LOCK_INIT ;
@@ -4679,7 +4681,8 @@ static void create_autostash_internal(struct repository *r,struct object_id oid;strvec_pushl (&stash.args, -"stash" ,"create" ,"autostash" ,NULL ); +"stash" ,"create" , + message ? message :"autostash" ,NULL ); stash.git_cmd =1 ; stash.no_stdin =1 ;strbuf_reset (&buf);
@@ -4702,7 +4705,8 @@ static void create_autostash_internal(struct repository *r, &oid, null_oid(the_hash_algo), 0, UPDATE_REFS_DIE_ON_ERR); } -printf (_ ("Created autostash: %s\n" ), buf.buf); +if (!silent) +printf (_ ("Created autostash: %s\n" ), buf.buf);if (reset_head (r, &ropts) <0 )die (_ ("could not reset --hard" ));discard_index (r->index);
@@ -4714,12 +4718,13 @@ static void create_autostash_internal(struct repository *r,void create_autostash (struct repository *r,const char *path) { -create_autostash_internal (r, path,NULL ); +create_autostash_internal (r, path,NULL ,NULL , false); } -void create_autostash_ref (struct repository *r,const char *refname) +void create_autostash_ref (struct repository *r,const char *refname, +const char *message,bool silent) { -create_autostash_internal (r,NULL , refname); +create_autostash_internal (r,NULL , refname, message, silent); }static int apply_save_autostash_oid (const char *stash_oid,int attempt_apply)
sequencer.h +2 -1
@@ -229,7 +229,8 @@ void commit_post_rewrite(struct repository *r, const struct object_id *new_head);void create_autostash (struct repository *r,const char *path); -void create_autostash_ref (struct repository *r,const char *refname); +void create_autostash_ref (struct repository *r,const char *refname, +const char *message,bool silent);int save_autostash (const char *path);int save_autostash_ref (struct repository *r,const char *refname);int apply_autostash (const char *path);
Signature · 1 line
gitgitgadget
PATCH v14 3/5 sequencer: teach autostash apply to take optional conflict marker labels
In reply to Harald Nordgren via GitGitGadgetFrom: Harald Nordgren <[email protected]>
Add label_ours, label_theirs, label_base, and stash_msg parameters to apply_autostash_ref() and the autostash apply machinery so callers can pass custom conflict marker labels through to
"git stash apply --label-ours/--label-theirs/--label-base", as well as a custom stash message for "git stash store -m".
--- builtin/commit.c | 3 ++- builtin/merge.c | 9 ++++++--- sequencer.c | 38 +++++++++++++++++++++++++++++--------- sequencer.h | 4 +++- 4 files changed, 40 insertions(+), 14 deletions(-)
builtin/commit.c +2 -1
@@ -1979,7 +1979,8 @@ int cmd_commit(int argc, &oid, flags); } -apply_autostash_ref (the_repository,"MERGE_AUTOSTASH" ); +apply_autostash_ref (the_repository,"MERGE_AUTOSTASH" , +NULL ,NULL ,NULL ,NULL ); cleanup:free_commit_extra_headers (extra);
builtin/merge.c +6 -3
@@ -537,7 +537,8 @@ static void finish(struct commit *head_commit,run_hooks_l (the_repository,"post-merge" , squash ?"1" :"0" ,NULL );if (new_head) -apply_autostash_ref (the_repository,"MERGE_AUTOSTASH" ); +apply_autostash_ref (the_repository,"MERGE_AUTOSTASH" , +NULL ,NULL ,NULL ,NULL );strbuf_release (&reflog_message); }
@@ -1678,7 +1679,8 @@ int cmd_merge(int argc, &head_commit->object.oid, &commit->object.oid, overwrite_ignore)) { -apply_autostash_ref (the_repository,"MERGE_AUTOSTASH" ); +apply_autostash_ref (the_repository,"MERGE_AUTOSTASH" , +NULL ,NULL ,NULL ,NULL ); ret =1 ; goto done; }
@@ -1851,7 +1853,8 @@ int cmd_merge(int argc,else fprintf (stderr,_ ("Merge with strategy %s failed.\n" ), use_strategies[0 ]->name); -apply_autostash_ref (the_repository,"MERGE_AUTOSTASH" ); +apply_autostash_ref (the_repository,"MERGE_AUTOSTASH" , +NULL ,NULL ,NULL ,NULL ); ret =2 ; goto done; }else if (best_strategy == wt_strategy)
sequencer.c +29 -9
@@ -4727,7 +4727,10 @@ void create_autostash_ref(struct repository *r, const char *refname,create_autostash_internal (r,NULL , refname, message, silent); } -static int apply_save_autostash_oid (const char *stash_oid,int attempt_apply) +static int apply_save_autostash_oid (const char *stash_oid,int attempt_apply, +const char *label_ours,const char *label_theirs, +const char *label_base, +const char *stash_msg) {struct child_process child =CHILD_PROCESS_INIT ;int ret =0 ;
@@ -4738,6 +4741,12 @@ static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply) child.no_stderr =1 ;strvec_push (&child.args,"stash" );strvec_push (&child.args,"apply" ); +if (label_ours) +strvec_pushf (&child.args,"--label-ours=%s" , label_ours); +if (label_theirs) +strvec_pushf (&child.args,"--label-theirs=%s" , label_theirs); +if (label_base) +strvec_pushf (&child.args,"--label-base=%s" , label_base);strvec_push (&child.args, stash_oid); ret =run_command (&child); }
@@ -4751,7 +4760,7 @@ static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply)strvec_push (&store.args,"stash" );strvec_push (&store.args,"store" );strvec_push (&store.args,"-m" ); -strvec_push (&store.args,"autostash" ); +strvec_push (&store.args, stash_msg ? stash_msg :"autostash" );strvec_push (&store.args,"-q" );strvec_push (&store.args, stash_oid);if (run_command (&store))
@@ -4782,7 +4791,8 @@ static int apply_save_autostash(const char *path, int attempt_apply) }strbuf_trim (&stash_oid); - ret =apply_save_autostash_oid (stash_oid.buf, attempt_apply); + ret =apply_save_autostash_oid (stash_oid.buf, attempt_apply, +NULL ,NULL ,NULL ,NULL );unlink (path);strbuf_release (&stash_oid);
@@ -4801,11 +4811,14 @@ int apply_autostash(const char *path)int apply_autostash_oid (const char *stash_oid) { -return apply_save_autostash_oid (stash_oid,1 ); +return apply_save_autostash_oid (stash_oid,1 ,NULL ,NULL ,NULL ,NULL ); }static int apply_save_autostash_ref (struct repository *r,const char *refname, -int attempt_apply) +int attempt_apply, +const char *label_ours,const char *label_theirs, +const char *label_base, +const char *stash_msg) {struct object_id stash_oid;char stash_oid_hex[GIT_MAX_HEXSZ +1 ];
@@ -4821,7 +4834,9 @@ static int apply_save_autostash_ref(struct repository *r, const char *refname,return error (_ ("autostash reference is a symref" ));oid_to_hex_r (stash_oid_hex, &stash_oid); - ret =apply_save_autostash_oid (stash_oid_hex, attempt_apply); + ret =apply_save_autostash_oid (stash_oid_hex, attempt_apply, + label_ours, label_theirs, label_base, + stash_msg);refs_delete_ref (get_main_ref_store (r),"" , refname, &stash_oid,REF_NO_DEREF );
@@ -4831,12 +4846,17 @@ static int apply_save_autostash_ref(struct repository *r, const char *refname,int save_autostash_ref (struct repository *r,const char *refname) { -return apply_save_autostash_ref (r, refname,0 ); +return apply_save_autostash_ref (r, refname,0 , +NULL ,NULL ,NULL ,NULL ); } -int apply_autostash_ref (struct repository *r,const char *refname) +int apply_autostash_ref (struct repository *r,const char *refname, +const char *label_ours,const char *label_theirs, +const char *label_base,const char *stash_msg) { -return apply_save_autostash_ref (r, refname,1 ); +return apply_save_autostash_ref (r, refname,1 , + label_ours, label_theirs, label_base, + stash_msg); } static int checkout_onto(struct repository *r, struct replay_opts *opts,
sequencer.h +3 -1
@@ -235,7 +235,9 @@ int save_autostash(const char *path);int save_autostash_ref (struct repository *r,const char *refname);int apply_autostash (const char *path);int apply_autostash_oid (const char *stash_oid); -int apply_autostash_ref (struct repository *r,const char *refname); +int apply_autostash_ref (struct repository *r,const char *refname, +const char *label_ours,const char *label_theirs, +const char *label_base,const char *stash_msg);#define SUMMARY_INITIAL_COMMIT (1 << 0)#define SUMMARY_SHOW_AUTHOR_DATE (1 << 1)
Signature · 1 line
gitgitgadget
PATCH v14 4/5 checkout: rollback lock on early returns in merge_working_tree
In reply to Harald Nordgren via GitGitGadgetFrom: Harald Nordgren <[email protected]>
merge_working_tree() acquires the index lock via repo_hold_locked_index() but several early return paths exit without calling rollback_lock_file(), leaving the lock held. While this is currently harmless because the process exits soon after, it becomes a problem if the function is ever called more than once in the same process.
Add rollback_lock_file() calls to all early return paths.
--- builtin/checkout.c | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-)
builtin/checkout.c +22 -7
@@ -783,8 +783,10 @@ static int merge_working_tree(const struct checkout_opts *opts,struct tree *new_tree;repo_hold_locked_index (the_repository, &lock_file,LOCK_DIE_ON_ERROR ); -if (repo_read_index_preload (the_repository,NULL ,0 ) <0 ) +if (repo_read_index_preload (the_repository,NULL ,0 ) <0 ) { +rollback_lock_file (&lock_file);return error (_ ("index file corrupt" )); + }resolve_undo_clear_index (the_repository->index);if (opts->new_orphan_branch && opts->orphan_from_empty_tree) {
@@ -797,14 +799,18 @@ static int merge_working_tree(const struct checkout_opts *opts, }else { new_tree =repo_get_commit_tree (the_repository, new_branch_info->commit); -if (!new_tree) +if (!new_tree) { +rollback_lock_file (&lock_file);return error (_ ("unable to read tree (%s)" ),oid_to_hex (&new_branch_info->commit->object.oid)); + } }if (opts->discard_changes) { ret =reset_tree (new_tree, opts,1 , writeout_error, new_branch_info); -if (ret) +if (ret) { +rollback_lock_file (&lock_file);return ret; + } }else {struct tree_desc trees[2 ];struct tree *tree;
@@ -814,6 +820,7 @@ static int merge_working_tree(const struct checkout_opts *opts,refresh_index (the_repository->index,REFRESH_QUIET ,NULL ,NULL ,NULL );if (unmerged_index (the_repository->index)) { +rollback_lock_file (&lock_file);error (_ ("you need to resolve your current index first" ));return 1 ; }
@@ -857,15 +864,19 @@ static int merge_working_tree(const struct checkout_opts *opts,struct strbuf sb =STRBUF_INIT ;struct strbuf old_commit_shortname =STRBUF_INIT ; -if (!opts->merge) +if (!opts->merge) { +rollback_lock_file (&lock_file);return 1 ; + }/* * Without old_branch_info->commit, the below is the same as * the two-tree unpack we already tried and failed. */ -if (!old_branch_info->commit) +if (!old_branch_info->commit) { +rollback_lock_file (&lock_file);return 1 ; + } old_tree =repo_get_commit_tree (the_repository, old_branch_info->commit);
@@ -897,8 +908,10 @@ static int merge_working_tree(const struct checkout_opts *opts, ret =reset_tree (new_tree, opts,1 , writeout_error, new_branch_info); -if (ret) +if (ret) { +rollback_lock_file (&lock_file);return ret; + } o.ancestor = old_branch_info->name;if (!old_branch_info->name) { strbuf_add_unique_abbrev(&old_commit_shortname,
@@ -920,8 +933,10 @@ static int merge_working_tree(const struct checkout_opts *opts, writeout_error, new_branch_info);strbuf_release (&o.obuf);strbuf_release (&old_commit_shortname); -if (ret) +if (ret) { +rollback_lock_file (&lock_file);return ret; + } } }
Signature · 1 line
gitgitgadget
PATCH v14 5/5 checkout -m: autostash when switching branches
In reply to Harald Nordgren via GitGitGadgetFrom: Harald Nordgren <[email protected]>
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.
--- Documentation/git-checkout.adoc | 58 ++++----- Documentation/git-switch.adoc | 33 ++--- builtin/checkout.c | 144 +++++++++------------- sequencer.c | 18 ++- t/t3420-rebase-autostash.sh | 24 +++- t/t7201-co.sh | 208 ++++++++++++++++++++++++++++++++ t/t7600-merge.sh | 2 +- xdiff-interface.c | 12 ++ xdiff-interface.h | 1 + 9 files changed, 359 insertions(+), 141 deletions(-)
Documentation/git-checkout.adoc +31 -27
@@ -251,20 +251,19 @@ working tree, by copying them from elsewhere, extracting a tarball, etc. are different between the current branch and the branch to which you are switching, the command refuses to switch branches in order to preserve your modifications in context. - However, with this option, a three-way merge between the current - branch, your working tree contents, and the new branch - is done, and you will be on the new branch. -+ -When a merge conflict happens, the index entries for conflicting -paths are left unmerged, and you need to resolve the conflicts -and mark the resolved paths with `git add` (or `git rm` if the merge -should result in deletion of the path). + With this option, the conflicting local changes are + automatically stashed before the switch and reapplied + afterwards. If the local changes do not overlap with the + differences between branches, the switch proceeds without + stashing. If reapplying the stash results in conflicts, the + entry is saved to the stash list. Resolve the conflicts + and run `git stash drop` when done, or clear the working + tree (e.g. with `git reset --hard`) before running `git stash + pop` later to re-apply your changes. + When checking out paths from the index, this option lets you recreate the conflicted merge in the specified paths. This option cannot be used when checking out paths from a tree-ish. -+ -When switching branches with `--merge`, staged changes may be lost. `--conflict=<style>`:: The same as `--merge` option above, but changes the way the
@@ -578,39 +577,44 @@ $ git checkout mytopic error: You have local changes to 'frotz'; not switching branches. ------------ -You can give the `-m` flag to the command, which would try a -three-way merge: +You can give the `-m` flag to the command, which would carry your local +changes to the new branch: ------------ $ git checkout -m mytopic -Auto-merging frotz +Switched to branch 'mytopic' ------------ -After this three-way merge, the local modifications are _not_ +After the switch, the local modifications are reapplied and are _not_ registered in your index file, so `git diff` would show you what changes you made since the tip of the new branch. === 3. Merge conflict -When a merge conflict happens during switching branches with -the `-m` option, you would see something like this: +When the `--merge` (`-m`) option is in effect and the locally +modified files overlap with files that need to be updated by the +branch switch, the changes are stashed and reapplied after the +switch. If this process results in conflicts, a stash entry is saved +and made available in `git stash list`: ------------ $ git checkout -m mytopic -Auto-merging frotz -ERROR: Merge conflict in frotz -fatal: merge program failed ------------- +Your local changes are stashed, however, applying it to carry +forward your local changes resulted in conflicts: -At this point, `git diff` shows the changes cleanly merged as in -the previous example, as well as the changes in the conflicted -files. Edit and resolve the conflict and mark it resolved with -`git add` as usual: + - You can try resolving them now. If you resolved them + successfully, discard the stash entry with "git stash drop". + - Alternatively you can "git reset --hard" if you do not want + to deal with them right now, and later "git stash pop" to + recover your local changes. ------------ -$ edit frotz -$ git add frotz ------------- + +You can try resolving the conflicts now. Edit the conflicting files +and mark them resolved with `git add` as usual, then run `git stash +drop` to discard the stash entry. Alternatively, you can clear the +working tree with `git reset --hard` and recover your local changes +later with `git stash pop`. CONFIGURATION -------------
Documentation/git-switch.adoc +17 -16
@@ -123,18 +123,19 @@ variable. `-m`:: `--merge`:: - If you have local modifications to one or more files that are - different between the current branch and the branch to which - you are switching, the command refuses to switch branches in - order to preserve your modifications in context. However, - with this option, a three-way merge between the current - branch, your working tree contents, and the new branch is - done, and you will be on the new branch. -+ -When a merge conflict happens, the index entries for conflicting -paths are left unmerged, and you need to resolve the conflicts -and mark the resolved paths with `git add` (or `git rm` if the merge -should result in deletion of the path). + If you have local modifications to one or more files that + are different between the current branch and the branch to + which you are switching, the command normally refuses to + switch branches in order to preserve your modifications in + context. However, with this option, the conflicting local + changes are automatically stashed before the switch and + reapplied afterwards. If the local changes do not overlap + with the differences between branches, the switch proceeds + without stashing. If reapplying the stash results in + conflicts, the entry is saved to the stash list. Resolve + the conflicts and run `git stash drop` when done, or clear + the working tree (e.g. with `git reset --hard`) before + running `git stash pop` later to re-apply your changes. `--conflict=<style>`:: The same as `--merge` option above, but changes the way the
@@ -217,15 +218,15 @@ $ git switch mytopic error: You have local changes to 'frotz'; not switching branches. ------------ -You can give the `-m` flag to the command, which would try a three-way -merge: +You can give the `-m` flag to the command, which would carry your local +changes to the new branch: ------------ $ git switch -m mytopic -Auto-merging frotz +Switched to branch 'mytopic' ------------ -After this three-way merge, the local modifications are _not_ +After the switch, the local modifications are reapplied and are _not_ registered in your index file, so `git diff` would show you what changes you made since the tip of the new branch.
builtin/checkout.c +58 -86
186 lines not on this page · raw message
sequencer.c +13 -5
@@ -4765,15 +4765,23 @@ static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply,strvec_push (&store.args, stash_oid);if (run_command (&store)) ret =error (_ ("cannot store %s" ), stash_oid); +else if (attempt_apply) +fprintf (stderr, +_ ("Your local changes are stashed, however, applying it to carry\n" +"forward your local changes resulted in conflicts:\n" +"\n" +" - You can try resolving them now. If you resolved them\n" +" successfully, discard the stash entry with \"git stash drop\".\n" +"\n" +" - Alternatively you can \"git reset --hard\" if you do not want\n" +" to deal with them right now, and later \"git stash pop\" to\n" +" recover your local changes.\n" ));else fprintf (stderr, -_ ("%s\n" +_ ("Autostash exists; creating a new stash entry.\n" "Your changes are safe in the stash.\n" "You can run \"git stash pop\" or" -" \"git stash drop\" at any time.\n" ), - attempt_apply ? -_ ("Applying autostash resulted in conflicts." ) : -_ ("Autostash exists; creating a new stash entry." )); +" \"git stash drop\" at any time.\n" )); }return ret;
t/t3420-rebase-autostash.sh +18 -6
@@ -61,18 +61,30 @@ create_expected_failure_apply () { First, rewinding head to replay your work on top of it... Applying: second commit Applying: third commit - Applying autostash resulted in conflicts. - Your changes are safe in the stash. - You can run "git stash pop" or "git stash drop" at any time. + Your local changes are stashed, however, applying it to carry + forward your local changes resulted in conflicts: + + - You can try resolving them now. If you resolved them + successfully, discard the stash entry with "git stash drop". + + - Alternatively you can "git reset --hard" if you do not want + to deal with them right now, and later "git stash pop" to + recover your local changes. EOF } create_expected_failure_merge () { cat >expected <<-EOF $(grep "^Created autostash: [0-9a-f][0-9a-f]*\$" actual) - Applying autostash resulted in conflicts. - Your changes are safe in the stash. - You can run "git stash pop" or "git stash drop" at any time. + Your local changes are stashed, however, applying it to carry + forward your local changes resulted in conflicts: + + - You can try resolving them now. If you resolved them + successfully, discard the stash entry with "git stash drop". + + - Alternatively you can "git reset --hard" if you do not want + to deal with them right now, and later "git stash pop" to + recover your local changes. Successfully rebased and updated refs/heads/rebased-feature-branch. EOF }
t/t7201-co.sh +208 -0
214 lines not on this page · raw message
t/t7600-merge.sh +1 -1
@@ -914,7 +914,7 @@ test_expect_success 'merge with conflicted --autostash changes' ' git diff >expect && test_when_finished "test_might_fail git stash drop" && git merge --autostash c3 2>err && - test_grep "Applying autostash resulted in conflicts." err && + test_grep "your local changes resulted in conflicts" err && git show HEAD:file >merge-result && test_cmp result.1-9 merge-result && git stash show -p >actual &&
xdiff-interface.c +12 -0
@@ -325,6 +325,18 @@ int parse_conflict_style_name(const char *value)return -1 ; } +const char *conflict_style_name (int style) +{ +switch (style) { +case XDL_MERGE_DIFF3 : +return "diff3" ; +case XDL_MERGE_ZEALOUS_DIFF3 : +return "zdiff3" ; +default : +return "merge" ; + } +} +int git_xmerge_style =-1 ; int git_xmerge_config(const char *var, const char *value,
xdiff-interface.h +1 -0
@@ -55,6 +55,7 @@ void xdiff_set_find_func(xdemitconf_t *xecfg, const char *line, int cflags);void xdiff_clear_find_func (xdemitconf_t *xecfg);struct config_context ;int parse_conflict_style_name (const char *value); +const char *conflict_style_name (int style);int git_xmerge_config (const char *var,const char *value,const struct config_context *ctx,void *cb);extern int git_xmerge_style;
Signature · 1 line
gitgitgadget
Re: [PATCH v14 5/5] checkout -m: autostash when switching branches
Hi Harald
On 15/04/2026 17:24, Harald Nordgren via GitGitGadget wrote:
(trimming the documentation - I'll try and look at that next time)
Quoted 100 lines
builtin/checkout.c +1
-77@@ -17,7 +17,6 @@#include "merge-ll.h" #include "lockfile.h" #include "mem-pool.h" -#include "merge-ort-wrappers.h" #include "object-file.h" #include "object-name.h" #include "odb.h" @@ -30,6 +29,7 @@#include "repo-settings.h" #include "resolve-undo.h" #include "revision.h" +#include "sequencer.h" #include "setup.h" #include "submodule.h" #include "symlinks.h" @@ -853,90 +853,8 @@ static int merge_working_tree(const struct checkout_opts *opts, ret =unpack_trees (2 , trees, &topts);clear_unpack_trees_porcelain (&topts);if (ret ==-1 ) { -/* -* Unpack couldn't do a trivial merge; either -* give up or do a real merge, depending on -* whether the merge flag was used. -*/ -struct tree *work; -struct tree *old_tree; -struct merge_options o; -struct strbuf sb =STRBUF_INIT ; -struct strbuf old_commit_shortname =STRBUF_INIT ; - -if (!opts->merge) { -rollback_lock_file (&lock_file); -return 1 ; - } - -/* -* Without old_branch_info->commit, the below is the same as -* the two-tree unpack we already tried and failed. -*/ -if (!old_branch_info->commit) { -rollback_lock_file (&lock_file); -return 1 ; - } - old_tree =repo_get_commit_tree (the_repository, - old_branch_info->commit); - -if (repo_index_has_changes (the_repository, old_tree, &sb)) -die (_ ("cannot continue with staged changes in " -"the following files:\n%s" ), sb.buf); -strbuf_release (&sb); - -/* Do more real merge */ - -/* -* We update the index fully, then write the -* tree from the index, then merge the new -* branch with the current tree, with the old -* branch as the base. Then we reset the index -* (but not the working tree) to the new -* branch, leaving the working tree as the -* merged version, but skipping unmerged -* entries in the index. -*/ - -add_files_to_cache (the_repository,NULL ,NULL ,NULL ,0 , -0 ,0 ); -init_ui_merge_options (&o, the_repository); - o.verbosity =0 ; - work =write_in_core_index_as_tree (the_repository, - the_repository->index); - - ret =reset_tree (new_tree, - opts,1 , - writeout_error, new_branch_info); -if (ret) { -rollback_lock_file (&lock_file); -return ret; - } - o.ancestor = old_branch_info->name; -if (!old_branch_info->name) { -strbuf_add_unique_abbrev (&old_commit_shortname, - &old_branch_info->commit->object.oid, -DEFAULT_ABBREV ); - o.ancestor = old_commit_shortname.buf; - } - o.branch1 = new_branch_info->name; - o.branch2 ="local" ; - o.conflict_style = opts->conflict_style; - ret =merge_ort_nonrecursive (&o, - new_tree, - work, - old_tree); -if (ret <0 ) -die (NULL ); - ret = reset_tree(new_tree,
builtin/checkout.c static int merge_working_tree(const struct checkout_opts *opts, · line 857
Quoted 10 lines
builtin/checkout.c +2
-8- opts,0 , - writeout_error, new_branch_info); -strbuf_release (&o.obuf); -strbuf_release (&old_commit_shortname); -if (ret) { -rollback_lock_file (&lock_file); -return ret; - } +rollback_lock_file (&lock_file); +return ret;
ret is -1 so we return the same value if unpack_trees() fails as do the checks at the top of the function do when they fail with "return error(...)". Therefore we cannot determine whether a failure of this function is due to unpack_trees() or not and so we wont know whether to autostash or not. You need to return a unique value here like -2 (or ideally a named constant)
Quoted 15 lines
builtin/checkout.c +4
-0} }@@ -1181,6 +1099,10 @@ static int switch_branches(const struct checkout_opts *opts,struct object_id rev;int flag, writeout_error =0 ;int do_merge =1 ; +int created_autostash =0 ; +struct strbuf old_commit_shortname =STRBUF_INIT ; +struct strbuf autostash_msg =STRBUF_INIT ; +const char *stash_label_base =NULL ;trace2_cmd_mode ("branch" );
builtin/checkout.c static int switch_branches(const struct checkout_opts *opts, · line 1145
Quoted 6 lines
builtin/checkout.c +3
-0@@ -1218,11 +1140,39 @@ static int switch_branches(const struct checkout_opts *opts, do_merge =0 ; } +if (old_branch_info.name) + stash_label_base = old_branch_info.name; +else if (old_branch_info.commit) {
Style: if one branch of an if statement has braces then all branch should.
Quoted 16 lines
builtin/checkout.c +14
-0+strbuf_add_unique_abbrev (&old_commit_shortname, + &old_branch_info.commit->object.oid, +DEFAULT_ABBREV ); + stash_label_base = old_commit_shortname.buf; + } +if (do_merge) { ret =merge_working_tree (opts, &old_branch_info, new_branch_info, &writeout_error); +if (ret ==-1 && opts->merge) { +strbuf_addf (&autostash_msg, +"autostash while switching to '%s'" , + new_branch_info->name); +create_autostash_ref (the_repository, +"CHECKOUT_AUTOSTASH_HEAD" , + autostash_msg.buf, true); + created_autostash =1 ;
builtin/checkout.c static int switch_branches(const struct checkout_opts *opts, · line 1171
Quoted 10 lines
builtin/checkout.c +9
-0+ ret =merge_working_tree (opts, &old_branch_info, new_branch_info, &writeout_error); + }if (ret) { +if (created_autostash) +apply_autostash_ref (the_repository, +"CHECKOUT_AUTOSTASH_HEAD" , + new_branch_info->name, +"local" , + stash_label_base, + autostash_msg.buf);
Good - now we only try to restore the stashed changes if we actually stashed. However we only restore the stashed changes if there was an error(). If there isn't an error we call update_refs_for_switch() before restoring them. It would be safer to restore them straight away in case that function ends up dying for any reason (though I think that's pretty unlikely)
if (created_autostash) {
if (opts->conflict_style >= 0)
/* set up confilct style */
apply_autostash_ref(...);
}
if (ret) {builtin/checkout.c static int switch_branches(const struct checkout_opts *opts, · line 1175
Quoted 5 lines
builtin/checkout.c +3
-1branch_info_release (&old_branch_info); -return ret; +strbuf_release (&old_commit_shortname); +strbuf_release (&autostash_msg); +return ret <0 ?1 : ret;
This changes the return value for all errors from merge_working_tree() - that's probably a good this as this value is used for the exit code and we don't really want an exit code of -1
Quoted 8 lines
builtin/checkout.c +1
-0} }@@ -1231,8 +1181,30 @@ static int switch_branches(const struct checkout_opts *opts,update_refs_for_switch (opts, &old_branch_info, new_branch_info); +if (opts->conflict_style >=0 ) {
builtin/checkout.c static int switch_branches(const struct checkout_opts *opts, · line 1194
Quoted 10 lines
builtin/checkout.c +10
-0+struct strbuf cfg =STRBUF_INIT ; +strbuf_addf (&cfg,"merge.conflictStyle=%s" , +conflict_style_name (opts->conflict_style)); +git_config_push_parameter (cfg.buf); +strbuf_release (&cfg); + } +apply_autostash_ref (the_repository,"CHECKOUT_AUTOSTASH_HEAD" , + new_branch_info->name,"local" , + stash_label_base, + autostash_msg.buf);> + discard_index(the_repository->index);
As I said last time we should not be calling apply_autostash() if we have not created an autostash. We should also not discard and re-read the index if we haven't stashed. I do think we'd be better restoring the stashed changes in a single place as I said above.
builtin/checkout.c static int switch_branches(const struct checkout_opts *opts, · line 1202
Quoted 6 lines
builtin/checkout.c +6
-0+if (repo_read_index (the_repository) <0 ) +die (_ ("index file corrupt" )); + +if (created_autostash && !opts->quiet && new_branch_info->commit) +show_local_changes (&new_branch_info->commit->object, + &opts->diff_options);
This shows the local changes, but it doesn't give any explanation of what the output is. For example when switching branches with a conflict I see
Your local changes are stashed, however, applying it to carry forward your local changes resulted in conflicts:
- You can try resolving them now. If you resolved them successfully, discard the stash entry with "git stash drop".
- Alternatively you can "git reset --hard" if you do not want to deal with them right now, and later "git stash pop" to recover your local changes.
M t/t7201-co.sh
where the changes appear to be part of the advice message. Perhaps we should print a short (i.e. one sentance) message along the lines of
The following paths have local changes
We should test what the user sees here as well.
Quoted 13 lines
builtin/checkout.c +3
-0+ ret =post_checkout_hook (old_branch_info.commit, new_branch_info->commit,1 );branch_info_release (&old_branch_info); +strbuf_release (&old_commit_shortname); +strbuf_release (&autostash_msg);return ret || writeout_error; }sequencer.c +0
-0
sequencer.c static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply, · line 4771
Quoted 7 lines
sequencer.c +4
-0@@ -4765,15 +4765,23 @@ static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply,strvec_push (&store.args, stash_oid);if (run_command (&store)) ret =error (_ ("cannot store %s" ), stash_oid); +else if (attempt_apply) + fprintf(stderr, + _("Your local changes are stashed, however, applying it to carry\n" + "forward your local changes resulted in conflicts:\n"
I'm not sure we need to say "local changes" twice here
Your local changes are stashed, however applying them resulted in conflicts.
sequencer.c static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply, · line 4774
Quoted 3 lines
sequencer.c +3
-0+"\n" +" - You can try resolving them now. If you resolved them\n" +" successfully, discard the stash entry with \"git stash drop\".\n"
s/resolved/resolve/
sequencer.c static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply, · line 4778
Quoted 4 lines
sequencer.c +4
-0+"\n" +" - Alternatively you can \"git reset --hard\" if you do not want\n" +" to deal with them right now, and later \"git stash pop\" to\n" + " recover your local changes.\n"));
I find the bulleted list a bit odd, maybe
You can either resolve the conflicts and then discard the stash with "git stash drop", or, if you do not want to resolve them now, run "git reset --hard" and apply the local changes later by running "git stash pop"
would be better?
Quoted 60 lines
sequencer.c +2
-5else fprintf (stderr, -_ ("%s\n" +_ ("Autostash exists; creating a new stash entry.\n" "Your changes are safe in the stash.\n" "You can run \"git stash pop\" or" -" \"git stash drop\" at any time.\n" ), - attempt_apply ? -_ ("Applying autostash resulted in conflicts." ) : -_ ("Autostash exists; creating a new stash entry." )); +" \"git stash drop\" at any time.\n" )); }return ret;t/t3420-rebase-autostash.sh +18
-6@@ -61,18 +61,30 @@ create_expected_failure_apply () { First, rewinding head to replay your work on top of it... Applying: second commit Applying: third commit - Applying autostash resulted in conflicts. - Your changes are safe in the stash. - You can run "git stash pop" or "git stash drop" at any time. + Your local changes are stashed, however, applying it to carry + forward your local changes resulted in conflicts: + + - You can try resolving them now. If you resolved them + successfully, discard the stash entry with "git stash drop". + + - Alternatively you can "git reset --hard" if you do not want + to deal with them right now, and later "git stash pop" to + recover your local changes. EOF } create_expected_failure_merge () { cat >expected <<-EOF $(grep "^Created autostash: [0-9a-f][0-9a-f]*\$" actual) - Applying autostash resulted in conflicts. - Your changes are safe in the stash. - You can run "git stash pop" or "git stash drop" at any time. + Your local changes are stashed, however, applying it to carry + forward your local changes resulted in conflicts: + + - You can try resolving them now. If you resolved them + successfully, discard the stash entry with "git stash drop". + + - Alternatively you can "git reset --hard" if you do not want + to deal with them right now, and later "git stash pop" to + recover your local changes. Successfully rebased and updated refs/heads/rebased-feature-branch. EOF }t/t7201-co.sh +0
-0
t/t7201-co.sh test_expect_success 'checkout --merge --conflict=diff3 <branch>' ' · line 219
Quoted 10 lines
t/t7201-co.sh +7
-0@@ -210,6 +210,214 @@ test_expect_success 'checkout --merge --conflict=diff3 <branch>' ' test_cmp expect two ' +test_expect_success 'checkout --merge --conflict=zdiff3 <branch>' ' + git checkout -f main && + git reset --hard && + git clean -f && + + fill a b X d e >two && + git checkout --merge --conflict=zdiff3 simple &&
If I change "zdiff3" to "diff3" this test still passes which is disappointing. As the code that parses the conflict style is shared with other commands and we already have tests for --conflict=diff3 and --conflict=merge I'm not sure this test adds much.
Quoted 10 lines
t/t7201-co.sh +10
-0+ + cat <<-EOF >expect && + a + <<<<<<< simple + c + ||||||| main + b + c + d + =======
t/t7201-co.sh test_expect_success 'checkout --merge --conflict=diff3 <branch>' ' · line 239
Quoted 10 lines
t/t7201-co.sh +10
-0+ b + X + d + >>>>>>> local + e + EOF + test_cmp expect two +' + +test_expect_success 'checkout -m respects merge.conflictStyle config' '
Looking at the existing tests, 'checkout with --merge, in diff3 -m style' and 'checkout --conflict=merge, overriding config' already test that we respect merge.conflictStyle and that --conflict overrides it so I don't see what new coverage this test adds.
Quoted 24 lines
t/t7201-co.sh +24
-0+ git checkout -f main && + git reset --hard && + git clean -f && + + test_config merge.conflictStyle diff3 && + fill b d >two && + git checkout -m simple && + + cat <<-EOF >expect && + <<<<<<< simple + a + c + e + ||||||| main + a + b + c + d + e + ======= + b + d + >>>>>>> local + EOF
t/t7201-co.sh test_expect_success 'checkout --merge --conflict=diff3 <branch>' ' · line 273
Quoted 10 lines
t/t7201-co.sh +10
-0+ test_cmp expect two +' + +test_expect_success 'checkout -m skips stash when no conflict' ' + git checkout -f main && + git clean -f && + + fill 0 x y z >same && + git stash list >stash-before && + git checkout -m side >actual 2>&1 &&
file "same" is unchanged between branch "side" and "branch" main so we do not need to stash it.
t/t7201-co.sh test_expect_success 'checkout --merge --conflict=diff3 <branch>' ' · line 278
Quoted 5 lines
t/t7201-co.sh +5
-0+ test_grep ! "Created autostash" actual && + git stash list >stash-after && + test_cmp stash-before stash-after && + fill 0 x y z >expect && + test_cmp expect same
Even if we created an autostash this test would not pick it up as the stash is not written to refs/stash unless there are merge conflicts and we don't print "Created autostash" even when we do create an autostash. The same is true for "checkout -m -b skips stash with dirty tree" below. I don't see how we can check that a stash was not created without using GIT_TRACE to see if we run "git stash". Even that is fragile as we might start stashing without forking a separate process in future.
Quoted 15 lines
t/t7201-co.sh +15
-0+' + +test_expect_success 'checkout -m skips stash with non-conflicting dirty index' ' + git checkout -f main && + git clean -f && + + fill 0 x y z >same && + git add same && + git checkout -m side >actual 2>&1 && + test_grep ! "Created autostash" actual && + fill 0 x y z >expect && + test_cmp expect same +' + +test_expect_success 'checkout -m stashes and applies on conflicting changes' '
t/t7201-co.sh test_expect_success 'checkout --merge --conflict=diff3 <branch>' ' · line 303
Quoted 10 lines
t/t7201-co.sh +10
-0+ git checkout -f main && + git clean -f && + + fill 1 2 3 4 5 6 7 >one && + git checkout -m side >actual 2>&1 && + test_grep ! "Created autostash" actual && + test_grep "Applied autostash" actual && + fill 1 2 3 4 5 6 7 >expect && + test_cmp expect one +'
I don't think the two tests above add any extra coverage when we have the one below so they can be deleted. Our test suite is slow enough already - we only need one test to fail for any given issue.
Quoted 7 lines
t/t7201-co.sh +7
-0+test_expect_success 'checkout -m with mixed staged and unstaged changes' ' + git checkout -f main && + git clean -f && + + fill 0 x y z >same && + git add same && + fill 1 2 3 4 5 6 7 >one &&
t/t7201-co.sh test_expect_success 'checkout --merge --conflict=diff3 <branch>' ' · line 321
Quoted 10 lines
t/t7201-co.sh +10
-0+ git checkout -m side >actual 2>&1 && + test_grep ! "Created autostash" actual && + test_grep "Applied autostash" actual && + fill 0 x y z >expect && + test_cmp expect same && + fill 1 2 3 4 5 6 7 >expect && + test_cmp expect one +' + +test_expect_success 'checkout -m stashes on truly conflicting changes' '
This use of conflicting is rather confusing - what's the difference between a conflicting change and a truly conflicting change?
I think a single test is sufficient to check that we create a valid stash entry
test_expect_success 'checkout -m stashes on truly conflicting changes' '
git checkout -f main &&
git clean -f &&
fill 1 2 3 4 5 >one &&
test_must_fail git checkout side 2>stderr &&
test_grep "Your local changes" stderr &&
git checkout -m side >actual 2>&1 &&
test_grep ! "Created autostash" actual &&
test_grep "resulted in conflicts" actual &&
test_grep "git stash drop" actual &&
test_grep "recover your local changes" actual &&
git show --format=%B --diff-merges=1 refs/stash >actual &&
sed /^index/d actual >actual.trimmed &&
cat >expect <<-EOF &&
On main: autostash while switching to ${SQ}side${SQ}
diff --git a/one b/one
--- a/one
+++ b/one
@@ -3,6 +3,3 @@
3
4
5
-6
-7
-8
EOF
test_cmp expect actual.trimmed &&'
Then we can delete from here to ...
Quoted 43 lines
t/t7201-co.sh +43
-0+ git checkout -f main && + git clean -f && + + fill 1 2 3 4 5 >one && + test_must_fail git checkout side 2>stderr && + test_grep "Your local changes" stderr && + git checkout -m side >actual 2>&1 && + test_grep ! "Created autostash" actual && + test_grep "resulted in conflicts" actual && + test_grep "git stash drop" actual && + git stash drop && + git reset --hard +' + +test_expect_success 'checkout -m produces usable stash on conflict' ' + git checkout -f main && + git clean -f && + + fill 1 2 3 4 5 >one && + git checkout -m side >actual 2>&1 && + test_grep "recover your local changes" actual && + git checkout -f main && + git stash pop && + fill 1 2 3 4 5 >expect && + test_cmp expect one +' + +test_expect_success 'checkout -m autostash message includes target branch' ' + git checkout -f main && + git clean -f && + + fill 1 2 3 4 5 >one && + git checkout -m side >actual 2>&1 && + git stash list >stash-list && + test_grep "autostash while switching to .side." stash-list && + git stash drop && + git checkout -f main && + git reset --hard +' + +test_expect_success 'checkout -m stashes on staged conflicting changes' ' + git checkout -f main && + git clean -f &&
t/t7201-co.sh test_expect_success 'checkout --merge --conflict=diff3 <branch>' ' · line 374
Quoted 10 lines
t/t7201-co.sh +10
-0+ + fill 1 2 3 4 5 >one && + git add one && + git checkout -m side >actual 2>&1 && + test_grep ! "Created autostash" actual && + test_grep "resulted in conflicts" actual && + test_grep "git stash drop" actual && + git stash drop && + git reset --hard +'
... here
t/t7201-co.sh test_expect_success 'checkout --merge --conflict=diff3 <branch>' ' · line 376
Quoted 1 line
t/t7201-co.sh +1
-0+test_expect_success 'checkout -m applies stash cleanly with non-overlapping changes in same file' '
I've no idea what this is trying to do - it looks more like it is testing that "git stash" works rather than anything to do with "git checkout"
Quoted 33 lines
t/t7201-co.sh +33
-0+ git checkout -f main && + git reset --hard && + git clean -f && + + git checkout -b nonoverlap_base && + fill a b c d >file && + git add file && + git commit -m "add file" && + + git checkout -b nonoverlap_child && + fill a b c INSERTED d >file && + git commit -a -m "insert line near end of file" && + + fill DIRTY a b c INSERTED d >file && + + git stash list >stash-before && + git checkout -m nonoverlap_base 2>stderr && + test_grep "Applied autostash" stderr && + test_grep ! "resulted in conflicts" stderr && + + git stash list >stash-after && + test_cmp stash-before stash-after && + + fill DIRTY a b c d >expect && + test_cmp expect file && + + git checkout -f main && + git branch -D nonoverlap_base && + git branch -D nonoverlap_child +' + +test_expect_success 'checkout -m -b skips stash with dirty tree' ' + git checkout -f main &&
t/t7201-co.sh test_expect_success 'checkout --merge --conflict=diff3 <branch>' ' · line 419
Quoted 10 lines
t/t7201-co.sh +10
-0+ git clean -f && + + fill 0 x y z >same && + git checkout -m -b newbranch >actual 2>&1 && + test_grep ! "Created autostash" actual && + fill 0 x y z >expect && + test_cmp expect same && + git checkout main && + git branch -D newbranch +'
As I said above I don't think this test is testing what it claims to.
I'd suggest adding the following test
test_expect_success 'checkout -m which would overwrite untracked file' '
git checkout -f --detach main &&
test_commit another-file &&
git checkout HEAD^ &&
>another-file.t &&
test_must_fail git checkout -m @{-1} 2>err &&
test_grep "another-file.t.*overwritten" err'
which passes on master but fails with these patches applied. We need to make sure that we don't set "quiet" in unpack_tree_opts the second time we call merge_working_tree(). The test could be improved by adding some local changes.
This is looking better, but there are still a couple of problems that need addressing before it can be considered ready for merging.
Thanks
Phillip
Quoted 51 lines
t/t7201-co.sh +0
-0test_expect_success 'switch to another branch while carrying a deletion' ' git checkout -f main && git reset --hard &&t/t7600-merge.sh +1
-1@@ -914,7 +914,7 @@ test_expect_success 'merge with conflicted --autostash changes' ' git diff >expect && test_when_finished "test_might_fail git stash drop" && git merge --autostash c3 2>err && - test_grep "Applying autostash resulted in conflicts." err && + test_grep "your local changes resulted in conflicts" err && git show HEAD:file >merge-result && test_cmp result.1-9 merge-result && git stash show -p >actual &&xdiff-interface.c +12
-0@@ -325,6 +325,18 @@ int parse_conflict_style_name(const char *value)return -1 ; } +const char *conflict_style_name (int style) +{ +switch (style) { +case XDL_MERGE_DIFF3 : +return "diff3" ; +case XDL_MERGE_ZEALOUS_DIFF3 : +return "zdiff3" ; +default : +return "merge" ; + } +} +int git_xmerge_style =-1 ; int git_xmerge_config(const char *var, const char *value,xdiff-interface.h +1
-0@@ -55,6 +55,7 @@ void xdiff_set_find_func(xdemitconf_t *xecfg, const char *line, int cflags);void xdiff_clear_find_func (xdemitconf_t *xecfg);struct config_context ;int parse_conflict_style_name (const char *value); +const char *conflict_style_name (int style);int git_xmerge_config (const char *var,const char *value,const struct config_context *ctx,void *cb);extern int git_xmerge_style;
Comments on Phillip's review
Quoted 6 lines
ret is -1 so we return the same value if
unpack_trees()fails as do the checks at the top of the function do when they fail with "return error(...)". Therefore we cannot determine whether a failure of this function is due tounpack_trees()or not and so we wont know whether to autostash or not. You need to return a unique value here like -2 (or ideally a named constant)
👍
Quoted 6 lines
Good - now we only try to restore the stashed changes if we actually stashed. However we only restore the stashed changes if there was an
error(). If there isn't an error we callupdate_refs_for_switch()before restoring them. It would be safer to restore them straight away in case that function ends up dying for any reason (though I think that's pretty unlikely)
I hope I understand correctly, the code becomes easier this way, so that's nice!
Quoted 4 lines
As I said last time we should not be calling
apply_autostash()if we have not created an autostash. We should also not discard and re-read the index if we haven't stashed. I do think we'd be better restoring the stashed changes in a single place as I said above.
Makes sense.
Quoted 6 lines
where the changes appear to be part of the advice message. Perhaps we should print a short (i.e. one sentance) message along the lines of
The following paths have local changes
We should test what the user sees here as well.
Add that message.
Do you mean to test the full output? I'm not against it at all, but that seems to be going against the convention of the other tests in this file. But it would be a more robust test.
Quoted 1 line
I'm not sure we need to say "local changes" twice here
👍
Quoted 8 lines
I find the bulleted list a bit odd, maybe
You can either resolve the conflicts and then discard the stash with "git stash drop", or, if you do not want to resolve them now, run "git reset --hard" and apply the local changes later by running "git stash pop"
would be better?
Much cleaner, thanks!
Quoted 2 lines
we already have tests for --conflict=diff3 and --conflict=merge I'm not sure this test adds much.
Deleted.
Quoted 17 lines
Quoted 17 lines
t/t7201-co.sh +17
-0+ + cat <<-EOF >expect && + a + <<<<<<< simple + c + ||||||| main + b + c + d + ======= + b + X + d + >>>>>>> local + e + EOF + test_cmp expect two
Quoted 8 lines
Review
Quoted 3 lines
+'
(quoted diff) +2
-0+ +test_expect_success 'checkout -m respects merge.conflictStyle config' 'Looking at the existing tests, 'checkout with --merge, in diff3 -m style' and 'checkout --conflict=merge, overriding config' already test that we respect merge.conflictStyle and that --conflict overrides it so I don't see what new coverage this test adds.
Deleted.
Quoted 10 lines
t/t7201-co.sh test_expect_success 'checkout --merge --conflict=diff3 <branch>' ' · line 273
Quoted 7 lines
t/t7201-co.sh +7
-0+test_expect_success 'checkout -m skips stash when no conflict' ' + git checkout -f main && + git clean -f && + + fill 0 x y z >same && + git stash list >stash-before && + git checkout -m side >actual 2>&1 &&file "same" is unchanged between branch "side" and "branch" main so we do not need to stash it.
Deleted.
Quoted 6 lines
t/t7201-co.sh test_expect_success 'checkout --merge --conflict=diff3 <branch>' ' · line 278
Quoted 5 lines
t/t7201-co.sh +5
-0+ test_grep ! "Created autostash" actual && + git stash list >stash-after && + test_cmp stash-before stash-after && + fill 0 x y z >expect && + test_cmp expect same
Quoted 7 lines
Even if we created an autostash this test would not pick it up as the stash is not written to refs/stash unless there are merge conflicts and we don't print "Created autostash" even when we do create an autostash. The same is true for "checkout -m -b skips stash with dirty tree" below. I don't see how we can check that a stash was not created without using
GIT_TRACEto see if we run "git stash". Even that is fragile as we might start stashing without forking a separate process in future.
Deleted.
Quoted 3 lines
I don't think the two tests above add any extra coverage when we have the one below so they can be deleted. Our test suite is slow enough already - we only need one test to fail for any given issue.
Deleted.
Quoted 7 lines
Quoted 1 line
+test_expect_success 'checkout -m stashes on truly conflicting changes' '
This use of conflicting is rather confusing - what's the difference between a conflicting change and a truly conflicting change?
I think a single test is sufficient to check that we create a valid stash entry
Updated.
Quoted 7 lines
test_expect_success'checkout -m which would overwrite untracked file' 'git checkout -f --detach main && test_commit another-file && git checkout HEAD^ && >another-file.t && test_must_fail git checkout -m @{-1} 2>err && test_grep "another-file.t.*overwritten" err
Quoted 6 lines
'
which passes on master but fails with these patches applied. We need to make sure that we don't set "quiet" in
unpack_tree_optsthe second time we callmerge_working_tree(). The test could be improved by adding some local changes.
Tricky to get right, the test if very good to have! I rewrote the logic now to make this test pass, I hope it looks better now.
Harald
Hi Phillip, did you have a chance to look at the latest changes?
Harald
Quoted 1 line · Harald Nordgren
Hi Phillip, did you have a chance to look at the latest changes?
Not yet, I should get round to it later this week. Junio is offline for at least the next week, I'll make sure I've reviewed them by the time he returns.
Thanks
Phillip
Quoted 2 lines
Harald
👍
Harald
Re: [PATCH v14 0/5] checkout: 'autostash' for branch switching
In reply to Harald Nordgren via GitGitGadgetHi Harald
Quoted 2 lines · Harald Nordgren via GitGitGadget
Simplifying the tests in
t/t3903-stash.shaccording to Phillip Wood's comment. I believe everything sound be fixed now or responded to.
This is better than the last cover letter, but it would be helpful to describe the changes you have made rather than just say your responding to a reviewers comment. Junio has given some suggestions for coverletters to take inspiration from.
Also when you reply to messages can you try and keep the subject line please - nearly all the responses from you have the subject
Re: [PATCH] checkout: add --autostash option for branch switching
regardless of the message they're actually replying to. That makes it hard to see which message you're replying to.
I've left some comments on patch 5, it is definitely looking better but there are still a couple of things that need fixing.
Thanks
Phillip
Quoted 88 lines
Also rebasing against upstream.
Harald Nordgren (5):
stash: add --label-ours, --label-theirs, --label-base for apply
sequencer: allowcreate_autostashto run silently
sequencer: teach autostash apply to take optional conflict marker
labels
checkout: rollback lock on early returns inmerge_working_treecheckout -m: autostash when switching branchesDocumentation/git-checkout.adoc | 58 ++++----- Documentation/git-stash.adoc | 11 +- Documentation/git-switch.adoc | 33 ++--- builtin/checkout.c | 149 +++++++++++------------ builtin/commit.c | 3 +- builtin/merge.c | 15 ++- builtin/stash.c | 28 +++-- sequencer.c | 73 ++++++++--- sequencer.h | 7 +- t/t3420-rebase-autostash.sh | 24 +++- t/t3903-stash.sh | 24 ++++ t/t7201-co.sh | 208 ++++++++++++++++++++++++++++++++ t/t7600-merge.sh | 2 +- xdiff-interface.c | 12 ++ xdiff-interface.h | 1 + xdiff/xmerge.c | 6 +- 16 files changed, 483 insertions(+), 171 deletions(-)base-commit:
9f223ef1c026d91c7ac68cc0211bde255dda6199
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2234%2FHaraldNordgren%2Fcheckout_autostash-v14
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2234/HaraldNordgren/checkout_autostash-v14
Pull-Request: https://github.com/git/git/pull/2234Range-diff vs v13:
1:
43bfdf2136! 1:e18c25599astash: add --label-ours, --label-theirs, --label-base for apply@@ builtin/stash.c: static int branch_stash(int argc, const char **argv, const char ## t/t3903-stash.sh ## -@@ t/t3903-stash.sh: test_expect_success 'restore untracked files even when we hit conflicts' ' - ) +@@ t/t3903-stash.sh: setup_stash() { + git add other-file && + test_tick && + git commit -m initial && ++ git tag initial && + echo 2 >file && + git add file && + echo 3 >file && +@@ t/t3903-stash.sh: test_expect_success 'stash.index=false overridden by --index' ' + test_cmp expect file ' +test_expect_success 'apply with custom conflict labels' ' -+ test_when_finished "git reset --hard && git stash drop" && -+ git reset --hard && ++ git reset --hard initial && + test_commit label-base conflict-file base-content && + echo stashed >conflict-file && + git stash push -m "stashed" && @@ t/t3903-stash.sh: test_expect_success 'restore untracked files even when we hit +' + +test_expect_success 'apply with empty conflict labels' ' -+ test_when_finished "git reset --hard && git stash drop" && -+ git reset --hard && ++ git reset --hard initial && + test_commit empty-label-base conflict-file base-content && + echo stashed >conflict-file && + git stash push -m "stashed" && @@ t/t3903-stash.sh: test_expect_success 'restore untracked files even when we hit + test_grep "^>>>>>>>$" conflict-file +' + - test_expect_success 'stash create reports a locked index' ' - test_when_finished "rm -rf repo" && - git init repo && + test_done ## xdiff/xmerge.c ## @@ xdiff/xmerge.c: static int fill_conflict_hunk(xdfenv_t *xe1, const char *name1, 2: 7f3c32f5e9 = 2: ce29b10264 sequencer: allow create_autostash to run silently 3: b279d1dac8 = 3: 73051d1762 sequencer: teach autostash apply to take optional conflict marker labels 4: 04869314ec = 4: 191058d8e3 checkout: rollback lock on early returns in merge_working_tree 5: 4b3c6025ac = 5: 86f33df1eb checkout -m: autostash when switching branches
v15 cover checkout: 'autostash' for branch switching
In reply to Harald Nordgren via GitGitGadget* When the initial merge fails, the caller now knows for sure it was a conflict (not some unrelated error) before kicking off the autostash.
* If the second attempt also fails, the user now actually sees why. Previously errors like "would be overwritten by checkout" were being swallowed.
* The stash is reapplied right after the second attempt, so it won't get left behind if something else goes wrong later in the checkout.
* A couple of clean-up steps only run when we actually stashed, so regular checkouts aren't paying for work they don't need.
* Added a small header "The following paths have local changes:" before the list of changed files, so it doesn't look like part of the conflict advice above it.
* Reworded the conflict advice into a single paragraph. Feels less shouty and doesn't say "local changes" three times.
* Trimmed and consolidated tests in t7201. A bunch of the old ones couldn't actually fail, or were really testing other things; the conflict-path tests are now a single test that checks the stash entry directly.
Harald Nordgren (5):
stash: add --label-ours, --label-theirs, --label-base for apply
sequencer: allow create_autostash to run silently
sequencer: teach autostash apply to take optional conflict marker
labels
checkout: rollback lock on early returns in merge_working_tree checkout -m: autostash when switching branches
Documentation/git-checkout.adoc | 58 +++++------ Documentation/git-stash.adoc | 11 ++- Documentation/git-switch.adoc | 33 +++---- builtin/checkout.c | 165 +++++++++++++++----------------- builtin/commit.c | 3 +- builtin/merge.c | 15 ++- builtin/stash.c | 28 ++++-- sequencer.c | 69 +++++++++---- sequencer.h | 7 +- t/t3420-rebase-autostash.sh | 16 ++-- t/t3903-stash.sh | 24 +++++ t/t7201-co.sh | 61 +++++++++++- t/t7600-merge.sh | 3 +- xdiff-interface.c | 12 +++ xdiff-interface.h | 1 + xdiff/xmerge.c | 6 +- 16 files changed, 335 insertions(+), 177 deletions(-)
base-commit: 94f057755b7941b321fd11fec1b2e3ca5313a4e0
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2234%2FHaraldNordgren%2Fcheckout_autostash-v15
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2234/HaraldNordgren/checkout_autostash-v15
Pull-Request: https://github.com/git/git/pull/2234
Range-diff vs v14:
1: e18c25599a = 1: aba8e6a9dc stash: add --label-ours, --label-theirs, --label-base for apply
2: ce29b10264 = 2: 89e0bfa803 sequencer: allow create_autostash to run silently
3: 73051d1762 = 3: a428ce7328 sequencer: teach autostash apply to take optional conflict marker labels
4: 191058d8e3 = 4: f358424085 checkout: rollback lock on early returns in merge_working_tree
5: 86f33df1eb ! 5: 96b14db827 checkout -m: autostash when switching branches
@@ builtin/checkout.c
#include "setup.h"
#include "submodule.h"
#include "symlinks.h"
+@@ builtin/checkout.c: struct checkout_opts {
+ .auto_advance = 1, \
+ }
+
++#define MERGE_WORKING_TREE_UNPACK_FAILED (-2)
++
+ struct branch_info {
+ char *name; /* The short name used */
+ char *path; /* The full name of a real branch */
+@@ builtin/checkout.c: static void setup_branch_path(struct branch_info *branch)
+
+ static void init_topts(struct unpack_trees_options *topts, int merge,
+ int show_progress, int overwrite_ignore,
+- struct commit *old_commit)
++ struct commit *old_commit, bool show_unpack_errors)
+ {
+ memset(topts, 0, sizeof(*topts));
+ topts->head_idx = -1;
+@@ builtin/checkout.c: static void init_topts(struct unpack_trees_options *topts, int merge,
+ topts->initial_checkout = is_index_unborn(the_repository->index);
+ topts->update = 1;
+ topts->merge = 1;
+- topts->quiet = merge && old_commit;
++ topts->quiet = merge && old_commit && !show_unpack_errors;
+ topts->verbose_update = show_progress;
+ topts->fn = twoway_merge;
+ topts->preserve_ignored = !overwrite_ignore;
+@@ builtin/checkout.c: static void init_topts(struct unpack_trees_options *topts, int merge,
+ static int merge_working_tree(const struct checkout_opts *opts,
+ struct branch_info *old_branch_info,
+ struct branch_info *new_branch_info,
+- int *writeout_error)
++ int *writeout_error,
++ bool show_unpack_errors)
+ {
+ int ret;
+ struct lock_file lock_file = LOCK_INIT;
+@@ builtin/checkout.c: static int merge_working_tree(const struct checkout_opts *opts,
+
+ /* 2-way merge to the new branch */
+ init_topts(&topts, opts->merge, opts->show_progress,
+- opts->overwrite_ignore, old_branch_info->commit);
++ opts->overwrite_ignore, old_branch_info->commit,
++ show_unpack_errors);
+ init_checkout_metadata(&topts.meta, new_branch_info->refname,
+ new_branch_info->commit ?
+ &new_branch_info->commit->object.oid :
@@ builtin/checkout.c: static int merge_working_tree(const struct checkout_opts *opts,
ret = unpack_trees(2, trees, &topts);
clear_unpack_trees_porcelain(&topts);
@@ builtin/checkout.c: static int merge_working_tree(const struct checkout_opts *op
- return ret;
- }
+ rollback_lock_file(&lock_file);
-+ return ret;
++ return MERGE_WORKING_TREE_UNPACK_FAILED;
}
}
@@ builtin/checkout.c: static int switch_branches(const struct checkout_opts *opts,
do_merge = 0;
}
-+ if (old_branch_info.name)
++ if (old_branch_info.name) {
+ stash_label_base = old_branch_info.name;
-+ else if (old_branch_info.commit) {
++ } else if (old_branch_info.commit) {
+ strbuf_add_unique_abbrev(&old_commit_shortname,
+ &old_branch_info.commit->object.oid,
+ DEFAULT_ABBREV);
@@ builtin/checkout.c: static int switch_branches(const struct checkout_opts *opts,
+ }
+
if (do_merge) {
- ret = merge_working_tree(opts, &old_branch_info, new_branch_info, &writeout_error);
-+ if (ret == -1 && opts->merge) {
+- ret = merge_working_tree(opts, &old_branch_info, new_branch_info, &writeout_error);
++ ret = merge_working_tree(opts, &old_branch_info, new_branch_info,
++ &writeout_error, false);
++ if (ret == MERGE_WORKING_TREE_UNPACK_FAILED && opts->merge) {
+ strbuf_addf(&autostash_msg,
+ "autostash while switching to '%s'",
+ new_branch_info->name);
@@ builtin/checkout.c: static int switch_branches(const struct checkout_opts *opts,
+ "CHECKOUT_AUTOSTASH_HEAD",
+ autostash_msg.buf, true);
+ created_autostash = 1;
-+ ret = merge_working_tree(opts, &old_branch_info, new_branch_info, &writeout_error);
++ ret = merge_working_tree(opts, &old_branch_info, new_branch_info,
++ &writeout_error, true);
++ }
++ if (created_autostash) {
++ if (opts->conflict_style >= 0) {
++ struct strbuf cfg = STRBUF_INIT;
++ strbuf_addf(&cfg, "merge.conflictStyle=%s",
++ conflict_style_name(opts->conflict_style));
++ git_config_push_parameter(cfg.buf);
++ strbuf_release(&cfg);
++ }
++ apply_autostash_ref(the_repository,
++ "CHECKOUT_AUTOSTASH_HEAD",
++ new_branch_info->name,
++ "local",
++ stash_label_base,
++ autostash_msg.buf);
+ }
if (ret) {
-+ if (created_autostash)
-+ apply_autostash_ref(the_repository,
-+ "CHECKOUT_AUTOSTASH_HEAD",
-+ new_branch_info->name,
-+ "local",
-+ stash_label_base,
-+ autostash_msg.buf);
branch_info_release(&old_branch_info);
- return ret;
+ strbuf_release(&old_commit_shortname);
@@ builtin/checkout.c: static int switch_branches(const struct checkout_opts *opts,
update_refs_for_switch(opts, &old_branch_info, new_branch_info);
-+ if (opts->conflict_style >= 0) {
-+ struct strbuf cfg = STRBUF_INIT;
-+ strbuf_addf(&cfg, "merge.conflictStyle=%s",
-+ conflict_style_name(opts->conflict_style));
-+ git_config_push_parameter(cfg.buf);
-+ strbuf_release(&cfg);
++ if (created_autostash) {
++ discard_index(the_repository->index);
++ if (repo_read_index(the_repository) < 0)
++ die(_("index file corrupt"));
++
++ if (!opts->quiet && new_branch_info->commit) {
++ printf(_("The following paths have local changes:\n"));
++ show_local_changes(&new_branch_info->commit->object,
++ &opts->diff_options);
++ }
+ }
-+ apply_autostash_ref(the_repository, "CHECKOUT_AUTOSTASH_HEAD",
-+ new_branch_info->name, "local",
-+ stash_label_base,
-+ autostash_msg.buf);
-+
-+ discard_index(the_repository->index);
-+ if (repo_read_index(the_repository) < 0)
-+ die(_("index file corrupt"));
-+
-+ if (created_autostash && !opts->quiet && new_branch_info->commit)
-+ show_local_changes(&new_branch_info->commit->object,
-+ &opts->diff_options);
+
ret = post_checkout_hook(old_branch_info.commit, new_branch_info->commit, 1);
branch_info_release(&old_branch_info);
@@ sequencer.c: static int apply_save_autostash_oid(const char *stash_oid, int atte
ret = error(_("cannot store %s"), stash_oid);
+ else if (attempt_apply)
+ fprintf(stderr,
-+ _("Your local changes are stashed, however, applying it to carry\n"
-+ "forward your local changes resulted in conflicts:\n"
-+ "\n"
-+ " - You can try resolving them now. If you resolved them\n"
-+ " successfully, discard the stash entry with \"git stash drop\".\n"
-+ "\n"
-+ " - Alternatively you can \"git reset --hard\" if you do not want\n"
-+ " to deal with them right now, and later \"git stash pop\" to\n"
-+ " recover your local changes.\n"));
++ _("Your local changes are stashed, however applying them\n"
++ "resulted in conflicts. You can either resolve the conflicts\n"
++ "and then discard the stash with \"git stash drop\", or, if you\n"
++ "do not want to resolve them now, run \"git reset --hard\" and\n"
++ "apply the local changes later by running \"git stash pop\".\n"));
else
fprintf(stderr,
- _("%s\n"
@@ t/t3420-rebase-autostash.sh: create_expected_failure_apply () {
- Applying autostash resulted in conflicts.
- Your changes are safe in the stash.
- You can run "git stash pop" or "git stash drop" at any time.
-+ Your local changes are stashed, however, applying it to carry
-+ forward your local changes resulted in conflicts:
-+
-+ - You can try resolving them now. If you resolved them
-+ successfully, discard the stash entry with "git stash drop".
-+
-+ - Alternatively you can "git reset --hard" if you do not want
-+ to deal with them right now, and later "git stash pop" to
-+ recover your local changes.
++ Your local changes are stashed, however applying them
++ resulted in conflicts. You can either resolve the conflicts
++ and then discard the stash with "git stash drop", or, if you
++ do not want to resolve them now, run "git reset --hard" and
++ apply the local changes later by running "git stash pop".
EOF
}
@@ t/t3420-rebase-autostash.sh: create_expected_failure_apply () {
- Applying autostash resulted in conflicts.
- Your changes are safe in the stash.
- You can run "git stash pop" or "git stash drop" at any time.
-+ Your local changes are stashed, however, applying it to carry
-+ forward your local changes resulted in conflicts:
-+
-+ - You can try resolving them now. If you resolved them
-+ successfully, discard the stash entry with "git stash drop".
-+
-+ - Alternatively you can "git reset --hard" if you do not want
-+ to deal with them right now, and later "git stash pop" to
-+ recover your local changes.
++ Your local changes are stashed, however applying them
++ resulted in conflicts. You can either resolve the conflicts
++ and then discard the stash with "git stash drop", or, if you
++ do not want to resolve them now, run "git reset --hard" and
++ apply the local changes later by running "git stash pop".
Successfully rebased and updated refs/heads/rebased-feature-branch.
EOF
}
## t/t7201-co.sh ##
+@@ t/t7201-co.sh: test_expect_success 'checkout -m with dirty tree' '
+
+ test "$(git symbolic-ref HEAD)" = "refs/heads/side" &&
+
+- printf "M\t%s\n" one >expect.messages &&
++ printf "The following paths have local changes:\nM\t%s\n" one >expect.messages &&
+ test_cmp expect.messages messages &&
+
+ fill "M one" "A three" "D two" >expect.main &&
@@ t/t7201-co.sh: test_expect_success 'checkout --merge --conflict=diff3 <branch>' '
test_cmp expect two
'
-+test_expect_success 'checkout --merge --conflict=zdiff3 <branch>' '
-+ git checkout -f main &&
-+ git reset --hard &&
-+ git clean -f &&
-+
-+ fill a b X d e >two &&
-+ git checkout --merge --conflict=zdiff3 simple &&
-+
-+ cat <<-EOF >expect &&
-+ a
-+ <<<<<<< simple
-+ c
-+ ||||||| main
-+ b
-+ c
-+ d
-+ =======
-+ b
-+ X
-+ d
-+ >>>>>>> local
-+ e
-+ EOF
-+ test_cmp expect two
-+'
-+
-+test_expect_success 'checkout -m respects merge.conflictStyle config' '
-+ git checkout -f main &&
-+ git reset --hard &&
-+ git clean -f &&
-+
-+ test_config merge.conflictStyle diff3 &&
-+ fill b d >two &&
-+ git checkout -m simple &&
-+
-+ cat <<-EOF >expect &&
-+ <<<<<<< simple
-+ a
-+ c
-+ e
-+ ||||||| main
-+ a
-+ b
-+ c
-+ d
-+ e
-+ =======
-+ b
-+ d
-+ >>>>>>> local
-+ EOF
-+ test_cmp expect two
-+'
-+
-+test_expect_success 'checkout -m skips stash when no conflict' '
-+ git checkout -f main &&
-+ git clean -f &&
-+
-+ fill 0 x y z >same &&
-+ git stash list >stash-before &&
-+ git checkout -m side >actual 2>&1 &&
-+ test_grep ! "Created autostash" actual &&
-+ git stash list >stash-after &&
-+ test_cmp stash-before stash-after &&
-+ fill 0 x y z >expect &&
-+ test_cmp expect same
-+'
-+
-+test_expect_success 'checkout -m skips stash with non-conflicting dirty index' '
-+ git checkout -f main &&
-+ git clean -f &&
-+
-+ fill 0 x y z >same &&
-+ git add same &&
-+ git checkout -m side >actual 2>&1 &&
-+ test_grep ! "Created autostash" actual &&
-+ fill 0 x y z >expect &&
-+ test_cmp expect same
-+'
-+
-+test_expect_success 'checkout -m stashes and applies on conflicting changes' '
-+ git checkout -f main &&
-+ git clean -f &&
-+
-+ fill 1 2 3 4 5 6 7 >one &&
-+ git checkout -m side >actual 2>&1 &&
-+ test_grep ! "Created autostash" actual &&
-+ test_grep "Applied autostash" actual &&
-+ fill 1 2 3 4 5 6 7 >expect &&
-+ test_cmp expect one
-+'
-+
+test_expect_success 'checkout -m with mixed staged and unstaged changes' '
+ git checkout -f main &&
+ git clean -f &&
@@ t/t7201-co.sh: test_expect_success 'checkout --merge --conflict=diff3 <branch>'
+ git add same &&
+ fill 1 2 3 4 5 6 7 >one &&
+ git checkout -m side >actual 2>&1 &&
-+ test_grep ! "Created autostash" actual &&
+ test_grep "Applied autostash" actual &&
+ fill 0 x y z >expect &&
+ test_cmp expect same &&
@@ t/t7201-co.sh: test_expect_success 'checkout --merge --conflict=diff3 <branch>'
+ test_cmp expect one
+'
+
-+test_expect_success 'checkout -m stashes on truly conflicting changes' '
++test_expect_success 'checkout -m creates a recoverable stash on conflict' '
+ git checkout -f main &&
+ git clean -f &&
+
@@ t/t7201-co.sh: test_expect_success 'checkout --merge --conflict=diff3 <branch>'
+ test_must_fail git checkout side 2>stderr &&
+ test_grep "Your local changes" stderr &&
+ git checkout -m side >actual 2>&1 &&
-+ test_grep ! "Created autostash" actual &&
+ test_grep "resulted in conflicts" actual &&
+ test_grep "git stash drop" actual &&
++ test_grep "git stash pop" actual &&
++ test_grep "The following paths have local changes" actual &&
++ git show --format=%B --diff-merges=1 refs/stash >actual &&
++ sed /^index/d actual >actual.trimmed &&
++ cat >expect <<-EOF &&
++ On main: autostash while switching to ${SQ}side${SQ}
++
++ diff --git a/one b/one
++ --- a/one
++ +++ b/one
++ @@ -3,6 +3,3 @@
++ 3
++ 4
++ 5
++ -6
++ -7
++ -8
++ EOF
++ test_cmp expect actual.trimmed &&
+ git stash drop &&
+ git reset --hard
+'
+
-+test_expect_success 'checkout -m produces usable stash on conflict' '
-+ git checkout -f main &&
-+ git clean -f &&
-+
++test_expect_success 'checkout -m which would overwrite untracked file' '
++ git checkout -f --detach main &&
++ test_commit another-file &&
++ git checkout HEAD^ &&
++ >another-file.t &&
+ fill 1 2 3 4 5 >one &&
-+ git checkout -m side >actual 2>&1 &&
-+ test_grep "recover your local changes" actual &&
-+ git checkout -f main &&
-+ git stash pop &&
-+ fill 1 2 3 4 5 >expect &&
-+ test_cmp expect one
-+'
-+
-+test_expect_success 'checkout -m autostash message includes target branch' '
-+ git checkout -f main &&
-+ git clean -f &&
-+
-+ fill 1 2 3 4 5 >one &&
-+ git checkout -m side >actual 2>&1 &&
-+ git stash list >stash-list &&
-+ test_grep "autostash while switching to .side." stash-list &&
-+ git stash drop &&
-+ git checkout -f main &&
-+ git reset --hard
-+'
-+
-+test_expect_success 'checkout -m stashes on staged conflicting changes' '
-+ git checkout -f main &&
-+ git clean -f &&
-+
-+ fill 1 2 3 4 5 >one &&
-+ git add one &&
-+ git checkout -m side >actual 2>&1 &&
-+ test_grep ! "Created autostash" actual &&
-+ test_grep "resulted in conflicts" actual &&
-+ test_grep "git stash drop" actual &&
-+ git stash drop &&
-+ git reset --hard
-+'
-+
-+test_expect_success 'checkout -m applies stash cleanly with non-overlapping changes in same file' '
-+ git checkout -f main &&
-+ git reset --hard &&
-+ git clean -f &&
-+
-+ git checkout -b nonoverlap_base &&
-+ fill a b c d >file &&
-+ git add file &&
-+ git commit -m "add file" &&
-+
-+ git checkout -b nonoverlap_child &&
-+ fill a b c INSERTED d >file &&
-+ git commit -a -m "insert line near end of file" &&
-+
-+ fill DIRTY a b c INSERTED d >file &&
-+
-+ git stash list >stash-before &&
-+ git checkout -m nonoverlap_base 2>stderr &&
-+ test_grep "Applied autostash" stderr &&
-+ test_grep ! "resulted in conflicts" stderr &&
-+
-+ git stash list >stash-after &&
-+ test_cmp stash-before stash-after &&
-+
-+ fill DIRTY a b c d >expect &&
-+ test_cmp expect file &&
-+
-+ git checkout -f main &&
-+ git branch -D nonoverlap_base &&
-+ git branch -D nonoverlap_child
-+'
-+
-+test_expect_success 'checkout -m -b skips stash with dirty tree' '
-+ git checkout -f main &&
-+ git clean -f &&
-+
-+ fill 0 x y z >same &&
-+ git checkout -m -b newbranch >actual 2>&1 &&
-+ test_grep ! "Created autostash" actual &&
-+ fill 0 x y z >expect &&
-+ test_cmp expect same &&
-+ git checkout main &&
-+ git branch -D newbranch
++ test_must_fail git checkout -m @{-1} 2>err &&
++ test_grep "would be overwritten by checkout" err &&
++ test_grep "another-file.t" err
+'
+
test_expect_success 'switch to another branch while carrying a deletion' '
@@ t/t7600-merge.sh: test_expect_success 'merge with conflicted --autostash changes
test_when_finished "test_might_fail git stash drop" &&
git merge --autostash c3 2>err &&
- test_grep "Applying autostash resulted in conflicts." err &&
-+ test_grep "your local changes resulted in conflicts" err &&
++ test_grep "applying them" err &&
++ test_grep "resulted in conflicts" err &&
git show HEAD:file >merge-result &&
test_cmp result.1-9 merge-result &&
git stash show -p >actual &&Signature · 1 line
gitgitgadget
PATCH v15 1/5 stash: add --label-ours, --label-theirs, --label-base for apply
From: Harald Nordgren <[email protected]>
Allow callers of "git stash apply" to pass custom labels for conflict markers instead of the default "Updated upstream" and "Stashed changes". Document the new options and add a test.
--- Documentation/git-stash.adoc | 11 ++++++++++- builtin/stash.c | 28 ++++++++++++++++++++-------- t/t3903-stash.sh | 24 ++++++++++++++++++++++++ xdiff/xmerge.c | 6 +++--- 4 files changed, 57 insertions(+), 12 deletions(-)
Documentation/git-stash.adoc +10 -1
@@ -12,7 +12,7 @@ git stash list [<log-options>] git stash show [-u | --include-untracked | --only-untracked] [<diff-options>] [<stash>] git stash drop [-q | --quiet] [<stash>] git stash pop [--index] [-q | --quiet] [<stash>] -git stash apply [--index] [-q | --quiet] [<stash>] +git stash apply [--index] [-q | --quiet] [--label-ours=<label>] [--label-theirs=<label>] [--label-base=<label>] [<stash>] git stash branch <branchname> [<stash>] git stash [push] [-p | --patch] [-S | --staged] [-k | --[no-]keep-index] [-q | --quiet] [-u | --include-untracked] [-a | --all] [(-m | --message) <message>]
@@ -195,6 +195,15 @@ the index's ones. However, this can fail, when you have conflicts (which are stored in the index, where you therefore can no longer apply the changes as they were originally). +`--label-ours=<label>`:: +`--label-theirs=<label>`:: +`--label-base=<label>`:: + These options are only valid for the `apply` command. ++ +Use the given labels in conflict markers instead of the default +"Updated upstream", "Stashed changes", and "Stash base". +`--label-base` only has an effect with merge.conflictStyle=diff3. + `-k`:: `--keep-index`:: `--no-keep-index`::
builtin/stash.c +20 -8
@@ -44,7 +44,7 @@#define BUILTIN_STASH_POP_USAGE \ N_("git stash pop [--index] [-q | --quiet] [<stash>]")#define BUILTIN_STASH_APPLY_USAGE \ - N_("git stash apply [--index] [-q | --quiet] [<stash>]") + N_("git stash apply [--index] [-q | --quiet] [--label-ours=<label>] [--label-theirs=<label>] [--label-base=<label>] [<stash>]")#define BUILTIN_STASH_BRANCH_USAGE \ N_("git stash branch <branchname> [<stash>]")#define BUILTIN_STASH_STORE_USAGE \
@@ -591,7 +591,9 @@ static void unstage_changes_unless_new(struct object_id *orig_tree) }static int do_apply_stash (const char *prefix,struct stash_info *info, -int index,int quiet) +int index,int quiet, +const char *label_ours,const char *label_theirs, +const char *label_base) {int clean, ret;int has_index = index;
@@ -643,9 +645,9 @@ static int do_apply_stash(const char *prefix, struct stash_info *info,init_ui_merge_options (&o, the_repository); - o.branch1 ="Updated upstream" ; - o.branch2 ="Stashed changes" ; - o.ancestor ="Stash base" ; + o.branch1 = label_ours ? label_ours :"Updated upstream" ; + o.branch2 = label_theirs ? label_theirs :"Stashed changes" ; + o.ancestor = label_base ? label_base :"Stash base" ;if (oideq (&info->b_tree, &c_tree)) o.branch1 ="Version stash was based on" ;
@@ -723,11 +725,18 @@ static int apply_stash(int argc, const char **argv, const char *prefix,int ret =-1 ;int quiet =0 ;int index = use_index; +const char *label_ours =NULL , *label_theirs =NULL , *label_base =NULL ;struct stash_info info =STASH_INFO_INIT ;struct option options[] = {OPT__QUIET (&quiet,N_ ("be quiet, only report errors" )),OPT_BOOL (0 ,"index" , &index,N_ ("attempt to recreate the index" )), +OPT_STRING (0 ,"label-ours" , &label_ours,N_ ("label" ), +N_ ("label for the upstream side in conflict markers" )), +OPT_STRING (0 ,"label-theirs" , &label_theirs,N_ ("label" ), +N_ ("label for the stashed side in conflict markers" )), +OPT_STRING (0 ,"label-base" , &label_base,N_ ("label" ), +N_ ("label for the base in diff3 conflict markers" )),OPT_END () };
@@ -737,7 +746,8 @@ static int apply_stash(int argc, const char **argv, const char *prefix,if (get_stash_info (&info, argc, argv)) goto cleanup; - ret =do_apply_stash (prefix, &info, index, quiet); + ret =do_apply_stash (prefix, &info, index, quiet, + label_ours, label_theirs, label_base); cleanup:free_stash_info (&info);return ret;
@@ -836,7 +846,8 @@ static int pop_stash(int argc, const char **argv, const char *prefix,if (get_stash_info_assert (&info, argc, argv)) goto cleanup; -if ((ret =do_apply_stash (prefix, &info, index, quiet))) +if ((ret =do_apply_stash (prefix, &info, index, quiet, +NULL ,NULL ,NULL )))printf_ln (_ ("The stash entry is kept in case " "you need it again." ));else
@@ -877,7 +888,8 @@ static int branch_stash(int argc, const char **argv, const char *prefix,strvec_push (&cp.args,oid_to_hex (&info.b_commit)); ret =run_command (&cp);if (!ret) - ret =do_apply_stash (prefix, &info,1 ,0 ); + ret =do_apply_stash (prefix, &info,1 ,0 , +NULL ,NULL ,NULL );if (!ret && info.is_stash_ref) ret =do_drop_stash (&info,0 );
t/t3903-stash.sh +24 -0
@@ -56,6 +56,7 @@ setup_stash() { git add other-file && test_tick && git commit -m initial && + git tag initial && echo 2 >file && git add file && echo 3 >file &&
@@ -1790,4 +1791,27 @@ test_expect_success 'stash.index=false overridden by --index' ' test_cmp expect file ' +test_expect_success 'apply with custom conflict labels' ' + git reset --hard initial && + test_commit label-base conflict-file base-content && + echo stashed >conflict-file && + git stash push -m "stashed" && + test_commit label-upstream conflict-file upstream-content && + test_must_fail git -c merge.conflictStyle=diff3 stash apply --label-ours=UP --label-theirs=STASH && + test_grep "^<<<<<<< UP" conflict-file && + test_grep "^||||||| Stash base" conflict-file && + test_grep "^>>>>>>> STASH" conflict-file +' + +test_expect_success 'apply with empty conflict labels' ' + git reset --hard initial && + test_commit empty-label-base conflict-file base-content && + echo stashed >conflict-file && + git stash push -m "stashed" && + test_commit empty-label-upstream conflict-file upstream-content && + test_must_fail git stash apply --label-ours= --label-theirs= && + test_grep "^<<<<<<<$" conflict-file && + test_grep "^>>>>>>>$" conflict-file +' + test_done
xdiff/xmerge.c +3 -3
@@ -199,9 +199,9 @@ static int fill_conflict_hunk(xdfenv_t *xe1, const char *name1, int size, int i, int style, xdmerge_t *m, char *dest, int marker_size) { - int marker1_size = (name1 ? strlen(name1) + 1 : 0); -int marker2_size = (name2 ?strlen (name2) +1 :0 ); -int marker3_size = (name3 ?strlen (name3) +1 :0 ); + int marker1_size = (name1 && *name1 ? strlen(name1) + 1 : 0); +int marker2_size = (name2 && *name2 ?strlen (name2) +1 :0 ); +int marker3_size = (name3 && *name3 ?strlen (name3) +1 :0 );int needs_cr =is_cr_needed (xe1, xe2, m);if (marker_size <=0 )
Signature · 1 line
gitgitgadget
Re: [PATCH v15 1/5] stash: add --label-ours, --label-theirs, --label-base for apply
Hi Harald
Quoted 5 lines · Harald Nordgren via GitGitGadget
From: Harald Nordgren <[email protected]>
Allow callers of "git stash apply" to pass custom labels for conflict markers instead of the default "Updated upstream" and "Stashed changes". Document the new options and add a test.
This all looks good, just one small comment below
builtin/stash.c static int do_apply_stash(const char *prefix, struct stash_info *info, · line 650
Quoted 6 lines
builtin/stash.c +3
-3- o.branch1 ="Updated upstream" ; - o.branch2 ="Stashed changes" ; - o.ancestor ="Stash base" ; + o.branch1 = label_ours ? label_ours :"Updated upstream" ; + o.branch2 = label_theirs ? label_theirs :"Stashed changes" ; + o.ancestor = label_base ? label_base :"Stash base" ;
This uses the existing label which is sensible, but I wonder if "Stash HEAD" would be a better choice as the merge base is always HEAD commit that the stash is based on.
We can always change that later
Thanks
Phillip
Quoted 109 lines
if (
oideq(&info->b_tree, &c_tree))
o.branch1 = "Version stash was based on";builtin/stash.c +13
-3@@ -723,11 +725,18 @@ static int apply_stash(int argc, const char **argv, const char *prefix,int ret =-1 ;int quiet =0 ;int index = use_index; +const char *label_ours =NULL , *label_theirs =NULL , *label_base =NULL ;struct stash_info info =STASH_INFO_INIT ;struct option options[] = {OPT__QUIET (&quiet,N_ ("be quiet, only report errors" )),OPT_BOOL (0 ,"index" , &index,N_ ("attempt to recreate the index" )), +OPT_STRING (0 ,"label-ours" , &label_ours,N_ ("label" ), +N_ ("label for the upstream side in conflict markers" )), +OPT_STRING (0 ,"label-theirs" , &label_theirs,N_ ("label" ), +N_ ("label for the stashed side in conflict markers" )), +OPT_STRING (0 ,"label-base" , &label_base,N_ ("label" ), +N_ ("label for the base in diff3 conflict markers" )),OPT_END () };@@ -737,7 +746,8 @@ static int apply_stash(int argc, const char **argv, const char *prefix,if (get_stash_info (&info, argc, argv)) goto cleanup; - ret =do_apply_stash (prefix, &info, index, quiet); + ret =do_apply_stash (prefix, &info, index, quiet, + label_ours, label_theirs, label_base); cleanup:free_stash_info (&info);return ret;@@ -836,7 +846,8 @@ static int pop_stash(int argc, const char **argv, const char *prefix,if (get_stash_info_assert (&info, argc, argv)) goto cleanup; -if ((ret =do_apply_stash (prefix, &info, index, quiet))) +if ((ret =do_apply_stash (prefix, &info, index, quiet, +NULL ,NULL ,NULL )))printf_ln (_ ("The stash entry is kept in case " "you need it again." ));else @@ -877,7 +888,8 @@ static int branch_stash(int argc, const char **argv, const char *prefix,strvec_push (&cp.args,oid_to_hex (&info.b_commit)); ret =run_command (&cp);if (!ret) - ret =do_apply_stash (prefix, &info,1 ,0 ); + ret =do_apply_stash (prefix, &info,1 ,0 , +NULL ,NULL ,NULL );if (!ret && info.is_stash_ref) ret =do_drop_stash (&info,0 );t/t3903-stash.sh +24
-0@@ -56,6 +56,7 @@ setup_stash() { git add other-file && test_tick && git commit -m initial && + git tag initial && echo 2 >file && git add file && echo 3 >file &&@@ -1790,4 +1791,27 @@ test_expect_success 'stash.index=false overridden by --index' ' test_cmp expect file ' +test_expect_success 'apply with custom conflict labels' ' + git reset --hard initial && + test_commit label-base conflict-file base-content && + echo stashed >conflict-file && + git stash push -m "stashed" && + test_commit label-upstream conflict-file upstream-content && + test_must_fail git -c merge.conflictStyle=diff3 stash apply --label-ours=UP --label-theirs=STASH && + test_grep "^<<<<<<< UP" conflict-file && + test_grep "^||||||| Stash base" conflict-file && + test_grep "^>>>>>>> STASH" conflict-file +' + +test_expect_success 'apply with empty conflict labels' ' + git reset --hard initial && + test_commit empty-label-base conflict-file base-content && + echo stashed >conflict-file && + git stash push -m "stashed" && + test_commit empty-label-upstream conflict-file upstream-content && + test_must_fail git stash apply --label-ours= --label-theirs= && + test_grep "^<<<<<<<$" conflict-file && + test_grep "^>>>>>>>$" conflict-file +' + test_donexdiff/xmerge.c +3
-3@@ -199,9 +199,9 @@ static int fill_conflict_hunk(xdfenv_t *xe1, const char *name1, int size, int i, int style, xdmerge_t *m, char *dest, int marker_size) { - int marker1_size = (name1 ? strlen(name1) + 1 : 0); -int marker2_size = (name2 ?strlen (name2) +1 :0 ); -int marker3_size = (name3 ?strlen (name3) +1 :0 ); + int marker1_size = (name1 && *name1 ? strlen(name1) + 1 : 0); +int marker2_size = (name2 && *name2 ?strlen (name2) +1 :0 ); +int marker3_size = (name3 && *name3 ?strlen (name3) +1 :0 );int needs_cr =is_cr_needed (xe1, xe2, m);if (marker_size <=0 )
PATCH 1/1 checkout: add --autostash option for branch switching
Quoted 5 lines
This uses the existing label which is sensible, but I wonder if "Stash HEAD" would be a better choice as the merge base is always HEAD commit that the stash is based on.
We can always change that later
Yeah, seems better to do later.
Harald
PATCH v15 2/5 sequencer: allow create_autostash to run silently
In reply to Harald Nordgren via GitGitGadgetFrom: Harald Nordgren <[email protected]>
Add a silent parameter to create_autostash_internal and introduce create_autostash_ref_silent so that callers can create an autostash without printing the "Created autostash" message.
--- builtin/merge.c | 6 ++++-- sequencer.c | 17 +++++++++++------ sequencer.h | 3 ++- 3 files changed, 17 insertions(+), 9 deletions(-)
builtin/merge.c +4 -2
@@ -1672,7 +1672,8 @@ int cmd_merge(int argc, }if (autostash) -create_autostash_ref (the_repository,"MERGE_AUTOSTASH" ); +create_autostash_ref (the_repository,"MERGE_AUTOSTASH" , +NULL , false); if (checkout_fast_forward(the_repository, &head_commit->object.oid, &commit->object.oid,
@@ -1764,7 +1765,8 @@ int cmd_merge(int argc,die_ff_impossible ();if (autostash) -create_autostash_ref (the_repository,"MERGE_AUTOSTASH" ); +create_autostash_ref (the_repository,"MERGE_AUTOSTASH" , +NULL , false);/* We are going to make a new commit. */ git_committer_info (IDENT_STRICT );
sequencer.c +11 -6
@@ -4657,7 +4657,9 @@ static enum todo_command peek_command(struct todo_list *todo_list, int offset)static void create_autostash_internal (struct repository *r,const char *path, -const char *refname) +const char *refname, +const char *message, +bool silent) {struct strbuf buf =STRBUF_INIT ;struct lock_file lock_file =LOCK_INIT ;
@@ -4679,7 +4681,8 @@ static void create_autostash_internal(struct repository *r,struct object_id oid;strvec_pushl (&stash.args, -"stash" ,"create" ,"autostash" ,NULL ); +"stash" ,"create" , + message ? message :"autostash" ,NULL ); stash.git_cmd =1 ; stash.no_stdin =1 ;strbuf_reset (&buf);
@@ -4702,7 +4705,8 @@ static void create_autostash_internal(struct repository *r, &oid, null_oid(the_hash_algo), 0, UPDATE_REFS_DIE_ON_ERR); } -printf (_ ("Created autostash: %s\n" ), buf.buf); +if (!silent) +printf (_ ("Created autostash: %s\n" ), buf.buf);if (reset_head (r, &ropts) <0 )die (_ ("could not reset --hard" ));discard_index (r->index);
@@ -4714,12 +4718,13 @@ static void create_autostash_internal(struct repository *r,void create_autostash (struct repository *r,const char *path) { -create_autostash_internal (r, path,NULL ); +create_autostash_internal (r, path,NULL ,NULL , false); } -void create_autostash_ref (struct repository *r,const char *refname) +void create_autostash_ref (struct repository *r,const char *refname, +const char *message,bool silent) { -create_autostash_internal (r,NULL , refname); +create_autostash_internal (r,NULL , refname, message, silent); }static int apply_save_autostash_oid (const char *stash_oid,int attempt_apply)
sequencer.h +2 -1
@@ -229,7 +229,8 @@ void commit_post_rewrite(struct repository *r, const struct object_id *new_head);void create_autostash (struct repository *r,const char *path); -void create_autostash_ref (struct repository *r,const char *refname); +void create_autostash_ref (struct repository *r,const char *refname, +const char *message,bool silent);int save_autostash (const char *path);int save_autostash_ref (struct repository *r,const char *refname);int apply_autostash (const char *path);
Signature · 1 line
gitgitgadget
Re: [PATCH v15 2/5] sequencer: allow create_autostash to run silently
Hi Harald
This looks good
Thanks
Phillip
Quoted 103 lines · Harald Nordgren via GitGitGadget
From: Harald Nordgren <[email protected]>
Add a silent parameter to
create_autostash_internaland introducecreate_autostash_ref_silentso that callers can create an autostash without printing the "Created autostash" message.Signed-off-by Harald Nordgren <[email protected]>--- builtin/merge.c | 6 ++++-- sequencer.c | 17 +++++++++++------ sequencer.h | 3 ++- 3 files changed, 17 insertions(+), 9 deletions(-)builtin/merge.c +4
-2@@ -1672,7 +1672,8 @@ int cmd_merge(int argc, }if (autostash) -create_autostash_ref (the_repository,"MERGE_AUTOSTASH" ); +create_autostash_ref (the_repository,"MERGE_AUTOSTASH" , +NULL , false); if (checkout_fast_forward(the_repository, &head_commit->object.oid, &commit->object.oid,@@ -1764,7 +1765,8 @@ int cmd_merge(int argc,die_ff_impossible ();if (autostash) -create_autostash_ref (the_repository,"MERGE_AUTOSTASH" ); +create_autostash_ref (the_repository,"MERGE_AUTOSTASH" , +NULL , false);/* We are going to make a new commit. */ git_committer_info (IDENT_STRICT );sequencer.c +11
-6@@ -4657,7 +4657,9 @@ static enum todo_command peek_command(struct todo_list *todo_list, int offset)static void create_autostash_internal (struct repository *r,const char *path, -const char *refname) +const char *refname, +const char *message, +bool silent) {struct strbuf buf =STRBUF_INIT ;struct lock_file lock_file =LOCK_INIT ;@@ -4679,7 +4681,8 @@ static void create_autostash_internal(struct repository *r,struct object_id oid;strvec_pushl (&stash.args, -"stash" ,"create" ,"autostash" ,NULL ); +"stash" ,"create" , + message ? message :"autostash" ,NULL ); stash.git_cmd =1 ; stash.no_stdin =1 ;strbuf_reset (&buf);@@ -4702,7 +4705,8 @@ static void create_autostash_internal(struct repository *r, &oid, null_oid(the_hash_algo), 0, UPDATE_REFS_DIE_ON_ERR); } -printf (_ ("Created autostash: %s\n" ), buf.buf); +if (!silent) +printf (_ ("Created autostash: %s\n" ), buf.buf);if (reset_head (r, &ropts) <0 )die (_ ("could not reset --hard" ));discard_index (r->index);@@ -4714,12 +4718,13 @@ static void create_autostash_internal(struct repository *r,void create_autostash (struct repository *r,const char *path) { -create_autostash_internal (r, path,NULL ); +create_autostash_internal (r, path,NULL ,NULL , false); } -void create_autostash_ref (struct repository *r,const char *refname) +void create_autostash_ref (struct repository *r,const char *refname, +const char *message,bool silent) { -create_autostash_internal (r,NULL , refname); +create_autostash_internal (r,NULL , refname, message, silent); }static int apply_save_autostash_oid (const char *stash_oid,int attempt_apply)sequencer.h +2
-1@@ -229,7 +229,8 @@ void commit_post_rewrite(struct repository *r, const struct object_id *new_head);void create_autostash (struct repository *r,const char *path); -void create_autostash_ref (struct repository *r,const char *refname); +void create_autostash_ref (struct repository *r,const char *refname, +const char *message,bool silent);int save_autostash (const char *path);int save_autostash_ref (struct repository *r,const char *refname);int apply_autostash (const char *path);
PATCH v15 3/5 sequencer: teach autostash apply to take optional conflict marker labels
In reply to Harald Nordgren via GitGitGadgetFrom: Harald Nordgren <[email protected]>
Add label_ours, label_theirs, label_base, and stash_msg parameters to apply_autostash_ref() and the autostash apply machinery so callers can pass custom conflict marker labels through to
"git stash apply --label-ours/--label-theirs/--label-base", as well as a custom stash message for "git stash store -m".
--- builtin/commit.c | 3 ++- builtin/merge.c | 9 ++++++--- sequencer.c | 38 +++++++++++++++++++++++++++++--------- sequencer.h | 4 +++- 4 files changed, 40 insertions(+), 14 deletions(-)
builtin/commit.c +2 -1
@@ -1979,7 +1979,8 @@ int cmd_commit(int argc, &oid, flags); } -apply_autostash_ref (the_repository,"MERGE_AUTOSTASH" ); +apply_autostash_ref (the_repository,"MERGE_AUTOSTASH" , +NULL ,NULL ,NULL ,NULL ); cleanup:free_commit_extra_headers (extra);
builtin/merge.c +6 -3
@@ -537,7 +537,8 @@ static void finish(struct commit *head_commit,run_hooks_l (the_repository,"post-merge" , squash ?"1" :"0" ,NULL );if (new_head) -apply_autostash_ref (the_repository,"MERGE_AUTOSTASH" ); +apply_autostash_ref (the_repository,"MERGE_AUTOSTASH" , +NULL ,NULL ,NULL ,NULL );strbuf_release (&reflog_message); }
@@ -1678,7 +1679,8 @@ int cmd_merge(int argc, &head_commit->object.oid, &commit->object.oid, overwrite_ignore)) { -apply_autostash_ref (the_repository,"MERGE_AUTOSTASH" ); +apply_autostash_ref (the_repository,"MERGE_AUTOSTASH" , +NULL ,NULL ,NULL ,NULL ); ret =1 ; goto done; }
@@ -1851,7 +1853,8 @@ int cmd_merge(int argc,else fprintf (stderr,_ ("Merge with strategy %s failed.\n" ), use_strategies[0 ]->name); -apply_autostash_ref (the_repository,"MERGE_AUTOSTASH" ); +apply_autostash_ref (the_repository,"MERGE_AUTOSTASH" , +NULL ,NULL ,NULL ,NULL ); ret =2 ; goto done; }else if (best_strategy == wt_strategy)
sequencer.c +29 -9
@@ -4727,7 +4727,10 @@ void create_autostash_ref(struct repository *r, const char *refname,create_autostash_internal (r,NULL , refname, message, silent); } -static int apply_save_autostash_oid (const char *stash_oid,int attempt_apply) +static int apply_save_autostash_oid (const char *stash_oid,int attempt_apply, +const char *label_ours,const char *label_theirs, +const char *label_base, +const char *stash_msg) {struct child_process child =CHILD_PROCESS_INIT ;int ret =0 ;
@@ -4738,6 +4741,12 @@ static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply) child.no_stderr =1 ;strvec_push (&child.args,"stash" );strvec_push (&child.args,"apply" ); +if (label_ours) +strvec_pushf (&child.args,"--label-ours=%s" , label_ours); +if (label_theirs) +strvec_pushf (&child.args,"--label-theirs=%s" , label_theirs); +if (label_base) +strvec_pushf (&child.args,"--label-base=%s" , label_base);strvec_push (&child.args, stash_oid); ret =run_command (&child); }
@@ -4751,7 +4760,7 @@ static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply)strvec_push (&store.args,"stash" );strvec_push (&store.args,"store" );strvec_push (&store.args,"-m" ); -strvec_push (&store.args,"autostash" ); +strvec_push (&store.args, stash_msg ? stash_msg :"autostash" );strvec_push (&store.args,"-q" );strvec_push (&store.args, stash_oid);if (run_command (&store))
@@ -4782,7 +4791,8 @@ static int apply_save_autostash(const char *path, int attempt_apply) }strbuf_trim (&stash_oid); - ret =apply_save_autostash_oid (stash_oid.buf, attempt_apply); + ret =apply_save_autostash_oid (stash_oid.buf, attempt_apply, +NULL ,NULL ,NULL ,NULL );unlink (path);strbuf_release (&stash_oid);
@@ -4801,11 +4811,14 @@ int apply_autostash(const char *path)int apply_autostash_oid (const char *stash_oid) { -return apply_save_autostash_oid (stash_oid,1 ); +return apply_save_autostash_oid (stash_oid,1 ,NULL ,NULL ,NULL ,NULL ); }static int apply_save_autostash_ref (struct repository *r,const char *refname, -int attempt_apply) +int attempt_apply, +const char *label_ours,const char *label_theirs, +const char *label_base, +const char *stash_msg) {struct object_id stash_oid;char stash_oid_hex[GIT_MAX_HEXSZ +1 ];
@@ -4821,7 +4834,9 @@ static int apply_save_autostash_ref(struct repository *r, const char *refname,return error (_ ("autostash reference is a symref" ));oid_to_hex_r (stash_oid_hex, &stash_oid); - ret =apply_save_autostash_oid (stash_oid_hex, attempt_apply); + ret =apply_save_autostash_oid (stash_oid_hex, attempt_apply, + label_ours, label_theirs, label_base, + stash_msg);refs_delete_ref (get_main_ref_store (r),"" , refname, &stash_oid,REF_NO_DEREF );
@@ -4831,12 +4846,17 @@ static int apply_save_autostash_ref(struct repository *r, const char *refname,int save_autostash_ref (struct repository *r,const char *refname) { -return apply_save_autostash_ref (r, refname,0 ); +return apply_save_autostash_ref (r, refname,0 , +NULL ,NULL ,NULL ,NULL ); } -int apply_autostash_ref (struct repository *r,const char *refname) +int apply_autostash_ref (struct repository *r,const char *refname, +const char *label_ours,const char *label_theirs, +const char *label_base,const char *stash_msg) { -return apply_save_autostash_ref (r, refname,1 ); +return apply_save_autostash_ref (r, refname,1 , + label_ours, label_theirs, label_base, + stash_msg); } static int checkout_onto(struct repository *r, struct replay_opts *opts,
sequencer.h +3 -1
@@ -235,7 +235,9 @@ int save_autostash(const char *path);int save_autostash_ref (struct repository *r,const char *refname);int apply_autostash (const char *path);int apply_autostash_oid (const char *stash_oid); -int apply_autostash_ref (struct repository *r,const char *refname); +int apply_autostash_ref (struct repository *r,const char *refname, +const char *label_ours,const char *label_theirs, +const char *label_base,const char *stash_msg);#define SUMMARY_INITIAL_COMMIT (1 << 0)#define SUMMARY_SHOW_AUTHOR_DATE (1 << 1)
Signature · 1 line
gitgitgadget
Re: [PATCH v15 3/5] sequencer: teach autostash apply to take optional conflict marker labels
Hi Harald
Quoted 7 lines · Harald Nordgren via GitGitGadget
From: Harald Nordgren <[email protected]>
Add
label_ours,label_theirs,label_base, andstash_msgparameters toapply_autostash_ref()and the autostash apply machinery so callers can pass custom conflict marker labels through to
"git stash apply --label-ours/--label-theirs/--label-base", as well as a custom stash message for "git stash store -m".
I said before that I was confused as to why we need to pass a message when all we're doing is saving an existing stash and the stash already has the message we want. As that didn't get any response I've had a more detailed look. The message here is used in the reflog (which is what "git stash list" shows) and it appears that the reason for passing a message when storing the stash is because "git stash store" is not smart enough to reuse the subject from the stash commit message by default. It would be nice to improve that rather than having to pass a message here. "git rebase --autostash" and "git merge --autostash" do not have the luxury of calling "git stash store" from the same process as "git stash create" and so to improve the autostash message for those commands we'd need to generate it twice which seems like an unnecessary complication.
One problem with that approach in the context of this series is that the stash commit message begins "On <branch we're switching from>: " rather than "On <branch we're switching to>: " but that means this commit is just hiding the fact that commit message is wrong when we create the stash.
It may well be that fixing all that turns out to be a lot of work as it would mean modifying do_create_stash() to allow the branch name to be overridden and modifying store_stash() to use the commit subject as the reflog message in which case we should leave that for a future series.
Thanks
Phillip
PATCH 1/1 checkout: add --autostash option for branch switching
Quoted 4 lines
It may well be that fixing all that turns out to be a lot of work as it would mean modifying
do_create_stash()to allow the branch name to be overridden and modifyingstore_stash()to use the commit subject as the reflog message in which case we should leave that for a future series.
I suspect that it is a lot of work, so maybe also better to do later.
Harald
PATCH v15 4/5 checkout: rollback lock on early returns in merge_working_tree
In reply to Harald Nordgren via GitGitGadgetFrom: Harald Nordgren <[email protected]>
merge_working_tree() acquires the index lock via repo_hold_locked_index() but several early return paths exit without calling rollback_lock_file(), leaving the lock held. While this is currently harmless because the process exits soon after, it becomes a problem if the function is ever called more than once in the same process.
Add rollback_lock_file() calls to all early return paths.
--- builtin/checkout.c | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-)
builtin/checkout.c +22 -7
@@ -783,8 +783,10 @@ static int merge_working_tree(const struct checkout_opts *opts,struct tree *new_tree;repo_hold_locked_index (the_repository, &lock_file,LOCK_DIE_ON_ERROR ); -if (repo_read_index_preload (the_repository,NULL ,0 ) <0 ) +if (repo_read_index_preload (the_repository,NULL ,0 ) <0 ) { +rollback_lock_file (&lock_file);return error (_ ("index file corrupt" )); + }resolve_undo_clear_index (the_repository->index);if (opts->new_orphan_branch && opts->orphan_from_empty_tree) {
@@ -797,14 +799,18 @@ static int merge_working_tree(const struct checkout_opts *opts, }else { new_tree =repo_get_commit_tree (the_repository, new_branch_info->commit); -if (!new_tree) +if (!new_tree) { +rollback_lock_file (&lock_file);return error (_ ("unable to read tree (%s)" ),oid_to_hex (&new_branch_info->commit->object.oid)); + } }if (opts->discard_changes) { ret =reset_tree (new_tree, opts,1 , writeout_error, new_branch_info); -if (ret) +if (ret) { +rollback_lock_file (&lock_file);return ret; + } }else {struct tree_desc trees[2 ];struct tree *tree;
@@ -814,6 +820,7 @@ static int merge_working_tree(const struct checkout_opts *opts,refresh_index (the_repository->index,REFRESH_QUIET ,NULL ,NULL ,NULL );if (unmerged_index (the_repository->index)) { +rollback_lock_file (&lock_file);error (_ ("you need to resolve your current index first" ));return 1 ; }
@@ -857,15 +864,19 @@ static int merge_working_tree(const struct checkout_opts *opts,struct strbuf sb =STRBUF_INIT ;struct strbuf old_commit_shortname =STRBUF_INIT ; -if (!opts->merge) +if (!opts->merge) { +rollback_lock_file (&lock_file);return 1 ; + }/* * Without old_branch_info->commit, the below is the same as * the two-tree unpack we already tried and failed. */ -if (!old_branch_info->commit) +if (!old_branch_info->commit) { +rollback_lock_file (&lock_file);return 1 ; + } old_tree =repo_get_commit_tree (the_repository, old_branch_info->commit);
@@ -897,8 +908,10 @@ static int merge_working_tree(const struct checkout_opts *opts, ret =reset_tree (new_tree, opts,1 , writeout_error, new_branch_info); -if (ret) +if (ret) { +rollback_lock_file (&lock_file);return ret; + } o.ancestor = old_branch_info->name;if (!old_branch_info->name) { strbuf_add_unique_abbrev(&old_commit_shortname,
@@ -920,8 +933,10 @@ static int merge_working_tree(const struct checkout_opts *opts, writeout_error, new_branch_info);strbuf_release (&o.obuf);strbuf_release (&old_commit_shortname); -if (ret) +if (ret) { +rollback_lock_file (&lock_file);return ret; + } } }
Signature · 1 line
gitgitgadget
Re: [PATCH v15 4/5] checkout: rollback lock on early returns in merge_working_tree
Hi Harald
Quoted 10 lines · Harald Nordgren via GitGitGadget
From: Harald Nordgren <[email protected]>
merge_working_tree()acquires the index lock viarepo_hold_locked_index()but several early return paths exit without callingrollback_lock_file(), leaving the lock held. While this is currently harmless because the process exits soon after, it becomes a problem if the function is ever called more than once in the same process.Add
rollback_lock_file()calls to all early return paths.
Thanks for splitting this out, it looks good
Phillip
Quoted 97 lines
Signed-off-by Harald Nordgren <[email protected]>--- builtin/checkout.c | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-)builtin/checkout.c +22
-7@@ -783,8 +783,10 @@ static int merge_working_tree(const struct checkout_opts *opts,struct tree *new_tree;repo_hold_locked_index (the_repository, &lock_file,LOCK_DIE_ON_ERROR ); -if (repo_read_index_preload (the_repository,NULL ,0 ) <0 ) +if (repo_read_index_preload (the_repository,NULL ,0 ) <0 ) { +rollback_lock_file (&lock_file);return error (_ ("index file corrupt" )); + }resolve_undo_clear_index (the_repository->index);if (opts->new_orphan_branch && opts->orphan_from_empty_tree) {@@ -797,14 +799,18 @@ static int merge_working_tree(const struct checkout_opts *opts, }else { new_tree =repo_get_commit_tree (the_repository, new_branch_info->commit); -if (!new_tree) +if (!new_tree) { +rollback_lock_file (&lock_file);return error (_ ("unable to read tree (%s)" ),oid_to_hex (&new_branch_info->commit->object.oid)); + } }if (opts->discard_changes) { ret =reset_tree (new_tree, opts,1 , writeout_error, new_branch_info); -if (ret) +if (ret) { +rollback_lock_file (&lock_file);return ret; + } }else {struct tree_desc trees[2 ];struct tree *tree;@@ -814,6 +820,7 @@ static int merge_working_tree(const struct checkout_opts *opts,refresh_index (the_repository->index,REFRESH_QUIET ,NULL ,NULL ,NULL );if (unmerged_index (the_repository->index)) { +rollback_lock_file (&lock_file);error (_ ("you need to resolve your current index first" ));return 1 ; }@@ -857,15 +864,19 @@ static int merge_working_tree(const struct checkout_opts *opts,struct strbuf sb =STRBUF_INIT ;struct strbuf old_commit_shortname =STRBUF_INIT ; -if (!opts->merge) +if (!opts->merge) { +rollback_lock_file (&lock_file);return 1 ; + }/* * Without old_branch_info->commit, the below is the same as * the two-tree unpack we already tried and failed. */ -if (!old_branch_info->commit) +if (!old_branch_info->commit) { +rollback_lock_file (&lock_file);return 1 ; + } old_tree =repo_get_commit_tree (the_repository, old_branch_info->commit);@@ -897,8 +908,10 @@ static int merge_working_tree(const struct checkout_opts *opts, ret =reset_tree (new_tree, opts,1 , writeout_error, new_branch_info); -if (ret) +if (ret) { +rollback_lock_file (&lock_file);return ret; + } o.ancestor = old_branch_info->name;if (!old_branch_info->name) { strbuf_add_unique_abbrev(&old_commit_shortname,@@ -920,8 +933,10 @@ static int merge_working_tree(const struct checkout_opts *opts, writeout_error, new_branch_info);strbuf_release (&o.obuf);strbuf_release (&old_commit_shortname); -if (ret) +if (ret) { +rollback_lock_file (&lock_file);return ret; + } } }
PATCH v15 5/5 checkout -m: autostash when switching branches
In reply to Harald Nordgren via GitGitGadgetFrom: Harald Nordgren <[email protected]>
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.
--- Documentation/git-checkout.adoc | 58 ++++++------ Documentation/git-switch.adoc | 33 +++---- builtin/checkout.c | 160 ++++++++++++++------------------ sequencer.c | 14 ++- t/t3420-rebase-autostash.sh | 16 ++-- t/t7201-co.sh | 61 +++++++++++- t/t7600-merge.sh | 3 +- xdiff-interface.c | 12 +++ xdiff-interface.h | 1 + 9 files changed, 211 insertions(+), 147 deletions(-)
Documentation/git-checkout.adoc +31 -27
@@ -251,20 +251,19 @@ working tree, by copying them from elsewhere, extracting a tarball, etc. are different between the current branch and the branch to which you are switching, the command refuses to switch branches in order to preserve your modifications in context. - However, with this option, a three-way merge between the current - branch, your working tree contents, and the new branch - is done, and you will be on the new branch. -+ -When a merge conflict happens, the index entries for conflicting -paths are left unmerged, and you need to resolve the conflicts -and mark the resolved paths with `git add` (or `git rm` if the merge -should result in deletion of the path). + With this option, the conflicting local changes are + automatically stashed before the switch and reapplied + afterwards. If the local changes do not overlap with the + differences between branches, the switch proceeds without + stashing. If reapplying the stash results in conflicts, the + entry is saved to the stash list. Resolve the conflicts + and run `git stash drop` when done, or clear the working + tree (e.g. with `git reset --hard`) before running `git stash + pop` later to re-apply your changes. + When checking out paths from the index, this option lets you recreate the conflicted merge in the specified paths. This option cannot be used when checking out paths from a tree-ish. -+ -When switching branches with `--merge`, staged changes may be lost. `--conflict=<style>`:: The same as `--merge` option above, but changes the way the
@@ -578,39 +577,44 @@ $ git checkout mytopic error: You have local changes to 'frotz'; not switching branches. ------------ -You can give the `-m` flag to the command, which would try a -three-way merge: +You can give the `-m` flag to the command, which would carry your local +changes to the new branch: ------------ $ git checkout -m mytopic -Auto-merging frotz +Switched to branch 'mytopic' ------------ -After this three-way merge, the local modifications are _not_ +After the switch, the local modifications are reapplied and are _not_ registered in your index file, so `git diff` would show you what changes you made since the tip of the new branch. === 3. Merge conflict -When a merge conflict happens during switching branches with -the `-m` option, you would see something like this: +When the `--merge` (`-m`) option is in effect and the locally +modified files overlap with files that need to be updated by the +branch switch, the changes are stashed and reapplied after the +switch. If this process results in conflicts, a stash entry is saved +and made available in `git stash list`: ------------ $ git checkout -m mytopic -Auto-merging frotz -ERROR: Merge conflict in frotz -fatal: merge program failed ------------- +Your local changes are stashed, however, applying it to carry +forward your local changes resulted in conflicts: -At this point, `git diff` shows the changes cleanly merged as in -the previous example, as well as the changes in the conflicted -files. Edit and resolve the conflict and mark it resolved with -`git add` as usual: + - You can try resolving them now. If you resolved them + successfully, discard the stash entry with "git stash drop". + - Alternatively you can "git reset --hard" if you do not want + to deal with them right now, and later "git stash pop" to + recover your local changes. ------------ -$ edit frotz -$ git add frotz ------------- + +You can try resolving the conflicts now. Edit the conflicting files +and mark them resolved with `git add` as usual, then run `git stash +drop` to discard the stash entry. Alternatively, you can clear the +working tree with `git reset --hard` and recover your local changes +later with `git stash pop`. CONFIGURATION -------------
Documentation/git-switch.adoc +17 -16
@@ -123,18 +123,19 @@ variable. `-m`:: `--merge`:: - If you have local modifications to one or more files that are - different between the current branch and the branch to which - you are switching, the command refuses to switch branches in - order to preserve your modifications in context. However, - with this option, a three-way merge between the current - branch, your working tree contents, and the new branch is - done, and you will be on the new branch. -+ -When a merge conflict happens, the index entries for conflicting -paths are left unmerged, and you need to resolve the conflicts -and mark the resolved paths with `git add` (or `git rm` if the merge -should result in deletion of the path). + If you have local modifications to one or more files that + are different between the current branch and the branch to + which you are switching, the command normally refuses to + switch branches in order to preserve your modifications in + context. However, with this option, the conflicting local + changes are automatically stashed before the switch and + reapplied afterwards. If the local changes do not overlap + with the differences between branches, the switch proceeds + without stashing. If reapplying the stash results in + conflicts, the entry is saved to the stash list. Resolve + the conflicts and run `git stash drop` when done, or clear + the working tree (e.g. with `git reset --hard`) before + running `git stash pop` later to re-apply your changes. `--conflict=<style>`:: The same as `--merge` option above, but changes the way the
@@ -217,15 +218,15 @@ $ git switch mytopic error: You have local changes to 'frotz'; not switching branches. ------------ -You can give the `-m` flag to the command, which would try a three-way -merge: +You can give the `-m` flag to the command, which would carry your local +changes to the new branch: ------------ $ git switch -m mytopic -Auto-merging frotz +Switched to branch 'mytopic' ------------ -After this three-way merge, the local modifications are _not_ +After the switch, the local modifications are reapplied and are _not_ registered in your index file, so `git diff` would show you what changes you made since the tip of the new branch.
builtin/checkout.c +69 -91
231 lines not on this page · raw message
sequencer.c +9 -5
@@ -4765,15 +4765,19 @@ static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply,strvec_push (&store.args, stash_oid);if (run_command (&store)) ret =error (_ ("cannot store %s" ), stash_oid); +else if (attempt_apply) +fprintf (stderr, +_ ("Your local changes are stashed, however applying them\n" +"resulted in conflicts. You can either resolve the conflicts\n" +"and then discard the stash with \"git stash drop\", or, if you\n" +"do not want to resolve them now, run \"git reset --hard\" and\n" +"apply the local changes later by running \"git stash pop\".\n" ));else fprintf (stderr, -_ ("%s\n" +_ ("Autostash exists; creating a new stash entry.\n" "Your changes are safe in the stash.\n" "You can run \"git stash pop\" or" -" \"git stash drop\" at any time.\n" ), - attempt_apply ? -_ ("Applying autostash resulted in conflicts." ) : -_ ("Autostash exists; creating a new stash entry." )); +" \"git stash drop\" at any time.\n" )); }return ret;
t/t3420-rebase-autostash.sh +10 -6
@@ -61,18 +61,22 @@ create_expected_failure_apply () { First, rewinding head to replay your work on top of it... Applying: second commit Applying: third commit - Applying autostash resulted in conflicts. - Your changes are safe in the stash. - You can run "git stash pop" or "git stash drop" at any time. + Your local changes are stashed, however applying them + resulted in conflicts. You can either resolve the conflicts + and then discard the stash with "git stash drop", or, if you + do not want to resolve them now, run "git reset --hard" and + apply the local changes later by running "git stash pop". EOF } create_expected_failure_merge () { cat >expected <<-EOF $(grep "^Created autostash: [0-9a-f][0-9a-f]*\$" actual) - Applying autostash resulted in conflicts. - Your changes are safe in the stash. - You can run "git stash pop" or "git stash drop" at any time. + Your local changes are stashed, however applying them + resulted in conflicts. You can either resolve the conflicts + and then discard the stash with "git stash drop", or, if you + do not want to resolve them now, run "git reset --hard" and + apply the local changes later by running "git stash pop". Successfully rebased and updated refs/heads/rebased-feature-branch. EOF }
t/t7201-co.sh +60 -1
73 lines not on this page · raw message
t/t7600-merge.sh +2 -1
@@ -914,7 +914,8 @@ test_expect_success 'merge with conflicted --autostash changes' ' git diff >expect && test_when_finished "test_might_fail git stash drop" && git merge --autostash c3 2>err && - test_grep "Applying autostash resulted in conflicts." err && + test_grep "applying them" err && + test_grep "resulted in conflicts" err && git show HEAD:file >merge-result && test_cmp result.1-9 merge-result && git stash show -p >actual &&
xdiff-interface.c +12 -0
@@ -325,6 +325,18 @@ int parse_conflict_style_name(const char *value)return -1 ; } +const char *conflict_style_name (int style) +{ +switch (style) { +case XDL_MERGE_DIFF3 : +return "diff3" ; +case XDL_MERGE_ZEALOUS_DIFF3 : +return "zdiff3" ; +default : +return "merge" ; + } +} +int git_xmerge_style =-1 ; int git_xmerge_config(const char *var, const char *value,
xdiff-interface.h +1 -0
@@ -55,6 +55,7 @@ void xdiff_set_find_func(xdemitconf_t *xecfg, const char *line, int cflags);void xdiff_clear_find_func (xdemitconf_t *xecfg);struct config_context ;int parse_conflict_style_name (const char *value); +const char *conflict_style_name (int style);int git_xmerge_config (const char *var,const char *value,const struct config_context *ctx,void *cb);extern int git_xmerge_style;
Signature · 1 line
gitgitgadget
Re: [PATCH v15 5/5] checkout -m: autostash when switching branches
Hi Harald
Quoted 10 lines · Harald Nordgren via GitGitGadget
From: Harald Nordgren <[email protected]>
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.
Quoted 8 lines
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.
This is looking good, there are just a few small issues. Hopefully the next iteration will be the last.
Quoted 48 lines
Signed-off-by Harald Nordgren <[email protected]>--- Documentation/git-checkout.adoc | 58 ++++++------ Documentation/git-switch.adoc | 33 +++---- builtin/checkout.c | 160 ++++++++++++++------------------ sequencer.c | 14 ++- t/t3420-rebase-autostash.sh | 16 ++-- t/t7201-co.sh | 61 +++++++++++- t/t7600-merge.sh | 3 +- xdiff-interface.c | 12 +++ xdiff-interface.h | 1 + 9 files changed, 211 insertions(+), 147 deletions(-)Documentation/git-checkout.adoc +9
-10@@ -251,20 +251,19 @@ working tree, by copying them from elsewhere, extracting a tarball, etc. are different between the current branch and the branch to which you are switching, the command refuses to switch branches in order to preserve your modifications in context. - However, with this option, a three-way merge between the current - branch, your working tree contents, and the new branch - is done, and you will be on the new branch. -+ -When a merge conflict happens, the index entries for conflicting -paths are left unmerged, and you need to resolve the conflicts -and mark the resolved paths with `git add` (or `git rm` if the merge -should result in deletion of the path). + With this option, the conflicting local changes are + automatically stashed before the switch and reapplied + afterwards. If the local changes do not overlap with the + differences between branches, the switch proceeds without + stashing. If reapplying the stash results in conflicts, the + entry is saved to the stash list. Resolve the conflicts + and run `git stash drop` when done, or clear the working + tree (e.g. with `git reset --hard`) before running `git stash + pop` later to re-apply your changes. + When checking out paths from the index, this option lets you recreate the conflicted merge in the specified paths. This option cannot be used when checking out paths from a tree-ish. -+ -When switching branches with `--merge`, staged changes may be lost. `--conflict=<style>`:: The same as `--merge` option above, but changes the way the
Documentation/git-checkout.adoc $ git checkout mytopic · line 580
Quoted 6 lines
Documentation/git-checkout.adoc +1
-2@@ -578,39 +577,44 @@ $ git checkout mytopic error: You have local changes to 'frotz'; not switching branches. ------------ -You can give the `-m` flag to the command, which would try a -three-way merge: +You can give the `-m` flag to the command, which would carry your local
s/would/will/ or s/would carry/carries/
Documentation/git-checkout.adoc $ git checkout mytopic
Quoted 11 lines
Documentation/git-checkout.adoc +3
-2+changes to the new branch: ------------ $ git checkout -m mytopic -Auto-merging frotz +Switched to branch 'mytopic' ------------ -After this three-way merge, the local modifications are _not_ +After the switch, the local modifications are reapplied and are _not_ registered in your index file,
We don't test this but we do test that "git stash apply" does not update the index so we should be fine.
Documentation/git-checkout.adoc $ git checkout mytopic
Quoted 10 lines
so
git diffwould show you what
changes you made since the tip of the new branch.=== 3. Merge conflict
Documentation/git-checkout.adoc +3
-2-When a merge conflict happens during switching branches with -the `-m` option, you would see something like this: +When the `--merge` (`-m`) option is in effect and the locally +modified files overlap with files that need to be updated by the +branch switch,
It is the changes in the files overlapping that causes the merge conflict, not the files overlapping
When the --merge (-m) option is given and the local changes overlap with the changes in the branch we're switching to,
Quoted 2 lines
the changes are stashed and reapplied after the
+switch. If this process results in conflicts, a stash entry is saved
s/a/the/
Quoted 1 line
+and made available in
git stash list:
I'd drop this line and say instead "a message is printed"
Quoted 9 lines
------------
$ git checkout -m mytopicDocumentation/git-checkout.adoc +2
-4-Auto-merging frotz -ERROR: Merge conflict in frotz -fatal: merge program failed ------------- +Your local changes are stashed, however, applying it to carry +forward your local changes resulted in conflicts:
Documentation/git-checkout.adoc $ git checkout mytopic · line 610
Quoted 10 lines
Documentation/git-checkout.adoc +5
-4-At this point, `git diff` shows the changes cleanly merged as in -the previous example, as well as the changes in the conflicted -files. Edit and resolve the conflict and mark it resolved with -`git add` as usual: + - You can try resolving them now. If you resolved them + successfully, discard the stash entry with "git stash drop". + - Alternatively you can "git reset --hard" if you do not want + to deal with them right now, and later "git stash pop" to + recover your local changes.
This needs updating to match the new conflict advice.
Documentation/git-checkout.adoc $ git checkout mytopic · line 617
Quoted 10 lines
------------
-$ edit frotz
-$ git add frotz
-------------Documentation/git-checkout.adoc +6
-0+ +You can try resolving the conflicts now. Edit the conflicting files +and mark them resolved with `git add` as usual, then run `git stash +drop` to discard the stash entry. Alternatively, you can clear the +working tree with `git reset --hard` and recover your local changes +later with `git stash pop`.
Is this documentation, or program output?
If you've not done so already it would be well worth checking the generated git-checkout.html and the man page
Quoted 39 lines
CONFIGURATION
-------------Documentation/git-switch.adoc +13
-12@@ -123,18 +123,19 @@ variable. `-m`:: `--merge`:: - If you have local modifications to one or more files that are - different between the current branch and the branch to which - you are switching, the command refuses to switch branches in - order to preserve your modifications in context. However, - with this option, a three-way merge between the current - branch, your working tree contents, and the new branch is - done, and you will be on the new branch. -+ -When a merge conflict happens, the index entries for conflicting -paths are left unmerged, and you need to resolve the conflicts -and mark the resolved paths with `git add` (or `git rm` if the merge -should result in deletion of the path). + If you have local modifications to one or more files that + are different between the current branch and the branch to + which you are switching, the command normally refuses to + switch branches in order to preserve your modifications in + context. However, with this option, the conflicting local + changes are automatically stashed before the switch and + reapplied afterwards. If the local changes do not overlap + with the differences between branches, the switch proceeds + without stashing. If reapplying the stash results in + conflicts, the entry is saved to the stash list. Resolve + the conflicts and run `git stash drop` when done, or clear + the working tree (e.g. with `git reset --hard`) before + running `git stash pop` later to re-apply your changes. `--conflict=<style>`:: The same as `--merge` option above, but changes the way the
Documentation/git-switch.adoc $ git switch mytopic · line 222
Quoted 7 lines
Documentation/git-switch.adoc +2
-2@@ -217,15 +218,15 @@ $ git switch mytopic error: You have local changes to 'frotz'; not switching branches. ------------ -You can give the `-m` flag to the command, which would try a three-way -merge: +You can give the `-m` flag to the command, which would carry your local +changes to the new branch:
Same comment as for git-checkout.adoc
Documentation/git-switch.adoc $ git switch mytopic · line 226
Quoted 4 lines
Documentation/git-switch.adoc +1
-1------------ $ git switch -m mytopic -Auto-merging frotz +Switched to branch 'mytopic'
Don't we show the modified files as well now?
Quoted 9 lines
Documentation/git-switch.adoc +1
-1------------ -After this three-way merge, the local modifications are _not_ +After the switch, the local modifications are reapplied and are _not_ registered in your index file, so `git diff` would show you what changes you made since the tip of the new branch.
builtin/checkout.c static void setup_branch_path(struct branch_info *branch) · line 760
Quoted 5 lines
builtin/checkout.c +1
-1@@ -755,7 +757,7 @@ static void setup_branch_path(struct branch_info *branch)static void init_topts (struct unpack_trees_options *topts,int merge,int show_progress,int overwrite_ignore, -struct commit *old_commit) +struct commit *old_commit,bool show_unpack_errors)
As this function only sets up the flags for unpack_trees() I think we could call this "quiet" or "show_errors"
builtin/checkout.c static void init_topts(struct unpack_trees_options *topts, int merge, · line 772
Quoted 9 lines
builtin/checkout.c +1
-1{memset (topts,0 ,sizeof (*topts)); topts->head_idx =-1 ;@@ -767,7 +769,7 @@ static void init_topts(struct unpack_trees_options *topts, int merge, topts->initial_checkout =is_index_unborn (the_repository->index); topts->update =1 ; topts->merge =1 ; - topts->quiet = merge && old_commit; + topts->quiet = merge && old_commit && !show_unpack_errors;
We've added a function parameter for this option but then we ignore it unless "merge" and "old_commit" are true which is confusing. The reason we used to check those was to set "quiet" automatically but we can't do that now, so why not just use the value the call requested?
builtin/checkout.c static void init_topts(struct unpack_trees_options *topts, int merge, · line 781
Quoted 9 lines
builtin/checkout.c +1
-1topts->verbose_update = show_progress; topts->fn = twoway_merge; topts->preserve_ignored = !overwrite_ignore;@@ -776,7 +778,8 @@ static void init_topts(struct unpack_trees_options *topts, int merge, static int merge_working_tree(const struct checkout_opts *opts, struct branch_info *old_branch_info, struct branch_info *new_branch_info, -int *writeout_error) + int *writeout_error,
This is an "out" parameter, so it would make sense to keep it at the end of the parameter list.
builtin/checkout.c static int merge_working_tree(const struct checkout_opts *opts,
Quoted 9 lines
builtin/checkout.c +3
-0+ bool show_unpack_errors) {@@ -853,90 +857,8 @@ static int merge_working_tree(const struct checkout_opts *opts, ret =unpack_trees (2 , trees, &topts);clear_unpack_trees_porcelain (&topts);if (ret ==-1 ) { +rollback_lock_file (&lock_file); +return MERGE_WORKING_TREE_UNPACK_FAILED ;
We now use a unique return value when unpack_trees() fails - good.
Quoted 15 lines
} }builtin/checkout.c +4
-0@@ -1181,6 +1103,10 @@ static int switch_branches(const struct checkout_opts *opts,struct object_id rev;int flag, writeout_error =0 ;int do_merge =1 ; +int created_autostash =0 ; +struct strbuf old_commit_shortname =STRBUF_INIT ; +struct strbuf autostash_msg =STRBUF_INIT ; +const char *stash_label_base =NULL ;trace2_cmd_mode ("branch" );
builtin/checkout.c static int switch_branches(const struct checkout_opts *opts, · line 1161
Quoted 8 lines
builtin/checkout.c +5
-1@@ -1218,11 +1144,49 @@ static int switch_branches(const struct checkout_opts *opts,if (do_merge) { - ret =merge_working_tree (opts, &old_branch_info, new_branch_info, &writeout_error); + ret =merge_working_tree (opts, &old_branch_info, new_branch_info, + &writeout_error, false); +if (ret ==MERGE_WORKING_TREE_UNPACK_FAILED && opts->merge) { + strbuf_addf(&autostash_msg, + "autostash while switching to '%s'",
In an ideal world we'd create a message that said
On <new branch>: autostash while switching from '<old branch>'
However I as discussed on patch 3, that would require us to be able to override the branch name when creating a stash as that's where the "On <branch>: " prefix gets added.
Quoted 13 lines
builtin/checkout.c +13
-0+ new_branch_info->name); +create_autostash_ref (the_repository, +"CHECKOUT_AUTOSTASH_HEAD" , + autostash_msg.buf, true); + created_autostash =1 ; + ret =merge_working_tree (opts, &old_branch_info, new_branch_info, + &writeout_error, true); + } +if (created_autostash) { +if (opts->conflict_style >=0 ) { +struct strbuf cfg =STRBUF_INIT ; +strbuf_addf (&cfg,"merge.conflictStyle=%s" , +conflict_style_name (opts->conflict_style));
builtin/checkout.c static int switch_branches(const struct checkout_opts *opts, · line 1184
Quoted 10 lines
builtin/checkout.c +10
-0+git_config_push_parameter (cfg.buf); +strbuf_release (&cfg); + } +apply_autostash_ref (the_repository, +"CHECKOUT_AUTOSTASH_HEAD" , + new_branch_info->name, +"local" , + stash_label_base, + autostash_msg.buf); + }
We now have a single place where we restore the stashed changes - good
Quoted 17 lines
sequencer.c +7
-0@@ -4765,15 +4765,19 @@ static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply,strvec_push (&store.args, stash_oid);if (run_command (&store)) ret =error (_ ("cannot store %s" ), stash_oid); +else if (attempt_apply) +fprintf (stderr, +_ ("Your local changes are stashed, however applying them\n" +"resulted in conflicts. You can either resolve the conflicts\n" +"and then discard the stash with \"git stash drop\", or, if you\n" +"do not want to resolve them now, run \"git reset --hard\" and\n" +"apply the local changes later by running \"git stash pop\".\n" ));else fprintf(stderr,
sequencer.c static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply, · line 4781
Quoted 10 lines
sequencer.c +2
-5-_ ("%s\n" +_ ("Autostash exists; creating a new stash entry.\n" "Your changes are safe in the stash.\n" "You can run \"git stash pop\" or" -" \"git stash drop\" at any time.\n" ), - attempt_apply ? -_ ("Applying autostash resulted in conflicts." ) : - _("Autostash exists; creating a new stash entry.")); + " \"git stash drop\" at any time.\n")); }
This looks good now
t/t7201-co.sh test_expect_success 'checkout -m with dirty tree' ' · line 105
Quoted 10 lines
t/t7201-co.sh +1
-1@@ -102,7 +102,7 @@ test_expect_success 'checkout -m with dirty tree' ' test "$(git symbolic-ref HEAD)" = "refs/heads/side" && - printf "M\t%s\n" one >expect.messages && + printf "The following paths have local changes:\nM\t%s\n" one >expect.messages &&
To create a multi-line file it is clearer to use
cat >expect.messages <<-\EOF && The following paths have local changes: M one EOF
Quoted 25 lines
t/t7201-co.sh +18
-0test_cmp expect.messages messages && fill "M one" "A three" "D two" >expect.main &&@@ -210,6 +210,65 @@ test_expect_success 'checkout --merge --conflict=diff3 <branch>' ' test_cmp expect two ' +test_expect_success 'checkout -m with mixed staged and unstaged changes' ' + git checkout -f main && + git clean -f && + + fill 0 x y z >same && + git add same && + fill 1 2 3 4 5 6 7 >one && + git checkout -m side >actual 2>&1 && + test_grep "Applied autostash" actual && + fill 0 x y z >expect && + test_cmp expect same && + fill 1 2 3 4 5 6 7 >expect && + test_cmp expect one +' + +test_expect_success 'checkout -m creates a recoverable stash on conflict' ' + git checkout -f main && + git clean -f &&
t/t7201-co.sh test_expect_success 'checkout --merge --conflict=diff3 <branch>' ' · line 240
Quoted 10 lines
t/t7201-co.sh +10
-0+ + fill 1 2 3 4 5 >one && + test_must_fail git checkout side 2>stderr && + test_grep "Your local changes" stderr && + git checkout -m side >actual 2>&1 && + test_grep "resulted in conflicts" actual && + test_grep "git stash drop" actual && + test_grep "git stash pop" actual && + test_grep "The following paths have local changes" actual && + git show --format=%B --diff-merges=1 refs/stash >actual &&
I've realized since I suggested this that we should be checking the reflog message as well since that's what's shown by "git stash list" so we need to run
git log -p -1 --format="%gs%n%B" -g --diff-merges=1 refs/stash >actual
t/t7201-co.sh test_expect_success 'checkout --merge --conflict=diff3 <branch>' ' · line 242
Quoted 2 lines
t/t7201-co.sh +2
-0+ sed /^index/d actual >actual.trimmed && + cat >expect <<-EOF &&
and add
autostash while switching to ${SQ}side${SQ}Quoted 17 lines
t/t7201-co.sh +17
-0+ On main: autostash while switching to ${SQ}side${SQ} + + diff --git a/one b/one + --- a/one + +++ b/one + @@ -3,6 +3,3 @@ + 3 + 4 + 5 + -6 + -7 + -8 + EOF + test_cmp expect actual.trimmed && + git stash drop && + git reset --hard +'
t/t7201-co.sh test_expect_success 'checkout --merge --conflict=diff3 <branch>' ' · line 269
Quoted 10 lines
t/t7201-co.sh +10
-0+ +test_expect_success 'checkout -m which would overwrite untracked file' ' + git checkout -f --detach main && + test_commit another-file && + git checkout HEAD^ && + >another-file.t && + fill 1 2 3 4 5 >one && + test_must_fail git checkout -m @{-1} 2>err && + test_grep "would be overwritten by checkout" err && + test_grep "another-file.t" err
Why the two calls to test_grep, rather than one? Anyway I've realized since I suggested this test that we also need to check the message only appears once to prevent a regression where merge_working_tree() calls unpack_trees() without setting "quiet" the first time it is called. We can do that by writing an expect file and calling test_cmp(), or by using "test_line_count = 1 err"
Hopefully the next re-roll with be the final one
Thanks
Phillip
Quoted 54 lines
t/t7201-co.sh +2
-0+' + test_expect_success 'switch to another branch while carrying a deletion' ' git checkout -f main && git reset --hard &&t/t7600-merge.sh +2
-1@@ -914,7 +914,8 @@ test_expect_success 'merge with conflicted --autostash changes' ' git diff >expect && test_when_finished "test_might_fail git stash drop" && git merge --autostash c3 2>err && - test_grep "Applying autostash resulted in conflicts." err && + test_grep "applying them" err && + test_grep "resulted in conflicts" err && git show HEAD:file >merge-result && test_cmp result.1-9 merge-result && git stash show -p >actual &&xdiff-interface.c +12
-0@@ -325,6 +325,18 @@ int parse_conflict_style_name(const char *value)return -1 ; } +const char *conflict_style_name (int style) +{ +switch (style) { +case XDL_MERGE_DIFF3 : +return "diff3" ; +case XDL_MERGE_ZEALOUS_DIFF3 : +return "zdiff3" ; +default : +return "merge" ; + } +} +int git_xmerge_style =-1 ; int git_xmerge_config(const char *var, const char *value,xdiff-interface.h +1
-0@@ -55,6 +55,7 @@ void xdiff_set_find_func(xdemitconf_t *xecfg, const char *line, int cflags);void xdiff_clear_find_func (xdemitconf_t *xecfg);struct config_context ;int parse_conflict_style_name (const char *value); +const char *conflict_style_name (int style);int git_xmerge_config (const char *var,const char *value,const struct config_context *ctx,void *cb);extern int git_xmerge_style;
PATCH 1/1 checkout: add --autostash option for branch switching
Quoted 2 lines
This is looking good, there are just a few small issues. Hopefully the next iteration will be the last.
Thanks for the encouragement! 💪🏻
Quoted 1 line
s/would/will/
👍
Quoted 5 lines
It is the changes in the files overlapping that causes the merge conflict, not the files overlapping
When the
--merge(-m) option is given and the local changes overlap with the changes in the branch we're switching to,
👍
Quoted 1 line
I'd drop this line and say instead "a message is printed"
👍
Quoted 1 line
This needs updating to match the new conflict advice.
👍
Quoted 2 lines
If you've not done so already it would be well worth checking the generated git-checkout.html and the man page
Good catch, I generated it now and yes it didn't look correct. I dropped that last section now.
Quoted 1 line
Don't we show the modified files as well now?
Good catch, very good idea to actually generate the man html file and check.
Quoted 2 lines
As this function only sets up the flags for
unpack_trees()I think we could call this "quiet" or "show_errors"
Good point!
Quoted 4 lines
We've added a function parameter for this option but then we ignore it unless "merge" and "
old_commit" are true which is confusing. The reason we used to check those was to set "quiet" automatically but we can't do that now, so why not just use the value the call requested?
Good point! I attempted to change this, hopefully it doesn't break anything!
Quoted 2 lines
This is an "out" parameter, so it would make sense to keep it at the end of the parameter list.
👍
Quoted 6 lines
To create a multi-line file it is clearer to use
cat >expect.messages <<-\EOF &&
The following paths have local changes:
M one
EOF
👍
Quoted 6 lines
I've realized since I suggested this that we should be checking the reflog message as well since that's what's shown by "git stash list" so we need to run
git log -p -1 --format="%gs%n%B" -g --diff-merges=1 refs/stash >actual
Quoted 7 lines
t/t7201-co.sh test_expect_success 'checkout --merge --conflict=diff3 <branch>' ' · line 242
Quoted 2 lines
t/t7201-co.sh +2
-0+ sed /^index/d actual >actual.trimmed && + cat >expect <<-EOF &&and add
autostash while switching to ${SQ}side${SQ}
Make sense!
Quoted 6 lines
Why the two calls to
test_grep, rather than one? Anyway I've realized since I suggested this test that we also need to check the message only appears once to prevent a regression wheremerge_working_tree()callsunpack_trees()without setting "quiet" the first time it is called. We can do that by writing an expect file and callingtest_cmp(), or by using "test_line_count= 1 err"
Excellent point. I went with test_cmp since it's multi-line output and "test_line_count = 1" seemed to not work then.
Harald
Re: [PATCH v15 0/5] checkout: 'autostash' for branch switching
In reply to Harald Nordgren via GitGitGadgetHi Harald
Quoted 17 lines · Harald Nordgren via GitGitGadget
* When the initial merge fails, the caller now knows for sure it was a conflict (not some unrelated error) before kicking off the autostash.
* If the second attempt also fails, the user now actually sees why. Previously errors like "would be overwritten by checkout" were being swallowed.
* The stash is reapplied right after the second attempt, so it won't get left behind if something else goes wrong later in the checkout.
* A couple of clean-up steps only run when we actually stashed, so regular checkouts aren't paying for work they don't need.
* Added a small header "The following paths have local changes:" before the list of changed files, so it doesn't look like part of the conflict advice above it.
Quoted 6 lines
* Reworded the conflict advice into a single paragraph. Feels less shouty and doesn't say "local changes" three times.
* Trimmed and consolidated tests in t7201. A bunch of the old ones couldn't actually fail, or were really testing other things; the conflict-path tests are now a single test that checks the stash entry directly.
Thanks for the summary, that all looks good. I've left some comments on Patches 1 and 3 but they can be future work and don't need to hold this series up. There are a few small issues that need addressing on Patch 5 but hopefully the next iteration will be the last.
Thanks
Phillip
Quoted 522 lines
Harald Nordgren (5):
stash: add --label-ours, --label-theirs, --label-base for apply
sequencer: allowcreate_autostashto run silently
sequencer: teach autostash apply to take optional conflict marker
labels
checkout: rollback lock on early returns inmerge_working_treecheckout -m: autostash when switching branchesDocumentation/git-checkout.adoc | 58 +++++------ Documentation/git-stash.adoc | 11 ++- Documentation/git-switch.adoc | 33 +++---- builtin/checkout.c | 165 +++++++++++++++----------------- builtin/commit.c | 3 +- builtin/merge.c | 15 ++- builtin/stash.c | 28 ++++-- sequencer.c | 69 +++++++++---- sequencer.h | 7 +- t/t3420-rebase-autostash.sh | 16 ++-- t/t3903-stash.sh | 24 +++++ t/t7201-co.sh | 61 +++++++++++- t/t7600-merge.sh | 3 +- xdiff-interface.c | 12 +++ xdiff-interface.h | 1 + xdiff/xmerge.c | 6 +- 16 files changed, 335 insertions(+), 177 deletions(-)base-commit:
94f057755b7941b321fd11fec1b2e3ca5313a4e0
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2234%2FHaraldNordgren%2Fcheckout_autostash-v15
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2234/HaraldNordgren/checkout_autostash-v15
Pull-Request: https://github.com/git/git/pull/2234Range-diff vs v14:
1: e18c25599a = 1: aba8e6a9dc stash: add --label-ours, --label-theirs, --label-base for apply 2: ce29b10264 = 2: 89e0bfa803 sequencer: allow create_autostash to run silently 3: 73051d1762 = 3: a428ce7328 sequencer: teach autostash apply to take optional conflict marker labels 4: 191058d8e3 = 4: f358424085 checkout: rollback lock on early returns in merge_working_tree 5: 86f33df1eb ! 5: 96b14db827 checkout -m: autostash when switching branches @@ builtin/checkout.c #include "setup.h" #include "submodule.h" #include "symlinks.h" +@@ builtin/checkout.c: struct checkout_opts { + .auto_advance = 1, \ + } + ++#define MERGE_WORKING_TREE_UNPACK_FAILED (-2) ++ + struct branch_info { + char *name; /* The short name used */ + char *path; /* The full name of a real branch */ +@@ builtin/checkout.c: static void setup_branch_path(struct branch_info *branch) + + static void init_topts(struct unpack_trees_options *topts, int merge, + int show_progress, int overwrite_ignore, +- struct commit *old_commit) ++ struct commit *old_commit, bool show_unpack_errors) + { + memset(topts, 0, sizeof(*topts)); + topts->head_idx = -1; +@@ builtin/checkout.c: static void init_topts(struct unpack_trees_options *topts, int merge, + topts->initial_checkout = is_index_unborn(the_repository->index); + topts->update = 1; + topts->merge = 1; +- topts->quiet = merge && old_commit; ++ topts->quiet = merge && old_commit && !show_unpack_errors; + topts->verbose_update = show_progress; + topts->fn = twoway_merge; + topts->preserve_ignored = !overwrite_ignore; +@@ builtin/checkout.c: static void init_topts(struct unpack_trees_options *topts, int merge, + static int merge_working_tree(const struct checkout_opts *opts, + struct branch_info *old_branch_info, + struct branch_info *new_branch_info, +- int *writeout_error) ++ int *writeout_error, ++ bool show_unpack_errors) + { + int ret; + struct lock_file lock_file = LOCK_INIT; +@@ builtin/checkout.c: static int merge_working_tree(const struct checkout_opts *opts, + + /* 2-way merge to the new branch */ + init_topts(&topts, opts->merge, opts->show_progress, +- opts->overwrite_ignore, old_branch_info->commit); ++ opts->overwrite_ignore, old_branch_info->commit, ++ show_unpack_errors); + init_checkout_metadata(&topts.meta, new_branch_info->refname, + new_branch_info->commit ? + &new_branch_info->commit->object.oid : @@ builtin/checkout.c: static int merge_working_tree(const struct checkout_opts *opts, ret = unpack_trees(2, trees, &topts); clear_unpack_trees_porcelain(&topts); @@ builtin/checkout.c: static int merge_working_tree(const struct checkout_opts *op - return ret; - } + rollback_lock_file(&lock_file); -+ return ret; ++ return MERGE_WORKING_TREE_UNPACK_FAILED; } } @@ builtin/checkout.c: static int switch_branches(const struct checkout_opts *opts, do_merge = 0; } -+ if (old_branch_info.name) ++ if (old_branch_info.name) { + stash_label_base = old_branch_info.name; -+ else if (old_branch_info.commit) { ++ } else if (old_branch_info.commit) { + strbuf_add_unique_abbrev(&old_commit_shortname, + &old_branch_info.commit->object.oid, + DEFAULT_ABBREV); @@ builtin/checkout.c: static int switch_branches(const struct checkout_opts *opts, + } + if (do_merge) { - ret = merge_working_tree(opts, &old_branch_info, new_branch_info, &writeout_error); -+ if (ret == -1 && opts->merge) { +- ret = merge_working_tree(opts, &old_branch_info, new_branch_info, &writeout_error); ++ ret = merge_working_tree(opts, &old_branch_info, new_branch_info, ++ &writeout_error, false); ++ if (ret == MERGE_WORKING_TREE_UNPACK_FAILED && opts->merge) { + strbuf_addf(&autostash_msg, + "autostash while switching to '%s'", + new_branch_info->name); @@ builtin/checkout.c: static int switch_branches(const struct checkout_opts *opts, + "CHECKOUT_AUTOSTASH_HEAD", + autostash_msg.buf, true); + created_autostash = 1; -+ ret = merge_working_tree(opts, &old_branch_info, new_branch_info, &writeout_error); ++ ret = merge_working_tree(opts, &old_branch_info, new_branch_info, ++ &writeout_error, true); ++ } ++ if (created_autostash) { ++ if (opts->conflict_style >= 0) { ++ struct strbuf cfg = STRBUF_INIT; ++ strbuf_addf(&cfg, "merge.conflictStyle=%s", ++ conflict_style_name(opts->conflict_style)); ++ git_config_push_parameter(cfg.buf); ++ strbuf_release(&cfg); ++ } ++ apply_autostash_ref(the_repository, ++ "CHECKOUT_AUTOSTASH_HEAD", ++ new_branch_info->name, ++ "local", ++ stash_label_base, ++ autostash_msg.buf); + } if (ret) { -+ if (created_autostash) -+ apply_autostash_ref(the_repository, -+ "CHECKOUT_AUTOSTASH_HEAD", -+ new_branch_info->name, -+ "local", -+ stash_label_base, -+ autostash_msg.buf); branch_info_release(&old_branch_info); - return ret; + strbuf_release(&old_commit_shortname); @@ builtin/checkout.c: static int switch_branches(const struct checkout_opts *opts, update_refs_for_switch(opts, &old_branch_info, new_branch_info); -+ if (opts->conflict_style >= 0) { -+ struct strbuf cfg = STRBUF_INIT; -+ strbuf_addf(&cfg, "merge.conflictStyle=%s", -+ conflict_style_name(opts->conflict_style)); -+ git_config_push_parameter(cfg.buf); -+ strbuf_release(&cfg); ++ if (created_autostash) { ++ discard_index(the_repository->index); ++ if (repo_read_index(the_repository) < 0) ++ die(_("index file corrupt")); ++ ++ if (!opts->quiet && new_branch_info->commit) { ++ printf(_("The following paths have local changes:\n")); ++ show_local_changes(&new_branch_info->commit->object, ++ &opts->diff_options); ++ } + } -+ apply_autostash_ref(the_repository, "CHECKOUT_AUTOSTASH_HEAD", -+ new_branch_info->name, "local", -+ stash_label_base, -+ autostash_msg.buf); -+ -+ discard_index(the_repository->index); -+ if (repo_read_index(the_repository) < 0) -+ die(_("index file corrupt")); -+ -+ if (created_autostash && !opts->quiet && new_branch_info->commit) -+ show_local_changes(&new_branch_info->commit->object, -+ &opts->diff_options); + ret = post_checkout_hook(old_branch_info.commit, new_branch_info->commit, 1); branch_info_release(&old_branch_info); @@ sequencer.c: static int apply_save_autostash_oid(const char *stash_oid, int atte ret = error(_("cannot store %s"), stash_oid); + else if (attempt_apply) + fprintf(stderr, -+ _("Your local changes are stashed, however, applying it to carry\n" -+ "forward your local changes resulted in conflicts:\n" -+ "\n" -+ " - You can try resolving them now. If you resolved them\n" -+ " successfully, discard the stash entry with \"git stash drop\".\n" -+ "\n" -+ " - Alternatively you can \"git reset --hard\" if you do not want\n" -+ " to deal with them right now, and later \"git stash pop\" to\n" -+ " recover your local changes.\n")); ++ _("Your local changes are stashed, however applying them\n" ++ "resulted in conflicts. You can either resolve the conflicts\n" ++ "and then discard the stash with \"git stash drop\", or, if you\n" ++ "do not want to resolve them now, run \"git reset --hard\" and\n" ++ "apply the local changes later by running \"git stash pop\".\n")); else fprintf(stderr, - _("%s\n" @@ t/t3420-rebase-autostash.sh: create_expected_failure_apply () { - Applying autostash resulted in conflicts. - Your changes are safe in the stash. - You can run "git stash pop" or "git stash drop" at any time. -+ Your local changes are stashed, however, applying it to carry -+ forward your local changes resulted in conflicts: -+ -+ - You can try resolving them now. If you resolved them -+ successfully, discard the stash entry with "git stash drop". -+ -+ - Alternatively you can "git reset --hard" if you do not want -+ to deal with them right now, and later "git stash pop" to -+ recover your local changes. ++ Your local changes are stashed, however applying them ++ resulted in conflicts. You can either resolve the conflicts ++ and then discard the stash with "git stash drop", or, if you ++ do not want to resolve them now, run "git reset --hard" and ++ apply the local changes later by running "git stash pop". EOF } @@ t/t3420-rebase-autostash.sh: create_expected_failure_apply () { - Applying autostash resulted in conflicts. - Your changes are safe in the stash. - You can run "git stash pop" or "git stash drop" at any time. -+ Your local changes are stashed, however, applying it to carry -+ forward your local changes resulted in conflicts: -+ -+ - You can try resolving them now. If you resolved them -+ successfully, discard the stash entry with "git stash drop". -+ -+ - Alternatively you can "git reset --hard" if you do not want -+ to deal with them right now, and later "git stash pop" to -+ recover your local changes. ++ Your local changes are stashed, however applying them ++ resulted in conflicts. You can either resolve the conflicts ++ and then discard the stash with "git stash drop", or, if you ++ do not want to resolve them now, run "git reset --hard" and ++ apply the local changes later by running "git stash pop". Successfully rebased and updated refs/heads/rebased-feature-branch. EOF } ## t/t7201-co.sh ## +@@ t/t7201-co.sh: test_expect_success 'checkout -m with dirty tree' ' + + test "$(git symbolic-ref HEAD)" = "refs/heads/side" && + +- printf "M\t%s\n" one >expect.messages && ++ printf "The following paths have local changes:\nM\t%s\n" one >expect.messages && + test_cmp expect.messages messages && + + fill "M one" "A three" "D two" >expect.main && @@ t/t7201-co.sh: test_expect_success 'checkout --merge --conflict=diff3 <branch>' ' test_cmp expect two ' -+test_expect_success 'checkout --merge --conflict=zdiff3 <branch>' ' -+ git checkout -f main && -+ git reset --hard && -+ git clean -f && -+ -+ fill a b X d e >two && -+ git checkout --merge --conflict=zdiff3 simple && -+ -+ cat <<-EOF >expect && -+ a -+ <<<<<<< simple -+ c -+ ||||||| main -+ b -+ c -+ d -+ ======= -+ b -+ X -+ d -+ >>>>>>> local -+ e -+ EOF -+ test_cmp expect two -+' -+ -+test_expect_success 'checkout -m respects merge.conflictStyle config' ' -+ git checkout -f main && -+ git reset --hard && -+ git clean -f && -+ -+ test_config merge.conflictStyle diff3 && -+ fill b d >two && -+ git checkout -m simple && -+ -+ cat <<-EOF >expect && -+ <<<<<<< simple -+ a -+ c -+ e -+ ||||||| main -+ a -+ b -+ c -+ d -+ e -+ ======= -+ b -+ d -+ >>>>>>> local -+ EOF -+ test_cmp expect two -+' -+ -+test_expect_success 'checkout -m skips stash when no conflict' ' -+ git checkout -f main && -+ git clean -f && -+ -+ fill 0 x y z >same && -+ git stash list >stash-before && -+ git checkout -m side >actual 2>&1 && -+ test_grep ! "Created autostash" actual && -+ git stash list >stash-after && -+ test_cmp stash-before stash-after && -+ fill 0 x y z >expect && -+ test_cmp expect same -+' -+ -+test_expect_success 'checkout -m skips stash with non-conflicting dirty index' ' -+ git checkout -f main && -+ git clean -f && -+ -+ fill 0 x y z >same && -+ git add same && -+ git checkout -m side >actual 2>&1 && -+ test_grep ! "Created autostash" actual && -+ fill 0 x y z >expect && -+ test_cmp expect same -+' -+ -+test_expect_success 'checkout -m stashes and applies on conflicting changes' ' -+ git checkout -f main && -+ git clean -f && -+ -+ fill 1 2 3 4 5 6 7 >one && -+ git checkout -m side >actual 2>&1 && -+ test_grep ! "Created autostash" actual && -+ test_grep "Applied autostash" actual && -+ fill 1 2 3 4 5 6 7 >expect && -+ test_cmp expect one -+' -+ +test_expect_success 'checkout -m with mixed staged and unstaged changes' ' + git checkout -f main && + git clean -f && @@ t/t7201-co.sh: test_expect_success 'checkout --merge --conflict=diff3 <branch>' + git add same && + fill 1 2 3 4 5 6 7 >one && + git checkout -m side >actual 2>&1 && -+ test_grep ! "Created autostash" actual && + test_grep "Applied autostash" actual && + fill 0 x y z >expect && + test_cmp expect same && @@ t/t7201-co.sh: test_expect_success 'checkout --merge --conflict=diff3 <branch>' + test_cmp expect one +' + -+test_expect_success 'checkout -m stashes on truly conflicting changes' ' ++test_expect_success 'checkout -m creates a recoverable stash on conflict' ' + git checkout -f main && + git clean -f && + @@ t/t7201-co.sh: test_expect_success 'checkout --merge --conflict=diff3 <branch>' + test_must_fail git checkout side 2>stderr && + test_grep "Your local changes" stderr && + git checkout -m side >actual 2>&1 && -+ test_grep ! "Created autostash" actual && + test_grep "resulted in conflicts" actual && + test_grep "git stash drop" actual && ++ test_grep "git stash pop" actual && ++ test_grep "The following paths have local changes" actual && ++ git show --format=%B --diff-merges=1 refs/stash >actual && ++ sed /^index/d actual >actual.trimmed && ++ cat >expect <<-EOF && ++ On main: autostash while switching to ${SQ}side${SQ} ++ ++ diff --git a/one b/one ++ --- a/one ++ +++ b/one ++ @@ -3,6 +3,3 @@ ++ 3 ++ 4 ++ 5 ++ -6 ++ -7 ++ -8 ++ EOF ++ test_cmp expect actual.trimmed && + git stash drop && + git reset --hard +' + -+test_expect_success 'checkout -m produces usable stash on conflict' ' -+ git checkout -f main && -+ git clean -f && -+ ++test_expect_success 'checkout -m which would overwrite untracked file' ' ++ git checkout -f --detach main && ++ test_commit another-file && ++ git checkout HEAD^ && ++ >another-file.t && + fill 1 2 3 4 5 >one && -+ git checkout -m side >actual 2>&1 && -+ test_grep "recover your local changes" actual && -+ git checkout -f main && -+ git stash pop && -+ fill 1 2 3 4 5 >expect && -+ test_cmp expect one -+' -+ -+test_expect_success 'checkout -m autostash message includes target branch' ' -+ git checkout -f main && -+ git clean -f && -+ -+ fill 1 2 3 4 5 >one && -+ git checkout -m side >actual 2>&1 && -+ git stash list >stash-list && -+ test_grep "autostash while switching to .side." stash-list && -+ git stash drop && -+ git checkout -f main && -+ git reset --hard -+' -+ -+test_expect_success 'checkout -m stashes on staged conflicting changes' ' -+ git checkout -f main && -+ git clean -f && -+ -+ fill 1 2 3 4 5 >one && -+ git add one && -+ git checkout -m side >actual 2>&1 && -+ test_grep ! "Created autostash" actual && -+ test_grep "resulted in conflicts" actual && -+ test_grep "git stash drop" actual && -+ git stash drop && -+ git reset --hard -+' -+ -+test_expect_success 'checkout -m applies stash cleanly with non-overlapping changes in same file' ' -+ git checkout -f main && -+ git reset --hard && -+ git clean -f && -+ -+ git checkout -b nonoverlap_base && -+ fill a b c d >file && -+ git add file && -+ git commit -m "add file" && -+ -+ git checkout -b nonoverlap_child && -+ fill a b c INSERTED d >file && -+ git commit -a -m "insert line near end of file" && -+ -+ fill DIRTY a b c INSERTED d >file && -+ -+ git stash list >stash-before && -+ git checkout -m nonoverlap_base 2>stderr && -+ test_grep "Applied autostash" stderr && -+ test_grep ! "resulted in conflicts" stderr && -+ -+ git stash list >stash-after && -+ test_cmp stash-before stash-after && -+ -+ fill DIRTY a b c d >expect && -+ test_cmp expect file && -+ -+ git checkout -f main && -+ git branch -D nonoverlap_base && -+ git branch -D nonoverlap_child -+' -+ -+test_expect_success 'checkout -m -b skips stash with dirty tree' ' -+ git checkout -f main && -+ git clean -f && -+ -+ fill 0 x y z >same && -+ git checkout -m -b newbranch >actual 2>&1 && -+ test_grep ! "Created autostash" actual && -+ fill 0 x y z >expect && -+ test_cmp expect same && -+ git checkout main && -+ git branch -D newbranch ++ test_must_fail git checkout -m @{-1} 2>err && ++ test_grep "would be overwritten by checkout" err && ++ test_grep "another-file.t" err +' + test_expect_success 'switch to another branch while carrying a deletion' ' @@ t/t7600-merge.sh: test_expect_success 'merge with conflicted --autostash changes test_when_finished "test_might_fail git stash drop" && git merge --autostash c3 2>err && - test_grep "Applying autostash resulted in conflicts." err && -+ test_grep "your local changes resulted in conflicts" err && ++ test_grep "applying them" err && ++ test_grep "resulted in conflicts" err && git show HEAD:file >merge-result && test_cmp result.1-9 merge-result && git stash show -p >actual &&
v16 cover checkout: 'autostash' for branch switching
In reply to Harald Nordgren via GitGitGadget * Updated the git checkout and git switch docs to show the actual output when using -m to carry local changes across a branch switch, and changed "would carry" to "will carry".
* Rewrote the merge-conflict example in both docs to match the new, more concise message printed on autostash conflicts.
* Replaced the show_unpack_errors flag and old_commit parameter in merge_working_tree()/init_topts() with a plain quiet boolean, so the caller decides directly whether to suppress unpack errors.
* Tightened the 'checkout -m with dirty tree' test by replacing a printf with a heredoc.
* Made the 'checkout -m creates a recoverable stash on conflict' test also assert the reflog subject of the new stash entry.
* Replaced two test_grep calls in the 'checkout -m which would overwrite untracked file' test with a single test_cmp, which also catches a regression where the "would be overwritten" message could end up printed twice.
Harald Nordgren (5):
stash: add --label-ours, --label-theirs, --label-base for apply
sequencer: allow create_autostash to run silently
sequencer: teach autostash apply to take optional conflict marker
labels
checkout: rollback lock on early returns in merge_working_tree checkout -m: autostash when switching branches
Documentation/git-checkout.adoc | 55 +++++------ Documentation/git-stash.adoc | 11 ++- Documentation/git-switch.adoc | 36 ++++--- builtin/checkout.c | 166 +++++++++++++++----------------- builtin/commit.c | 3 +- builtin/merge.c | 15 ++- builtin/stash.c | 28 ++++-- sequencer.c | 69 +++++++++---- sequencer.h | 7 +- t/t3420-rebase-autostash.sh | 16 +-- t/t3903-stash.sh | 24 +++++ t/t7201-co.sh | 71 +++++++++++++- t/t7600-merge.sh | 3 +- xdiff-interface.c | 12 +++ xdiff-interface.h | 1 + xdiff/xmerge.c | 6 +- 16 files changed, 343 insertions(+), 180 deletions(-)
base-commit: 94f057755b7941b321fd11fec1b2e3ca5313a4e0
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2234%2FHaraldNordgren%2Fcheckout_autostash-v16
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2234/HaraldNordgren/checkout_autostash-v16
Pull-Request: https://github.com/git/git/pull/2234
Range-diff vs v15:
1: aba8e6a9dc = 1: aba8e6a9dc stash: add --label-ours, --label-theirs, --label-base for apply 2: 89e0bfa803 = 2: 89e0bfa803 sequencer: allow create_autostash to run silently 3: a428ce7328 = 3: a428ce7328 sequencer: teach autostash apply to take optional conflict marker labels 4: f358424085 = 4: f358424085 checkout: rollback lock on early returns in merge_working_tree 5: 96b14db827 ! 5: 07d25fda91 checkout -m: autostash when switching branches
@@ Documentation/git-checkout.adoc: $ git checkout mytopic
-You can give the -m flag to the command, which would try a -three-way merge:
-+You can give the -m flag to the command, which would carry your local ++You can give the -m flag to the command, which will carry your local
+changes to the new branch:
------------
$ git checkout -m mytopic
-Auto-merging frotz
++Applied autostash.
+Switched to branch 'mytopic'
++The following paths have local changes:
++M frotz
------------
-After this three-way merge, the local modifications are _not_
@@ Documentation/git-checkout.adoc: $ git checkout mytopic
-When a merge conflict happens during switching branches with -the -m option, you would see something like this:
-+When the --merge (-m) option is in effect and the locally -+modified files overlap with files that need to be updated by the -+branch switch, the changes are stashed and reapplied after the -+switch. If this process results in conflicts, a stash entry is saved -+and made available in git stash list:
++When the --merge (-m) option is given and the local changes ++overlap with the changes in the branch we're switching to, the ++changes are stashed and reapplied after the switch. If this ++process results in conflicts, the stash entry is saved and a ++message is printed:
------------
$ git checkout -m mytopic
@@ Documentation/git-checkout.adoc: $ git checkout mytopic
-ERROR: Merge conflict in frotz
-fatal: merge program failed
-------------
-+Your local changes are stashed, however, applying it to carry -+forward your local changes resulted in conflicts:
-
+-
-At this point, git diff shows the changes cleanly merged as in -the previous example, as well as the changes in the conflicted -files. Edit and resolve the conflict and mark it resolved with -git add as usual:
-+ - You can try resolving them now. If you resolved them
-+ successfully, discard the stash entry with "git stash drop".
-
-+ - Alternatively you can "git reset --hard" if you do not want
-+ to deal with them right now, and later "git stash pop" to
-+ recover your local changes.
- ------------
+-
+-------------
-$ edit frotz
-$ git add frotz
--------------
-+
-+You can try resolving the conflicts now. Edit the conflicting files -+and mark them resolved with git add as usual, then run git stash -+drop to discard the stash entry. Alternatively, you can clear the -+working tree with git reset --hard and recover your local changes -+later with git stash pop.
++Your local changes are stashed, however applying them ++resulted in conflicts. You can either resolve the conflicts ++and then discard the stash with "git stash drop", or, if you ++do not want to resolve them now, run "git reset --hard" and ++apply the local changes later by running "git stash pop".
+ ------------
CONFIGURATION
- -------------
## Documentation/git-switch.adoc ##
@@ Documentation/git-switch.adoc: variable.
@@ Documentation/git-switch.adoc: $ git switch mytopic
-You can give the -m flag to the command, which would try a three-way -merge:
-+You can give the -m flag to the command, which would carry your local ++You can give the -m flag to the command, which will carry your local
+changes to the new branch:
------------
$ git switch -m mytopic
-Auto-merging frotz
++Applied autostash.
+Switched to branch 'mytopic'
++The following paths have local changes:
++M frotz
------------
-After this three-way merge, the local modifications are _not_
@@ builtin/checkout.c: struct checkout_opts {
char *name; /* The short name used */
char *path; /* The full name of a real branch */
@@ builtin/checkout.c: static void setup_branch_path(struct branch_info *branch)
+ branch->path = strbuf_detach(&buf, NULL);
+ }
- static void init_topts(struct unpack_trees_options *topts, int merge, +-static void init_topts(struct unpack_trees_options *topts, int merge, ++static void init_topts(struct unpack_trees_options *topts,
int show_progress, int overwrite_ignore,
- struct commit *old_commit)
-+ struct commit *old_commit, bool show_unpack_errors)
++ bool quiet)
{
memset(topts, 0, sizeof(*topts));
topts->head_idx = -1;
@@ builtin/checkout.c: static void init_topts(struct unpack_trees_options *topts, i
topts->update = 1;
topts->merge = 1;
- topts->quiet = merge && old_commit;
-+ topts->quiet = merge && old_commit && !show_unpack_errors;
++ topts->quiet = quiet;
topts->verbose_update = show_progress;
topts->fn = twoway_merge;
topts->preserve_ignored = !overwrite_ignore;
@@ builtin/checkout.c: static void init_topts(struct unpack_trees_options *topts, i
static int merge_working_tree(const struct checkout_opts *opts,
struct branch_info *old_branch_info,
struct branch_info *new_branch_info,
-- int *writeout_error)
-+ int *writeout_error,
-+ bool show_unpack_errors)
++ bool quiet,
+ int *writeout_error)
{
int ret;
- struct lock_file lock_file = LOCK_INIT;
@@ builtin/checkout.c: static int merge_working_tree(const struct checkout_opts *opts,
+ }
/* 2-way merge to the new branch */
- init_topts(&topts, opts->merge, opts->show_progress,
+- init_topts(&topts, opts->merge, opts->show_progress,
- opts->overwrite_ignore, old_branch_info->commit);
-+ opts->overwrite_ignore, old_branch_info->commit,
-+ show_unpack_errors);
++ init_topts(&topts, opts->show_progress,
++ opts->overwrite_ignore, quiet);
init_checkout_metadata(&topts.meta, new_branch_info->refname,
new_branch_info->commit ? &new_branch_info->commit->object.oid :
@@ builtin/checkout.c: static int switch_branches(const struct checkout_opts *opts,
if (do_merge) {
- ret = merge_working_tree(opts, &old_branch_info, new_branch_info, &writeout_error);
+ ret = merge_working_tree(opts, &old_branch_info, new_branch_info,
-+ &writeout_error, false);
++ opts->merge, &writeout_error);
+ if (ret == MERGE_WORKING_TREE_UNPACK_FAILED && opts->merge) {
+ strbuf_addf(&autostash_msg,
+ "autostash while switching to '%s'",
@@ builtin/checkout.c: static int switch_branches(const struct checkout_opts *opts,
+ autostash_msg.buf, true);
+ created_autostash = 1;
+ ret = merge_working_tree(opts, &old_branch_info, new_branch_info,
-+ &writeout_error, true);
++ false, &writeout_error);
+ }
+ if (created_autostash) {
+ if (opts->conflict_style >= 0) {
@@ t/t7201-co.sh: test_expect_success 'checkout -m with dirty tree' '
test "$(git symbolic-ref HEAD)" = "refs/heads/side" &&
- printf "M\t%s\n" one >expect.messages &&
-+ printf "The following paths have local changes:\nM\t%s\n" one >expect.messages &&
++ cat >expect.messages <<-\EOF &&
++ The following paths have local changes:
++ M one
++ EOF
test_cmp expect.messages messages &&
fill "M one" "A three" "D two" >expect.main &&
@@ t/t7201-co.sh: test_expect_success 'checkout --merge --conflict=diff3 <branch>'
+ test_grep "git stash drop" actual &&
+ test_grep "git stash pop" actual &&
+ test_grep "The following paths have local changes" actual &&
-+ git show --format=%B --diff-merges=1 refs/stash >actual &&
++ git log -p -1 --format="%gs%n%B" -g --diff-merges=1 refs/stash >actual &&
+ sed /^index/d actual >actual.trimmed &&
+ cat >expect <<-EOF &&
++ autostash while switching to ${SQ}side${SQ}
+ On main: autostash while switching to ${SQ}side${SQ}
+
+ diff --git a/one b/one
@@ t/t7201-co.sh: test_expect_success 'checkout --merge --conflict=diff3 <branch>'
+ >another-file.t &&
+ fill 1 2 3 4 5 >one &&
+ test_must_fail git checkout -m @{-1} 2>err &&
-+ test_grep "would be overwritten by checkout" err &&
-+ test_grep "another-file.t" err
++ q_to_tab >expect <<-\EOF &&
++ error: The following untracked working tree files would be overwritten by checkout:
++ Qanother-file.t
++ Please move or remove them before you switch branches.
++ Aborting
++ Applied autostash.
++ EOF
++ test_cmp expect err
+'
+
test_expect_success 'switch to another branch while carrying a deletion' '
Signature · 1 line
gitgitgadget
PATCH v16 1/5 stash: add --label-ours, --label-theirs, --label-base for apply
From: Harald Nordgren <[email protected]>
Allow callers of "git stash apply" to pass custom labels for conflict markers instead of the default "Updated upstream" and "Stashed changes". Document the new options and add a test.
--- Documentation/git-stash.adoc | 11 ++++++++++- builtin/stash.c | 28 ++++++++++++++++++++-------- t/t3903-stash.sh | 24 ++++++++++++++++++++++++ xdiff/xmerge.c | 6 +++--- 4 files changed, 57 insertions(+), 12 deletions(-)
Documentation/git-stash.adoc +10 -1
@@ -12,7 +12,7 @@ git stash list [<log-options>] git stash show [-u | --include-untracked | --only-untracked] [<diff-options>] [<stash>] git stash drop [-q | --quiet] [<stash>] git stash pop [--index] [-q | --quiet] [<stash>] -git stash apply [--index] [-q | --quiet] [<stash>] +git stash apply [--index] [-q | --quiet] [--label-ours=<label>] [--label-theirs=<label>] [--label-base=<label>] [<stash>] git stash branch <branchname> [<stash>] git stash [push] [-p | --patch] [-S | --staged] [-k | --[no-]keep-index] [-q | --quiet] [-u | --include-untracked] [-a | --all] [(-m | --message) <message>]
@@ -195,6 +195,15 @@ the index's ones. However, this can fail, when you have conflicts (which are stored in the index, where you therefore can no longer apply the changes as they were originally). +`--label-ours=<label>`:: +`--label-theirs=<label>`:: +`--label-base=<label>`:: + These options are only valid for the `apply` command. ++ +Use the given labels in conflict markers instead of the default +"Updated upstream", "Stashed changes", and "Stash base". +`--label-base` only has an effect with merge.conflictStyle=diff3. + `-k`:: `--keep-index`:: `--no-keep-index`::
builtin/stash.c +20 -8
@@ -44,7 +44,7 @@#define BUILTIN_STASH_POP_USAGE \ N_("git stash pop [--index] [-q | --quiet] [<stash>]")#define BUILTIN_STASH_APPLY_USAGE \ - N_("git stash apply [--index] [-q | --quiet] [<stash>]") + N_("git stash apply [--index] [-q | --quiet] [--label-ours=<label>] [--label-theirs=<label>] [--label-base=<label>] [<stash>]")#define BUILTIN_STASH_BRANCH_USAGE \ N_("git stash branch <branchname> [<stash>]")#define BUILTIN_STASH_STORE_USAGE \
@@ -591,7 +591,9 @@ static void unstage_changes_unless_new(struct object_id *orig_tree) }static int do_apply_stash (const char *prefix,struct stash_info *info, -int index,int quiet) +int index,int quiet, +const char *label_ours,const char *label_theirs, +const char *label_base) {int clean, ret;int has_index = index;
@@ -643,9 +645,9 @@ static int do_apply_stash(const char *prefix, struct stash_info *info,init_ui_merge_options (&o, the_repository); - o.branch1 ="Updated upstream" ; - o.branch2 ="Stashed changes" ; - o.ancestor ="Stash base" ; + o.branch1 = label_ours ? label_ours :"Updated upstream" ; + o.branch2 = label_theirs ? label_theirs :"Stashed changes" ; + o.ancestor = label_base ? label_base :"Stash base" ;if (oideq (&info->b_tree, &c_tree)) o.branch1 ="Version stash was based on" ;
@@ -723,11 +725,18 @@ static int apply_stash(int argc, const char **argv, const char *prefix,int ret =-1 ;int quiet =0 ;int index = use_index; +const char *label_ours =NULL , *label_theirs =NULL , *label_base =NULL ;struct stash_info info =STASH_INFO_INIT ;struct option options[] = {OPT__QUIET (&quiet,N_ ("be quiet, only report errors" )),OPT_BOOL (0 ,"index" , &index,N_ ("attempt to recreate the index" )), +OPT_STRING (0 ,"label-ours" , &label_ours,N_ ("label" ), +N_ ("label for the upstream side in conflict markers" )), +OPT_STRING (0 ,"label-theirs" , &label_theirs,N_ ("label" ), +N_ ("label for the stashed side in conflict markers" )), +OPT_STRING (0 ,"label-base" , &label_base,N_ ("label" ), +N_ ("label for the base in diff3 conflict markers" )),OPT_END () };
@@ -737,7 +746,8 @@ static int apply_stash(int argc, const char **argv, const char *prefix,if (get_stash_info (&info, argc, argv)) goto cleanup; - ret =do_apply_stash (prefix, &info, index, quiet); + ret =do_apply_stash (prefix, &info, index, quiet, + label_ours, label_theirs, label_base); cleanup:free_stash_info (&info);return ret;
@@ -836,7 +846,8 @@ static int pop_stash(int argc, const char **argv, const char *prefix,if (get_stash_info_assert (&info, argc, argv)) goto cleanup; -if ((ret =do_apply_stash (prefix, &info, index, quiet))) +if ((ret =do_apply_stash (prefix, &info, index, quiet, +NULL ,NULL ,NULL )))printf_ln (_ ("The stash entry is kept in case " "you need it again." ));else
@@ -877,7 +888,8 @@ static int branch_stash(int argc, const char **argv, const char *prefix,strvec_push (&cp.args,oid_to_hex (&info.b_commit)); ret =run_command (&cp);if (!ret) - ret =do_apply_stash (prefix, &info,1 ,0 ); + ret =do_apply_stash (prefix, &info,1 ,0 , +NULL ,NULL ,NULL );if (!ret && info.is_stash_ref) ret =do_drop_stash (&info,0 );
t/t3903-stash.sh +24 -0
@@ -56,6 +56,7 @@ setup_stash() { git add other-file && test_tick && git commit -m initial && + git tag initial && echo 2 >file && git add file && echo 3 >file &&
@@ -1790,4 +1791,27 @@ test_expect_success 'stash.index=false overridden by --index' ' test_cmp expect file ' +test_expect_success 'apply with custom conflict labels' ' + git reset --hard initial && + test_commit label-base conflict-file base-content && + echo stashed >conflict-file && + git stash push -m "stashed" && + test_commit label-upstream conflict-file upstream-content && + test_must_fail git -c merge.conflictStyle=diff3 stash apply --label-ours=UP --label-theirs=STASH && + test_grep "^<<<<<<< UP" conflict-file && + test_grep "^||||||| Stash base" conflict-file && + test_grep "^>>>>>>> STASH" conflict-file +' + +test_expect_success 'apply with empty conflict labels' ' + git reset --hard initial && + test_commit empty-label-base conflict-file base-content && + echo stashed >conflict-file && + git stash push -m "stashed" && + test_commit empty-label-upstream conflict-file upstream-content && + test_must_fail git stash apply --label-ours= --label-theirs= && + test_grep "^<<<<<<<$" conflict-file && + test_grep "^>>>>>>>$" conflict-file +' + test_done
xdiff/xmerge.c +3 -3
@@ -199,9 +199,9 @@ static int fill_conflict_hunk(xdfenv_t *xe1, const char *name1, int size, int i, int style, xdmerge_t *m, char *dest, int marker_size) { - int marker1_size = (name1 ? strlen(name1) + 1 : 0); -int marker2_size = (name2 ?strlen (name2) +1 :0 ); -int marker3_size = (name3 ?strlen (name3) +1 :0 ); + int marker1_size = (name1 && *name1 ? strlen(name1) + 1 : 0); +int marker2_size = (name2 && *name2 ?strlen (name2) +1 :0 ); +int marker3_size = (name3 && *name3 ?strlen (name3) +1 :0 );int needs_cr =is_cr_needed (xe1, xe2, m);if (marker_size <=0 )
Signature · 1 line
gitgitgadget
PATCH v16 2/5 sequencer: allow create_autostash to run silently
In reply to Harald Nordgren via GitGitGadgetFrom: Harald Nordgren <[email protected]>
Add a silent parameter to create_autostash_internal and introduce create_autostash_ref_silent so that callers can create an autostash without printing the "Created autostash" message.
--- builtin/merge.c | 6 ++++-- sequencer.c | 17 +++++++++++------ sequencer.h | 3 ++- 3 files changed, 17 insertions(+), 9 deletions(-)
builtin/merge.c +4 -2
@@ -1672,7 +1672,8 @@ int cmd_merge(int argc, }if (autostash) -create_autostash_ref (the_repository,"MERGE_AUTOSTASH" ); +create_autostash_ref (the_repository,"MERGE_AUTOSTASH" , +NULL , false); if (checkout_fast_forward(the_repository, &head_commit->object.oid, &commit->object.oid,
@@ -1764,7 +1765,8 @@ int cmd_merge(int argc,die_ff_impossible ();if (autostash) -create_autostash_ref (the_repository,"MERGE_AUTOSTASH" ); +create_autostash_ref (the_repository,"MERGE_AUTOSTASH" , +NULL , false);/* We are going to make a new commit. */ git_committer_info (IDENT_STRICT );
sequencer.c +11 -6
@@ -4657,7 +4657,9 @@ static enum todo_command peek_command(struct todo_list *todo_list, int offset)static void create_autostash_internal (struct repository *r,const char *path, -const char *refname) +const char *refname, +const char *message, +bool silent) {struct strbuf buf =STRBUF_INIT ;struct lock_file lock_file =LOCK_INIT ;
@@ -4679,7 +4681,8 @@ static void create_autostash_internal(struct repository *r,struct object_id oid;strvec_pushl (&stash.args, -"stash" ,"create" ,"autostash" ,NULL ); +"stash" ,"create" , + message ? message :"autostash" ,NULL ); stash.git_cmd =1 ; stash.no_stdin =1 ;strbuf_reset (&buf);
@@ -4702,7 +4705,8 @@ static void create_autostash_internal(struct repository *r, &oid, null_oid(the_hash_algo), 0, UPDATE_REFS_DIE_ON_ERR); } -printf (_ ("Created autostash: %s\n" ), buf.buf); +if (!silent) +printf (_ ("Created autostash: %s\n" ), buf.buf);if (reset_head (r, &ropts) <0 )die (_ ("could not reset --hard" ));discard_index (r->index);
@@ -4714,12 +4718,13 @@ static void create_autostash_internal(struct repository *r,void create_autostash (struct repository *r,const char *path) { -create_autostash_internal (r, path,NULL ); +create_autostash_internal (r, path,NULL ,NULL , false); } -void create_autostash_ref (struct repository *r,const char *refname) +void create_autostash_ref (struct repository *r,const char *refname, +const char *message,bool silent) { -create_autostash_internal (r,NULL , refname); +create_autostash_internal (r,NULL , refname, message, silent); }static int apply_save_autostash_oid (const char *stash_oid,int attempt_apply)
sequencer.h +2 -1
@@ -229,7 +229,8 @@ void commit_post_rewrite(struct repository *r, const struct object_id *new_head);void create_autostash (struct repository *r,const char *path); -void create_autostash_ref (struct repository *r,const char *refname); +void create_autostash_ref (struct repository *r,const char *refname, +const char *message,bool silent);int save_autostash (const char *path);int save_autostash_ref (struct repository *r,const char *refname);int apply_autostash (const char *path);
Signature · 1 line
gitgitgadget
PATCH v16 3/5 sequencer: teach autostash apply to take optional conflict marker labels
In reply to Harald Nordgren via GitGitGadgetFrom: Harald Nordgren <[email protected]>
Add label_ours, label_theirs, label_base, and stash_msg parameters to apply_autostash_ref() and the autostash apply machinery so callers can pass custom conflict marker labels through to
"git stash apply --label-ours/--label-theirs/--label-base", as well as a custom stash message for "git stash store -m".
--- builtin/commit.c | 3 ++- builtin/merge.c | 9 ++++++--- sequencer.c | 38 +++++++++++++++++++++++++++++--------- sequencer.h | 4 +++- 4 files changed, 40 insertions(+), 14 deletions(-)
builtin/commit.c +2 -1
@@ -1979,7 +1979,8 @@ int cmd_commit(int argc, &oid, flags); } -apply_autostash_ref (the_repository,"MERGE_AUTOSTASH" ); +apply_autostash_ref (the_repository,"MERGE_AUTOSTASH" , +NULL ,NULL ,NULL ,NULL ); cleanup:free_commit_extra_headers (extra);
builtin/merge.c +6 -3
@@ -537,7 +537,8 @@ static void finish(struct commit *head_commit,run_hooks_l (the_repository,"post-merge" , squash ?"1" :"0" ,NULL );if (new_head) -apply_autostash_ref (the_repository,"MERGE_AUTOSTASH" ); +apply_autostash_ref (the_repository,"MERGE_AUTOSTASH" , +NULL ,NULL ,NULL ,NULL );strbuf_release (&reflog_message); }
@@ -1678,7 +1679,8 @@ int cmd_merge(int argc, &head_commit->object.oid, &commit->object.oid, overwrite_ignore)) { -apply_autostash_ref (the_repository,"MERGE_AUTOSTASH" ); +apply_autostash_ref (the_repository,"MERGE_AUTOSTASH" , +NULL ,NULL ,NULL ,NULL ); ret =1 ; goto done; }
@@ -1851,7 +1853,8 @@ int cmd_merge(int argc,else fprintf (stderr,_ ("Merge with strategy %s failed.\n" ), use_strategies[0 ]->name); -apply_autostash_ref (the_repository,"MERGE_AUTOSTASH" ); +apply_autostash_ref (the_repository,"MERGE_AUTOSTASH" , +NULL ,NULL ,NULL ,NULL ); ret =2 ; goto done; }else if (best_strategy == wt_strategy)
sequencer.c +29 -9
@@ -4727,7 +4727,10 @@ void create_autostash_ref(struct repository *r, const char *refname,create_autostash_internal (r,NULL , refname, message, silent); } -static int apply_save_autostash_oid (const char *stash_oid,int attempt_apply) +static int apply_save_autostash_oid (const char *stash_oid,int attempt_apply, +const char *label_ours,const char *label_theirs, +const char *label_base, +const char *stash_msg) {struct child_process child =CHILD_PROCESS_INIT ;int ret =0 ;
@@ -4738,6 +4741,12 @@ static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply) child.no_stderr =1 ;strvec_push (&child.args,"stash" );strvec_push (&child.args,"apply" ); +if (label_ours) +strvec_pushf (&child.args,"--label-ours=%s" , label_ours); +if (label_theirs) +strvec_pushf (&child.args,"--label-theirs=%s" , label_theirs); +if (label_base) +strvec_pushf (&child.args,"--label-base=%s" , label_base);strvec_push (&child.args, stash_oid); ret =run_command (&child); }
@@ -4751,7 +4760,7 @@ static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply)strvec_push (&store.args,"stash" );strvec_push (&store.args,"store" );strvec_push (&store.args,"-m" ); -strvec_push (&store.args,"autostash" ); +strvec_push (&store.args, stash_msg ? stash_msg :"autostash" );strvec_push (&store.args,"-q" );strvec_push (&store.args, stash_oid);if (run_command (&store))
@@ -4782,7 +4791,8 @@ static int apply_save_autostash(const char *path, int attempt_apply) }strbuf_trim (&stash_oid); - ret =apply_save_autostash_oid (stash_oid.buf, attempt_apply); + ret =apply_save_autostash_oid (stash_oid.buf, attempt_apply, +NULL ,NULL ,NULL ,NULL );unlink (path);strbuf_release (&stash_oid);
@@ -4801,11 +4811,14 @@ int apply_autostash(const char *path)int apply_autostash_oid (const char *stash_oid) { -return apply_save_autostash_oid (stash_oid,1 ); +return apply_save_autostash_oid (stash_oid,1 ,NULL ,NULL ,NULL ,NULL ); }static int apply_save_autostash_ref (struct repository *r,const char *refname, -int attempt_apply) +int attempt_apply, +const char *label_ours,const char *label_theirs, +const char *label_base, +const char *stash_msg) {struct object_id stash_oid;char stash_oid_hex[GIT_MAX_HEXSZ +1 ];
@@ -4821,7 +4834,9 @@ static int apply_save_autostash_ref(struct repository *r, const char *refname,return error (_ ("autostash reference is a symref" ));oid_to_hex_r (stash_oid_hex, &stash_oid); - ret =apply_save_autostash_oid (stash_oid_hex, attempt_apply); + ret =apply_save_autostash_oid (stash_oid_hex, attempt_apply, + label_ours, label_theirs, label_base, + stash_msg);refs_delete_ref (get_main_ref_store (r),"" , refname, &stash_oid,REF_NO_DEREF );
@@ -4831,12 +4846,17 @@ static int apply_save_autostash_ref(struct repository *r, const char *refname,int save_autostash_ref (struct repository *r,const char *refname) { -return apply_save_autostash_ref (r, refname,0 ); +return apply_save_autostash_ref (r, refname,0 , +NULL ,NULL ,NULL ,NULL ); } -int apply_autostash_ref (struct repository *r,const char *refname) +int apply_autostash_ref (struct repository *r,const char *refname, +const char *label_ours,const char *label_theirs, +const char *label_base,const char *stash_msg) { -return apply_save_autostash_ref (r, refname,1 ); +return apply_save_autostash_ref (r, refname,1 , + label_ours, label_theirs, label_base, + stash_msg); } static int checkout_onto(struct repository *r, struct replay_opts *opts,
sequencer.h +3 -1
@@ -235,7 +235,9 @@ int save_autostash(const char *path);int save_autostash_ref (struct repository *r,const char *refname);int apply_autostash (const char *path);int apply_autostash_oid (const char *stash_oid); -int apply_autostash_ref (struct repository *r,const char *refname); +int apply_autostash_ref (struct repository *r,const char *refname, +const char *label_ours,const char *label_theirs, +const char *label_base,const char *stash_msg);#define SUMMARY_INITIAL_COMMIT (1 << 0)#define SUMMARY_SHOW_AUTHOR_DATE (1 << 1)
Signature · 1 line
gitgitgadget
PATCH v16 4/5 checkout: rollback lock on early returns in merge_working_tree
In reply to Harald Nordgren via GitGitGadgetFrom: Harald Nordgren <[email protected]>
merge_working_tree() acquires the index lock via repo_hold_locked_index() but several early return paths exit without calling rollback_lock_file(), leaving the lock held. While this is currently harmless because the process exits soon after, it becomes a problem if the function is ever called more than once in the same process.
Add rollback_lock_file() calls to all early return paths.
--- builtin/checkout.c | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-)
builtin/checkout.c +22 -7
@@ -783,8 +783,10 @@ static int merge_working_tree(const struct checkout_opts *opts,struct tree *new_tree;repo_hold_locked_index (the_repository, &lock_file,LOCK_DIE_ON_ERROR ); -if (repo_read_index_preload (the_repository,NULL ,0 ) <0 ) +if (repo_read_index_preload (the_repository,NULL ,0 ) <0 ) { +rollback_lock_file (&lock_file);return error (_ ("index file corrupt" )); + }resolve_undo_clear_index (the_repository->index);if (opts->new_orphan_branch && opts->orphan_from_empty_tree) {
@@ -797,14 +799,18 @@ static int merge_working_tree(const struct checkout_opts *opts, }else { new_tree =repo_get_commit_tree (the_repository, new_branch_info->commit); -if (!new_tree) +if (!new_tree) { +rollback_lock_file (&lock_file);return error (_ ("unable to read tree (%s)" ),oid_to_hex (&new_branch_info->commit->object.oid)); + } }if (opts->discard_changes) { ret =reset_tree (new_tree, opts,1 , writeout_error, new_branch_info); -if (ret) +if (ret) { +rollback_lock_file (&lock_file);return ret; + } }else {struct tree_desc trees[2 ];struct tree *tree;
@@ -814,6 +820,7 @@ static int merge_working_tree(const struct checkout_opts *opts,refresh_index (the_repository->index,REFRESH_QUIET ,NULL ,NULL ,NULL );if (unmerged_index (the_repository->index)) { +rollback_lock_file (&lock_file);error (_ ("you need to resolve your current index first" ));return 1 ; }
@@ -857,15 +864,19 @@ static int merge_working_tree(const struct checkout_opts *opts,struct strbuf sb =STRBUF_INIT ;struct strbuf old_commit_shortname =STRBUF_INIT ; -if (!opts->merge) +if (!opts->merge) { +rollback_lock_file (&lock_file);return 1 ; + }/* * Without old_branch_info->commit, the below is the same as * the two-tree unpack we already tried and failed. */ -if (!old_branch_info->commit) +if (!old_branch_info->commit) { +rollback_lock_file (&lock_file);return 1 ; + } old_tree =repo_get_commit_tree (the_repository, old_branch_info->commit);
@@ -897,8 +908,10 @@ static int merge_working_tree(const struct checkout_opts *opts, ret =reset_tree (new_tree, opts,1 , writeout_error, new_branch_info); -if (ret) +if (ret) { +rollback_lock_file (&lock_file);return ret; + } o.ancestor = old_branch_info->name;if (!old_branch_info->name) { strbuf_add_unique_abbrev(&old_commit_shortname,
@@ -920,8 +933,10 @@ static int merge_working_tree(const struct checkout_opts *opts, writeout_error, new_branch_info);strbuf_release (&o.obuf);strbuf_release (&old_commit_shortname); -if (ret) +if (ret) { +rollback_lock_file (&lock_file);return ret; + } } }
Signature · 1 line
gitgitgadget
PATCH v16 5/5 checkout -m: autostash when switching branches
In reply to Harald Nordgren via GitGitGadgetFrom: Harald Nordgren <[email protected]>
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.
--- Documentation/git-checkout.adoc | 55 ++++++----- Documentation/git-switch.adoc | 36 +++---- builtin/checkout.c | 161 ++++++++++++++------------------ sequencer.c | 14 ++- t/t3420-rebase-autostash.sh | 16 ++-- t/t7201-co.sh | 71 +++++++++++++- t/t7600-merge.sh | 3 +- xdiff-interface.c | 12 +++ xdiff-interface.h | 1 + 9 files changed, 219 insertions(+), 150 deletions(-)
Documentation/git-checkout.adoc +26 -29
@@ -251,20 +251,19 @@ working tree, by copying them from elsewhere, extracting a tarball, etc. are different between the current branch and the branch to which you are switching, the command refuses to switch branches in order to preserve your modifications in context. - However, with this option, a three-way merge between the current - branch, your working tree contents, and the new branch - is done, and you will be on the new branch. -+ -When a merge conflict happens, the index entries for conflicting -paths are left unmerged, and you need to resolve the conflicts -and mark the resolved paths with `git add` (or `git rm` if the merge -should result in deletion of the path). + With this option, the conflicting local changes are + automatically stashed before the switch and reapplied + afterwards. If the local changes do not overlap with the + differences between branches, the switch proceeds without + stashing. If reapplying the stash results in conflicts, the + entry is saved to the stash list. Resolve the conflicts + and run `git stash drop` when done, or clear the working + tree (e.g. with `git reset --hard`) before running `git stash + pop` later to re-apply your changes. + When checking out paths from the index, this option lets you recreate the conflicted merge in the specified paths. This option cannot be used when checking out paths from a tree-ish. -+ -When switching branches with `--merge`, staged changes may be lost. `--conflict=<style>`:: The same as `--merge` option above, but changes the way the
@@ -578,38 +577,36 @@ $ git checkout mytopic error: You have local changes to 'frotz'; not switching branches. ------------ -You can give the `-m` flag to the command, which would try a -three-way merge: +You can give the `-m` flag to the command, which will carry your local +changes to the new branch: ------------ $ git checkout -m mytopic -Auto-merging frotz +Applied autostash. +Switched to branch 'mytopic' +The following paths have local changes: +M frotz ------------ -After this three-way merge, the local modifications are _not_ +After the switch, the local modifications are reapplied and are _not_ registered in your index file, so `git diff` would show you what changes you made since the tip of the new branch. === 3. Merge conflict -When a merge conflict happens during switching branches with -the `-m` option, you would see something like this: +When the `--merge` (`-m`) option is given and the local changes +overlap with the changes in the branch we're switching to, the +changes are stashed and reapplied after the switch. If this +process results in conflicts, the stash entry is saved and a +message is printed: ------------ $ git checkout -m mytopic -Auto-merging frotz -ERROR: Merge conflict in frotz -fatal: merge program failed ------------- - -At this point, `git diff` shows the changes cleanly merged as in -the previous example, as well as the changes in the conflicted -files. Edit and resolve the conflict and mark it resolved with -`git add` as usual: - ------------- -$ edit frotz -$ git add frotz +Your local changes are stashed, however applying them +resulted in conflicts. You can either resolve the conflicts +and then discard the stash with "git stash drop", or, if you +do not want to resolve them now, run "git reset --hard" and +apply the local changes later by running "git stash pop". ------------ CONFIGURATION
Documentation/git-switch.adoc +20 -16
@@ -123,18 +123,19 @@ variable. `-m`:: `--merge`:: - If you have local modifications to one or more files that are - different between the current branch and the branch to which - you are switching, the command refuses to switch branches in - order to preserve your modifications in context. However, - with this option, a three-way merge between the current - branch, your working tree contents, and the new branch is - done, and you will be on the new branch. -+ -When a merge conflict happens, the index entries for conflicting -paths are left unmerged, and you need to resolve the conflicts -and mark the resolved paths with `git add` (or `git rm` if the merge -should result in deletion of the path). + If you have local modifications to one or more files that + are different between the current branch and the branch to + which you are switching, the command normally refuses to + switch branches in order to preserve your modifications in + context. However, with this option, the conflicting local + changes are automatically stashed before the switch and + reapplied afterwards. If the local changes do not overlap + with the differences between branches, the switch proceeds + without stashing. If reapplying the stash results in + conflicts, the entry is saved to the stash list. Resolve + the conflicts and run `git stash drop` when done, or clear + the working tree (e.g. with `git reset --hard`) before + running `git stash pop` later to re-apply your changes. `--conflict=<style>`:: The same as `--merge` option above, but changes the way the
@@ -217,15 +218,18 @@ $ git switch mytopic error: You have local changes to 'frotz'; not switching branches. ------------ -You can give the `-m` flag to the command, which would try a three-way -merge: +You can give the `-m` flag to the command, which will carry your local +changes to the new branch: ------------ $ git switch -m mytopic -Auto-merging frotz +Applied autostash. +Switched to branch 'mytopic' +The following paths have local changes: +M frotz ------------ -After this three-way merge, the local modifications are _not_ +After the switch, the local modifications are reapplied and are _not_ registered in your index file, so `git diff` would show you what changes you made since the tip of the new branch.
builtin/checkout.c +69 -92
233 lines not on this page · raw message
sequencer.c +9 -5
@@ -4765,15 +4765,19 @@ static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply,strvec_push (&store.args, stash_oid);if (run_command (&store)) ret =error (_ ("cannot store %s" ), stash_oid); +else if (attempt_apply) +fprintf (stderr, +_ ("Your local changes are stashed, however applying them\n" +"resulted in conflicts. You can either resolve the conflicts\n" +"and then discard the stash with \"git stash drop\", or, if you\n" +"do not want to resolve them now, run \"git reset --hard\" and\n" +"apply the local changes later by running \"git stash pop\".\n" ));else fprintf (stderr, -_ ("%s\n" +_ ("Autostash exists; creating a new stash entry.\n" "Your changes are safe in the stash.\n" "You can run \"git stash pop\" or" -" \"git stash drop\" at any time.\n" ), - attempt_apply ? -_ ("Applying autostash resulted in conflicts." ) : -_ ("Autostash exists; creating a new stash entry." )); +" \"git stash drop\" at any time.\n" )); }return ret;
t/t3420-rebase-autostash.sh +10 -6
@@ -61,18 +61,22 @@ create_expected_failure_apply () { First, rewinding head to replay your work on top of it... Applying: second commit Applying: third commit - Applying autostash resulted in conflicts. - Your changes are safe in the stash. - You can run "git stash pop" or "git stash drop" at any time. + Your local changes are stashed, however applying them + resulted in conflicts. You can either resolve the conflicts + and then discard the stash with "git stash drop", or, if you + do not want to resolve them now, run "git reset --hard" and + apply the local changes later by running "git stash pop". EOF } create_expected_failure_merge () { cat >expected <<-EOF $(grep "^Created autostash: [0-9a-f][0-9a-f]*\$" actual) - Applying autostash resulted in conflicts. - Your changes are safe in the stash. - You can run "git stash pop" or "git stash drop" at any time. + Your local changes are stashed, however applying them + resulted in conflicts. You can either resolve the conflicts + and then discard the stash with "git stash drop", or, if you + do not want to resolve them now, run "git reset --hard" and + apply the local changes later by running "git stash pop". Successfully rebased and updated refs/heads/rebased-feature-branch. EOF }
t/t7201-co.sh +70 -1
83 lines not on this page · raw message
t/t7600-merge.sh +2 -1
@@ -914,7 +914,8 @@ test_expect_success 'merge with conflicted --autostash changes' ' git diff >expect && test_when_finished "test_might_fail git stash drop" && git merge --autostash c3 2>err && - test_grep "Applying autostash resulted in conflicts." err && + test_grep "applying them" err && + test_grep "resulted in conflicts" err && git show HEAD:file >merge-result && test_cmp result.1-9 merge-result && git stash show -p >actual &&
xdiff-interface.c +12 -0
@@ -325,6 +325,18 @@ int parse_conflict_style_name(const char *value)return -1 ; } +const char *conflict_style_name (int style) +{ +switch (style) { +case XDL_MERGE_DIFF3 : +return "diff3" ; +case XDL_MERGE_ZEALOUS_DIFF3 : +return "zdiff3" ; +default : +return "merge" ; + } +} +int git_xmerge_style =-1 ; int git_xmerge_config(const char *var, const char *value,
xdiff-interface.h +1 -0
@@ -55,6 +55,7 @@ void xdiff_set_find_func(xdemitconf_t *xecfg, const char *line, int cflags);void xdiff_clear_find_func (xdemitconf_t *xecfg);struct config_context ;int parse_conflict_style_name (const char *value); +const char *conflict_style_name (int style);int git_xmerge_config (const char *var,const char *value,const struct config_context *ctx,void *cb);extern int git_xmerge_style;
Signature · 1 line
gitgitgadget
Re: [PATCH v16 5/5] checkout -m: autostash when switching branches
Hi Harald
builtin/checkout.c static int switch_branches(const struct checkout_opts *opts, · line 1164
Quoted 13 lines · Harald Nordgren via GitGitGadget
From: Harald Nordgren <[email protected]>
if (do_merge) {builtin/checkout.c +9
-1- ret =merge_working_tree (opts, &old_branch_info, new_branch_info, &writeout_error); + ret =merge_working_tree (opts, &old_branch_info, new_branch_info, + opts->merge, &writeout_error); +if (ret ==MERGE_WORKING_TREE_UNPACK_FAILED && opts->merge) { +strbuf_addf (&autostash_msg, +"autostash while switching to '%s'" , + new_branch_info->name); +create_autostash_ref (the_repository, +"CHECKOUT_AUTOSTASH_HEAD" , + autostash_msg.buf, true);
If there are no local changes then doing this is pointless - it means unpack_trees() failed for another reason. Having said that the current code also tries a 3-way merge unconditionally so I think we can happily leave this for the future as #leftoverbits
Quoted 17 lines
builtin/checkout.c +17
-0+ created_autostash =1 ; + ret =merge_working_tree (opts, &old_branch_info, new_branch_info, + false, &writeout_error); + } +if (created_autostash) { +if (opts->conflict_style >=0 ) { +struct strbuf cfg =STRBUF_INIT ; +strbuf_addf (&cfg,"merge.conflictStyle=%s" , +conflict_style_name (opts->conflict_style)); +git_config_push_parameter (cfg.buf); +strbuf_release (&cfg); + } + apply_autostash_ref(the_repository, + "CHECKOUT_AUTOSTASH_HEAD", + new_branch_info->name, + "local", + stash_label_base,
builtin/checkout.c static int switch_branches(const struct checkout_opts *opts, · line 1190
Quoted 10 lines
builtin/checkout.c +5
-1+ autostash_msg.buf); + }if (ret) {branch_info_release (&old_branch_info); -return ret; +strbuf_release (&old_commit_shortname); +strbuf_release (&autostash_msg); +return ret <0 ?1 : ret; } }
If popping the stash created merge conflicts then it would be nice to print a blank line before the message about which branch we've switched to so that it is visually separated from the conflicts advice. That would mean apply_autostash_ref() would have to tell us if there we're conflicts. Again we can happily leave that for the future as #leftoverbits
Thanks
Phillip
Re: [PATCH v16 0/5] checkout: 'autostash' for branch switching
In reply to Harald Nordgren via GitGitGadgetHi Harald
Quoted 8 lines · Harald Nordgren via GitGitGadget
* Updated the git checkout and git switch docs to show the actual output when using -m to carry local changes across a branch switch, and changed "would carry" to "will carry".
* Rewrote the merge-conflict example in both docs to match the new, more concise message printed on autostash conflicts.
* Replaced theshow_unpack_errorsflag andold_commitparameter in merge_working_tree()/init_topts() with a plain quiet boolean, so the caller decides directly whether to suppress unpack errors.
Quoted 8 lines
* Tightened the 'checkout -m with dirty tree' test by replacing a printf with a heredoc.
* Made the 'checkout -m creates a recoverable stash on conflict' test also assert the reflog subject of the new stash entry.
* Replaced twotest_grepcalls in the 'checkout -m which would overwrite untracked file' test with a singletest_cmp, which also catches a regression where the "would be overwritten" message could end up printed twice.
That all sounds good and the range-diff below looks as I would expect it to. I've left some suggestions for possible future work on patch 5 but I think this is ready to be merged as-is.
Thanks for working on it
Phillip
Quoted 246 lines
Harald Nordgren (5):
stash: add --label-ours, --label-theirs, --label-base for apply
sequencer: allowcreate_autostashto run silently
sequencer: teach autostash apply to take optional conflict marker
labels
checkout: rollback lock on early returns inmerge_working_treecheckout -m: autostash when switching branchesDocumentation/git-checkout.adoc | 55 +++++------ Documentation/git-stash.adoc | 11 ++- Documentation/git-switch.adoc | 36 ++++--- builtin/checkout.c | 166 +++++++++++++++----------------- builtin/commit.c | 3 +- builtin/merge.c | 15 ++- builtin/stash.c | 28 ++++-- sequencer.c | 69 +++++++++---- sequencer.h | 7 +- t/t3420-rebase-autostash.sh | 16 +-- t/t3903-stash.sh | 24 +++++ t/t7201-co.sh | 71 +++++++++++++- t/t7600-merge.sh | 3 +- xdiff-interface.c | 12 +++ xdiff-interface.h | 1 + xdiff/xmerge.c | 6 +- 16 files changed, 343 insertions(+), 180 deletions(-)base-commit:
94f057755b7941b321fd11fec1b2e3ca5313a4e0
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2234%2FHaraldNordgren%2Fcheckout_autostash-v16
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2234/HaraldNordgren/checkout_autostash-v16
Pull-Request: https://github.com/git/git/pull/2234Range-diff vs v15:
1: aba8e6a9dc = 1: aba8e6a9dc stash: add --label-ours, --label-theirs, --label-base for apply 2: 89e0bfa803 = 2: 89e0bfa803 sequencer: allow create_autostash to run silently 3: a428ce7328 = 3: a428ce7328 sequencer: teach autostash apply to take optional conflict marker labels 4: f358424085 = 4: f358424085 checkout: rollback lock on early returns in merge_working_tree 5: 96b14db827 ! 5: 07d25fda91 checkout -m: autostash when switching branches@@
Documentation/git-checkout.adoc: $ git checkout mytopic-You can give the
-mflag to the command, which would try a -three-way merge:
-+You can give the-mflag to the command, which would carry your local ++You can give the-mflag to the command, which will carry your local
+changes to the new branch:------------
$ git checkout -m mytopic
-Auto-merging frotz
++Applied autostash.
+Switched to branch 'mytopic'
++The following paths have local changes:
++M frotz
-------------After this three-way merge, the local modifications are _not_
@@Documentation/git-checkout.adoc: $ git checkout mytopic-When a merge conflict happens during switching branches with -the
-moption, you would see something like this:
-+When the--merge(-m) option is in effect and the locally -+modified files overlap with files that need to be updated by the -+branch switch, the changes are stashed and reapplied after the -+switch. If this process results in conflicts, a stash entry is saved -+and made available ingit stash list:
++When the--merge(-m) option is given and the local changes ++overlap with the changes in the branch we're switching to, the ++changes are stashed and reapplied after the switch. If this ++process results in conflicts, the stash entry is saved and a ++message is printed:------------
$ git checkout -m mytopic
@@Documentation/git-checkout.adoc: $ git checkout mytopic
-ERROR: Merge conflict in frotz
-fatal: merge program failed
-------------
-+Your local changes are stashed, however, applying it to carry -+forward your local changes resulted in conflicts:
-
+-
-At this point,git diffshows the changes cleanly merged as in -the previous example, as well as the changes in the conflicted -files. Edit and resolve the conflict and mark it resolved with -git addas usual:
-+ - You can try resolving them now. If you resolved them
-+ successfully, discard the stash entry with "git stash drop".
-
-+ - Alternatively you can "git reset --hard" if you do not want
-+ to deal with them right now, and later "git stash pop" to
-+ recover your local changes.
- ------------
+-
+-------------
-$ edit frotz
-$ git add frotz
--------------
-+
-+You can try resolving the conflicts now. Edit the conflicting files -+and mark them resolved withgit addas usual, then rungit stash -+dropto discard the stash entry. Alternatively, you can clear the -+working tree withgit reset --hardand recover your local changes -+later withgit stash pop.
++Your local changes are stashed, however applying them ++resulted in conflicts. You can either resolve the conflicts ++and then discard the stash with "git stash drop", or, if you ++do not want to resolve them now, run "git reset --hard" and ++apply the local changes later by running "git stash pop".
+ ------------CONFIGURATION
- -------------##
Documentation/git-switch.adoc##
@@Documentation/git-switch.adoc: variable.
@@Documentation/git-switch.adoc: $ git switch mytopic-You can give the
-mflag to the command, which would try a three-way -merge:
-+You can give the-mflag to the command, which would carry your local ++You can give the-mflag to the command, which will carry your local
+changes to the new branch:------------
$ git switch -m mytopic
-Auto-merging frotz
++Applied autostash.
+Switched to branch 'mytopic'
++The following paths have local changes:
++M frotz
-------------After this three-way merge, the local modifications are _not_
@@builtin/checkout.c: structcheckout_opts{
char *name; /* The short name used */
char *path; /* The full name of a real branch */
@@builtin/checkout.c: static voidsetup_branch_path(struct branch_info *branch)
+branch->path=strbuf_detach(&buf, NULL);
+ }- static void init_topts(struct
unpack_trees_options*topts, int merge, +-static void init_topts(structunpack_trees_options*topts, int merge, ++static void init_topts(structunpack_trees_options*topts,
intshow_progress, intoverwrite_ignore,
- struct commit*old_commit)
-+ struct commit*old_commit, boolshow_unpack_errors)
++ bool quiet)
{
memset(topts, 0, sizeof(*topts));
topts->head_idx= -1;
@@builtin/checkout.c: static void init_topts(structunpack_trees_options*topts, i
topts->update= 1;
topts->merge= 1;
-topts->quiet= merge &&old_commit;
-+topts->quiet= merge &&old_commit&& !show_unpack_errors;
++topts->quiet= quiet;
topts->verbose_update=show_progress;
topts->fn=twoway_merge;
topts->preserve_ignored= !overwrite_ignore;
@@builtin/checkout.c: static void init_topts(structunpack_trees_options*topts, i
static int merge_working_tree(const structcheckout_opts*opts,
structbranch_info*old_branch_info, structbranch_info*new_branch_info,
-- int*writeout_error)
-+ int*writeout_error,
-+ boolshow_unpack_errors)
++ bool quiet,
+ int*writeout_error)
{
int ret;
- structlock_filelock_file=LOCK_INIT;
@@builtin/checkout.c: static int merge_working_tree(const structcheckout_opts*opts,
+ }/* 2-way merge to the new branch */
- init_topts(&topts,opts->merge,opts->show_progress,
+- init_topts(&topts,opts->merge,opts->show_progress,
-opts->overwrite_ignore,old_branch_info->commit);
-+opts->overwrite_ignore,old_branch_info->commit,
-+show_unpack_errors);
++ init_topts(&topts,opts->show_progress,
++opts->overwrite_ignore, quiet);
init_checkout_metadata(&topts.meta,new_branch_info->refname,
new_branch_info->commit?&new_branch_info->commit->object.oid:
@@builtin/checkout.c: static int switch_branches(const structcheckout_opts*opts,
if (do_merge) {
- ret =merge_working_tree(opts, &old_branch_info, new_branch_info, &writeout_error);
+ ret = merge_working_tree(opts,&old_branch_info,new_branch_info,
-+&writeout_error, false);
++opts->merge,&writeout_error);
+ if (ret ==MERGE_WORKING_TREE_UNPACK_FAILED&&opts->merge) {
+ strbuf_addf(&autostash_msg,
+ "autostash while switching to '%s'",
@@builtin/checkout.c: static int switch_branches(const structcheckout_opts*opts,
+ autostash_msg.buf, true);
+created_autostash= 1;
+ ret = merge_working_tree(opts,&old_branch_info,new_branch_info,
-+&writeout_error, true);
++ false,&writeout_error);
+ }
+ if (created_autostash) {
+ if (opts->conflict_style>= 0) {
@@t/t7201-co.sh:test_expect_success'checkout -m with dirty tree' '
test "$(git symbolic-ref HEAD)" = "refs/heads/side" &&- printf "M\t%s\n" one >expect.messages &&
-+ printf "The following paths have local changes:\nM\t%s\n" one >expect.messages &&
++ cat >expect.messages <<-\EOF &&
++ The following paths have local changes:
++ M one
++ EOF
test_cmpexpect.messages messages &&fill "M one" "A three" "D two" >expect.main &&
@@t/t7201-co.sh:test_expect_success'checkout --merge --conflict=diff3 <branch>'
+test_grep"git stash drop" actual &&
+test_grep"git stash pop" actual &&
+test_grep"The following paths have local changes" actual &&
-+ git show --format=%B --diff-merges=1 refs/stash >actual &&
++ git log -p -1 --format="%gs%n%B" -g --diff-merges=1 refs/stash >actual &&
+ sed /^index/d actual >actual.trimmed &&
+ cat >expect <<-EOF&&
++ autostash while switching to ${SQ}side${SQ}
+ On main: autostash while switching to ${SQ}side${SQ}
+
+ diff --git a/one b/one
@@t/t7201-co.sh:test_expect_success'checkout --merge --conflict=diff3 <branch>'
+ >another-file.t &&
+ fill 1 2 3 4 5 >one &&
+test_must_failgit checkout -m @{-1} 2>err &&
-+test_grep"would be overwritten by checkout" err &&
-+test_grep"another-file.t" err
++q_to_tab>expect <<-\EOF &&
++ error: The following untracked working tree files would be overwritten by checkout:
++ Qanother-file.t
++ Please move or remove them before you switch branches.
++ Aborting
++ Applied autostash.
++ EOF
++test_cmpexpect err
+'
+
test_expect_success'switch to another branch while carrying a deletion' '
PATCH 1/1 checkout: add --autostash option for branch switching
Quoted 5 lines
That all sounds good and the range-diff below looks as I would expect it to. I've left some suggestions for possible future work on patch 5 but I think this is ready to be merged as-is.
Thanks for working on it
Thank you too!
Harald
v16 cover checkout: 'autostash' for branch switching
In reply to Harald Nordgren via GitGitGadgetIs this ready to be merged?
Harald
Re: [PATCH v16 0/5] checkout: 'autostash' for branch switching
Hi Harald
Quoted 1 line · Harald Nordgren
Is this ready to be merged?
You can see the current status by reading the latest what's cooking email from Junio. I don't think he's officially back online until next week so I wouldn't be surprised if this hasn't been merged yet. You can always check
git log --merges --grep hn/ origin/next
to see which of your topics are in next.
Thanks
Phillip