All of lore.kernel.org
 help / color / mirror / Atom feed
From: "Harald Nordgren via GitGitGadget" <gitgitgadget@gmail.com>
To: git@vger.kernel.org
Cc: Phillip Wood <phillip.wood123@gmail.com>,
	Harald Nordgren <haraldnordgren@gmail.com>
Subject: [PATCH v4 0/2] checkout -m: refine autostash fallback
Date: Wed, 02 Sep 2026 18:29:01 +0000	[thread overview]
Message-ID: <pull.2364.v4.git.git.1788373743.gitgitgadget@gmail.com> (raw)
In-Reply-To: <pull.2364.git.git.1784993669.gitgitgadget@gmail.com>

Avoiding checkout -m autostash retries when no tracked local changes exist
and visually separating autostash conflict advice from the subsequent
branch-switch message. Addresses #leftoverbits from here:
https://lore.kernel.org/git/cfd09dbf-8d77-4464-8030-3a0ffb4aeae7@gmail.com/

Changes in v4:

 * Conflicts now exit with status 1 like merge-tree, other failures exit 128
   so exit 1 unambiguously means conflicts. Stash changes split into their
   own commit.
 * The autostash apply helpers use the return value (enum
   stash_apply_result) instead of an out-parameter, and only claim conflicts
   when git stash apply actually reported them.

Changes in v3:

 * Use enum for git stash return values, to separate conflict from generic
   error.

Changes in v2:

 * Simplify logic and combine to one commit.
 * Test full output with test_cmp.

Harald Nordgren (2):
  stash: reserve exit status 1 for conflicts
  checkout: separate autostash conflict advice from branch-switch
    message

 Documentation/git-stash.adoc |  9 +++++
 builtin/checkout.c           | 15 ++++----
 builtin/stash.c              | 32 ++++++++++++-----
 sequencer.c                  | 66 ++++++++++++++++++++++--------------
 sequencer.h                  | 19 +++++++----
 stash.h                      | 21 ++++++++++++
 t/t3903-stash.sh             | 25 ++++++++++++--
 t/t7201-co.sh                | 16 ++++++---
 8 files changed, 149 insertions(+), 54 deletions(-)
 create mode 100644 stash.h


base-commit: 1630431f326e15fcde608827b5ff38422528eb59
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2364%2FHaraldNordgren%2Fhn%2Fgit-checkout-m-leftoverbits-v4
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2364/HaraldNordgren/hn/git-checkout-m-leftoverbits-v4
Pull-Request: https://github.com/git/git/pull/2364

Range-diff vs v3:

 1:  8e1979dd6c ! 1:  ff43221802 checkout: separate autostash conflict advice from branch-switch message
     @@ Metadata
      Author: Harald Nordgren <haraldnordgren@gmail.com>
      
       ## Commit message ##
     -    checkout: separate autostash conflict advice from branch-switch message
     +    stash: reserve exit status 1 for conflicts
      
     -    "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.
     +    "git stash apply", "pop" and "branch" exit with status 1 both when
     +    applying the stash entry resulted in conflicts and when they fail for
     +    other reasons, so callers cannot tell the two apart.
      
     -    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.
     +    Follow the convention of "git merge-tree" and the merge strategies,
     +    which exit with status 1 to indicate conflicts and with a different
     +    non-zero status for errors: those subcommands now exit with status 1
     +    only when applying the stash entry resulted in conflicts, in which
     +    case the stash entry is left in place, and exit with status 128, the
     +    status die() uses, when they fail for other reasons.  Document the
     +    exit statuses.
     +
     +    cmd_stash() used to collapse the return values of the subcommand
     +    implementations to a boolean.  It now maps negative values, which
     +    signal a failure, to 128 and passes everything else through as-is.
     +    The only implementations that return a positive value are "apply",
     +    "pop" and "branch", which return the value of do_apply_stash():
     +    "apply" returns it directly, and "pop" and "branch" drop the stash
     +    entry, via do_drop_stash(), which always returns 0, only when the
     +    application succeeded.  The positive value is always 1, as
     +    do_apply_stash() only returns a positive value when the three-way
     +    merge was unclean.
     +
     +    Make the convention explicit by introducing enum stash_apply_result
     +    with the values STASH_APPLY_CLEAN, STASH_APPLY_CONFLICT and
     +    STASH_APPLY_ERROR, and use it for the in-process autostash helpers,
     +    too.  They spawn "git stash apply" and can now tell conflicts apart
     +    from other failures, e.g. a crash or death by signal of the child,
     +    which map to exit statuses above 1.  Since we know the stash entry
     +    was saved, tell users so in the error message instead of leaving them
     +    wondering what happened to their stashed changes.
      
          Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
      
     @@ Documentation/git-stash.adoc: include::includes/cmd-config-section-all.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).
     ++The `git stash` subcommands exit with status 0 on success.  The
     ++subcommands that apply a stash entry, i.e. `apply`, `pop` and `branch`,
     ++exit with status 1 when applying the stash entry resulted in conflicts,
     ++in which case the stash entry is left in place, and with a non-zero
     ++status other than 1 when they fail for other reasons.
      +
       
       SEE ALSO
       --------
      
     - ## builtin/checkout.c ##
     -@@ builtin/checkout.c: static int switch_branches(const struct checkout_opts *opts,
     - 	int flag, writeout_error = 0;
     - 	int do_merge = 1;
     - 	int created_autostash = 0;
     -+	enum stash_apply_result autostash_res = STASH_APPLY_CLEAN;
     - 	struct strbuf old_commit_shortname = STRBUF_INIT;
     - 	struct strbuf autostash_msg = STRBUF_INIT;
     - 	const char *stash_label_base = NULL;
     -@@ builtin/checkout.c: static int switch_branches(const struct checkout_opts *opts,
     - 				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);
     -+			autostash_res = apply_autostash_ref(the_repository,
     -+				    "CHECKOUT_AUTOSTASH_HEAD",
     -+				    new_branch_info->name,
     -+				    "local",
     -+				    stash_label_base,
     -+				    autostash_msg.buf);
     - 		}
     - 		if (ret) {
     - 			branch_info_release(&old_branch_info);
     -@@ builtin/checkout.c: static int switch_branches(const struct checkout_opts *opts,
     - 	if (!opts->quiet && !old_branch_info.path && old_branch_info.commit && new_branch_info->commit != old_branch_info.commit)
     - 		orphaned_commit_warning(old_branch_info.commit, new_branch_info->commit);
     - 
     -+	if (autostash_res == STASH_APPLY_CONFLICT && !opts->quiet)
     -+		fputc('\n', stderr);
     - 	update_refs_for_switch(opts, &old_branch_info, new_branch_info);
     - 
     - 	if (created_autostash) {
     -
       ## builtin/stash.c ##
      @@
       #include "object-name.h"
     @@ builtin/stash.c: static void unstage_changes_unless_new(struct object_id *orig_t
       
      -static int 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)
      +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)
     ++					      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 *info,
     - 	clean = merge_ort_nonrecursive(&o, head, merge, merge_base);
       
       	/*
     --	 * If 'clean' >= 0, reverse the value for 'ret' so 'ret' is 0 when the
     + 	 * 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.
     ++	 * merge was clean, and 1 if the merge was unclean or a negative value
     ++	 * if it encountered an error.
       	 */
     --	ret = clean >= 0 ? !clean : clean;
     -+	ret = clean >= 0 ? (clean ? STASH_APPLY_CLEAN : STASH_APPLY_CONFLICT)
     -+			 : clean;
     + 	ret = clean >= 0 ? !clean : clean;
       
     - 	if (ret < 0)
     - 		rollback_lock_file(&lock);
     -@@ builtin/stash.c: 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;
     - 	} else {
     - 		unstage_changes_unless_new(&c_tree);
     - 	}
      @@ builtin/stash.c: int cmd_stash(int argc,
       	strbuf_addf(&stash_index_path, "%s.stash.%" PRIuMAX, index_file,
       		    (uintmax_t)pid);
     @@ builtin/stash.c: int cmd_stash(int argc,
      +	if (fn) {
      +		ret = fn(argc, argv, prefix, repo);
      +
     ++		/*
     ++		 * The subcommand implementations return 0 on success, a
     ++		 * negative value on failure, and STASH_APPLY_CONFLICT
     ++		 * when applying a stash entry resulted in conflicts.
     ++		 * Map failures to 128, the status die() uses, so that
     ++		 * exit status 1 unambiguously indicates conflicts.
     ++		 */
      +		if (ret < 0)
     -+			return 1;
     ++			return 128;
      +		return ret;
      +	} else if (!argc)
       		return !!push_stash_unassumed(0, NULL, prefix, repo);
     @@ sequencer.c: static int apply_save_autostash_oid(const char *stash_oid, int atte
       			strvec_pushf(&child.args, "--label-base=%s", label_base);
       		strvec_push(&child.args, stash_oid);
       		ret = run_command(&child);
     -+		if (ret && ret != STASH_APPLY_CONFLICT)
     ++		if (ret > 1)
      +			ret = STASH_APPLY_ERROR;
       	}
       
     @@ sequencer.c: static int apply_save_autostash_oid(const char *stash_oid, int atte
       				  "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 if (attempt_apply)
     -+			ret = error(_("could not apply autostash"));
     ++			ret = error(_("could not apply autostash; "
     ++				      "your changes are safe in the stash"));
       		else
       			fprintf(stderr,
       				_("Autostash exists; creating a new stash entry.\n"
     @@ stash.h (new)
      +	 * The stash could not be applied because it resulted in
      +	 * conflicts.  The stash entry is left in place.  The "git stash
      +	 * apply", "pop" and "branch" subcommands exit with this status
     -+	 * in this case.
     ++	 * in this case, mirroring the convention of "git merge-tree" and
     ++	 * the merge strategies.
      +	 */
     -+	STASH_APPLY_CONFLICT = 2,
     ++	STASH_APPLY_CONFLICT = 1,
      +
      +	/* Something went wrong. */
      +	STASH_APPLY_ERROR = -1,
     @@ stash.h (new)
      +#endif /* STASH_H */
      
       ## t/t3903-stash.sh ##
     -@@ t/t3903-stash.sh: test_expect_success 'apply with custom conflict labels' '
     +@@ 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_expect_success 'apply exits 1 on conflicts' '
     + 	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_expect_code 2 git -c merge.conflictStyle=diff3 stash apply --label-ours=UP --label-theirs=STASH &&
     ++	test_expect_code 1 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
     @@ t/t3903-stash.sh: test_expect_success 'apply with empty conflict labels' '
       	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_expect_code 1 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' '
     ++test_expect_success 'pop exits 1 on conflicts and keeps the stash entry' '
      +	git reset --hard initial &&
     -+	test_commit exit-code-base conflict-file base-content &&
     -+	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 &&
     ++	echo stashed >file &&
     ++	git stash push -m pop-stashed &&
     ++	test_commit pop-upstream file upstream-content &&
     ++	test_expect_code 1 git stash pop &&
      +	git stash list >list &&
     -+	test_grep stashed list
     ++	test_grep pop-stashed list
      +'
      +
     -+test_expect_success 'pop exits 2 on conflicts and keeps the stash entry' '
     ++test_expect_success 'stash branch exits with a non-1 status on errors' '
      +	git reset --hard initial &&
     -+	test_commit pop-exit-code-base pop-file base-content &&
     -+	echo stashed >pop-file &&
     -+	git stash push -m pop-stashed &&
     -+	test_commit pop-exit-code-upstream pop-file upstream-content &&
     -+	test_expect_code 2 git stash pop &&
     ++	echo stashed >file &&
     ++	git stash push -m branch-stashed &&
     ++	test_expect_code 128 git stash branch conflicting-branch refs/heads/does-not-exist &&
      +	git stash list >list &&
     -+	test_grep pop-stashed list
     ++	test_grep branch-stashed list
      +'
      +
       test_expect_success 'stash show --include-untracked includes untracked files' '
       	git reset --hard &&
       
     -
     - ## t/t7201-co.sh ##
     -@@ t/t7201-co.sh: 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 &&
     - 	git log -p -1 --format="%gs%n%B" -g --diff-merges=1 refs/stash >actual &&
     - 	sed /^index/d actual >actual.trimmed &&
     - 	cat >expect <<-EOF &&
 -:  ---------- > 2:  935fa0a9ae checkout: separate autostash conflict advice from branch-switch message

-- 
gitgitgadget

  parent reply	other threads:[~2026-09-02 18:29 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
2026-09-01 17:31     ` Junio C Hamano
2026-09-02 18:29 ` Harald Nordgren via GitGitGadget [this message]
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=pull.2364.v4.git.git.1788373743.gitgitgadget@gmail.com \
    --to=gitgitgadget@gmail.com \
    --cc=git@vger.kernel.org \
    --cc=haraldnordgren@gmail.com \
    --cc=phillip.wood123@gmail.com \
    /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.