Git development
 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>,
	Harald Nordgren <haraldnordgren@gmail.com>
Subject: [PATCH v2] checkout: print blank line after autostash conflict advice
Date: Mon, 31 Aug 2026 12:00:01 +0000	[thread overview]
Message-ID: <pull.2364.v2.git.git.1788177601572.gitgitgadget@gmail.com> (raw)
In-Reply-To: <pull.2364.git.git.1784993669.gitgitgadget@gmail.com>

From: Harald Nordgren <haraldnordgren@gmail.com>

When "git checkout -m" stashes the user's local changes and then fails
to re-apply them because of conflicts, the conflict advice is printed
directly on top of the branch-switch message ("Switched to branch ..."),
making the two messages hard to tell apart.  Print a blank line in
between when the stash apply conflicted, so that the advice and the
branch-switch message are visually distinct.

To learn whether the stash apply resulted in conflicts, make the
autostash apply functions return 1 when the stash could not be applied
due to conflicts (and was stored back), while success and error remain
0 and -1 respectively.  Checkout can then use the return value to decide
whether to print the blank line.

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
    checkout -m: refine autostash fallback
    
    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 v2:
    
     * Simplify logic and combine to one commit.
     * Test full output with test_cmp.

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

Range-diff vs v1:

 1:  b501b5fcd0 < -:  ---------- sequencer: teach autostash apply to report conflicts
 2:  37becf38c2 ! 1:  b44c53fcf2 checkout -m: refine autostash fallback
     @@ Metadata
      Author: Harald Nordgren <haraldnordgren@gmail.com>
      
       ## Commit message ##
     -    checkout -m: refine autostash fallback
     +    checkout: print blank line after autostash conflict advice
      
     -    When unpack_trees() fails under "git checkout -m", only create an
     -    autostash and retry if there are tracked local changes.  Without such
     -    changes, the fallback cannot help and merely repeats the same failure.
     +    When "git checkout -m" stashes the user's local changes and then fails
     +    to re-apply them because of conflicts, the conflict advice is printed
     +    directly on top of the branch-switch message ("Switched to branch ..."),
     +    making the two messages hard to tell apart.  Print a blank line in
     +    between when the stash apply conflicted, so that the advice and the
     +    branch-switch message are visually distinct.
      
     -    Use the conflict result from apply_autostash_ref() to print a blank line
     -    before the branch-switch message, visually separating it from the
     -    conflict advice.
     +    To learn whether the stash apply resulted in conflicts, make the
     +    autostash apply functions return 1 when the stash could not be applied
     +    due to conflicts (and was stored back), while success and error remain
     +    0 and -1 respectively.  Checkout can then use the return value to decide
     +    whether to print the blank line.
      
          Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
      
       ## builtin/checkout.c ##
     -@@ builtin/checkout.c: static void init_topts(struct unpack_trees_options *topts,
     - static int merge_working_tree(const struct checkout_opts *opts,
     - 			      struct branch_info *old_branch_info,
     - 			      struct branch_info *new_branch_info,
     --			      bool quiet,
     -+			      bool allow_autostash,
     - 			      int *writeout_error)
     - {
     - 	int ret;
     -+	bool can_autostash = false;
     - 	struct lock_file lock_file = LOCK_INIT;
     - 	struct tree *new_tree;
     - 
     -@@ builtin/checkout.c: static int merge_working_tree(const struct checkout_opts *opts,
     - 			return 1;
     - 		}
     - 
     -+		if (allow_autostash)
     -+			can_autostash = has_unstaged_changes(the_repository, 1) ||
     -+				has_uncommitted_changes(the_repository, 1);
     -+
     - 		/* 2-way merge to the new branch */
     - 		init_topts(&topts, opts->show_progress,
     --			   opts->overwrite_ignore, quiet);
     -+			   opts->overwrite_ignore, can_autostash);
     - 		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,
     - 		clear_unpack_trees_porcelain(&topts);
     - 		if (ret == -1) {
     - 			rollback_lock_file(&lock_file);
     --			return MERGE_WORKING_TREE_UNPACK_FAILED;
     -+			return can_autostash ?
     -+				MERGE_WORKING_TREE_UNPACK_FAILED : 1;
     - 		}
     - 	}
     - 
      @@ 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;
     -+	bool autostash_conflicted = false;
     ++	int autostash_res = 0;
       	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,
     - 					    "local",
     - 					    stash_label_base,
     - 					    autostash_msg.buf,
     --					    NULL);
     -+					    &autostash_conflicted);
     + 				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_conflicted && !opts->quiet)
     ++	if (autostash_res == 1 && !opts->quiet)
      +		fputc('\n', stderr);
       	update_refs_for_switch(opts, &old_branch_info, new_branch_info);
       
       	if (created_autostash) {
      
     + ## sequencer.c ##
     +@@ sequencer.c: static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply,
     + 		if (label_base)
     + 			strvec_pushf(&child.args, "--label-base=%s", label_base);
     + 		strvec_push(&child.args, stash_oid);
     +-		ret = run_command(&child);
     ++		if (run_command(&child))
     ++			ret = 1;
     + 	}
     + 
     + 	if (attempt_apply && !ret)
     +
       ## t/t7201-co.sh ##
      @@ t/t7201-co.sh: test_expect_success 'checkout -m creates a recoverable stash on conflict' '
     - 	test_grep "git stash drop" actual &&
     - 	test_grep "git stash pop" actual &&
     - 	test_grep "The following paths have local changes" actual &&
     -+	sed -n "/apply the local changes later/,/Switched to branch/p" \
     -+		actual >separator.actual &&
     -+	cat >separator.expect <<-EOF &&
     + 	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 separator.expect separator.actual &&
     ++	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 &&
     -@@ t/t7201-co.sh: test_expect_success 'checkout -m creates a recoverable stash on conflict' '
     - 	git reset --hard
     - '
     - 
     --test_expect_success 'checkout -m which would overwrite untracked file' '
     -+test_expect_success 'checkout -m only retries untracked-file failure with local changes' '
     - 	git checkout -f --detach main &&
     - 	test_commit another-file &&
     - 	git checkout HEAD^ &&
     - 	>another-file.t &&
     -+	test_must_fail env GIT_TRACE2_EVENT="$(pwd)/trace" \
     -+		git checkout -m @{-1} 2>err &&
     -+	test_grep "untracked working tree files" err &&
     -+	grep "\"region_enter\".*\"category\":\"index\",\"label\":\"refresh\"" \
     -+		trace >refresh.events &&
     -+	test_line_count = 1 refresh.events &&
     -+
     - 	fill 1 2 3 4 5 >one &&
     - 	test_must_fail git checkout -m @{-1} 2>err &&
     - 	q_to_tab >expect <<-\EOF &&


 builtin/checkout.c | 15 +++++++++------
 sequencer.c        |  3 ++-
 t/t7201-co.sh      | 16 ++++++++++++----
 3 files changed, 23 insertions(+), 11 deletions(-)

diff --git a/builtin/checkout.c b/builtin/checkout.c
index 55e3a89a85..e4b7383f10 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -1166,6 +1166,7 @@ static int switch_branches(const struct checkout_opts *opts,
 	int flag, writeout_error = 0;
 	int do_merge = 1;
 	int created_autostash = 0;
+	int autostash_res = 0;
 	struct strbuf old_commit_shortname = STRBUF_INIT;
 	struct strbuf autostash_msg = STRBUF_INIT;
 	const char *stash_label_base = NULL;
@@ -1237,12 +1238,12 @@ 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);
@@ -1255,6 +1256,8 @@ 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 == 1 && !opts->quiet)
+		fputc('\n', stderr);
 	update_refs_for_switch(opts, &old_branch_info, new_branch_info);
 
 	if (created_autostash) {
diff --git a/sequencer.c b/sequencer.c
index 65afd100d9..5ed9ae86c4 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -4815,7 +4815,8 @@ static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply,
 		if (label_base)
 			strvec_pushf(&child.args, "--label-base=%s", label_base);
 		strvec_push(&child.args, stash_oid);
-		ret = run_command(&child);
+		if (run_command(&child))
+			ret = 1;
 	}
 
 	if (attempt_apply && !ret)
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 &&
 	git log -p -1 --format="%gs%n%B" -g --diff-merges=1 refs/stash >actual &&
 	sed /^index/d actual >actual.trimmed &&
 	cat >expect <<-EOF &&

base-commit: c73e85354c275c9d409b26445089bc16940fc527
-- 
gitgitgadget

  parent reply	other threads:[~2026-08-31 12:00 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 ` Harald Nordgren via GitGitGadget [this message]
2026-08-31 17:19   ` [PATCH v2] checkout: print blank line after autostash conflict advice 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 ` [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=pull.2364.v2.git.git.1788177601572.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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox