Skip to contentSkip to conversation

checkout: add --autostash option for branch switching

PATCH Superseded Harald Nordgren via GitGitGadget 6 participants 164 messages Mar 12 – May 8 Page 2 of 2

Subsystems (2)
lore
Share & tools
lore
#112 lore raw
In reply to Phillip Wood
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 to unpack_trees() failed. Even then if you look at the list of errors in unpack-trees.h you'll see that only a few of them relate to problems that can be solved by stashing. The old code just tried merging whenever unpack_trees() failed so it probably not so bad to do the same here but we should not be stashing if merge_working_tree() returns before calling unpack_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() in merge_working_tree() to succeed and therefore return 0? If opts->merge is false then we should not be trying to apply the stash when merge_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

#113 lore raw
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 to unpack_trees() failed. Even then if you look at the list of errors in unpack-trees.h you'll see that only a few of them relate to problems that can be solved by stashing. The old code just tried merging whenever unpack_trees() failed so it probably not so bad to do the same here but we should not be stashing if merge_working_tree() returns before calling unpack_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() in merge_working_tree() to succeed and therefore return 0? If opts->merge is false then we should not be trying to apply the stash when merge_working_tree() fails.

Quoted 3 lines

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?

Yes, exactly

Thanks

Phillip

#114 lore raw

Re: [PATCH v12 0/4] checkout: 'autostash' for branch switching

In reply to Harald Nordgren via GitGitGadget
Quoted 7 lines · Harald Nordgren via GitGitGadget

Harald Nordgren (4):
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: -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
index 2adfefa085..a7a93e1c6b 100644
@@ -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
index febdb5b20a..b78a8ff092 100644
@@ -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
index 67de755053..b0c891d3b6 100644
@@ -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/2234

Range-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: 911e520431 checkout: -m (--merge) uses autostash when switching branches
@@ builtin/checkout.c: 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);
+ }
@@ builtin/checkout.c: static int switch_branches(const struct checkout_opts *opts,
}

## sequencer.c ##
-@@ sequencer.c: 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 *message,
- bool silent)
- {
- struct strbuf buf = STRBUF_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,
-
- void create_autostash(struct repository *r, const char *path)
- {
-- 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, false);
-+ 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, 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, 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 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))
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 int apply_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: int apply_autostash(const char *path)
-
- int apply_autostash_oid(const char *stash_oid)
- {
-- 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,
- 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 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, -
- int save_autostash_ref(struct repository *r, const char *refname)
- {
-- 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, 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 ##
-@@ 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_silent_with_msg(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);
-@@ sequencer.h: 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 *label_base,
-+ const char *stash_msg);
-
- #define SUMMARY_INITIAL_COMMIT (1 << 0)
- #define SUMMARY_SHOW_AUTHOR_DATE (1 << 1)

## t/t3420-rebase-autostash.sh ##
@@ t/t3420-rebase-autostash.sh: create_expected_failure_apply () {

#115 lore raw
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

#116 lore raw
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.

#117 lore raw
In reply to Junio C Hamano
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

#118 lore raw
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.

#119 lore raw
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

#120 lore raw

v13 cover checkout: 'autostash' for branch switching

In reply to Harald Nordgren via GitGitGadget

Handling 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
#121 lore raw

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
index b05c990ecd..50bb89f483 100644
@@ -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
index 0d27b2fb1f..32dbc97b47 100644
@@ -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
index 70879941c2..340854bc0a 100755
@@ -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
index 29dad98c49..659ad4ec97 100644
@@ -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
#122 lore raw

PATCH v13 2/5 sequencer: allow create_autostash to run silently

In reply to Harald Nordgren via GitGitGadget

From: 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
index 2cbce56f8d..3ebe190ef1 100644
@@ -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
index b7d8dca47f..ff5258f481 100644
@@ -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
index a6fa670c7c..02d2d9db06 100644
@@ -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
#123 lore raw

PATCH v13 3/5 sequencer: teach autostash apply to take optional conflict marker labels

In reply to Harald Nordgren via GitGitGadget

From: 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
index a3e52ac9ca..28f6174503 100644
@@ -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
index 3ebe190ef1..aacf8c524e 100644
@@ -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
index ff5258f481..7c0376d9e4 100644
@@ -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
index 02d2d9db06..3164bd437d 100644
@@ -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
#124 lore raw

PATCH v13 4/5 checkout: rollback lock on early returns in merge_working_tree

In reply to Harald Nordgren via GitGitGadget

From: 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
index e031e61886..c80c62b37b 100644
@@ -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
#125 lore raw

PATCH v13 5/5 checkout -m: autostash when switching branches

In reply to 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.

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
index 43ccf47cf6..70dd211ee3 100644
@@ -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
index 87707e9265..ee58a4d0fd 100644
@@ -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
index 7c0376d9e4..480e8e6c0b 100644
@@ -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
index ad3ba6a984..e4e2cb19ce 100755
@@ -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
index 9838094b66..cbef8a534e 100755
@@ -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
index f043330f2a..5ee2b96d0a 100644
@@ -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
index fbc4ceec40..ce54e1c0e0 100644
@@ -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
#126 lore raw

v14 cover checkout: 'autostash' for branch switching

In reply to Harald Nordgren via GitGitGadget

Simplifying 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 branches
Signature · 1 line
gitgitgadget
#127 lore raw

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
index b05c990ecd..50bb89f483 100644
@@ -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
index 0d27b2fb1f..32dbc97b47 100644
@@ -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
index 70879941c2..bdaad22e1f 100755
@@ -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
index 29dad98c49..659ad4ec97 100644
@@ -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
#128 lore raw

PATCH v14 2/5 sequencer: allow create_autostash to run silently

In reply to Harald Nordgren via GitGitGadget

From: 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
index 2cbce56f8d..3ebe190ef1 100644
@@ -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
index b7d8dca47f..ff5258f481 100644
@@ -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
index a6fa670c7c..02d2d9db06 100644
@@ -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
#129 lore raw

PATCH v14 3/5 sequencer: teach autostash apply to take optional conflict marker labels

In reply to Harald Nordgren via GitGitGadget

From: 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
index a3e52ac9ca..28f6174503 100644
@@ -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
index 3ebe190ef1..aacf8c524e 100644
@@ -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
index ff5258f481..7c0376d9e4 100644
@@ -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
index 02d2d9db06..3164bd437d 100644
@@ -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
#130 lore raw

PATCH v14 4/5 checkout: rollback lock on early returns in merge_working_tree

In reply to Harald Nordgren via GitGitGadget

From: 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
index e031e61886..c80c62b37b 100644
@@ -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
#131 lore raw

PATCH v14 5/5 checkout -m: autostash when switching branches

In reply to 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.

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
index 43ccf47cf6..70dd211ee3 100644
@@ -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
index 87707e9265..ee58a4d0fd 100644
@@ -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
index 7c0376d9e4..480e8e6c0b 100644
@@ -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
index ad3ba6a984..e4e2cb19ce 100755
@@ -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
index 9838094b66..cbef8a534e 100755
@@ -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
index f043330f2a..5ee2b96d0a 100644
@@ -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
index fbc4ceec40..ce54e1c0e0 100644
@@ -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
#132 lore raw

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
index c80c62b37b..55c4db04c6 100644
@@ -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 -1
  			branch_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
index 7c0376d9e4..480e8e6c0b 100644

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 -5
  		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
index ad3ba6a984..e4e2cb19ce 100755
@@ -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
index 9bcf7c0b40..c474c6759f 100755

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 -0
  test_expect_success 'switch to another branch while carrying a deletion' '
  	git checkout -f main &&
  	git reset --hard &&
t/t7600-merge.sh +1 -1
index 9838094b66..cbef8a534e 100755
@@ -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
index f043330f2a..5ee2b96d0a 100644
@@ -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
index fbc4ceec40..ce54e1c0e0 100644
@@ -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;
#133 lore raw

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 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 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 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)

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_TRACE to 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_opts the second time we call merge_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

#135 lore raw
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

#137 lore raw

Re: [PATCH v14 0/5] checkout: 'autostash' for branch switching

In reply to Harald Nordgren via GitGitGadget

Hi Harald

Quoted 2 lines · Harald Nordgren via GitGitGadget

Simplifying the tests in t/t3903-stash.sh according 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: 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 branches
#138 lore raw

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
#139 lore raw

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
index b05c990ecd..50bb89f483 100644
@@ -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
index 0d27b2fb1f..32dbc97b47 100644
@@ -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
index 70879941c2..bdaad22e1f 100755
@@ -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
index 29dad98c49..659ad4ec97 100644
@@ -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
#140 lore raw

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
index 70879941c2..bdaad22e1f 100755
@@ -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
index 29dad98c49..659ad4ec97 100644
@@ -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)
#141 lore raw

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

#142 lore raw

PATCH v15 2/5 sequencer: allow create_autostash to run silently

In reply to Harald Nordgren via GitGitGadget

From: 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
index 2cbce56f8d..3ebe190ef1 100644
@@ -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
index b7d8dca47f..ff5258f481 100644
@@ -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
index a6fa670c7c..02d2d9db06 100644
@@ -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
#143 lore raw

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_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
index 2cbce56f8d..3ebe190ef1 100644
@@ -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
index b7d8dca47f..ff5258f481 100644
@@ -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
index a6fa670c7c..02d2d9db06 100644
@@ -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);
#144 lore raw

PATCH v15 3/5 sequencer: teach autostash apply to take optional conflict marker labels

In reply to Harald Nordgren via GitGitGadget

From: 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
index a3e52ac9ca..28f6174503 100644
@@ -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
index 3ebe190ef1..aacf8c524e 100644
@@ -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
index ff5258f481..7c0376d9e4 100644
@@ -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
index 02d2d9db06..3164bd437d 100644
@@ -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
#145 lore raw

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, 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".

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

#146 lore raw

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 modifying store_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

#147 lore raw

PATCH v15 4/5 checkout: rollback lock on early returns in merge_working_tree

In reply to Harald Nordgren via GitGitGadget

From: 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
index e031e61886..c80c62b37b 100644
@@ -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
#148 lore raw

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 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.

Thanks for splitting this out, it looks good

Phillip

Quoted 97 lines
---
  builtin/checkout.c | 29 ++++++++++++++++++++++-------
  1 file changed, 22 insertions(+), 7 deletions(-)
builtin/checkout.c +22 -7
index e031e61886..c80c62b37b 100644
@@ -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;
+			}
  		}
  	}
 
#149 lore raw

PATCH v15 5/5 checkout -m: autostash when switching branches

In reply to 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.

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
index 43ccf47cf6..70dd211ee3 100644
@@ -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
index 87707e9265..ee58a4d0fd 100644
@@ -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
index 7c0376d9e4..746f85a442 100644
@@ -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
index ad3ba6a984..f0bbc476ff 100755
@@ -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
index 9838094b66..f877d9a433 100755
@@ -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
index f043330f2a..5ee2b96d0a 100644
@@ -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
index fbc4ceec40..ce54e1c0e0 100644
@@ -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
#150 lore raw

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
---
  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
index 43ccf47cf6..70dd211ee3 100644
@@ -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 diff would 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 mytopic
Documentation/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
index 87707e9265..ee58a4d0fd 100644
@@ -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 -1
  	topts->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
index 7c0376d9e4..746f85a442 100644
@@ -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
index 9bcf7c0b40..b3293ead8d 100755
@@ -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 -0
  	test_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
index 9838094b66..f877d9a433 100755
@@ -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
index f043330f2a..5ee2b96d0a 100644
@@ -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
index fbc4ceec40..ce54e1c0e0 100644
@@ -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;
#151 lore raw

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 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"

Excellent point. I went with test_cmp since it's multi-line output and "test_line_count = 1" seemed to not work then.

Harald

#152 lore raw

Re: [PATCH v15 0/5] checkout: 'autostash' for branch switching

In reply to Harald Nordgren via GitGitGadget

Hi 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: 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 &&
#153 lore raw

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
#154 lore raw

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
index b05c990ecd..50bb89f483 100644
@@ -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
index 0d27b2fb1f..32dbc97b47 100644
@@ -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
index 70879941c2..bdaad22e1f 100755
@@ -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
index 29dad98c49..659ad4ec97 100644
@@ -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
#155 lore raw

PATCH v16 2/5 sequencer: allow create_autostash to run silently

In reply to Harald Nordgren via GitGitGadget

From: 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
index 2cbce56f8d..3ebe190ef1 100644
@@ -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
index b7d8dca47f..ff5258f481 100644
@@ -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
index a6fa670c7c..02d2d9db06 100644
@@ -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
#156 lore raw

PATCH v16 3/5 sequencer: teach autostash apply to take optional conflict marker labels

In reply to Harald Nordgren via GitGitGadget

From: 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
index a3e52ac9ca..28f6174503 100644
@@ -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
index 3ebe190ef1..aacf8c524e 100644
@@ -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
index ff5258f481..7c0376d9e4 100644
@@ -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
index 02d2d9db06..3164bd437d 100644
@@ -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
#157 lore raw

PATCH v16 4/5 checkout: rollback lock on early returns in merge_working_tree

In reply to Harald Nordgren via GitGitGadget

From: 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
index e031e61886..c80c62b37b 100644
@@ -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
#158 lore raw

PATCH v16 5/5 checkout -m: autostash when switching branches

In reply to 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.

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
index 43ccf47cf6..a8b3b8c2e2 100644
@@ -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
index 87707e9265..d6c4f229a5 100644
@@ -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
index 7c0376d9e4..746f85a442 100644
@@ -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
index ad3ba6a984..f0bbc476ff 100755
@@ -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
index 9838094b66..f877d9a433 100755
@@ -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
index f043330f2a..5ee2b96d0a 100644
@@ -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
index fbc4ceec40..ce54e1c0e0 100644
@@ -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
#159 lore raw

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

#160 lore raw

Re: [PATCH v16 0/5] checkout: 'autostash' for branch switching

In reply to Harald Nordgren via GitGitGadget

Hi 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 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.

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 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.

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: 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' '

#161 lore raw

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

#163 lore raw

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