All of lore.kernel.org
 help / color / mirror / Atom feed
From: Phillip Wood <phillip.wood123@gmail.com>
To: Harald Nordgren via GitGitGadget <gitgitgadget@gmail.com>,
	git@vger.kernel.org
Cc: Harald Nordgren <haraldnordgren@gmail.com>
Subject: Re: [PATCH v3] checkout: separate autostash conflict advice from branch-switch message
Date: Tue, 1 Sep 2026 14:42:19 +0100	[thread overview]
Message-ID: <7dbebce0-1814-4b0b-8167-6a464d893612@gmail.com> (raw)
In-Reply-To: <pull.2364.v3.git.git.1788256199679.gitgitgadget@gmail.com>

Hi Harald

On 01/09/2026 10:49, Harald Nordgren via GitGitGadget wrote:
> From: Harald Nordgren <haraldnordgren@gmail.com>
> 
> "git checkout -m" stashes the user's local changes when it cannot
> perform the checkout, and then applies the stash.  When applying the
> stash results in conflicts, the advice on how to deal with them is
> printed directly on top of the branch-switch message ("Switched to
> branch ..."), making the two hard to tell apart.  Print a blank line
> in between so that the advice and the branch-switch message are
> visually distinct.
> 
> To make this possible, "git stash apply", "pop" and "branch" now exit
> with status 2 when applying the stash entry resulted in conflicts, in
> which case the stash entry is left in place; other failures exit with
> status 1, as before.  The exit statuses are documented in the "git
> stash" documentation.

Other commands such as merge-tree and merge strategies use 1 to indicate 
conflicts and another non-zero exit code for errors. That matches the 
way grep and diff use the exit code to distinguish differences from 
errors. It is confusing if we start using a different convention here. 
I've left a few comments below, but the exit code is my main concern. It 
would be nice to separate out the stash changes into a separate commit 
as well.

> diff --git a/Documentation/git-stash.adoc b/Documentation/git-stash.adoc
> index 50bb89f483..3e41ffcf43 100644
> --- a/Documentation/git-stash.adoc
> +++ b/Documentation/git-stash.adoc
> @@ -426,6 +426,15 @@ include::includes/cmd-config-section-all.adoc[]
>   :git-stash: 1
>   include::config/stash.adoc[]
>   
> +EXIT STATUS
> +-----------
> +
> +The `git stash` subcommands exit with status 0 on success and non-zero
> +on failure.  The subcommands that apply a stash entry, i.e. `apply`,
> +`pop` and `branch`, exit with status 2 when applying the stash entry
> +resulted in conflicts, in which case the stash entry is left in place.
> +Other failures exit with status 1 (usage errors exit with status 129).

Thanks for documenting this, I think we'd be better to avoid giving 
specific exit codes for errors and say "a non-zero exit code other than 
1" unless we have a good way of enforcing that.

> diff --git a/builtin/stash.c b/builtin/stash.c
> index 72c52571f8..86c7ac4ffa 100644
> --- a/builtin/stash.c
> +++ b/builtin/stash.c
> @@ -10,6 +10,7 @@
>   #include "object-name.h"
>   #include "parse-options.h"
>   #include "refs.h"
> +#include "stash.h"
>   #include "lockfile.h"
>   #include "cache-tree.h"
>   #include "unpack-trees.h"
> @@ -640,8 +641,9 @@ 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,
> -			  int index, int quiet,
> +static enum stash_apply_result do_apply_stash(const char *prefix,
> +					      struct stash_info *info,
> +					      int index, int quiet,
>   			  const char *label_ours, const char *label_theirs,
>   			  const char *label_base)

The indentation is strange here

>   {
> @@ -716,11 +718,12 @@ static int do_apply_stash(const char *prefix, struct stash_info *info,
>   	clean = merge_ort_nonrecursive(&o, head, merge, merge_base);
>   
>   	/*
> -	 * If 'clean' >= 0, reverse the value for 'ret' so 'ret' is 0 when the
> -	 * merge was clean, and nonzero if the merge was unclean or encountered
> -	 * an error.
> +	 * Translate the value of 'clean' so 'ret' is STASH_APPLY_CLEAN
> +	 * when the merge was clean, STASH_APPLY_CONFLICT when it was
> +	 * unclean, and a negative value if it encountered an error.
>   	 */
> -	ret = clean >= 0 ? !clean : clean;
> +	ret = clean >= 0 ? (clean ? STASH_APPLY_CLEAN : STASH_APPLY_CONFLICT)
> +			 : clean;

Nested ternary operators are not particularly readable, if we stick with 
an exit code of 1 for conflicts the original code does not need to be 
modified.

>   
>   	if (ret < 0)
>   		rollback_lock_file(&lock);
> @@ -739,7 +742,7 @@ static int do_apply_stash(const char *prefix, struct stash_info *info,
>   
>   	if (has_index) {
>   		if (reset_tree(&index_tree, 0, 0))
> -			ret = -1;
> +			ret = STASH_APPLY_ERROR;

This seems a bit pointless when we're still returning -1 implicitly 
everywhere else where we have "return error(...).


>   	} else {
>   		unstage_changes_unless_new(&c_tree);
>   	}
> @@ -2492,9 +2495,13 @@ int cmd_stash(int argc,
>   	strbuf_addf(&stash_index_path, "%s.stash.%" PRIuMAX, index_file,
>   		    (uintmax_t)pid);
>   
> -	if (fn)
> -		return !!fn(argc, argv, prefix, repo);
> -	else if (!argc)
> +	if (fn) {
> +		ret = fn(argc, argv, prefix, repo);
> +
> +		if (ret < 0)
> +			return 1;
> +		return ret;

Looking at the callers of do_apply_stash(), apply_stash() returns the 
result of do_apply_stash(), pop_stash() and branch_stash() return the 
result of do_drop_stash() if do_apply_stash() returns 0. do_drop_stash() 
always returns 0 so we're safe, but that analysis should be in the 
commit message.


> @@ -4832,13 +4837,15 @@ 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)
> +		else if (attempt_apply && ret == STASH_APPLY_CONFLICT)
>   			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"));

We only print this if we know there were conflicts - good.

> +		else if (attempt_apply)
> +			ret = error(_("could not apply autostash"));

We know we've saved the stash so we should tell the user that we have, 
rather than leaving when wondering what's happened to their stashed changes.

The rest of the changes in this file look good.

> diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh
> index da27a6599a..93e8e98216 100755
> --- a/t/t3903-stash.sh
> +++ b/t/t3903-stash.sh
> @@ -1797,7 +1797,7 @@ test_expect_success 'apply with custom conflict labels' '
>   	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_expect_code 2 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
> @@ -1809,11 +1809,33 @@ test_expect_success 'apply with empty conflict labels' '
>   	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_expect_code 2 git stash apply --label-ours= --label-theirs= &&
>   	test_grep "^<<<<<<<$" conflict-file &&
>   	test_grep "^>>>>>>>$" conflict-file
>   '
>   
> +test_expect_success 'apply exits 2 on conflicts and keeps the stash entry' '

Aren't we testing that above?

> +	git reset --hard initial &&
> +	test_commit exit-code-base conflict-file base-content &&

We've just reset to a known starting point that has paths file and 
other-file, so why do we need to create a new commit in order to stash 
something?

> +	echo stashed >conflict-file &&
> +	git stash push -m stashed &&
> +	test_commit exit-code-upstream conflict-file upstream-content &&
> +	test_expect_code 2 git stash apply &&
> +	git stash list >list &&
> +	test_grep stashed list
> +'
> +
> +test_expect_success 'pop exits 2 on conflicts and keeps the stash entry' '

This is good, we should be checking "stash branch" as well.

> diff --git a/t/t7201-co.sh b/t/t7201-co.sh
> index 0ddd1ad7aa..9ea9462914 100755
> --- a/t/t7201-co.sh
> +++ b/t/t7201-co.sh
> @@ -236,10 +236,18 @@ test_expect_success 'checkout -m creates a recoverable stash on conflict' '
>   	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 &&
> +	cat >expect <<-EOF &&
> +	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".
> +
> +	Switched to branch ${SQ}side${SQ}
> +	The following paths have local changes:
> +	M	one
> +	EOF
> +	test_cmp expect actual &&

Nice, it is much easier to see what we're checking now

Thanks

Phillip


  reply	other threads:[~2026-09-01 13:42 UTC|newest]

Thread overview: 37+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-25 15:34 [PATCH 0/2] checkout -m: refine autostash fallback Harald Nordgren via GitGitGadget
2026-07-25 15:34 ` [PATCH 1/2] sequencer: teach autostash apply to report conflicts Harald Nordgren via GitGitGadget
2026-08-27 13:01   ` Phillip Wood
2026-08-31 10:18     ` Harald Nordgren
2026-08-31 13:15       ` phillip.wood123
2026-07-25 15:34 ` [PATCH 2/2] checkout -m: refine autostash fallback Harald Nordgren via GitGitGadget
2026-07-28 22:49   ` Junio C Hamano
2026-08-27 13:05   ` Phillip Wood
2026-08-26 19:14 ` [PATCH 0/2] " Junio C Hamano
2026-08-27 13:12   ` Phillip Wood
2026-08-31 12:00 ` [PATCH v2] checkout: print blank line after autostash conflict advice Harald Nordgren via GitGitGadget
2026-08-31 17:19   ` Junio C Hamano
2026-09-01  9:31     ` Phillip Wood
2026-09-01 13:50       ` Junio C Hamano
2026-09-01  9:49 ` [PATCH v3] checkout: separate autostash conflict advice from branch-switch message Harald Nordgren via GitGitGadget
2026-09-01 13:42   ` Phillip Wood [this message]
2026-09-01 17:31     ` Junio C Hamano
2026-09-02 18:29 ` [PATCH v4 0/2] checkout -m: refine autostash fallback Harald Nordgren via GitGitGadget
2026-09-02 18:29   ` [PATCH v4 1/2] stash: reserve exit status 1 for conflicts Harald Nordgren via GitGitGadget
2026-09-02 19:51     ` Junio C Hamano
2026-09-02 20:08       ` Junio C Hamano
2026-09-03 13:57       ` Phillip Wood
2026-09-03 14:45         ` Harald Nordgren
2026-09-03 18:42           ` Junio C Hamano
2026-09-03 19:09             ` Harald Nordgren
2026-09-03 19:45               ` Junio C Hamano
2026-09-04  8:16                 ` Harald Nordgren
2026-09-04 15:09                   ` Phillip Wood
2026-09-04 16:42                     ` Harald Nordgren
2026-09-04 15:21                   ` Junio C Hamano
2026-09-02 18:29   ` [PATCH v4 2/2] checkout: separate autostash conflict advice from branch-switch message Harald Nordgren via GitGitGadget
2026-09-02 19:52     ` Junio C Hamano
2026-09-03 14:00   ` [PATCH v4 0/2] checkout -m: refine autostash fallback Phillip Wood
2026-09-03 14:39 ` [PATCH v5 " Harald Nordgren via GitGitGadget
2026-09-03 14:39   ` [PATCH v5 1/2] stash: reserve exit status 1 for conflicts Harald Nordgren via GitGitGadget
2026-09-03 14:39   ` [PATCH v5 2/2] checkout: separate autostash conflict advice from branch-switch message Harald Nordgren via GitGitGadget
2026-09-03 18:53   ` [PATCH v5 0/2] checkout -m: refine autostash fallback Junio C Hamano

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=7dbebce0-1814-4b0b-8167-6a464d893612@gmail.com \
    --to=phillip.wood123@gmail.com \
    --cc=git@vger.kernel.org \
    --cc=gitgitgadget@gmail.com \
    --cc=haraldnordgren@gmail.com \
    --cc=phillip.wood@dunelm.org.uk \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.