{"thread":{"id":"66064","subject":"[PATCH 0/2] checkout -m: refine autostash fallback","startedAt":"2026-07-25T15:34:32Z","lastAt":"2026-09-04T16:43:14Z","messageCount":37,"participants":["Harald Nordgren via GitGitGadget","Junio C Hamano","Phillip Wood","Harald Nordgren","phillip.wood123@gmail.com"],"isPatch":true,"patchVersion":1,"patchTotal":2},"messages":[{"id":"548965","messageId":"pull.2364.git.git.1784993669.gitgitgadget@gmail.com","threadId":"66064","inReplyTo":null,"subject":"[PATCH 0/2] checkout -m: refine autostash fallback","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-07-25T15:34:27Z","receivedAt":"2026-07-25T15:34:32Z","isPatch":true,"body":"Avoiding checkout -m autostash retries when no tracked local changes exist\nand visually separating autostash conflict advice from the subsequent\nbranch-switch message.\n\nAddresses #leftoverbits from here:\nhttps://lore.kernel.org/git/cfd09dbf-8d77-4464-8030-3a0ffb4aeae7@gmail.com/\n\nHarald Nordgren (2):\n  sequencer: teach autostash apply to report conflicts\n  checkout -m: refine autostash fallback\n\n builtin/checkout.c | 18 ++++++++++++++----\n builtin/commit.c   |  2 +-\n builtin/merge.c    |  6 +++---\n sequencer.c        | 29 +++++++++++++++++++----------\n sequencer.h        |  3 ++-\n t/t7201-co.sh      | 17 ++++++++++++++++-\n 6 files changed, 55 insertions(+), 20 deletions(-)\n\n\nbase-commit: 9a0c4701dcd5725c4184599322b52933ff5005ca\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2364%2FHaraldNordgren%2Fhn%2Fgit-checkout-m-leftoverbits-v1\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2364/HaraldNordgren/hn/git-checkout-m-leftoverbits-v1\nPull-Request: https://github.com/git/git/pull/2364\n-- \ngitgitgadget\n"},{"id":"548966","messageId":"b501b5fcd0b9dde65c1ce358e2f4014dfa340c37.1784993669.git.gitgitgadget@gmail.com","threadId":"66064","inReplyTo":"pull.2364.git.git.1784993669.gitgitgadget@gmail.com","subject":"[PATCH 1/2] sequencer: teach autostash apply to report conflicts","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-07-25T15:34:28Z","receivedAt":"2026-07-25T15:34:34Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nAdd a conflicted parameter to apply_save_autostash_oid() and\napply_save_autostash_ref() so callers can learn whether applying the\nstash resulted in conflicts.  Thread the parameter through\napply_autostash_ref() and update existing callers to pass NULL.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n builtin/checkout.c |  3 ++-\n builtin/commit.c   |  2 +-\n builtin/merge.c    |  6 +++---\n sequencer.c        | 29 +++++++++++++++++++----------\n sequencer.h        |  3 ++-\n 5 files changed, 27 insertions(+), 16 deletions(-)\n\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex aee84ca897..72aafa4049 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -1242,7 +1242,8 @@ static int switch_branches(const struct checkout_opts *opts,\n \t\t\t\t\t    new_branch_info->name,\n \t\t\t\t\t    \"local\",\n \t\t\t\t\t    stash_label_base,\n-\t\t\t\t\t    autostash_msg.buf);\n+\t\t\t\t\t    autostash_msg.buf,\n+\t\t\t\t\t    NULL);\n \t\t}\n \t\tif (ret) {\n \t\t\tbranch_info_release(&old_branch_info);\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 28f6174503..d678a81865 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -1980,7 +1980,7 @@ int cmd_commit(int argc,\n \t}\n \n \tapply_autostash_ref(the_repository, \"MERGE_AUTOSTASH\",\n-\t\t\t    NULL, NULL, NULL, NULL);\n+\t\t\t    NULL, NULL, NULL, NULL, NULL);\n \n cleanup:\n \tfree_commit_extra_headers(extra);\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex 5b46a596f0..cecb8fb716 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -538,7 +538,7 @@ static void finish(struct commit *head_commit,\n \n \tif (new_head)\n \t\tapply_autostash_ref(the_repository, \"MERGE_AUTOSTASH\",\n-\t\t\t\t    NULL, NULL, NULL, NULL);\n+\t\t\t\t    NULL, NULL, NULL, NULL, NULL);\n \tstrbuf_release(&reflog_message);\n }\n \n@@ -1680,7 +1680,7 @@ int cmd_merge(int argc,\n \t\t\t\t\t  &commit->object.oid,\n \t\t\t\t\t  overwrite_ignore)) {\n \t\t\tapply_autostash_ref(the_repository, \"MERGE_AUTOSTASH\",\n-\t\t\t\t\t    NULL, NULL, NULL, NULL);\n+\t\t\t\t\t    NULL, NULL, NULL, NULL, NULL);\n \t\t\tret = 1;\n \t\t\tgoto done;\n \t\t}\n@@ -1844,7 +1844,7 @@ int cmd_merge(int argc,\n \t\t\tfprintf(stderr, _(\"Merge with strategy %s failed.\\n\"),\n \t\t\t\tuse_strategies[0]->name);\n \t\tapply_autostash_ref(the_repository, \"MERGE_AUTOSTASH\",\n-\t\t\t\t    NULL, NULL, NULL, NULL);\n+\t\t\t\t    NULL, NULL, NULL, NULL, NULL);\n \t\tret = 2;\n \t\tgoto done;\n \t} else if (best_strategy == wt_strategy)\ndiff --git a/sequencer.c b/sequencer.c\nindex 1355a99a09..91a70e39a1 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -4748,7 +4748,8 @@ void create_autostash_ref(struct repository *r, const char *refname,\n static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply,\n \t\t\t\t    const char *label_ours, const char *label_theirs,\n \t\t\t\t    const char *label_base,\n-\t\t\t\t    const char *stash_msg)\n+\t\t\t\t    const char *stash_msg,\n+\t\t\t\t    bool *conflicted)\n {\n \tstruct child_process child = CHILD_PROCESS_INIT;\n \tint ret = 0;\n@@ -4783,14 +4784,16 @@ static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply,\n \t\tstrvec_push(&store.args, stash_oid);\n \t\tif (run_command(&store))\n \t\t\tret = error(_(\"cannot store %s\"), stash_oid);\n-\t\telse if (attempt_apply)\n+\t\telse if (attempt_apply) {\n+\t\t\tif (conflicted)\n+\t\t\t\t*conflicted = true;\n \t\t\tfprintf(stderr,\n \t\t\t\t_(\"Your local changes are stashed, however applying them\\n\"\n \t\t\t\t  \"resulted in conflicts.  You can either resolve the conflicts\\n\"\n \t\t\t\t  \"and then discard the stash with \\\"git stash drop\\\", or, if you\\n\"\n \t\t\t\t  \"do not want to resolve them now, run \\\"git reset --hard\\\" and\\n\"\n \t\t\t\t  \"apply the local changes later by running \\\"git stash pop\\\".\\n\"));\n-\t\telse\n+\t\t} else\n \t\t\tfprintf(stderr,\n \t\t\t\t_(\"Autostash exists; creating a new stash entry.\\n\"\n \t\t\t\t  \"Your changes are safe in the stash.\\n\"\n@@ -4814,7 +4817,7 @@ static int apply_save_autostash(const char *path, int attempt_apply)\n \tstrbuf_trim(&stash_oid);\n \n \tret = apply_save_autostash_oid(stash_oid.buf, attempt_apply,\n-\t\t\t\t      NULL, NULL, NULL, NULL);\n+\t\t\t\t      NULL, NULL, NULL, NULL, NULL);\n \n \tunlink(path);\n \tstrbuf_release(&stash_oid);\n@@ -4833,19 +4836,24 @@ int apply_autostash(const char *path)\n \n int apply_autostash_oid(const char *stash_oid)\n {\n-\treturn apply_save_autostash_oid(stash_oid, 1, NULL, NULL, NULL, NULL);\n+\treturn apply_save_autostash_oid(stash_oid, 1, NULL, NULL, NULL, NULL,\n+\t\t\t\t       NULL);\n }\n \n static int apply_save_autostash_ref(struct repository *r, const char *refname,\n \t\t\t\t    int attempt_apply,\n \t\t\t\t    const char *label_ours, const char *label_theirs,\n \t\t\t\t    const char *label_base,\n-\t\t\t\t    const char *stash_msg)\n+\t\t\t\t    const char *stash_msg,\n+\t\t\t\t    bool *conflicted)\n {\n \tstruct object_id stash_oid;\n \tchar stash_oid_hex[GIT_MAX_HEXSZ + 1];\n \tint flag, ret;\n \n+\tif (conflicted)\n+\t\t*conflicted = false;\n+\n \tif (!refs_ref_exists(get_main_ref_store(r), refname))\n \t\treturn 0;\n \n@@ -4858,7 +4866,7 @@ static int apply_save_autostash_ref(struct repository *r, const char *refname,\n \toid_to_hex_r(stash_oid_hex, &stash_oid);\n \tret = apply_save_autostash_oid(stash_oid_hex, attempt_apply,\n \t\t\t\t       label_ours, label_theirs, label_base,\n-\t\t\t\t       stash_msg);\n+\t\t\t\t       stash_msg, conflicted);\n \n \trefs_delete_ref(get_main_ref_store(r), \"\", refname,\n \t\t\t&stash_oid, REF_NO_DEREF);\n@@ -4869,16 +4877,17 @@ static int apply_save_autostash_ref(struct repository *r, const char *refname,\n int save_autostash_ref(struct repository *r, const char *refname)\n {\n \treturn apply_save_autostash_ref(r, refname, 0,\n-\t\t\t\t\tNULL, NULL, NULL, NULL);\n+\t\t\t\t\tNULL, NULL, NULL, NULL, NULL);\n }\n \n int apply_autostash_ref(struct repository *r, const char *refname,\n \t\t\tconst char *label_ours, const char *label_theirs,\n-\t\t\tconst char *label_base, const char *stash_msg)\n+\t\t\tconst char *label_base, const char *stash_msg,\n+\t\t\tbool *conflicted)\n {\n \treturn apply_save_autostash_ref(r, refname, 1,\n \t\t\t\t\tlabel_ours, label_theirs, label_base,\n-\t\t\t\t\tstash_msg);\n+\t\t\t\t\tstash_msg, conflicted);\n }\n \n static int checkout_onto(struct repository *r, struct replay_opts *opts,\ndiff --git a/sequencer.h b/sequencer.h\nindex 64a9c7fb1b..b39528b6d0 100644\n--- a/sequencer.h\n+++ b/sequencer.h\n@@ -237,7 +237,8 @@ int apply_autostash(const char *path);\n int apply_autostash_oid(const char *stash_oid);\n int apply_autostash_ref(struct repository *r, const char *refname,\n \t\t\tconst char *label_ours, const char *label_theirs,\n-\t\t\tconst char *label_base, const char *stash_msg);\n+\t\t\tconst char *label_base, const char *stash_msg,\n+\t\t\tbool *conflicted);\n \n #define SUMMARY_INITIAL_COMMIT   (1 << 0)\n #define SUMMARY_SHOW_AUTHOR_DATE (1 << 1)\n-- \ngitgitgadget\n\n"},{"id":"548967","messageId":"37becf38c2ef175a3dadcf750e2cca836942d83e.1784993669.git.gitgitgadget@gmail.com","threadId":"66064","inReplyTo":"pull.2364.git.git.1784993669.gitgitgadget@gmail.com","subject":"[PATCH 2/2] checkout -m: refine autostash fallback","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-07-25T15:34:29Z","receivedAt":"2026-07-25T15:34:37Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nWhen unpack_trees() fails under \"git checkout -m\", only create an\nautostash and retry if there are tracked local changes.  Without such\nchanges, the fallback cannot help and merely repeats the same failure.\n\nUse the conflict result from apply_autostash_ref() to print a blank line\nbefore the branch-switch message, visually separating it from the\nconflict advice.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n builtin/checkout.c | 17 +++++++++++++----\n t/t7201-co.sh      | 17 ++++++++++++++++-\n 2 files changed, 29 insertions(+), 5 deletions(-)\n\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex 72aafa4049..2e8b2a2348 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -838,10 +838,11 @@ static void init_topts(struct unpack_trees_options *topts,\n static int merge_working_tree(const struct checkout_opts *opts,\n \t\t\t      struct branch_info *old_branch_info,\n \t\t\t      struct branch_info *new_branch_info,\n-\t\t\t      bool quiet,\n+\t\t\t      bool allow_autostash,\n \t\t\t      int *writeout_error)\n {\n \tint ret;\n+\tbool can_autostash = false;\n \tstruct lock_file lock_file = LOCK_INIT;\n \tstruct tree *new_tree;\n \n@@ -888,9 +889,13 @@ static int merge_working_tree(const struct checkout_opts *opts,\n \t\t\treturn 1;\n \t\t}\n \n+\t\tif (allow_autostash)\n+\t\t\tcan_autostash = has_unstaged_changes(the_repository, 1) ||\n+\t\t\t\thas_uncommitted_changes(the_repository, 1);\n+\n \t\t/* 2-way merge to the new branch */\n \t\tinit_topts(&topts, opts->show_progress,\n-\t\t\t   opts->overwrite_ignore, quiet);\n+\t\t\t   opts->overwrite_ignore, can_autostash);\n \t\tinit_checkout_metadata(&topts.meta, new_branch_info->refname,\n \t\t\t\t       new_branch_info->commit ?\n \t\t\t\t       &new_branch_info->commit->object.oid :\n@@ -917,7 +922,8 @@ static int merge_working_tree(const struct checkout_opts *opts,\n \t\tclear_unpack_trees_porcelain(&topts);\n \t\tif (ret == -1) {\n \t\t\trollback_lock_file(&lock_file);\n-\t\t\treturn MERGE_WORKING_TREE_UNPACK_FAILED;\n+\t\t\treturn can_autostash ?\n+\t\t\t\tMERGE_WORKING_TREE_UNPACK_FAILED : 1;\n \t\t}\n \t}\n \n@@ -1166,6 +1172,7 @@ static int switch_branches(const struct checkout_opts *opts,\n \tint flag, writeout_error = 0;\n \tint do_merge = 1;\n \tint created_autostash = 0;\n+\tbool autostash_conflicted = false;\n \tstruct strbuf old_commit_shortname = STRBUF_INIT;\n \tstruct strbuf autostash_msg = STRBUF_INIT;\n \tconst char *stash_label_base = NULL;\n@@ -1243,7 +1250,7 @@ static int switch_branches(const struct checkout_opts *opts,\n \t\t\t\t\t    \"local\",\n \t\t\t\t\t    stash_label_base,\n \t\t\t\t\t    autostash_msg.buf,\n-\t\t\t\t\t    NULL);\n+\t\t\t\t\t    &autostash_conflicted);\n \t\t}\n \t\tif (ret) {\n \t\t\tbranch_info_release(&old_branch_info);\n@@ -1256,6 +1263,8 @@ static int switch_branches(const struct checkout_opts *opts,\n \tif (!opts->quiet && !old_branch_info.path && old_branch_info.commit && new_branch_info->commit != old_branch_info.commit)\n \t\torphaned_commit_warning(old_branch_info.commit, new_branch_info->commit);\n \n+\tif (autostash_conflicted && !opts->quiet)\n+\t\tfputc('\\n', stderr);\n \tupdate_refs_for_switch(opts, &old_branch_info, new_branch_info);\n \n \tif (created_autostash) {\ndiff --git a/t/t7201-co.sh b/t/t7201-co.sh\nindex 0ddd1ad7aa..f9696dab36 100755\n--- a/t/t7201-co.sh\n+++ b/t/t7201-co.sh\n@@ -240,6 +240,14 @@ test_expect_success 'checkout -m creates a recoverable stash on conflict' '\n \ttest_grep \"git stash drop\" actual &&\n \ttest_grep \"git stash pop\" actual &&\n \ttest_grep \"The following paths have local changes\" actual &&\n+\tsed -n \"/apply the local changes later/,/Switched to branch/p\" \\\n+\t\tactual >separator.actual &&\n+\tcat >separator.expect <<-EOF &&\n+\tapply the local changes later by running \"git stash pop\".\n+\n+\tSwitched to branch ${SQ}side${SQ}\n+\tEOF\n+\ttest_cmp separator.expect separator.actual &&\n \tgit log -p -1 --format=\"%gs%n%B\" -g --diff-merges=1 refs/stash >actual &&\n \tsed /^index/d actual >actual.trimmed &&\n \tcat >expect <<-EOF &&\n@@ -262,11 +270,18 @@ test_expect_success 'checkout -m creates a recoverable stash on conflict' '\n \tgit reset --hard\n '\n \n-test_expect_success 'checkout -m which would overwrite untracked file' '\n+test_expect_success 'checkout -m only retries untracked-file failure with local changes' '\n \tgit checkout -f --detach main &&\n \ttest_commit another-file &&\n \tgit checkout HEAD^ &&\n \t>another-file.t &&\n+\ttest_must_fail env GIT_TRACE2_EVENT=\"$(pwd)/trace\" \\\n+\t\tgit checkout -m @{-1} 2>err &&\n+\ttest_grep \"untracked working tree files\" err &&\n+\tgrep \"\\\"region_enter\\\".*\\\"category\\\":\\\"index\\\",\\\"label\\\":\\\"refresh\\\"\" \\\n+\t\ttrace >refresh.events &&\n+\ttest_line_count = 1 refresh.events &&\n+\n \tfill 1 2 3 4 5 >one &&\n \ttest_must_fail git checkout -m @{-1} 2>err &&\n \tq_to_tab >expect <<-\\EOF &&\n-- \ngitgitgadget\n"},{"id":"549178","messageId":"xmqqwluebuhb.fsf@gitster.g","threadId":"66064","inReplyTo":"37becf38c2ef175a3dadcf750e2cca836942d83e.1784993669.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 2/2] checkout -m: refine autostash fallback","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-28T22:49:20Z","receivedAt":"2026-07-28T22:49:23Z","isPatch":true,"body":"\"Harald Nordgren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Harald Nordgren <haraldnordgren@gmail.com>\n>\n> When unpack_trees() fails under \"git checkout -m\", only create an\n> autostash and retry if there are tracked local changes.  Without such\n> changes, the fallback cannot help and merely repeats the same failure.\n>\n> Use the conflict result from apply_autostash_ref() to print a blank line\n> before the branch-switch message, visually separating it from the\n> conflict advice.\n>\n> Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n> ---\n>  builtin/checkout.c | 17 +++++++++++++----\n>  t/t7201-co.sh      | 17 ++++++++++++++++-\n>  2 files changed, 29 insertions(+), 5 deletions(-)\n\nThanks.  Will queue.\n"},{"id":"551309","messageId":"xmqqld9sg0di.fsf@gitster.g","threadId":"66064","inReplyTo":"pull.2364.git.git.1784993669.gitgitgadget@gmail.com","subject":"Re: [PATCH 0/2] checkout -m: refine autostash fallback","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-08-26T19:14:33Z","receivedAt":"2026-08-26T19:14:40Z","isPatch":true,"body":"\"Harald Nordgren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> Avoiding checkout -m autostash retries when no tracked local changes exist\n> and visually separating autostash conflict advice from the subsequent\n> branch-switch message.\n>\n> Addresses #leftoverbits from here:\n> https://lore.kernel.org/git/cfd09dbf-8d77-4464-8030-3a0ffb4aeae7@gmail.com/\n>\n> Harald Nordgren (2):\n>   sequencer: teach autostash apply to report conflicts\n>   checkout -m: refine autostash fallback\n>\n>  builtin/checkout.c | 18 ++++++++++++++----\n>  builtin/commit.c   |  2 +-\n>  builtin/merge.c    |  6 +++---\n>  sequencer.c        | 29 +++++++++++++++++++----------\n>  sequencer.h        |  3 ++-\n>  t/t7201-co.sh      | 17 ++++++++++++++++-\n>  6 files changed, 55 insertions(+), 20 deletions(-)\n>\n>\n> base-commit: 9a0c4701dcd5725c4184599322b52933ff5005ca\n> Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2364%2FHaraldNordgren%2Fhn%2Fgit-checkout-m-leftoverbits-v1\n> Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2364/HaraldNordgren/hn/git-checkout-m-leftoverbits-v1\n> Pull-Request: https://github.com/git/git/pull/2364\n\nThis topic unfortunately has seen no interests from others on the\nlist.  Asking Phillip for help, as the leftoverbits comment cited in\nthe cover letter is from him.\n\nThanks.\n\n\n\n\n"},{"id":"551353","messageId":"ec6c3986-94ab-4692-a4c2-47569b77e9ca@gmail.com","threadId":"66064","inReplyTo":"b501b5fcd0b9dde65c1ce358e2f4014dfa340c37.1784993669.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 1/2] sequencer: teach autostash apply to report conflicts","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-08-27T13:01:33Z","receivedAt":"2026-08-27T13:01:41Z","isPatch":true,"body":"Hi Harald\n\nOn 25/07/2026 16:34, Harald Nordgren via GitGitGadget wrote:\n> From: Harald Nordgren <haraldnordgren@gmail.com>\n> \n> Add a conflicted parameter to apply_save_autostash_oid() and\n> apply_save_autostash_ref() so callers can learn whether applying the\n> stash resulted in conflicts.  Thread the parameter through\n> apply_autostash_ref() and update existing callers to pass NULL.\n\nIt would be nicer to use the return value to indicate \nsuccess/error/conflicts rather than adding yet another parameter. Apart \nfrom that this looks good.\n\nThanks\n\nPhillip\n\n> Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n> ---\n>   builtin/checkout.c |  3 ++-\n>   builtin/commit.c   |  2 +-\n>   builtin/merge.c    |  6 +++---\n>   sequencer.c        | 29 +++++++++++++++++++----------\n>   sequencer.h        |  3 ++-\n>   5 files changed, 27 insertions(+), 16 deletions(-)\n> \n> diff --git a/builtin/checkout.c b/builtin/checkout.c\n> index aee84ca897..72aafa4049 100644\n> --- a/builtin/checkout.c\n> +++ b/builtin/checkout.c\n> @@ -1242,7 +1242,8 @@ static int switch_branches(const struct checkout_opts *opts,\n>   \t\t\t\t\t    new_branch_info->name,\n>   \t\t\t\t\t    \"local\",\n>   \t\t\t\t\t    stash_label_base,\n> -\t\t\t\t\t    autostash_msg.buf);\n> +\t\t\t\t\t    autostash_msg.buf,\n> +\t\t\t\t\t    NULL);\n>   \t\t}\n>   \t\tif (ret) {\n>   \t\t\tbranch_info_release(&old_branch_info);\n> diff --git a/builtin/commit.c b/builtin/commit.c\n> index 28f6174503..d678a81865 100644\n> --- a/builtin/commit.c\n> +++ b/builtin/commit.c\n> @@ -1980,7 +1980,7 @@ int cmd_commit(int argc,\n>   \t}\n>   \n>   \tapply_autostash_ref(the_repository, \"MERGE_AUTOSTASH\",\n> -\t\t\t    NULL, NULL, NULL, NULL);\n> +\t\t\t    NULL, NULL, NULL, NULL, NULL);\n>   \n>   cleanup:\n>   \tfree_commit_extra_headers(extra);\n> diff --git a/builtin/merge.c b/builtin/merge.c\n> index 5b46a596f0..cecb8fb716 100644\n> --- a/builtin/merge.c\n> +++ b/builtin/merge.c\n> @@ -538,7 +538,7 @@ static void finish(struct commit *head_commit,\n>   \n>   \tif (new_head)\n>   \t\tapply_autostash_ref(the_repository, \"MERGE_AUTOSTASH\",\n> -\t\t\t\t    NULL, NULL, NULL, NULL);\n> +\t\t\t\t    NULL, NULL, NULL, NULL, NULL);\n>   \tstrbuf_release(&reflog_message);\n>   }\n>   \n> @@ -1680,7 +1680,7 @@ int cmd_merge(int argc,\n>   \t\t\t\t\t  &commit->object.oid,\n>   \t\t\t\t\t  overwrite_ignore)) {\n>   \t\t\tapply_autostash_ref(the_repository, \"MERGE_AUTOSTASH\",\n> -\t\t\t\t\t    NULL, NULL, NULL, NULL);\n> +\t\t\t\t\t    NULL, NULL, NULL, NULL, NULL);\n>   \t\t\tret = 1;\n>   \t\t\tgoto done;\n>   \t\t}\n> @@ -1844,7 +1844,7 @@ int cmd_merge(int argc,\n>   \t\t\tfprintf(stderr, _(\"Merge with strategy %s failed.\\n\"),\n>   \t\t\t\tuse_strategies[0]->name);\n>   \t\tapply_autostash_ref(the_repository, \"MERGE_AUTOSTASH\",\n> -\t\t\t\t    NULL, NULL, NULL, NULL);\n> +\t\t\t\t    NULL, NULL, NULL, NULL, NULL);\n>   \t\tret = 2;\n>   \t\tgoto done;\n>   \t} else if (best_strategy == wt_strategy)\n> diff --git a/sequencer.c b/sequencer.c\n> index 1355a99a09..91a70e39a1 100644\n> --- a/sequencer.c\n> +++ b/sequencer.c\n> @@ -4748,7 +4748,8 @@ void create_autostash_ref(struct repository *r, const char *refname,\n>   static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply,\n>   \t\t\t\t    const char *label_ours, const char *label_theirs,\n>   \t\t\t\t    const char *label_base,\n> -\t\t\t\t    const char *stash_msg)\n> +\t\t\t\t    const char *stash_msg,\n> +\t\t\t\t    bool *conflicted)\n>   {\n>   \tstruct child_process child = CHILD_PROCESS_INIT;\n>   \tint ret = 0;\n> @@ -4783,14 +4784,16 @@ static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply,\n>   \t\tstrvec_push(&store.args, stash_oid);\n>   \t\tif (run_command(&store))\n>   \t\t\tret = error(_(\"cannot store %s\"), stash_oid);\n> -\t\telse if (attempt_apply)\n> +\t\telse if (attempt_apply) {\n> +\t\t\tif (conflicted)\n> +\t\t\t\t*conflicted = true;\n>   \t\t\tfprintf(stderr,\n>   \t\t\t\t_(\"Your local changes are stashed, however applying them\\n\"\n>   \t\t\t\t  \"resulted in conflicts.  You can either resolve the conflicts\\n\"\n>   \t\t\t\t  \"and then discard the stash with \\\"git stash drop\\\", or, if you\\n\"\n>   \t\t\t\t  \"do not want to resolve them now, run \\\"git reset --hard\\\" and\\n\"\n>   \t\t\t\t  \"apply the local changes later by running \\\"git stash pop\\\".\\n\"));\n> -\t\telse\n> +\t\t} else\n>   \t\t\tfprintf(stderr,\n>   \t\t\t\t_(\"Autostash exists; creating a new stash entry.\\n\"\n>   \t\t\t\t  \"Your changes are safe in the stash.\\n\"\n> @@ -4814,7 +4817,7 @@ static int apply_save_autostash(const char *path, int attempt_apply)\n>   \tstrbuf_trim(&stash_oid);\n>   \n>   \tret = apply_save_autostash_oid(stash_oid.buf, attempt_apply,\n> -\t\t\t\t      NULL, NULL, NULL, NULL);\n> +\t\t\t\t      NULL, NULL, NULL, NULL, NULL);\n>   \n>   \tunlink(path);\n>   \tstrbuf_release(&stash_oid);\n> @@ -4833,19 +4836,24 @@ int apply_autostash(const char *path)\n>   \n>   int apply_autostash_oid(const char *stash_oid)\n>   {\n> -\treturn apply_save_autostash_oid(stash_oid, 1, NULL, NULL, NULL, NULL);\n> +\treturn apply_save_autostash_oid(stash_oid, 1, NULL, NULL, NULL, NULL,\n> +\t\t\t\t       NULL);\n>   }\n>   \n>   static int apply_save_autostash_ref(struct repository *r, const char *refname,\n>   \t\t\t\t    int attempt_apply,\n>   \t\t\t\t    const char *label_ours, const char *label_theirs,\n>   \t\t\t\t    const char *label_base,\n> -\t\t\t\t    const char *stash_msg)\n> +\t\t\t\t    const char *stash_msg,\n> +\t\t\t\t    bool *conflicted)\n>   {\n>   \tstruct object_id stash_oid;\n>   \tchar stash_oid_hex[GIT_MAX_HEXSZ + 1];\n>   \tint flag, ret;\n>   \n> +\tif (conflicted)\n> +\t\t*conflicted = false;\n> +\n>   \tif (!refs_ref_exists(get_main_ref_store(r), refname))\n>   \t\treturn 0;\n>   \n> @@ -4858,7 +4866,7 @@ static int apply_save_autostash_ref(struct repository *r, const char *refname,\n>   \toid_to_hex_r(stash_oid_hex, &stash_oid);\n>   \tret = apply_save_autostash_oid(stash_oid_hex, attempt_apply,\n>   \t\t\t\t       label_ours, label_theirs, label_base,\n> -\t\t\t\t       stash_msg);\n> +\t\t\t\t       stash_msg, conflicted);\n>   \n>   \trefs_delete_ref(get_main_ref_store(r), \"\", refname,\n>   \t\t\t&stash_oid, REF_NO_DEREF);\n> @@ -4869,16 +4877,17 @@ static int apply_save_autostash_ref(struct repository *r, const char *refname,\n>   int save_autostash_ref(struct repository *r, const char *refname)\n>   {\n>   \treturn apply_save_autostash_ref(r, refname, 0,\n> -\t\t\t\t\tNULL, NULL, NULL, NULL);\n> +\t\t\t\t\tNULL, NULL, NULL, NULL, NULL);\n>   }\n>   \n>   int apply_autostash_ref(struct repository *r, const char *refname,\n>   \t\t\tconst char *label_ours, const char *label_theirs,\n> -\t\t\tconst char *label_base, const char *stash_msg)\n> +\t\t\tconst char *label_base, const char *stash_msg,\n> +\t\t\tbool *conflicted)\n>   {\n>   \treturn apply_save_autostash_ref(r, refname, 1,\n>   \t\t\t\t\tlabel_ours, label_theirs, label_base,\n> -\t\t\t\t\tstash_msg);\n> +\t\t\t\t\tstash_msg, conflicted);\n>   }\n>   \n>   static int checkout_onto(struct repository *r, struct replay_opts *opts,\n> diff --git a/sequencer.h b/sequencer.h\n> index 64a9c7fb1b..b39528b6d0 100644\n> --- a/sequencer.h\n> +++ b/sequencer.h\n> @@ -237,7 +237,8 @@ int apply_autostash(const char *path);\n>   int apply_autostash_oid(const char *stash_oid);\n>   int apply_autostash_ref(struct repository *r, const char *refname,\n>   \t\t\tconst char *label_ours, const char *label_theirs,\n> -\t\t\tconst char *label_base, const char *stash_msg);\n> +\t\t\tconst char *label_base, const char *stash_msg,\n> +\t\t\tbool *conflicted);\n>   \n>   #define SUMMARY_INITIAL_COMMIT   (1 << 0)\n>   #define SUMMARY_SHOW_AUTHOR_DATE (1 << 1)\n\n"},{"id":"551354","messageId":"28428451-8a56-43be-8ee4-af5a704977a9@gmail.com","threadId":"66064","inReplyTo":"37becf38c2ef175a3dadcf750e2cca836942d83e.1784993669.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 2/2] checkout -m: refine autostash fallback","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-08-27T13:05:12Z","receivedAt":"2026-08-27T13:05:19Z","isPatch":true,"body":"Hi Harald\n\nOn 25/07/2026 16:34, Harald Nordgren via GitGitGadget wrote:\n> From: Harald Nordgren <haraldnordgren@gmail.com>\n> \n> When unpack_trees() fails under \"git checkout -m\", only create an\n> autostash and retry if there are tracked local changes.  Without such\n> changes, the fallback cannot help and merely repeats the same failure.\n\nUnfortunately to do that we have to look for local changes before the \nfirst call to unpack_trees() so we're trading an occasional \ninconvenience of an unnecessary stash and unstash for the cost of \nlooking for local changes on every invocation of \"git checkout -m\". I \ndon't think that is a good trade off, especially as there is no \nguarantee that stashing the local changes will make unpack_trees() \nsucceed. To do this effectively would require refactoring unpack_trees() \nto write its error messages to a buffer and return an error flag that \nindicates all the errors that were encountered. We could then check if \nthe only thing that prevented upack_trees() from succeeding was local \nchanges to files and stash them, or if there are other errors print the \nerror message. I suspect such a change is far from straight forward.\n\n> Use the conflict result from apply_autostash_ref() to print a blank line\n> before the branch-switch message, visually separating it from the\n> conflict advice.\n\n\nThis change is very welcome and could happily be squashed into the first \npatch as it motivates the changes in it.\n\n> diff --git a/t/t7201-co.sh b/t/t7201-co.sh\n> index 0ddd1ad7aa..f9696dab36 100755\n> --- a/t/t7201-co.sh\n> +++ b/t/t7201-co.sh\n> @@ -240,6 +240,14 @@ test_expect_success 'checkout -m creates a recoverable stash on conflict' '\n>   \ttest_grep \"git stash drop\" actual &&\n>   \ttest_grep \"git stash pop\" actual &&\n>   \ttest_grep \"The following paths have local changes\" actual &&\n> +\tsed -n \"/apply the local changes later/,/Switched to branch/p\" \\\n> +\t\tactual >separator.actual &&\n> +\tcat >separator.expect <<-EOF &&\n> +\tapply the local changes later by running \"git stash pop\".\n> +\n> +\tSwitched to branch ${SQ}side${SQ}\n> +\tEOF\n> +\ttest_cmp separator.expect separator.actual &&\n\nI wonder whether we should just bite the bullet and check what gets \nprinted to the screen with test_cmp, rather than grepping for all these \nseparate parts of the message. Is there something in the message that \nmakes that difficult?\n\nThanks\n\nPhillip\n\n>   \tgit log -p -1 --format=\"%gs%n%B\" -g --diff-merges=1 refs/stash >actual &&\n>   \tsed /^index/d actual >actual.trimmed &&\n>   \tcat >expect <<-EOF &&\n> @@ -262,11 +270,18 @@ test_expect_success 'checkout -m creates a recoverable stash on conflict' '\n>   \tgit reset --hard\n>   '\n>   \n> -test_expect_success 'checkout -m which would overwrite untracked file' '\n> +test_expect_success 'checkout -m only retries untracked-file failure with local changes' '\n>   \tgit checkout -f --detach main &&\n>   \ttest_commit another-file &&\n>   \tgit checkout HEAD^ &&\n>   \t>another-file.t &&\n> +\ttest_must_fail env GIT_TRACE2_EVENT=\"$(pwd)/trace\" \\\n> +\t\tgit checkout -m @{-1} 2>err &&\n> +\ttest_grep \"untracked working tree files\" err &&\n> +\tgrep \"\\\"region_enter\\\".*\\\"category\\\":\\\"index\\\",\\\"label\\\":\\\"refresh\\\"\" \\\n> +\t\ttrace >refresh.events &&\n> +\ttest_line_count = 1 refresh.events &&\n> +\n>   \tfill 1 2 3 4 5 >one &&\n>   \ttest_must_fail git checkout -m @{-1} 2>err &&\n>   \tq_to_tab >expect <<-\\EOF &&\n\n"},{"id":"551355","messageId":"9f73c4b4-0336-46be-b39e-6c648f6a69bb@gmail.com","threadId":"66064","inReplyTo":"xmqqld9sg0di.fsf@gitster.g","subject":"Re: [PATCH 0/2] checkout -m: refine autostash fallback","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-08-27T13:12:37Z","receivedAt":"2026-08-27T13:12:49Z","isPatch":true,"body":"On 26/08/2026 20:14, Junio C Hamano wrote:\n>\n> This topic unfortunately has seen no interests from others on the\n> list.  Asking Phillip for help, as the leftoverbits comment cited in\n> the cover letter is from him.\n\nThat'll teach me to make suggestions for further work when I'm reviewing \npatches! Thanks for following up on those comments Harald. I've left \nsome comments, in short I think the changes to the error message are a \ngood idea, but I'm not sure sure about the other change in patch 2 though.\n\nThanks\n\nPhillip\n"},{"id":"551538","messageId":"CAHwyqnVDBihNE8+tbJheO3R5TgfBUgqLHRx=zDxwT3btUi2NZA@mail.gmail.com","threadId":"66064","inReplyTo":"ec6c3986-94ab-4692-a4c2-47569b77e9ca@gmail.com","subject":"Re: [PATCH 1/2] sequencer: teach autostash apply to report conflicts","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-08-31T10:18:08Z","receivedAt":"2026-08-31T10:18:47Z","isPatch":true,"body":"> > Add a conflicted parameter to apply_save_autostash_oid() and\n> > apply_save_autostash_ref() so callers can learn whether applying the\n> > stash resulted in conflicts.  Thread the parameter through\n> > apply_autostash_ref() and update existing callers to pass NULL.\n>\n> It would be nicer to use the return value to indicate\n> success/error/conflicts rather than adding yet another parameter. Apart\n> from that this looks good.\n\nIs the int flexible enough to separate autostash conflict from other\nerrors? Or should we introduce an enum here?\n\n\nHarald\n"},{"id":"551541","messageId":"pull.2364.v2.git.git.1788177601572.gitgitgadget@gmail.com","threadId":"66064","inReplyTo":"pull.2364.git.git.1784993669.gitgitgadget@gmail.com","subject":"[PATCH v2] checkout: print blank line after autostash conflict advice","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-08-31T12:00:01Z","receivedAt":"2026-08-31T12:00:03Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nWhen \"git checkout -m\" stashes the user's local changes and then fails\nto re-apply them because of conflicts, the conflict advice is printed\ndirectly on top of the branch-switch message (\"Switched to branch ...\"),\nmaking the two messages hard to tell apart.  Print a blank line in\nbetween when the stash apply conflicted, so that the advice and the\nbranch-switch message are visually distinct.\n\nTo learn whether the stash apply resulted in conflicts, make the\nautostash apply functions return 1 when the stash could not be applied\ndue to conflicts (and was stored back), while success and error remain\n0 and -1 respectively.  Checkout can then use the return value to decide\nwhether to print the blank line.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n    checkout -m: refine autostash fallback\n    \n    Avoiding checkout -m autostash retries when no tracked local changes\n    exist and visually separating autostash conflict advice from the\n    subsequent branch-switch message. Addresses #leftoverbits from here:\n    https://lore.kernel.org/git/cfd09dbf-8d77-4464-8030-3a0ffb4aeae7@gmail.com/\n    \n    Changes in v2:\n    \n     * Simplify logic and combine to one commit.\n     * Test full output with test_cmp.\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2364%2FHaraldNordgren%2Fhn%2Fgit-checkout-m-leftoverbits-v2\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2364/HaraldNordgren/hn/git-checkout-m-leftoverbits-v2\nPull-Request: https://github.com/git/git/pull/2364\n\nRange-diff vs v1:\n\n 1:  b501b5fcd0 < -:  ---------- sequencer: teach autostash apply to report conflicts\n 2:  37becf38c2 ! 1:  b44c53fcf2 checkout -m: refine autostash fallback\n     @@ Metadata\n      Author: Harald Nordgren <haraldnordgren@gmail.com>\n      \n       ## Commit message ##\n     -    checkout -m: refine autostash fallback\n     +    checkout: print blank line after autostash conflict advice\n      \n     -    When unpack_trees() fails under \"git checkout -m\", only create an\n     -    autostash and retry if there are tracked local changes.  Without such\n     -    changes, the fallback cannot help and merely repeats the same failure.\n     +    When \"git checkout -m\" stashes the user's local changes and then fails\n     +    to re-apply them because of conflicts, the conflict advice is printed\n     +    directly on top of the branch-switch message (\"Switched to branch ...\"),\n     +    making the two messages hard to tell apart.  Print a blank line in\n     +    between when the stash apply conflicted, so that the advice and the\n     +    branch-switch message are visually distinct.\n      \n     -    Use the conflict result from apply_autostash_ref() to print a blank line\n     -    before the branch-switch message, visually separating it from the\n     -    conflict advice.\n     +    To learn whether the stash apply resulted in conflicts, make the\n     +    autostash apply functions return 1 when the stash could not be applied\n     +    due to conflicts (and was stored back), while success and error remain\n     +    0 and -1 respectively.  Checkout can then use the return value to decide\n     +    whether to print the blank line.\n      \n          Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n      \n       ## builtin/checkout.c ##\n     -@@ builtin/checkout.c: static void init_topts(struct unpack_trees_options *topts,\n     - static int merge_working_tree(const struct checkout_opts *opts,\n     - \t\t\t      struct branch_info *old_branch_info,\n     - \t\t\t      struct branch_info *new_branch_info,\n     --\t\t\t      bool quiet,\n     -+\t\t\t      bool allow_autostash,\n     - \t\t\t      int *writeout_error)\n     - {\n     - \tint ret;\n     -+\tbool can_autostash = false;\n     - \tstruct lock_file lock_file = LOCK_INIT;\n     - \tstruct tree *new_tree;\n     - \n     -@@ builtin/checkout.c: static int merge_working_tree(const struct checkout_opts *opts,\n     - \t\t\treturn 1;\n     - \t\t}\n     - \n     -+\t\tif (allow_autostash)\n     -+\t\t\tcan_autostash = has_unstaged_changes(the_repository, 1) ||\n     -+\t\t\t\thas_uncommitted_changes(the_repository, 1);\n     -+\n     - \t\t/* 2-way merge to the new branch */\n     - \t\tinit_topts(&topts, opts->show_progress,\n     --\t\t\t   opts->overwrite_ignore, quiet);\n     -+\t\t\t   opts->overwrite_ignore, can_autostash);\n     - \t\tinit_checkout_metadata(&topts.meta, new_branch_info->refname,\n     - \t\t\t\t       new_branch_info->commit ?\n     - \t\t\t\t       &new_branch_info->commit->object.oid :\n     -@@ builtin/checkout.c: static int merge_working_tree(const struct checkout_opts *opts,\n     - \t\tclear_unpack_trees_porcelain(&topts);\n     - \t\tif (ret == -1) {\n     - \t\t\trollback_lock_file(&lock_file);\n     --\t\t\treturn MERGE_WORKING_TREE_UNPACK_FAILED;\n     -+\t\t\treturn can_autostash ?\n     -+\t\t\t\tMERGE_WORKING_TREE_UNPACK_FAILED : 1;\n     - \t\t}\n     - \t}\n     - \n      @@ builtin/checkout.c: static int switch_branches(const struct checkout_opts *opts,\n       \tint flag, writeout_error = 0;\n       \tint do_merge = 1;\n       \tint created_autostash = 0;\n     -+\tbool autostash_conflicted = false;\n     ++\tint autostash_res = 0;\n       \tstruct strbuf old_commit_shortname = STRBUF_INIT;\n       \tstruct strbuf autostash_msg = STRBUF_INIT;\n       \tconst char *stash_label_base = NULL;\n      @@ builtin/checkout.c: static int switch_branches(const struct checkout_opts *opts,\n     - \t\t\t\t\t    \"local\",\n     - \t\t\t\t\t    stash_label_base,\n     - \t\t\t\t\t    autostash_msg.buf,\n     --\t\t\t\t\t    NULL);\n     -+\t\t\t\t\t    &autostash_conflicted);\n     + \t\t\t\tgit_config_push_parameter(cfg.buf);\n     + \t\t\t\tstrbuf_release(&cfg);\n     + \t\t\t}\n     +-\t\t\tapply_autostash_ref(the_repository,\n     +-\t\t\t\t\t    \"CHECKOUT_AUTOSTASH_HEAD\",\n     +-\t\t\t\t\t    new_branch_info->name,\n     +-\t\t\t\t\t    \"local\",\n     +-\t\t\t\t\t    stash_label_base,\n     +-\t\t\t\t\t    autostash_msg.buf);\n     ++\t\t\tautostash_res = apply_autostash_ref(the_repository,\n     ++\t\t\t\t    \"CHECKOUT_AUTOSTASH_HEAD\",\n     ++\t\t\t\t    new_branch_info->name,\n     ++\t\t\t\t    \"local\",\n     ++\t\t\t\t    stash_label_base,\n     ++\t\t\t\t    autostash_msg.buf);\n       \t\t}\n       \t\tif (ret) {\n       \t\t\tbranch_info_release(&old_branch_info);\n     @@ builtin/checkout.c: static int switch_branches(const struct checkout_opts *opts,\n       \tif (!opts->quiet && !old_branch_info.path && old_branch_info.commit && new_branch_info->commit != old_branch_info.commit)\n       \t\torphaned_commit_warning(old_branch_info.commit, new_branch_info->commit);\n       \n     -+\tif (autostash_conflicted && !opts->quiet)\n     ++\tif (autostash_res == 1 && !opts->quiet)\n      +\t\tfputc('\\n', stderr);\n       \tupdate_refs_for_switch(opts, &old_branch_info, new_branch_info);\n       \n       \tif (created_autostash) {\n      \n     + ## sequencer.c ##\n     +@@ sequencer.c: static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply,\n     + \t\tif (label_base)\n     + \t\t\tstrvec_pushf(&child.args, \"--label-base=%s\", label_base);\n     + \t\tstrvec_push(&child.args, stash_oid);\n     +-\t\tret = run_command(&child);\n     ++\t\tif (run_command(&child))\n     ++\t\t\tret = 1;\n     + \t}\n     + \n     + \tif (attempt_apply && !ret)\n     +\n       ## t/t7201-co.sh ##\n      @@ t/t7201-co.sh: test_expect_success 'checkout -m creates a recoverable stash on conflict' '\n     - \ttest_grep \"git stash drop\" actual &&\n     - \ttest_grep \"git stash pop\" actual &&\n     - \ttest_grep \"The following paths have local changes\" actual &&\n     -+\tsed -n \"/apply the local changes later/,/Switched to branch/p\" \\\n     -+\t\tactual >separator.actual &&\n     -+\tcat >separator.expect <<-EOF &&\n     + \ttest_must_fail git checkout side 2>stderr &&\n     + \ttest_grep \"Your local changes\" stderr &&\n     + \tgit checkout -m side >actual 2>&1 &&\n     +-\ttest_grep \"resulted in conflicts\" actual &&\n     +-\ttest_grep \"git stash drop\" actual &&\n     +-\ttest_grep \"git stash pop\" actual &&\n     +-\ttest_grep \"The following paths have local changes\" actual &&\n     ++\tcat >expect <<-EOF &&\n     ++\tYour local changes are stashed, however applying them\n     ++\tresulted in conflicts.  You can either resolve the conflicts\n     ++\tand then discard the stash with \"git stash drop\", or, if you\n     ++\tdo not want to resolve them now, run \"git reset --hard\" and\n      +\tapply the local changes later by running \"git stash pop\".\n      +\n      +\tSwitched to branch ${SQ}side${SQ}\n     ++\tThe following paths have local changes:\n     ++\tM\tone\n      +\tEOF\n     -+\ttest_cmp separator.expect separator.actual &&\n     ++\ttest_cmp expect actual &&\n       \tgit log -p -1 --format=\"%gs%n%B\" -g --diff-merges=1 refs/stash >actual &&\n       \tsed /^index/d actual >actual.trimmed &&\n       \tcat >expect <<-EOF &&\n     -@@ t/t7201-co.sh: test_expect_success 'checkout -m creates a recoverable stash on conflict' '\n     - \tgit reset --hard\n     - '\n     - \n     --test_expect_success 'checkout -m which would overwrite untracked file' '\n     -+test_expect_success 'checkout -m only retries untracked-file failure with local changes' '\n     - \tgit checkout -f --detach main &&\n     - \ttest_commit another-file &&\n     - \tgit checkout HEAD^ &&\n     - \t>another-file.t &&\n     -+\ttest_must_fail env GIT_TRACE2_EVENT=\"$(pwd)/trace\" \\\n     -+\t\tgit checkout -m @{-1} 2>err &&\n     -+\ttest_grep \"untracked working tree files\" err &&\n     -+\tgrep \"\\\"region_enter\\\".*\\\"category\\\":\\\"index\\\",\\\"label\\\":\\\"refresh\\\"\" \\\n     -+\t\ttrace >refresh.events &&\n     -+\ttest_line_count = 1 refresh.events &&\n     -+\n     - \tfill 1 2 3 4 5 >one &&\n     - \ttest_must_fail git checkout -m @{-1} 2>err &&\n     - \tq_to_tab >expect <<-\\EOF &&\n\n\n builtin/checkout.c | 15 +++++++++------\n sequencer.c        |  3 ++-\n t/t7201-co.sh      | 16 ++++++++++++----\n 3 files changed, 23 insertions(+), 11 deletions(-)\n\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex 55e3a89a85..e4b7383f10 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -1166,6 +1166,7 @@ static int switch_branches(const struct checkout_opts *opts,\n \tint flag, writeout_error = 0;\n \tint do_merge = 1;\n \tint created_autostash = 0;\n+\tint autostash_res = 0;\n \tstruct strbuf old_commit_shortname = STRBUF_INIT;\n \tstruct strbuf autostash_msg = STRBUF_INIT;\n \tconst char *stash_label_base = NULL;\n@@ -1237,12 +1238,12 @@ static int switch_branches(const struct checkout_opts *opts,\n \t\t\t\tgit_config_push_parameter(cfg.buf);\n \t\t\t\tstrbuf_release(&cfg);\n \t\t\t}\n-\t\t\tapply_autostash_ref(the_repository,\n-\t\t\t\t\t    \"CHECKOUT_AUTOSTASH_HEAD\",\n-\t\t\t\t\t    new_branch_info->name,\n-\t\t\t\t\t    \"local\",\n-\t\t\t\t\t    stash_label_base,\n-\t\t\t\t\t    autostash_msg.buf);\n+\t\t\tautostash_res = apply_autostash_ref(the_repository,\n+\t\t\t\t    \"CHECKOUT_AUTOSTASH_HEAD\",\n+\t\t\t\t    new_branch_info->name,\n+\t\t\t\t    \"local\",\n+\t\t\t\t    stash_label_base,\n+\t\t\t\t    autostash_msg.buf);\n \t\t}\n \t\tif (ret) {\n \t\t\tbranch_info_release(&old_branch_info);\n@@ -1255,6 +1256,8 @@ static int switch_branches(const struct checkout_opts *opts,\n \tif (!opts->quiet && !old_branch_info.path && old_branch_info.commit && new_branch_info->commit != old_branch_info.commit)\n \t\torphaned_commit_warning(old_branch_info.commit, new_branch_info->commit);\n \n+\tif (autostash_res == 1 && !opts->quiet)\n+\t\tfputc('\\n', stderr);\n \tupdate_refs_for_switch(opts, &old_branch_info, new_branch_info);\n \n \tif (created_autostash) {\ndiff --git a/sequencer.c b/sequencer.c\nindex 65afd100d9..5ed9ae86c4 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -4815,7 +4815,8 @@ static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply,\n \t\tif (label_base)\n \t\t\tstrvec_pushf(&child.args, \"--label-base=%s\", label_base);\n \t\tstrvec_push(&child.args, stash_oid);\n-\t\tret = run_command(&child);\n+\t\tif (run_command(&child))\n+\t\t\tret = 1;\n \t}\n \n \tif (attempt_apply && !ret)\ndiff --git a/t/t7201-co.sh b/t/t7201-co.sh\nindex 0ddd1ad7aa..9ea9462914 100755\n--- a/t/t7201-co.sh\n+++ b/t/t7201-co.sh\n@@ -236,10 +236,18 @@ test_expect_success 'checkout -m creates a recoverable stash on conflict' '\n \ttest_must_fail git checkout side 2>stderr &&\n \ttest_grep \"Your local changes\" stderr &&\n \tgit checkout -m side >actual 2>&1 &&\n-\ttest_grep \"resulted in conflicts\" actual &&\n-\ttest_grep \"git stash drop\" actual &&\n-\ttest_grep \"git stash pop\" actual &&\n-\ttest_grep \"The following paths have local changes\" actual &&\n+\tcat >expect <<-EOF &&\n+\tYour local changes are stashed, however applying them\n+\tresulted in conflicts.  You can either resolve the conflicts\n+\tand then discard the stash with \"git stash drop\", or, if you\n+\tdo not want to resolve them now, run \"git reset --hard\" and\n+\tapply the local changes later by running \"git stash pop\".\n+\n+\tSwitched to branch ${SQ}side${SQ}\n+\tThe following paths have local changes:\n+\tM\tone\n+\tEOF\n+\ttest_cmp expect actual &&\n \tgit log -p -1 --format=\"%gs%n%B\" -g --diff-merges=1 refs/stash >actual &&\n \tsed /^index/d actual >actual.trimmed &&\n \tcat >expect <<-EOF &&\n\nbase-commit: c73e85354c275c9d409b26445089bc16940fc527\n-- \ngitgitgadget\n"},{"id":"551553","messageId":"4813f6a6-69f0-48ff-9558-6339949efdd3@gmail.com","threadId":"66064","inReplyTo":"CAHwyqnVDBihNE8+tbJheO3R5TgfBUgqLHRx=zDxwT3btUi2NZA@mail.gmail.com","subject":"Re: [PATCH 1/2] sequencer: teach autostash apply to report conflicts","fromName":"","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-08-31T13:15:07Z","receivedAt":"2026-08-31T13:15:16Z","isPatch":true,"body":"On 31/08/2026 11:18, Harald Nordgren wrote:\n>>> Add a conflicted parameter to apply_save_autostash_oid() and\n>>> apply_save_autostash_ref() so callers can learn whether applying the\n>>> stash resulted in conflicts.  Thread the parameter through\n>>> apply_autostash_ref() and update existing callers to pass NULL.\n>>\n>> It would be nicer to use the return value to indicate\n>> success/error/conflicts rather than adding yet another parameter. Apart\n>> from that this looks good.\n> \n> Is the int flexible enough to separate autostash conflict from other\n> errors? Or should we introduce an enum here?\n\nI think a tri-state enum would be clearest - the callers are only \ninterested it \"stash applied without conflicts\", \"stash applied with \nconflicts\" and \"something went wrong - could not apply stash\"\n\nThanks\n\nPhillip\n\n"},{"id":"551576","messageId":"xmqq4igaxl5t.fsf@gitster.g","threadId":"66064","inReplyTo":"pull.2364.v2.git.git.1788177601572.gitgitgadget@gmail.com","subject":"Re: [PATCH v2] checkout: print blank line after autostash conflict advice","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-08-31T17:19:26Z","receivedAt":"2026-08-31T17:19:29Z","isPatch":true,"body":"\"Harald Nordgren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> To learn whether the stash apply resulted in conflicts, make the\n> autostash apply functions return 1 when the stash could not be applied\n> due to conflicts (and was stored back), while success and error remain\n> 0 and -1 respectively.  Checkout can then use the return value to decide\n> whether to print the blank line.\n\n> @@ -1237,12 +1238,12 @@ static int switch_branches(const struct checkout_opts *opts,\n>  \t\t\t\tgit_config_push_parameter(cfg.buf);\n>  \t\t\t\tstrbuf_release(&cfg);\n>  \t\t\t}\n> -\t\t\tapply_autostash_ref(the_repository,\n> -\t\t\t\t\t    \"CHECKOUT_AUTOSTASH_HEAD\",\n> -\t\t\t\t\t    new_branch_info->name,\n> -\t\t\t\t\t    \"local\",\n> -\t\t\t\t\t    stash_label_base,\n> -\t\t\t\t\t    autostash_msg.buf);\n> +\t\t\tautostash_res = apply_autostash_ref(the_repository,\n> +\t\t\t\t    \"CHECKOUT_AUTOSTASH_HEAD\",\n> +\t\t\t\t    new_branch_info->name,\n> +\t\t\t\t    \"local\",\n> +\t\t\t\t    stash_label_base,\n> +\t\t\t\t    autostash_msg.buf);\n>  \t\t}\n>  \t\tif (ret) {\n>  \t\t\tbranch_info_release(&old_branch_info);\n> @@ -1255,6 +1256,8 @@ static int switch_branches(const struct checkout_opts *opts,\n>  \tif (!opts->quiet && !old_branch_info.path && old_branch_info.commit && new_branch_info->commit != old_branch_info.commit)\n>  \t\torphaned_commit_warning(old_branch_info.commit, new_branch_info->commit);\n>  \n> +\tif (autostash_res == 1 && !opts->quiet)\n> +\t\tfputc('\\n', stderr);\n>  \tupdate_refs_for_switch(opts, &old_branch_info, new_branch_info);\n>  \n>  \tif (created_autostash) {\n> diff --git a/sequencer.c b/sequencer.c\n> index 65afd100d9..5ed9ae86c4 100644\n> --- a/sequencer.c\n> +++ b/sequencer.c\n> @@ -4815,7 +4815,8 @@ static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply,\n>  \t\tif (label_base)\n>  \t\t\tstrvec_pushf(&child.args, \"--label-base=%s\", label_base);\n>  \t\tstrvec_push(&child.args, stash_oid);\n> -\t\tret = run_command(&child);\n> +\t\tif (run_command(&child))\n> +\t\t\tret = 1;\n>  \t}\n\nThis does not look like the right way to have the function return 1\nif the objective is to do so only when the spawned \"git stash apply\n<oid>\" process fails due to conflicts.\n\nThe reasons for a non-zero return value from run_command() range\nfrom failing to find the command to run, to the command crashing, to\nthe command voluntarily exiting with a fixed exit status to signal\nhow exactly it exited to the spawning process.\n\nSkimming bultin/stash.c::do_apply_stash(), I find this bit:\n\n\tclean = merge_ort_nonrecursive(&o, head, merge, merge_base);\n\n\t/*\n\t * If 'clean' >= 0, reverse the value for 'ret' so 'ret' is 0 when the\n\t * merge was clean, and nonzero if the merge was unclean or encountered\n\t * an error.\n\t */\n\tret = clean >= 0 ? !clean : clean;\n\nIf 'clean' is 0 (i.e., unclean merge), we assign 1 (== !clean) to\nret.  If 'clean' is 1 or more (presumably a clean merge?  Check what\nmerge_ort_nonrecursive() does for details---I didn't), return 0.  If\nmerge-ort gave an error (i.e., negative), use it as-is.  And this\n'ret' is eventually returned to the caller apply_stash() , which\nreturns that value as-is.\n\nThe value is given back to builtin/stash.c::cmd_stash() and the more\ndetailed exit value unfortunately discarded ehre:\n\n\tif (fn)\n\t\treturn !!fn(argc, argv, prefix, repo);\n\nbut if one wants to implement \n\n    > To learn whether the stash apply resulted in conflicts, make the\n    > autostash apply functions return 1 when the stash could not be applied\n    > due to conflicts (and was stored back),...\n\ncorrectly, I think this patch needs a bit more work.  \n\n (1) Find the values returned by foo_stash() implementations of all\n     'git stash foo' subcommands.  Do they follow the 0 == success,\n     negative == failure, positive == something else convention?\n\n (2) Update the code above to something like\n\n\tif (fn) {\n\t\tint ret = fn(argc, argv, prefix, repo);\n\n\t\tif (ret < 0)\n\t\t\treturn 1; /* as before */\n\t\tif (!ret)\n\t\t\treturn 0; /* as before */\n\t\treturn ret + 1;\n\t}\n\n      or with whatever necessary tweak to allow the positive \"I have\n      something to tell the spawning process\" case from the bog\n      standard and boring 'return error(\"message\");' cases.\n\n (3) document what each of 'git stash foo' signals with its exit\n     status.\n\nFor expediency, it may be OK to assume any and all failures from\n\"git stash apply <oid>\" come from a conflicted stash application in\nyour first version.  If that is what your reviewer recommended, I\nwould agree.  But let's help users and future developers (who do not\nnecessarily have to be you) by leaving a note that this code is not\ndoing what it claims to do and needs more work in the code.\n\nThanks.\n"},{"id":"551641","messageId":"af051ecf-0d94-4dc1-a6e5-0184b2b6e1f1@gmail.com","threadId":"66064","inReplyTo":"xmqq4igaxl5t.fsf@gitster.g","subject":"Re: [PATCH v2] checkout: print blank line after autostash conflict advice","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-09-01T09:31:42Z","receivedAt":"2026-09-01T09:31:54Z","isPatch":true,"body":"On 31/08/2026 18:19, Junio C Hamano wrote:\n> \"Harald Nordgren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n> \n>> diff --git a/sequencer.c b/sequencer.c\n>> index 65afd100d9..5ed9ae86c4 100644\n>> --- a/sequencer.c\n>> +++ b/sequencer.c\n>> @@ -4815,7 +4815,8 @@ static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply,\n>>   \t\tif (label_base)\n>>   \t\t\tstrvec_pushf(&child.args, \"--label-base=%s\", label_base);\n>>   \t\tstrvec_push(&child.args, stash_oid);\n>> -\t\tret = run_command(&child);\n>> +\t\tif (run_command(&child))\n>> +\t\t\tret = 1;\n>>   \t}\n> \n> This does not look like the right way to have the function return 1\n> if the objective is to do so only when the spawned \"git stash apply\n> <oid>\" process fails due to conflicts.\n> \n> [...]\n > > For expediency, it may be OK to assume any and all failures from\n> \"git stash apply <oid>\" come from a conflicted stash application in\n> your first version.  If that is what your reviewer recommended, I\n> would agree.  But let's help users and future developers (who do not\n> necessarily have to be you) by leaving a note that this code is not\n> doing what it claims to do and needs more work in the code.\n\nI think if the objective of this patch is to tell the caller whether the \nconflicts message was printed or not then it is correct because the \nexisting code is too caviler about printing that message. We should at \nleast tighten that even if we don't change \"git stash\" (which I agree we \nshould fix at some point).\n\n\tret = run_command(&child);\n\tif (ret > 1)\n\t\tret = -1;\n\nwould catch run_command() failing and stash dying or being killed by a \nsignal. Then we should change the code below so that it only claims \nthere were conflicts when \"ret == 1\" and prints a new error message \nexplaining that \"git stash apply\" failed when \"ret == -1\"\n\nThanks\n\nPhillip\n\n"},{"id":"551642","messageId":"pull.2364.v3.git.git.1788256199679.gitgitgadget@gmail.com","threadId":"66064","inReplyTo":"pull.2364.git.git.1784993669.gitgitgadget@gmail.com","subject":"[PATCH v3] checkout: separate autostash conflict advice from branch-switch message","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-01T09:49:59Z","receivedAt":"2026-09-01T09:50:02Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\n\"git checkout -m\" stashes the user's local changes when it cannot\nperform the checkout, and then applies the stash.  When applying the\nstash results in conflicts, the advice on how to deal with them is\nprinted directly on top of the branch-switch message (\"Switched to\nbranch ...\"), making the two hard to tell apart.  Print a blank line\nin between so that the advice and the branch-switch message are\nvisually distinct.\n\nTo make this possible, \"git stash apply\", \"pop\" and \"branch\" now exit\nwith status 2 when applying the stash entry resulted in conflicts, in\nwhich case the stash entry is left in place; other failures exit with\nstatus 1, as before.  The exit statuses are documented in the \"git\nstash\" documentation.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n    checkout -m: refine autostash fallback\n    \n    Avoiding checkout -m autostash retries when no tracked local changes\n    exist and visually separating autostash conflict advice from the\n    subsequent branch-switch message. Addresses #leftoverbits from here:\n    https://lore.kernel.org/git/cfd09dbf-8d77-4464-8030-3a0ffb4aeae7@gmail.com/\n    \n    Changes in v3:\n    \n     * Use enum for git stash return values, to separate conflict from\n       generic error.\n    \n    Changes in v2:\n    \n     * Simplify logic and combine to one commit.\n     * Test full output with test_cmp.\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2364%2FHaraldNordgren%2Fhn%2Fgit-checkout-m-leftoverbits-v3\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2364/HaraldNordgren/hn/git-checkout-m-leftoverbits-v3\nPull-Request: https://github.com/git/git/pull/2364\n\nRange-diff vs v2:\n\n 1:  b44c53fcf2 ! 1:  8e1979dd6c checkout: print blank line after autostash conflict advice\n     @@ Metadata\n      Author: Harald Nordgren <haraldnordgren@gmail.com>\n      \n       ## Commit message ##\n     -    checkout: print blank line after autostash conflict advice\n     +    checkout: separate autostash conflict advice from branch-switch message\n      \n     -    When \"git checkout -m\" stashes the user's local changes and then fails\n     -    to re-apply them because of conflicts, the conflict advice is printed\n     -    directly on top of the branch-switch message (\"Switched to branch ...\"),\n     -    making the two messages hard to tell apart.  Print a blank line in\n     -    between when the stash apply conflicted, so that the advice and the\n     -    branch-switch message are visually distinct.\n     +    \"git checkout -m\" stashes the user's local changes when it cannot\n     +    perform the checkout, and then applies the stash.  When applying the\n     +    stash results in conflicts, the advice on how to deal with them is\n     +    printed directly on top of the branch-switch message (\"Switched to\n     +    branch ...\"), making the two hard to tell apart.  Print a blank line\n     +    in between so that the advice and the branch-switch message are\n     +    visually distinct.\n      \n     -    To learn whether the stash apply resulted in conflicts, make the\n     -    autostash apply functions return 1 when the stash could not be applied\n     -    due to conflicts (and was stored back), while success and error remain\n     -    0 and -1 respectively.  Checkout can then use the return value to decide\n     -    whether to print the blank line.\n     +    To make this possible, \"git stash apply\", \"pop\" and \"branch\" now exit\n     +    with status 2 when applying the stash entry resulted in conflicts, in\n     +    which case the stash entry is left in place; other failures exit with\n     +    status 1, as before.  The exit statuses are documented in the \"git\n     +    stash\" documentation.\n      \n          Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n      \n     + ## Documentation/git-stash.adoc ##\n     +@@ Documentation/git-stash.adoc: include::includes/cmd-config-section-all.adoc[]\n     + :git-stash: 1\n     + include::config/stash.adoc[]\n     + \n     ++EXIT STATUS\n     ++-----------\n     ++\n     ++The `git stash` subcommands exit with status 0 on success and non-zero\n     ++on failure.  The subcommands that apply a stash entry, i.e. `apply`,\n     ++`pop` and `branch`, exit with status 2 when applying the stash entry\n     ++resulted in conflicts, in which case the stash entry is left in place.\n     ++Other failures exit with status 1 (usage errors exit with status 129).\n     ++\n     + \n     + SEE ALSO\n     + --------\n     +\n       ## builtin/checkout.c ##\n      @@ builtin/checkout.c: static int switch_branches(const struct checkout_opts *opts,\n       \tint flag, writeout_error = 0;\n       \tint do_merge = 1;\n       \tint created_autostash = 0;\n     -+\tint autostash_res = 0;\n     ++\tenum stash_apply_result autostash_res = STASH_APPLY_CLEAN;\n       \tstruct strbuf old_commit_shortname = STRBUF_INIT;\n       \tstruct strbuf autostash_msg = STRBUF_INIT;\n       \tconst char *stash_label_base = NULL;\n     @@ builtin/checkout.c: static int switch_branches(const struct checkout_opts *opts,\n       \tif (!opts->quiet && !old_branch_info.path && old_branch_info.commit && new_branch_info->commit != old_branch_info.commit)\n       \t\torphaned_commit_warning(old_branch_info.commit, new_branch_info->commit);\n       \n     -+\tif (autostash_res == 1 && !opts->quiet)\n     ++\tif (autostash_res == STASH_APPLY_CONFLICT && !opts->quiet)\n      +\t\tfputc('\\n', stderr);\n       \tupdate_refs_for_switch(opts, &old_branch_info, new_branch_info);\n       \n       \tif (created_autostash) {\n      \n     + ## builtin/stash.c ##\n     +@@\n     + #include \"object-name.h\"\n     + #include \"parse-options.h\"\n     + #include \"refs.h\"\n     ++#include \"stash.h\"\n     + #include \"lockfile.h\"\n     + #include \"cache-tree.h\"\n     + #include \"unpack-trees.h\"\n     +@@ builtin/stash.c: static void unstage_changes_unless_new(struct object_id *orig_tree)\n     + \t\tdie(_(\"could not write index\"));\n     + }\n     + \n     +-static int do_apply_stash(const char *prefix, struct stash_info *info,\n     +-\t\t\t  int index, int quiet,\n     ++static enum stash_apply_result do_apply_stash(const char *prefix,\n     ++\t\t\t\t\t      struct stash_info *info,\n     ++\t\t\t\t\t      int index, int quiet,\n     + \t\t\t  const char *label_ours, const char *label_theirs,\n     + \t\t\t  const char *label_base)\n     + {\n     +@@ builtin/stash.c: static int do_apply_stash(const char *prefix, struct stash_info *info,\n     + \tclean = merge_ort_nonrecursive(&o, head, merge, merge_base);\n     + \n     + \t/*\n     +-\t * If 'clean' >= 0, reverse the value for 'ret' so 'ret' is 0 when the\n     +-\t * merge was clean, and nonzero if the merge was unclean or encountered\n     +-\t * an error.\n     ++\t * Translate the value of 'clean' so 'ret' is STASH_APPLY_CLEAN\n     ++\t * when the merge was clean, STASH_APPLY_CONFLICT when it was\n     ++\t * unclean, and a negative value if it encountered an error.\n     + \t */\n     +-\tret = clean >= 0 ? !clean : clean;\n     ++\tret = clean >= 0 ? (clean ? STASH_APPLY_CLEAN : STASH_APPLY_CONFLICT)\n     ++\t\t\t : clean;\n     + \n     + \tif (ret < 0)\n     + \t\trollback_lock_file(&lock);\n     +@@ builtin/stash.c: static int do_apply_stash(const char *prefix, struct stash_info *info,\n     + \n     + \tif (has_index) {\n     + \t\tif (reset_tree(&index_tree, 0, 0))\n     +-\t\t\tret = -1;\n     ++\t\t\tret = STASH_APPLY_ERROR;\n     + \t} else {\n     + \t\tunstage_changes_unless_new(&c_tree);\n     + \t}\n     +@@ builtin/stash.c: int cmd_stash(int argc,\n     + \tstrbuf_addf(&stash_index_path, \"%s.stash.%\" PRIuMAX, index_file,\n     + \t\t    (uintmax_t)pid);\n     + \n     +-\tif (fn)\n     +-\t\treturn !!fn(argc, argv, prefix, repo);\n     +-\telse if (!argc)\n     ++\tif (fn) {\n     ++\t\tret = fn(argc, argv, prefix, repo);\n     ++\n     ++\t\tif (ret < 0)\n     ++\t\t\treturn 1;\n     ++\t\treturn ret;\n     ++\t} else if (!argc)\n     + \t\treturn !!push_stash_unassumed(0, NULL, prefix, repo);\n     + \n     + \t/* Assume 'stash push' */\n     +\n       ## sequencer.c ##\n     +@@\n     + #include \"commit.h\"\n     + #include \"sequencer.h\"\n     + #include \"run-command.h\"\n     ++#include \"stash.h\"\n     + #include \"hook.h\"\n     + #include \"utf8.h\"\n     + #include \"cache-tree.h\"\n     +@@ sequencer.c: void create_autostash_ref(struct repository *r, const char *refname,\n     + \tcreate_autostash_internal(r, NULL, refname, message, silent);\n     + }\n     + \n     +-static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply,\n     +-\t\t\t\t    const char *label_ours, const char *label_theirs,\n     +-\t\t\t\t    const char *label_base,\n     +-\t\t\t\t    const char *stash_msg)\n     ++static enum stash_apply_result apply_save_autostash_oid(const char *stash_oid,\n     ++\t\t\t\t\t\t\tint attempt_apply,\n     ++\t\t\t\t\t\t\tconst char *label_ours,\n     ++\t\t\t\t\t\t\tconst char *label_theirs,\n     ++\t\t\t\t\t\t\tconst char *label_base,\n     ++\t\t\t\t\t\t\tconst char *stash_msg)\n     + {\n     + \tstruct child_process child = CHILD_PROCESS_INIT;\n     +-\tint ret = 0;\n     ++\tenum stash_apply_result ret = STASH_APPLY_CLEAN;\n     + \n     + \tif (attempt_apply) {\n     + \t\tchild.git_cmd = 1;\n      @@ sequencer.c: static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply,\n     - \t\tif (label_base)\n       \t\t\tstrvec_pushf(&child.args, \"--label-base=%s\", label_base);\n       \t\tstrvec_push(&child.args, stash_oid);\n     --\t\tret = run_command(&child);\n     -+\t\tif (run_command(&child))\n     -+\t\t\tret = 1;\n     + \t\tret = run_command(&child);\n     ++\t\tif (ret && ret != STASH_APPLY_CONFLICT)\n     ++\t\t\tret = STASH_APPLY_ERROR;\n       \t}\n       \n     - \tif (attempt_apply && !ret)\n     +-\tif (attempt_apply && !ret)\n     ++\tif (attempt_apply && ret == STASH_APPLY_CLEAN)\n     + \t\tfprintf(stderr, _(\"Applied autostash.\\n\"));\n     + \telse {\n     + \t\tstruct child_process store = CHILD_PROCESS_INIT;\n     +@@ sequencer.c: static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply,\n     + \t\tstrvec_push(&store.args, stash_oid);\n     + \t\tif (run_command(&store))\n     + \t\t\tret = error(_(\"cannot store %s\"), stash_oid);\n     +-\t\telse if (attempt_apply)\n     ++\t\telse if (attempt_apply && ret == STASH_APPLY_CONFLICT)\n     + \t\t\tfprintf(stderr,\n     + \t\t\t\t_(\"Your local changes are stashed, however applying them\\n\"\n     + \t\t\t\t  \"resulted in conflicts.  You can either resolve the conflicts\\n\"\n     + \t\t\t\t  \"and then discard the stash with \\\"git stash drop\\\", or, if you\\n\"\n     + \t\t\t\t  \"do not want to resolve them now, run \\\"git reset --hard\\\" and\\n\"\n     + \t\t\t\t  \"apply the local changes later by running \\\"git stash pop\\\".\\n\"));\n     ++\t\telse if (attempt_apply)\n     ++\t\t\tret = error(_(\"could not apply autostash\"));\n     + \t\telse\n     + \t\t\tfprintf(stderr,\n     + \t\t\t\t_(\"Autostash exists; creating a new stash entry.\\n\"\n     +@@ sequencer.c: static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply,\n     + \treturn ret;\n     + }\n     + \n     +-static int apply_save_autostash(const char *path, int attempt_apply)\n     ++static enum stash_apply_result apply_save_autostash(const char *path,\n     ++\t\t\t\t\t\t    int attempt_apply)\n     + {\n     + \tstruct strbuf stash_oid = STRBUF_INIT;\n     +-\tint ret = 0;\n     ++\tenum stash_apply_result ret = STASH_APPLY_CLEAN;\n     + \n     + \tif (!read_oneliner(&stash_oid, path,\n     + \t\t\t   READ_ONELINER_SKIP_IF_EMPTY)) {\n     + \t\tstrbuf_release(&stash_oid);\n     +-\t\treturn 0;\n     ++\t\treturn STASH_APPLY_CLEAN;\n     + \t}\n     + \tstrbuf_trim(&stash_oid);\n     + \n     +@@ sequencer.c: static int apply_save_autostash(const char *path, int attempt_apply)\n     + \treturn ret;\n     + }\n     + \n     +-int save_autostash(const char *path)\n     ++enum stash_apply_result save_autostash(const char *path)\n     + {\n     + \treturn apply_save_autostash(path, 0);\n     + }\n     + \n     +-int apply_autostash(const char *path)\n     ++enum stash_apply_result apply_autostash(const char *path)\n     + {\n     + \treturn apply_save_autostash(path, 1);\n     + }\n     + \n     +-int apply_autostash_oid(const char *stash_oid)\n     ++enum stash_apply_result apply_autostash_oid(const char *stash_oid)\n     + {\n     + \treturn apply_save_autostash_oid(stash_oid, 1, NULL, NULL, NULL, NULL);\n     + }\n     + \n     +-static int apply_save_autostash_ref(struct repository *r, const char *refname,\n     +-\t\t\t\t    int attempt_apply,\n     +-\t\t\t\t    const char *label_ours, const char *label_theirs,\n     +-\t\t\t\t    const char *label_base,\n     +-\t\t\t\t    const char *stash_msg)\n     ++static enum stash_apply_result apply_save_autostash_ref(struct repository *r,\n     ++\t\t\t\t\t\t\tconst char *refname,\n     ++\t\t\t\t\t\t\tint attempt_apply,\n     ++\t\t\t\t\t\t\tconst char *label_ours,\n     ++\t\t\t\t\t\t\tconst char *label_theirs,\n     ++\t\t\t\t\t\t\tconst char *label_base,\n     ++\t\t\t\t\t\t\tconst char *stash_msg)\n     + {\n     + \tstruct object_id stash_oid;\n     + \tchar stash_oid_hex[GIT_MAX_HEXSZ + 1];\n     +-\tint flag, ret;\n     ++\tint flag;\n     ++\tenum stash_apply_result ret;\n     + \n     + \tif (!refs_ref_exists(get_main_ref_store(r), refname))\n     +-\t\treturn 0;\n     ++\t\treturn STASH_APPLY_CLEAN;\n     + \n     + \tif (!refs_resolve_ref_unsafe(get_main_ref_store(r), refname,\n     + \t\t\t\t     RESOLVE_REF_READING, &stash_oid, &flag))\n     +-\t\treturn -1;\n     ++\t\treturn STASH_APPLY_ERROR;\n     + \tif (flag & REF_ISSYMREF)\n     + \t\treturn error(_(\"autostash reference is a symref\"));\n     + \n     +@@ sequencer.c: static int apply_save_autostash_ref(struct repository *r, const char *refname,\n     + \treturn ret;\n     + }\n     + \n     +-int save_autostash_ref(struct repository *r, const char *refname)\n     ++enum stash_apply_result save_autostash_ref(struct repository *r,\n     ++\t\t\t\t\t   const char *refname)\n     + {\n     + \treturn apply_save_autostash_ref(r, refname, 0,\n     + \t\t\t\t\tNULL, NULL, NULL, NULL);\n     + }\n     + \n     +-int apply_autostash_ref(struct repository *r, const char *refname,\n     +-\t\t\tconst char *label_ours, const char *label_theirs,\n     +-\t\t\tconst char *label_base, const char *stash_msg)\n     ++enum stash_apply_result apply_autostash_ref(struct repository *r,\n     ++\t\t\t\t\t    const char *refname,\n     ++\t\t\t\t\t    const char *label_ours,\n     ++\t\t\t\t\t    const char *label_theirs,\n     ++\t\t\t\t\t    const char *label_base,\n     ++\t\t\t\t\t    const char *stash_msg)\n     + {\n     + \treturn apply_save_autostash_ref(r, refname, 1,\n     + \t\t\t\t\tlabel_ours, label_theirs, label_base,\n     +\n     + ## sequencer.h ##\n     +@@\n     + \n     + #include \"strbuf.h\"\n     + #include \"strvec.h\"\n     ++#include \"stash.h\"\n     + #include \"wt-status.h\"\n     + \n     + struct commit;\n     +@@ sequencer.h: void commit_post_rewrite(struct repository *r,\n     + void create_autostash(struct repository *r, const char *path);\n     + void create_autostash_ref(struct repository *r, const char *refname,\n     + \t\t\t  const char *message, bool silent);\n     +-int save_autostash(const char *path);\n     +-int save_autostash_ref(struct repository *r, const char *refname);\n     +-int apply_autostash(const char *path);\n     +-int apply_autostash_oid(const char *stash_oid);\n     +-int apply_autostash_ref(struct repository *r, const char *refname,\n     +-\t\t\tconst char *label_ours, const char *label_theirs,\n     +-\t\t\tconst char *label_base, const char *stash_msg);\n     ++enum stash_apply_result save_autostash(const char *path);\n     ++enum stash_apply_result save_autostash_ref(struct repository *r,\n     ++\t\t\t\t\t   const char *refname);\n     ++enum stash_apply_result apply_autostash(const char *path);\n     ++enum stash_apply_result apply_autostash_oid(const char *stash_oid);\n     ++enum stash_apply_result apply_autostash_ref(struct repository *r,\n     ++\t\t\t\t\t    const char *refname,\n     ++\t\t\t\t\t    const char *label_ours,\n     ++\t\t\t\t\t    const char *label_theirs,\n     ++\t\t\t\t\t    const char *label_base,\n     ++\t\t\t\t\t    const char *stash_msg);\n     + \n     + #define SUMMARY_INITIAL_COMMIT   (1 << 0)\n     + #define SUMMARY_SHOW_AUTHOR_DATE (1 << 1)\n     +\n     + ## stash.h (new) ##\n     +@@\n     ++#ifndef STASH_H\n     ++#define STASH_H\n     ++\n     ++enum stash_apply_result {\n     ++\t/* The stash was applied cleanly, or there was nothing to apply. */\n     ++\tSTASH_APPLY_CLEAN = 0,\n     ++\n     ++\t/*\n     ++\t * The stash could not be applied because it resulted in\n     ++\t * conflicts.  The stash entry is left in place.  The \"git stash\n     ++\t * apply\", \"pop\" and \"branch\" subcommands exit with this status\n     ++\t * in this case.\n     ++\t */\n     ++\tSTASH_APPLY_CONFLICT = 2,\n     ++\n     ++\t/* Something went wrong. */\n     ++\tSTASH_APPLY_ERROR = -1,\n     ++};\n     ++\n     ++#endif /* STASH_H */\n     +\n     + ## t/t3903-stash.sh ##\n     +@@ t/t3903-stash.sh: test_expect_success 'apply with custom conflict labels' '\n     + \techo stashed >conflict-file &&\n     + \tgit stash push -m \"stashed\" &&\n     + \ttest_commit label-upstream conflict-file upstream-content &&\n     +-\ttest_must_fail git -c merge.conflictStyle=diff3 stash apply --label-ours=UP --label-theirs=STASH &&\n     ++\ttest_expect_code 2 git -c merge.conflictStyle=diff3 stash apply --label-ours=UP --label-theirs=STASH &&\n     + \ttest_grep \"^<<<<<<< UP\" conflict-file &&\n     + \ttest_grep \"^||||||| Stash base\" conflict-file &&\n     + \ttest_grep \"^>>>>>>> STASH\" conflict-file\n     +@@ t/t3903-stash.sh: test_expect_success 'apply with empty conflict labels' '\n     + \techo stashed >conflict-file &&\n     + \tgit stash push -m \"stashed\" &&\n     + \ttest_commit empty-label-upstream conflict-file upstream-content &&\n     +-\ttest_must_fail git stash apply --label-ours= --label-theirs= &&\n     ++\ttest_expect_code 2 git stash apply --label-ours= --label-theirs= &&\n     + \ttest_grep \"^<<<<<<<$\" conflict-file &&\n     + \ttest_grep \"^>>>>>>>$\" conflict-file\n     + '\n     + \n     ++test_expect_success 'apply exits 2 on conflicts and keeps the stash entry' '\n     ++\tgit reset --hard initial &&\n     ++\ttest_commit exit-code-base conflict-file base-content &&\n     ++\techo stashed >conflict-file &&\n     ++\tgit stash push -m stashed &&\n     ++\ttest_commit exit-code-upstream conflict-file upstream-content &&\n     ++\ttest_expect_code 2 git stash apply &&\n     ++\tgit stash list >list &&\n     ++\ttest_grep stashed list\n     ++'\n     ++\n     ++test_expect_success 'pop exits 2 on conflicts and keeps the stash entry' '\n     ++\tgit reset --hard initial &&\n     ++\ttest_commit pop-exit-code-base pop-file base-content &&\n     ++\techo stashed >pop-file &&\n     ++\tgit stash push -m pop-stashed &&\n     ++\ttest_commit pop-exit-code-upstream pop-file upstream-content &&\n     ++\ttest_expect_code 2 git stash pop &&\n     ++\tgit stash list >list &&\n     ++\ttest_grep pop-stashed list\n     ++'\n     ++\n     + test_expect_success 'stash show --include-untracked includes untracked files' '\n     + \tgit reset --hard &&\n     + \n      \n       ## t/t7201-co.sh ##\n      @@ t/t7201-co.sh: test_expect_success 'checkout -m creates a recoverable stash on conflict' '\n\n\n Documentation/git-stash.adoc |  9 +++++\n builtin/checkout.c           | 15 +++++----\n builtin/stash.c              | 27 +++++++++------\n sequencer.c                  | 65 ++++++++++++++++++++++--------------\n sequencer.h                  | 19 +++++++----\n stash.h                      | 20 +++++++++++\n t/t3903-stash.sh             | 26 +++++++++++++--\n t/t7201-co.sh                | 16 ++++++---\n 8 files changed, 143 insertions(+), 54 deletions(-)\n create mode 100644 stash.h\n\ndiff --git a/Documentation/git-stash.adoc b/Documentation/git-stash.adoc\nindex 50bb89f483..3e41ffcf43 100644\n--- a/Documentation/git-stash.adoc\n+++ b/Documentation/git-stash.adoc\n@@ -426,6 +426,15 @@ include::includes/cmd-config-section-all.adoc[]\n :git-stash: 1\n include::config/stash.adoc[]\n \n+EXIT STATUS\n+-----------\n+\n+The `git stash` subcommands exit with status 0 on success and non-zero\n+on failure.  The subcommands that apply a stash entry, i.e. `apply`,\n+`pop` and `branch`, exit with status 2 when applying the stash entry\n+resulted in conflicts, in which case the stash entry is left in place.\n+Other failures exit with status 1 (usage errors exit with status 129).\n+\n \n SEE ALSO\n --------\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex 55e3a89a85..c960f98287 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -1166,6 +1166,7 @@ static int switch_branches(const struct checkout_opts *opts,\n \tint flag, writeout_error = 0;\n \tint do_merge = 1;\n \tint created_autostash = 0;\n+\tenum stash_apply_result autostash_res = STASH_APPLY_CLEAN;\n \tstruct strbuf old_commit_shortname = STRBUF_INIT;\n \tstruct strbuf autostash_msg = STRBUF_INIT;\n \tconst char *stash_label_base = NULL;\n@@ -1237,12 +1238,12 @@ static int switch_branches(const struct checkout_opts *opts,\n \t\t\t\tgit_config_push_parameter(cfg.buf);\n \t\t\t\tstrbuf_release(&cfg);\n \t\t\t}\n-\t\t\tapply_autostash_ref(the_repository,\n-\t\t\t\t\t    \"CHECKOUT_AUTOSTASH_HEAD\",\n-\t\t\t\t\t    new_branch_info->name,\n-\t\t\t\t\t    \"local\",\n-\t\t\t\t\t    stash_label_base,\n-\t\t\t\t\t    autostash_msg.buf);\n+\t\t\tautostash_res = apply_autostash_ref(the_repository,\n+\t\t\t\t    \"CHECKOUT_AUTOSTASH_HEAD\",\n+\t\t\t\t    new_branch_info->name,\n+\t\t\t\t    \"local\",\n+\t\t\t\t    stash_label_base,\n+\t\t\t\t    autostash_msg.buf);\n \t\t}\n \t\tif (ret) {\n \t\t\tbranch_info_release(&old_branch_info);\n@@ -1255,6 +1256,8 @@ static int switch_branches(const struct checkout_opts *opts,\n \tif (!opts->quiet && !old_branch_info.path && old_branch_info.commit && new_branch_info->commit != old_branch_info.commit)\n \t\torphaned_commit_warning(old_branch_info.commit, new_branch_info->commit);\n \n+\tif (autostash_res == STASH_APPLY_CONFLICT && !opts->quiet)\n+\t\tfputc('\\n', stderr);\n \tupdate_refs_for_switch(opts, &old_branch_info, new_branch_info);\n \n \tif (created_autostash) {\ndiff --git a/builtin/stash.c b/builtin/stash.c\nindex 72c52571f8..86c7ac4ffa 100644\n--- a/builtin/stash.c\n+++ b/builtin/stash.c\n@@ -10,6 +10,7 @@\n #include \"object-name.h\"\n #include \"parse-options.h\"\n #include \"refs.h\"\n+#include \"stash.h\"\n #include \"lockfile.h\"\n #include \"cache-tree.h\"\n #include \"unpack-trees.h\"\n@@ -640,8 +641,9 @@ static void unstage_changes_unless_new(struct object_id *orig_tree)\n \t\tdie(_(\"could not write index\"));\n }\n \n-static int do_apply_stash(const char *prefix, struct stash_info *info,\n-\t\t\t  int index, int quiet,\n+static enum stash_apply_result do_apply_stash(const char *prefix,\n+\t\t\t\t\t      struct stash_info *info,\n+\t\t\t\t\t      int index, int quiet,\n \t\t\t  const char *label_ours, const char *label_theirs,\n \t\t\t  const char *label_base)\n {\n@@ -716,11 +718,12 @@ static int do_apply_stash(const char *prefix, struct stash_info *info,\n \tclean = merge_ort_nonrecursive(&o, head, merge, merge_base);\n \n \t/*\n-\t * If 'clean' >= 0, reverse the value for 'ret' so 'ret' is 0 when the\n-\t * merge was clean, and nonzero if the merge was unclean or encountered\n-\t * an error.\n+\t * Translate the value of 'clean' so 'ret' is STASH_APPLY_CLEAN\n+\t * when the merge was clean, STASH_APPLY_CONFLICT when it was\n+\t * unclean, and a negative value if it encountered an error.\n \t */\n-\tret = clean >= 0 ? !clean : clean;\n+\tret = clean >= 0 ? (clean ? STASH_APPLY_CLEAN : STASH_APPLY_CONFLICT)\n+\t\t\t : clean;\n \n \tif (ret < 0)\n \t\trollback_lock_file(&lock);\n@@ -739,7 +742,7 @@ static int do_apply_stash(const char *prefix, struct stash_info *info,\n \n \tif (has_index) {\n \t\tif (reset_tree(&index_tree, 0, 0))\n-\t\t\tret = -1;\n+\t\t\tret = STASH_APPLY_ERROR;\n \t} else {\n \t\tunstage_changes_unless_new(&c_tree);\n \t}\n@@ -2492,9 +2495,13 @@ int cmd_stash(int argc,\n \tstrbuf_addf(&stash_index_path, \"%s.stash.%\" PRIuMAX, index_file,\n \t\t    (uintmax_t)pid);\n \n-\tif (fn)\n-\t\treturn !!fn(argc, argv, prefix, repo);\n-\telse if (!argc)\n+\tif (fn) {\n+\t\tret = fn(argc, argv, prefix, repo);\n+\n+\t\tif (ret < 0)\n+\t\t\treturn 1;\n+\t\treturn ret;\n+\t} else if (!argc)\n \t\treturn !!push_stash_unassumed(0, NULL, prefix, repo);\n \n \t/* Assume 'stash push' */\ndiff --git a/sequencer.c b/sequencer.c\nindex 65afd100d9..328fd4262e 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -19,6 +19,7 @@\n #include \"commit.h\"\n #include \"sequencer.h\"\n #include \"run-command.h\"\n+#include \"stash.h\"\n #include \"hook.h\"\n #include \"utf8.h\"\n #include \"cache-tree.h\"\n@@ -4794,13 +4795,15 @@ void create_autostash_ref(struct repository *r, const char *refname,\n \tcreate_autostash_internal(r, NULL, refname, message, silent);\n }\n \n-static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply,\n-\t\t\t\t    const char *label_ours, const char *label_theirs,\n-\t\t\t\t    const char *label_base,\n-\t\t\t\t    const char *stash_msg)\n+static enum stash_apply_result apply_save_autostash_oid(const char *stash_oid,\n+\t\t\t\t\t\t\tint attempt_apply,\n+\t\t\t\t\t\t\tconst char *label_ours,\n+\t\t\t\t\t\t\tconst char *label_theirs,\n+\t\t\t\t\t\t\tconst char *label_base,\n+\t\t\t\t\t\t\tconst char *stash_msg)\n {\n \tstruct child_process child = CHILD_PROCESS_INIT;\n-\tint ret = 0;\n+\tenum stash_apply_result ret = STASH_APPLY_CLEAN;\n \n \tif (attempt_apply) {\n \t\tchild.git_cmd = 1;\n@@ -4816,9 +4819,11 @@ static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply,\n \t\t\tstrvec_pushf(&child.args, \"--label-base=%s\", label_base);\n \t\tstrvec_push(&child.args, stash_oid);\n \t\tret = run_command(&child);\n+\t\tif (ret && ret != STASH_APPLY_CONFLICT)\n+\t\t\tret = STASH_APPLY_ERROR;\n \t}\n \n-\tif (attempt_apply && !ret)\n+\tif (attempt_apply && ret == STASH_APPLY_CLEAN)\n \t\tfprintf(stderr, _(\"Applied autostash.\\n\"));\n \telse {\n \t\tstruct child_process store = CHILD_PROCESS_INIT;\n@@ -4832,13 +4837,15 @@ static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply,\n \t\tstrvec_push(&store.args, stash_oid);\n \t\tif (run_command(&store))\n \t\t\tret = error(_(\"cannot store %s\"), stash_oid);\n-\t\telse if (attempt_apply)\n+\t\telse if (attempt_apply && ret == STASH_APPLY_CONFLICT)\n \t\t\tfprintf(stderr,\n \t\t\t\t_(\"Your local changes are stashed, however applying them\\n\"\n \t\t\t\t  \"resulted in conflicts.  You can either resolve the conflicts\\n\"\n \t\t\t\t  \"and then discard the stash with \\\"git stash drop\\\", or, if you\\n\"\n \t\t\t\t  \"do not want to resolve them now, run \\\"git reset --hard\\\" and\\n\"\n \t\t\t\t  \"apply the local changes later by running \\\"git stash pop\\\".\\n\"));\n+\t\telse if (attempt_apply)\n+\t\t\tret = error(_(\"could not apply autostash\"));\n \t\telse\n \t\t\tfprintf(stderr,\n \t\t\t\t_(\"Autostash exists; creating a new stash entry.\\n\"\n@@ -4850,15 +4857,16 @@ static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply,\n \treturn ret;\n }\n \n-static int apply_save_autostash(const char *path, int attempt_apply)\n+static enum stash_apply_result apply_save_autostash(const char *path,\n+\t\t\t\t\t\t    int attempt_apply)\n {\n \tstruct strbuf stash_oid = STRBUF_INIT;\n-\tint ret = 0;\n+\tenum stash_apply_result ret = STASH_APPLY_CLEAN;\n \n \tif (!read_oneliner(&stash_oid, path,\n \t\t\t   READ_ONELINER_SKIP_IF_EMPTY)) {\n \t\tstrbuf_release(&stash_oid);\n-\t\treturn 0;\n+\t\treturn STASH_APPLY_CLEAN;\n \t}\n \tstrbuf_trim(&stash_oid);\n \n@@ -4870,37 +4878,40 @@ static int apply_save_autostash(const char *path, int attempt_apply)\n \treturn ret;\n }\n \n-int save_autostash(const char *path)\n+enum stash_apply_result save_autostash(const char *path)\n {\n \treturn apply_save_autostash(path, 0);\n }\n \n-int apply_autostash(const char *path)\n+enum stash_apply_result apply_autostash(const char *path)\n {\n \treturn apply_save_autostash(path, 1);\n }\n \n-int apply_autostash_oid(const char *stash_oid)\n+enum stash_apply_result apply_autostash_oid(const char *stash_oid)\n {\n \treturn apply_save_autostash_oid(stash_oid, 1, NULL, NULL, NULL, NULL);\n }\n \n-static int apply_save_autostash_ref(struct repository *r, const char *refname,\n-\t\t\t\t    int attempt_apply,\n-\t\t\t\t    const char *label_ours, const char *label_theirs,\n-\t\t\t\t    const char *label_base,\n-\t\t\t\t    const char *stash_msg)\n+static enum stash_apply_result apply_save_autostash_ref(struct repository *r,\n+\t\t\t\t\t\t\tconst char *refname,\n+\t\t\t\t\t\t\tint attempt_apply,\n+\t\t\t\t\t\t\tconst char *label_ours,\n+\t\t\t\t\t\t\tconst char *label_theirs,\n+\t\t\t\t\t\t\tconst char *label_base,\n+\t\t\t\t\t\t\tconst char *stash_msg)\n {\n \tstruct object_id stash_oid;\n \tchar stash_oid_hex[GIT_MAX_HEXSZ + 1];\n-\tint flag, ret;\n+\tint flag;\n+\tenum stash_apply_result ret;\n \n \tif (!refs_ref_exists(get_main_ref_store(r), refname))\n-\t\treturn 0;\n+\t\treturn STASH_APPLY_CLEAN;\n \n \tif (!refs_resolve_ref_unsafe(get_main_ref_store(r), refname,\n \t\t\t\t     RESOLVE_REF_READING, &stash_oid, &flag))\n-\t\treturn -1;\n+\t\treturn STASH_APPLY_ERROR;\n \tif (flag & REF_ISSYMREF)\n \t\treturn error(_(\"autostash reference is a symref\"));\n \n@@ -4915,15 +4926,19 @@ static int apply_save_autostash_ref(struct repository *r, const char *refname,\n \treturn ret;\n }\n \n-int save_autostash_ref(struct repository *r, const char *refname)\n+enum stash_apply_result save_autostash_ref(struct repository *r,\n+\t\t\t\t\t   const char *refname)\n {\n \treturn apply_save_autostash_ref(r, refname, 0,\n \t\t\t\t\tNULL, NULL, NULL, NULL);\n }\n \n-int apply_autostash_ref(struct repository *r, const char *refname,\n-\t\t\tconst char *label_ours, const char *label_theirs,\n-\t\t\tconst char *label_base, const char *stash_msg)\n+enum stash_apply_result apply_autostash_ref(struct repository *r,\n+\t\t\t\t\t    const char *refname,\n+\t\t\t\t\t    const char *label_ours,\n+\t\t\t\t\t    const char *label_theirs,\n+\t\t\t\t\t    const char *label_base,\n+\t\t\t\t\t    const char *stash_msg)\n {\n \treturn apply_save_autostash_ref(r, refname, 1,\n \t\t\t\t\tlabel_ours, label_theirs, label_base,\ndiff --git a/sequencer.h b/sequencer.h\nindex 64a9c7fb1b..804501b64c 100644\n--- a/sequencer.h\n+++ b/sequencer.h\n@@ -3,6 +3,7 @@\n \n #include \"strbuf.h\"\n #include \"strvec.h\"\n+#include \"stash.h\"\n #include \"wt-status.h\"\n \n struct commit;\n@@ -231,13 +232,17 @@ void commit_post_rewrite(struct repository *r,\n void create_autostash(struct repository *r, const char *path);\n void create_autostash_ref(struct repository *r, const char *refname,\n \t\t\t  const char *message, bool silent);\n-int save_autostash(const char *path);\n-int save_autostash_ref(struct repository *r, const char *refname);\n-int apply_autostash(const char *path);\n-int apply_autostash_oid(const char *stash_oid);\n-int apply_autostash_ref(struct repository *r, const char *refname,\n-\t\t\tconst char *label_ours, const char *label_theirs,\n-\t\t\tconst char *label_base, const char *stash_msg);\n+enum stash_apply_result save_autostash(const char *path);\n+enum stash_apply_result save_autostash_ref(struct repository *r,\n+\t\t\t\t\t   const char *refname);\n+enum stash_apply_result apply_autostash(const char *path);\n+enum stash_apply_result apply_autostash_oid(const char *stash_oid);\n+enum stash_apply_result apply_autostash_ref(struct repository *r,\n+\t\t\t\t\t    const char *refname,\n+\t\t\t\t\t    const char *label_ours,\n+\t\t\t\t\t    const char *label_theirs,\n+\t\t\t\t\t    const char *label_base,\n+\t\t\t\t\t    const char *stash_msg);\n \n #define SUMMARY_INITIAL_COMMIT   (1 << 0)\n #define SUMMARY_SHOW_AUTHOR_DATE (1 << 1)\ndiff --git a/stash.h b/stash.h\nnew file mode 100644\nindex 0000000000..bf2f2e1a45\n--- /dev/null\n+++ b/stash.h\n@@ -0,0 +1,20 @@\n+#ifndef STASH_H\n+#define STASH_H\n+\n+enum stash_apply_result {\n+\t/* The stash was applied cleanly, or there was nothing to apply. */\n+\tSTASH_APPLY_CLEAN = 0,\n+\n+\t/*\n+\t * The stash could not be applied because it resulted in\n+\t * conflicts.  The stash entry is left in place.  The \"git stash\n+\t * apply\", \"pop\" and \"branch\" subcommands exit with this status\n+\t * in this case.\n+\t */\n+\tSTASH_APPLY_CONFLICT = 2,\n+\n+\t/* Something went wrong. */\n+\tSTASH_APPLY_ERROR = -1,\n+};\n+\n+#endif /* STASH_H */\ndiff --git a/t/t3903-stash.sh b/t/t3903-stash.sh\nindex da27a6599a..93e8e98216 100755\n--- a/t/t3903-stash.sh\n+++ b/t/t3903-stash.sh\n@@ -1797,7 +1797,7 @@ test_expect_success 'apply with custom conflict labels' '\n \techo stashed >conflict-file &&\n \tgit stash push -m \"stashed\" &&\n \ttest_commit label-upstream conflict-file upstream-content &&\n-\ttest_must_fail git -c merge.conflictStyle=diff3 stash apply --label-ours=UP --label-theirs=STASH &&\n+\ttest_expect_code 2 git -c merge.conflictStyle=diff3 stash apply --label-ours=UP --label-theirs=STASH &&\n \ttest_grep \"^<<<<<<< UP\" conflict-file &&\n \ttest_grep \"^||||||| Stash base\" conflict-file &&\n \ttest_grep \"^>>>>>>> STASH\" conflict-file\n@@ -1809,11 +1809,33 @@ test_expect_success 'apply with empty conflict labels' '\n \techo stashed >conflict-file &&\n \tgit stash push -m \"stashed\" &&\n \ttest_commit empty-label-upstream conflict-file upstream-content &&\n-\ttest_must_fail git stash apply --label-ours= --label-theirs= &&\n+\ttest_expect_code 2 git stash apply --label-ours= --label-theirs= &&\n \ttest_grep \"^<<<<<<<$\" conflict-file &&\n \ttest_grep \"^>>>>>>>$\" conflict-file\n '\n \n+test_expect_success 'apply exits 2 on conflicts and keeps the stash entry' '\n+\tgit reset --hard initial &&\n+\ttest_commit exit-code-base conflict-file base-content &&\n+\techo stashed >conflict-file &&\n+\tgit stash push -m stashed &&\n+\ttest_commit exit-code-upstream conflict-file upstream-content &&\n+\ttest_expect_code 2 git stash apply &&\n+\tgit stash list >list &&\n+\ttest_grep stashed list\n+'\n+\n+test_expect_success 'pop exits 2 on conflicts and keeps the stash entry' '\n+\tgit reset --hard initial &&\n+\ttest_commit pop-exit-code-base pop-file base-content &&\n+\techo stashed >pop-file &&\n+\tgit stash push -m pop-stashed &&\n+\ttest_commit pop-exit-code-upstream pop-file upstream-content &&\n+\ttest_expect_code 2 git stash pop &&\n+\tgit stash list >list &&\n+\ttest_grep pop-stashed list\n+'\n+\n test_expect_success 'stash show --include-untracked includes untracked files' '\n \tgit reset --hard &&\n \ndiff --git a/t/t7201-co.sh b/t/t7201-co.sh\nindex 0ddd1ad7aa..9ea9462914 100755\n--- a/t/t7201-co.sh\n+++ b/t/t7201-co.sh\n@@ -236,10 +236,18 @@ test_expect_success 'checkout -m creates a recoverable stash on conflict' '\n \ttest_must_fail git checkout side 2>stderr &&\n \ttest_grep \"Your local changes\" stderr &&\n \tgit checkout -m side >actual 2>&1 &&\n-\ttest_grep \"resulted in conflicts\" actual &&\n-\ttest_grep \"git stash drop\" actual &&\n-\ttest_grep \"git stash pop\" actual &&\n-\ttest_grep \"The following paths have local changes\" actual &&\n+\tcat >expect <<-EOF &&\n+\tYour local changes are stashed, however applying them\n+\tresulted in conflicts.  You can either resolve the conflicts\n+\tand then discard the stash with \"git stash drop\", or, if you\n+\tdo not want to resolve them now, run \"git reset --hard\" and\n+\tapply the local changes later by running \"git stash pop\".\n+\n+\tSwitched to branch ${SQ}side${SQ}\n+\tThe following paths have local changes:\n+\tM\tone\n+\tEOF\n+\ttest_cmp expect actual &&\n \tgit log -p -1 --format=\"%gs%n%B\" -g --diff-merges=1 refs/stash >actual &&\n \tsed /^index/d actual >actual.trimmed &&\n \tcat >expect <<-EOF &&\n\nbase-commit: c73e85354c275c9d409b26445089bc16940fc527\n-- \ngitgitgadget\n"},{"id":"551666","messageId":"7dbebce0-1814-4b0b-8167-6a464d893612@gmail.com","threadId":"66064","inReplyTo":"pull.2364.v3.git.git.1788256199679.gitgitgadget@gmail.com","subject":"Re: [PATCH v3] checkout: separate autostash conflict advice from branch-switch message","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-09-01T13:42:19Z","receivedAt":"2026-09-01T13:42:33Z","isPatch":true,"body":"Hi Harald\n\nOn 01/09/2026 10:49, Harald Nordgren via GitGitGadget wrote:\n> From: Harald Nordgren <haraldnordgren@gmail.com>\n> \n> \"git checkout -m\" stashes the user's local changes when it cannot\n> perform the checkout, and then applies the stash.  When applying the\n> stash results in conflicts, the advice on how to deal with them is\n> printed directly on top of the branch-switch message (\"Switched to\n> branch ...\"), making the two hard to tell apart.  Print a blank line\n> in between so that the advice and the branch-switch message are\n> visually distinct.\n> \n> To make this possible, \"git stash apply\", \"pop\" and \"branch\" now exit\n> with status 2 when applying the stash entry resulted in conflicts, in\n> which case the stash entry is left in place; other failures exit with\n> status 1, as before.  The exit statuses are documented in the \"git\n> stash\" documentation.\n\nOther commands such as merge-tree and merge strategies use 1 to indicate \nconflicts and another non-zero exit code for errors. That matches the \nway grep and diff use the exit code to distinguish differences from \nerrors. It is confusing if we start using a different convention here. \nI've left a few comments below, but the exit code is my main concern. It \nwould be nice to separate out the stash changes into a separate commit \nas well.\n\n> diff --git a/Documentation/git-stash.adoc b/Documentation/git-stash.adoc\n> index 50bb89f483..3e41ffcf43 100644\n> --- a/Documentation/git-stash.adoc\n> +++ b/Documentation/git-stash.adoc\n> @@ -426,6 +426,15 @@ include::includes/cmd-config-section-all.adoc[]\n>   :git-stash: 1\n>   include::config/stash.adoc[]\n>   \n> +EXIT STATUS\n> +-----------\n> +\n> +The `git stash` subcommands exit with status 0 on success and non-zero\n> +on failure.  The subcommands that apply a stash entry, i.e. `apply`,\n> +`pop` and `branch`, exit with status 2 when applying the stash entry\n> +resulted in conflicts, in which case the stash entry is left in place.\n> +Other failures exit with status 1 (usage errors exit with status 129).\n\nThanks for documenting this, I think we'd be better to avoid giving \nspecific exit codes for errors and say \"a non-zero exit code other than \n1\" unless we have a good way of enforcing that.\n\n> diff --git a/builtin/stash.c b/builtin/stash.c\n> index 72c52571f8..86c7ac4ffa 100644\n> --- a/builtin/stash.c\n> +++ b/builtin/stash.c\n> @@ -10,6 +10,7 @@\n>   #include \"object-name.h\"\n>   #include \"parse-options.h\"\n>   #include \"refs.h\"\n> +#include \"stash.h\"\n>   #include \"lockfile.h\"\n>   #include \"cache-tree.h\"\n>   #include \"unpack-trees.h\"\n> @@ -640,8 +641,9 @@ static void unstage_changes_unless_new(struct object_id *orig_tree)\n>   \t\tdie(_(\"could not write index\"));\n>   }\n>   \n> -static int do_apply_stash(const char *prefix, struct stash_info *info,\n> -\t\t\t  int index, int quiet,\n> +static enum stash_apply_result do_apply_stash(const char *prefix,\n> +\t\t\t\t\t      struct stash_info *info,\n> +\t\t\t\t\t      int index, int quiet,\n>   \t\t\t  const char *label_ours, const char *label_theirs,\n>   \t\t\t  const char *label_base)\n\nThe indentation is strange here\n\n>   {\n> @@ -716,11 +718,12 @@ static int do_apply_stash(const char *prefix, struct stash_info *info,\n>   \tclean = merge_ort_nonrecursive(&o, head, merge, merge_base);\n>   \n>   \t/*\n> -\t * If 'clean' >= 0, reverse the value for 'ret' so 'ret' is 0 when the\n> -\t * merge was clean, and nonzero if the merge was unclean or encountered\n> -\t * an error.\n> +\t * Translate the value of 'clean' so 'ret' is STASH_APPLY_CLEAN\n> +\t * when the merge was clean, STASH_APPLY_CONFLICT when it was\n> +\t * unclean, and a negative value if it encountered an error.\n>   \t */\n> -\tret = clean >= 0 ? !clean : clean;\n> +\tret = clean >= 0 ? (clean ? STASH_APPLY_CLEAN : STASH_APPLY_CONFLICT)\n> +\t\t\t : clean;\n\nNested ternary operators are not particularly readable, if we stick with \nan exit code of 1 for conflicts the original code does not need to be \nmodified.\n\n>   \n>   \tif (ret < 0)\n>   \t\trollback_lock_file(&lock);\n> @@ -739,7 +742,7 @@ static int do_apply_stash(const char *prefix, struct stash_info *info,\n>   \n>   \tif (has_index) {\n>   \t\tif (reset_tree(&index_tree, 0, 0))\n> -\t\t\tret = -1;\n> +\t\t\tret = STASH_APPLY_ERROR;\n\nThis seems a bit pointless when we're still returning -1 implicitly \neverywhere else where we have \"return error(...).\n\n\n>   \t} else {\n>   \t\tunstage_changes_unless_new(&c_tree);\n>   \t}\n> @@ -2492,9 +2495,13 @@ int cmd_stash(int argc,\n>   \tstrbuf_addf(&stash_index_path, \"%s.stash.%\" PRIuMAX, index_file,\n>   \t\t    (uintmax_t)pid);\n>   \n> -\tif (fn)\n> -\t\treturn !!fn(argc, argv, prefix, repo);\n> -\telse if (!argc)\n> +\tif (fn) {\n> +\t\tret = fn(argc, argv, prefix, repo);\n> +\n> +\t\tif (ret < 0)\n> +\t\t\treturn 1;\n> +\t\treturn ret;\n\nLooking at the callers of do_apply_stash(), apply_stash() returns the \nresult of do_apply_stash(), pop_stash() and branch_stash() return the \nresult of do_drop_stash() if do_apply_stash() returns 0. do_drop_stash() \nalways returns 0 so we're safe, but that analysis should be in the \ncommit message.\n\n\n> @@ -4832,13 +4837,15 @@ static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply,\n>   \t\tstrvec_push(&store.args, stash_oid);\n>   \t\tif (run_command(&store))\n>   \t\t\tret = error(_(\"cannot store %s\"), stash_oid);\n> -\t\telse if (attempt_apply)\n> +\t\telse if (attempt_apply && ret == STASH_APPLY_CONFLICT)\n>   \t\t\tfprintf(stderr,\n>   \t\t\t\t_(\"Your local changes are stashed, however applying them\\n\"\n>   \t\t\t\t  \"resulted in conflicts.  You can either resolve the conflicts\\n\"\n>   \t\t\t\t  \"and then discard the stash with \\\"git stash drop\\\", or, if you\\n\"\n>   \t\t\t\t  \"do not want to resolve them now, run \\\"git reset --hard\\\" and\\n\"\n>   \t\t\t\t  \"apply the local changes later by running \\\"git stash pop\\\".\\n\"));\n\nWe only print this if we know there were conflicts - good.\n\n> +\t\telse if (attempt_apply)\n> +\t\t\tret = error(_(\"could not apply autostash\"));\n\nWe know we've saved the stash so we should tell the user that we have, \nrather than leaving when wondering what's happened to their stashed changes.\n\nThe rest of the changes in this file look good.\n\n> diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh\n> index da27a6599a..93e8e98216 100755\n> --- a/t/t3903-stash.sh\n> +++ b/t/t3903-stash.sh\n> @@ -1797,7 +1797,7 @@ test_expect_success 'apply with custom conflict labels' '\n>   \techo stashed >conflict-file &&\n>   \tgit stash push -m \"stashed\" &&\n>   \ttest_commit label-upstream conflict-file upstream-content &&\n> -\ttest_must_fail git -c merge.conflictStyle=diff3 stash apply --label-ours=UP --label-theirs=STASH &&\n> +\ttest_expect_code 2 git -c merge.conflictStyle=diff3 stash apply --label-ours=UP --label-theirs=STASH &&\n>   \ttest_grep \"^<<<<<<< UP\" conflict-file &&\n>   \ttest_grep \"^||||||| Stash base\" conflict-file &&\n>   \ttest_grep \"^>>>>>>> STASH\" conflict-file\n> @@ -1809,11 +1809,33 @@ test_expect_success 'apply with empty conflict labels' '\n>   \techo stashed >conflict-file &&\n>   \tgit stash push -m \"stashed\" &&\n>   \ttest_commit empty-label-upstream conflict-file upstream-content &&\n> -\ttest_must_fail git stash apply --label-ours= --label-theirs= &&\n> +\ttest_expect_code 2 git stash apply --label-ours= --label-theirs= &&\n>   \ttest_grep \"^<<<<<<<$\" conflict-file &&\n>   \ttest_grep \"^>>>>>>>$\" conflict-file\n>   '\n>   \n> +test_expect_success 'apply exits 2 on conflicts and keeps the stash entry' '\n\nAren't we testing that above?\n\n> +\tgit reset --hard initial &&\n> +\ttest_commit exit-code-base conflict-file base-content &&\n\nWe've just reset to a known starting point that has paths file and \nother-file, so why do we need to create a new commit in order to stash \nsomething?\n\n> +\techo stashed >conflict-file &&\n> +\tgit stash push -m stashed &&\n> +\ttest_commit exit-code-upstream conflict-file upstream-content &&\n> +\ttest_expect_code 2 git stash apply &&\n> +\tgit stash list >list &&\n> +\ttest_grep stashed list\n> +'\n> +\n> +test_expect_success 'pop exits 2 on conflicts and keeps the stash entry' '\n\nThis is good, we should be checking \"stash branch\" as well.\n\n> diff --git a/t/t7201-co.sh b/t/t7201-co.sh\n> index 0ddd1ad7aa..9ea9462914 100755\n> --- a/t/t7201-co.sh\n> +++ b/t/t7201-co.sh\n> @@ -236,10 +236,18 @@ test_expect_success 'checkout -m creates a recoverable stash on conflict' '\n>   \ttest_must_fail git checkout side 2>stderr &&\n>   \ttest_grep \"Your local changes\" stderr &&\n>   \tgit checkout -m side >actual 2>&1 &&\n> -\ttest_grep \"resulted in conflicts\" actual &&\n> -\ttest_grep \"git stash drop\" actual &&\n> -\ttest_grep \"git stash pop\" actual &&\n> -\ttest_grep \"The following paths have local changes\" actual &&\n> +\tcat >expect <<-EOF &&\n> +\tYour local changes are stashed, however applying them\n> +\tresulted in conflicts.  You can either resolve the conflicts\n> +\tand then discard the stash with \"git stash drop\", or, if you\n> +\tdo not want to resolve them now, run \"git reset --hard\" and\n> +\tapply the local changes later by running \"git stash pop\".\n> +\n> +\tSwitched to branch ${SQ}side${SQ}\n> +\tThe following paths have local changes:\n> +\tM\tone\n> +\tEOF\n> +\ttest_cmp expect actual &&\n\nNice, it is much easier to see what we're checking now\n\nThanks\n\nPhillip\n\n"},{"id":"551669","messageId":"xmqqqzjdt71g.fsf@gitster.g","threadId":"66064","inReplyTo":"af051ecf-0d94-4dc1-a6e5-0184b2b6e1f1@gmail.com","subject":"Re: [PATCH v2] checkout: print blank line after autostash conflict advice","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-09-01T13:50:19Z","receivedAt":"2026-09-01T13:50:22Z","isPatch":true,"body":"Phillip Wood <phillip.wood123@gmail.com> writes:\n\n> \tret = run_command(&child);\n> \tif (ret > 1)\n> \t\tret = -1;\n>\n> would catch run_command() failing and stash dying or being killed by a \n> signal.\n\n;-)\n"},{"id":"551690","messageId":"xmqqtso8swso.fsf@gitster.g","threadId":"66064","inReplyTo":"7dbebce0-1814-4b0b-8167-6a464d893612@gmail.com","subject":"Re: [PATCH v3] checkout: separate autostash conflict advice from branch-switch message","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-09-01T17:31:35Z","receivedAt":"2026-09-01T17:31:38Z","isPatch":true,"body":"Phillip Wood <phillip.wood123@gmail.com> writes:\n\n> Hi Harald\n>\n> On 01/09/2026 10:49, Harald Nordgren via GitGitGadget wrote:\n>> From: Harald Nordgren <haraldnordgren@gmail.com>\n>> \n>> \"git checkout -m\" stashes the user's local changes when it cannot\n>> perform the checkout, and then applies the stash.  When applying the\n>> stash results in conflicts, the advice on how to deal with them is\n>> printed directly on top of the branch-switch message (\"Switched to\n>> branch ...\"), making the two hard to tell apart.  Print a blank line\n>> in between so that the advice and the branch-switch message are\n>> visually distinct.\n>> \n>> To make this possible, \"git stash apply\", \"pop\" and \"branch\" now exit\n>> with status 2 when applying the stash entry resulted in conflicts, in\n>> which case the stash entry is left in place; other failures exit with\n>> status 1, as before.  The exit statuses are documented in the \"git\n>> stash\" documentation.\n>\n> Other commands such as merge-tree and merge strategies use 1 to indicate \n> conflicts and another non-zero exit code for errors. That matches the \n> way grep and diff use the exit code to distinguish differences from \n> errors. It is confusing if we start using a different convention here. \n> I've left a few comments below, but the exit code is my main concern. It \n> would be nice to separate out the stash changes into a separate commit \n> as well.\n\nThis is half my fault.  I should have made it clear that my sample\ncode was merely to illustrate the need and ability to allow the\ncallers to tell general errors and specific failure modes.  I agree\nwith you that \"1 if X, other non-zero exit if error\" would fly much\nbetter.\n\nThanks.\n"},{"id":"551795","messageId":"pull.2364.v4.git.git.1788373743.gitgitgadget@gmail.com","threadId":"66064","inReplyTo":"pull.2364.git.git.1784993669.gitgitgadget@gmail.com","subject":"[PATCH v4 0/2] checkout -m: refine autostash fallback","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-02T18:29:01Z","receivedAt":"2026-09-02T18:29:05Z","isPatch":true,"body":"Avoiding checkout -m autostash retries when no tracked local changes exist\nand visually separating autostash conflict advice from the subsequent\nbranch-switch message. Addresses #leftoverbits from here:\nhttps://lore.kernel.org/git/cfd09dbf-8d77-4464-8030-3a0ffb4aeae7@gmail.com/\n\nChanges in v4:\n\n * Conflicts now exit with status 1 like merge-tree, other failures exit 128\n   so exit 1 unambiguously means conflicts. Stash changes split into their\n   own commit.\n * The autostash apply helpers use the return value (enum\n   stash_apply_result) instead of an out-parameter, and only claim conflicts\n   when git stash apply actually reported them.\n\nChanges in v3:\n\n * Use enum for git stash return values, to separate conflict from generic\n   error.\n\nChanges in v2:\n\n * Simplify logic and combine to one commit.\n * Test full output with test_cmp.\n\nHarald Nordgren (2):\n  stash: reserve exit status 1 for conflicts\n  checkout: separate autostash conflict advice from branch-switch\n    message\n\n Documentation/git-stash.adoc |  9 +++++\n builtin/checkout.c           | 15 ++++----\n builtin/stash.c              | 32 ++++++++++++-----\n sequencer.c                  | 66 ++++++++++++++++++++++--------------\n sequencer.h                  | 19 +++++++----\n stash.h                      | 21 ++++++++++++\n t/t3903-stash.sh             | 25 ++++++++++++--\n t/t7201-co.sh                | 16 ++++++---\n 8 files changed, 149 insertions(+), 54 deletions(-)\n create mode 100644 stash.h\n\n\nbase-commit: 1630431f326e15fcde608827b5ff38422528eb59\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2364%2FHaraldNordgren%2Fhn%2Fgit-checkout-m-leftoverbits-v4\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2364/HaraldNordgren/hn/git-checkout-m-leftoverbits-v4\nPull-Request: https://github.com/git/git/pull/2364\n\nRange-diff vs v3:\n\n 1:  8e1979dd6c ! 1:  ff43221802 checkout: separate autostash conflict advice from branch-switch message\n     @@ Metadata\n      Author: Harald Nordgren <haraldnordgren@gmail.com>\n      \n       ## Commit message ##\n     -    checkout: separate autostash conflict advice from branch-switch message\n     +    stash: reserve exit status 1 for conflicts\n      \n     -    \"git checkout -m\" stashes the user's local changes when it cannot\n     -    perform the checkout, and then applies the stash.  When applying the\n     -    stash results in conflicts, the advice on how to deal with them is\n     -    printed directly on top of the branch-switch message (\"Switched to\n     -    branch ...\"), making the two hard to tell apart.  Print a blank line\n     -    in between so that the advice and the branch-switch message are\n     -    visually distinct.\n     +    \"git stash apply\", \"pop\" and \"branch\" exit with status 1 both when\n     +    applying the stash entry resulted in conflicts and when they fail for\n     +    other reasons, so callers cannot tell the two apart.\n      \n     -    To make this possible, \"git stash apply\", \"pop\" and \"branch\" now exit\n     -    with status 2 when applying the stash entry resulted in conflicts, in\n     -    which case the stash entry is left in place; other failures exit with\n     -    status 1, as before.  The exit statuses are documented in the \"git\n     -    stash\" documentation.\n     +    Follow the convention of \"git merge-tree\" and the merge strategies,\n     +    which exit with status 1 to indicate conflicts and with a different\n     +    non-zero status for errors: those subcommands now exit with status 1\n     +    only when applying the stash entry resulted in conflicts, in which\n     +    case the stash entry is left in place, and exit with status 128, the\n     +    status die() uses, when they fail for other reasons.  Document the\n     +    exit statuses.\n     +\n     +    cmd_stash() used to collapse the return values of the subcommand\n     +    implementations to a boolean.  It now maps negative values, which\n     +    signal a failure, to 128 and passes everything else through as-is.\n     +    The only implementations that return a positive value are \"apply\",\n     +    \"pop\" and \"branch\", which return the value of do_apply_stash():\n     +    \"apply\" returns it directly, and \"pop\" and \"branch\" drop the stash\n     +    entry, via do_drop_stash(), which always returns 0, only when the\n     +    application succeeded.  The positive value is always 1, as\n     +    do_apply_stash() only returns a positive value when the three-way\n     +    merge was unclean.\n     +\n     +    Make the convention explicit by introducing enum stash_apply_result\n     +    with the values STASH_APPLY_CLEAN, STASH_APPLY_CONFLICT and\n     +    STASH_APPLY_ERROR, and use it for the in-process autostash helpers,\n     +    too.  They spawn \"git stash apply\" and can now tell conflicts apart\n     +    from other failures, e.g. a crash or death by signal of the child,\n     +    which map to exit statuses above 1.  Since we know the stash entry\n     +    was saved, tell users so in the error message instead of leaving them\n     +    wondering what happened to their stashed changes.\n      \n          Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n      \n     @@ Documentation/git-stash.adoc: include::includes/cmd-config-section-all.adoc[]\n      +EXIT STATUS\n      +-----------\n      +\n     -+The `git stash` subcommands exit with status 0 on success and non-zero\n     -+on failure.  The subcommands that apply a stash entry, i.e. `apply`,\n     -+`pop` and `branch`, exit with status 2 when applying the stash entry\n     -+resulted in conflicts, in which case the stash entry is left in place.\n     -+Other failures exit with status 1 (usage errors exit with status 129).\n     ++The `git stash` subcommands exit with status 0 on success.  The\n     ++subcommands that apply a stash entry, i.e. `apply`, `pop` and `branch`,\n     ++exit with status 1 when applying the stash entry resulted in conflicts,\n     ++in which case the stash entry is left in place, and with a non-zero\n     ++status other than 1 when they fail for other reasons.\n      +\n       \n       SEE ALSO\n       --------\n      \n     - ## builtin/checkout.c ##\n     -@@ builtin/checkout.c: static int switch_branches(const struct checkout_opts *opts,\n     - \tint flag, writeout_error = 0;\n     - \tint do_merge = 1;\n     - \tint created_autostash = 0;\n     -+\tenum stash_apply_result autostash_res = STASH_APPLY_CLEAN;\n     - \tstruct strbuf old_commit_shortname = STRBUF_INIT;\n     - \tstruct strbuf autostash_msg = STRBUF_INIT;\n     - \tconst char *stash_label_base = NULL;\n     -@@ builtin/checkout.c: static int switch_branches(const struct checkout_opts *opts,\n     - \t\t\t\tgit_config_push_parameter(cfg.buf);\n     - \t\t\t\tstrbuf_release(&cfg);\n     - \t\t\t}\n     --\t\t\tapply_autostash_ref(the_repository,\n     --\t\t\t\t\t    \"CHECKOUT_AUTOSTASH_HEAD\",\n     --\t\t\t\t\t    new_branch_info->name,\n     --\t\t\t\t\t    \"local\",\n     --\t\t\t\t\t    stash_label_base,\n     --\t\t\t\t\t    autostash_msg.buf);\n     -+\t\t\tautostash_res = apply_autostash_ref(the_repository,\n     -+\t\t\t\t    \"CHECKOUT_AUTOSTASH_HEAD\",\n     -+\t\t\t\t    new_branch_info->name,\n     -+\t\t\t\t    \"local\",\n     -+\t\t\t\t    stash_label_base,\n     -+\t\t\t\t    autostash_msg.buf);\n     - \t\t}\n     - \t\tif (ret) {\n     - \t\t\tbranch_info_release(&old_branch_info);\n     -@@ builtin/checkout.c: static int switch_branches(const struct checkout_opts *opts,\n     - \tif (!opts->quiet && !old_branch_info.path && old_branch_info.commit && new_branch_info->commit != old_branch_info.commit)\n     - \t\torphaned_commit_warning(old_branch_info.commit, new_branch_info->commit);\n     - \n     -+\tif (autostash_res == STASH_APPLY_CONFLICT && !opts->quiet)\n     -+\t\tfputc('\\n', stderr);\n     - \tupdate_refs_for_switch(opts, &old_branch_info, new_branch_info);\n     - \n     - \tif (created_autostash) {\n     -\n       ## builtin/stash.c ##\n      @@\n       #include \"object-name.h\"\n     @@ builtin/stash.c: static void unstage_changes_unless_new(struct object_id *orig_t\n       \n      -static int do_apply_stash(const char *prefix, struct stash_info *info,\n      -\t\t\t  int index, int quiet,\n     +-\t\t\t  const char *label_ours, const char *label_theirs,\n     +-\t\t\t  const char *label_base)\n      +static enum stash_apply_result do_apply_stash(const char *prefix,\n      +\t\t\t\t\t      struct stash_info *info,\n      +\t\t\t\t\t      int index, int quiet,\n     - \t\t\t  const char *label_ours, const char *label_theirs,\n     - \t\t\t  const char *label_base)\n     ++\t\t\t\t\t      const char *label_ours,\n     ++\t\t\t\t\t      const char *label_theirs,\n     ++\t\t\t\t\t      const char *label_base)\n       {\n     + \tint clean, ret;\n     + \tint has_index = index;\n      @@ builtin/stash.c: static int do_apply_stash(const char *prefix, struct stash_info *info,\n     - \tclean = merge_ort_nonrecursive(&o, head, merge, merge_base);\n       \n       \t/*\n     --\t * If 'clean' >= 0, reverse the value for 'ret' so 'ret' is 0 when the\n     + \t * If 'clean' >= 0, reverse the value for 'ret' so 'ret' is 0 when the\n      -\t * merge was clean, and nonzero if the merge was unclean or encountered\n      -\t * an error.\n     -+\t * Translate the value of 'clean' so 'ret' is STASH_APPLY_CLEAN\n     -+\t * when the merge was clean, STASH_APPLY_CONFLICT when it was\n     -+\t * unclean, and a negative value if it encountered an error.\n     ++\t * merge was clean, and 1 if the merge was unclean or a negative value\n     ++\t * if it encountered an error.\n       \t */\n     --\tret = clean >= 0 ? !clean : clean;\n     -+\tret = clean >= 0 ? (clean ? STASH_APPLY_CLEAN : STASH_APPLY_CONFLICT)\n     -+\t\t\t : clean;\n     + \tret = clean >= 0 ? !clean : clean;\n       \n     - \tif (ret < 0)\n     - \t\trollback_lock_file(&lock);\n     -@@ builtin/stash.c: static int do_apply_stash(const char *prefix, struct stash_info *info,\n     - \n     - \tif (has_index) {\n     - \t\tif (reset_tree(&index_tree, 0, 0))\n     --\t\t\tret = -1;\n     -+\t\t\tret = STASH_APPLY_ERROR;\n     - \t} else {\n     - \t\tunstage_changes_unless_new(&c_tree);\n     - \t}\n      @@ builtin/stash.c: int cmd_stash(int argc,\n       \tstrbuf_addf(&stash_index_path, \"%s.stash.%\" PRIuMAX, index_file,\n       \t\t    (uintmax_t)pid);\n     @@ builtin/stash.c: int cmd_stash(int argc,\n      +\tif (fn) {\n      +\t\tret = fn(argc, argv, prefix, repo);\n      +\n     ++\t\t/*\n     ++\t\t * The subcommand implementations return 0 on success, a\n     ++\t\t * negative value on failure, and STASH_APPLY_CONFLICT\n     ++\t\t * when applying a stash entry resulted in conflicts.\n     ++\t\t * Map failures to 128, the status die() uses, so that\n     ++\t\t * exit status 1 unambiguously indicates conflicts.\n     ++\t\t */\n      +\t\tif (ret < 0)\n     -+\t\t\treturn 1;\n     ++\t\t\treturn 128;\n      +\t\treturn ret;\n      +\t} else if (!argc)\n       \t\treturn !!push_stash_unassumed(0, NULL, prefix, repo);\n     @@ sequencer.c: static int apply_save_autostash_oid(const char *stash_oid, int atte\n       \t\t\tstrvec_pushf(&child.args, \"--label-base=%s\", label_base);\n       \t\tstrvec_push(&child.args, stash_oid);\n       \t\tret = run_command(&child);\n     -+\t\tif (ret && ret != STASH_APPLY_CONFLICT)\n     ++\t\tif (ret > 1)\n      +\t\t\tret = STASH_APPLY_ERROR;\n       \t}\n       \n     @@ sequencer.c: static int apply_save_autostash_oid(const char *stash_oid, int atte\n       \t\t\t\t  \"do not want to resolve them now, run \\\"git reset --hard\\\" and\\n\"\n       \t\t\t\t  \"apply the local changes later by running \\\"git stash pop\\\".\\n\"));\n      +\t\telse if (attempt_apply)\n     -+\t\t\tret = error(_(\"could not apply autostash\"));\n     ++\t\t\tret = error(_(\"could not apply autostash; \"\n     ++\t\t\t\t      \"your changes are safe in the stash\"));\n       \t\telse\n       \t\t\tfprintf(stderr,\n       \t\t\t\t_(\"Autostash exists; creating a new stash entry.\\n\"\n     @@ stash.h (new)\n      +\t * The stash could not be applied because it resulted in\n      +\t * conflicts.  The stash entry is left in place.  The \"git stash\n      +\t * apply\", \"pop\" and \"branch\" subcommands exit with this status\n     -+\t * in this case.\n     ++\t * in this case, mirroring the convention of \"git merge-tree\" and\n     ++\t * the merge strategies.\n      +\t */\n     -+\tSTASH_APPLY_CONFLICT = 2,\n     ++\tSTASH_APPLY_CONFLICT = 1,\n      +\n      +\t/* Something went wrong. */\n      +\tSTASH_APPLY_ERROR = -1,\n     @@ stash.h (new)\n      +#endif /* STASH_H */\n      \n       ## t/t3903-stash.sh ##\n     -@@ t/t3903-stash.sh: test_expect_success 'apply with custom conflict labels' '\n     +@@ t/t3903-stash.sh: test_expect_success 'stash.index=false overridden by --index' '\n     + \ttest_cmp expect file\n     + '\n     + \n     +-test_expect_success 'apply with custom conflict labels' '\n     ++test_expect_success 'apply exits 1 on conflicts' '\n     + \tgit reset --hard initial &&\n     + \ttest_commit label-base conflict-file base-content &&\n       \techo stashed >conflict-file &&\n       \tgit stash push -m \"stashed\" &&\n       \ttest_commit label-upstream conflict-file upstream-content &&\n      -\ttest_must_fail git -c merge.conflictStyle=diff3 stash apply --label-ours=UP --label-theirs=STASH &&\n     -+\ttest_expect_code 2 git -c merge.conflictStyle=diff3 stash apply --label-ours=UP --label-theirs=STASH &&\n     ++\ttest_expect_code 1 git -c merge.conflictStyle=diff3 stash apply --label-ours=UP --label-theirs=STASH &&\n       \ttest_grep \"^<<<<<<< UP\" conflict-file &&\n       \ttest_grep \"^||||||| Stash base\" conflict-file &&\n       \ttest_grep \"^>>>>>>> STASH\" conflict-file\n     @@ t/t3903-stash.sh: test_expect_success 'apply with empty conflict labels' '\n       \tgit stash push -m \"stashed\" &&\n       \ttest_commit empty-label-upstream conflict-file upstream-content &&\n      -\ttest_must_fail git stash apply --label-ours= --label-theirs= &&\n     -+\ttest_expect_code 2 git stash apply --label-ours= --label-theirs= &&\n     ++\ttest_expect_code 1 git stash apply --label-ours= --label-theirs= &&\n       \ttest_grep \"^<<<<<<<$\" conflict-file &&\n       \ttest_grep \"^>>>>>>>$\" conflict-file\n       '\n       \n     -+test_expect_success 'apply exits 2 on conflicts and keeps the stash entry' '\n     ++test_expect_success 'pop exits 1 on conflicts and keeps the stash entry' '\n      +\tgit reset --hard initial &&\n     -+\ttest_commit exit-code-base conflict-file base-content &&\n     -+\techo stashed >conflict-file &&\n     -+\tgit stash push -m stashed &&\n     -+\ttest_commit exit-code-upstream conflict-file upstream-content &&\n     -+\ttest_expect_code 2 git stash apply &&\n     ++\techo stashed >file &&\n     ++\tgit stash push -m pop-stashed &&\n     ++\ttest_commit pop-upstream file upstream-content &&\n     ++\ttest_expect_code 1 git stash pop &&\n      +\tgit stash list >list &&\n     -+\ttest_grep stashed list\n     ++\ttest_grep pop-stashed list\n      +'\n      +\n     -+test_expect_success 'pop exits 2 on conflicts and keeps the stash entry' '\n     ++test_expect_success 'stash branch exits with a non-1 status on errors' '\n      +\tgit reset --hard initial &&\n     -+\ttest_commit pop-exit-code-base pop-file base-content &&\n     -+\techo stashed >pop-file &&\n     -+\tgit stash push -m pop-stashed &&\n     -+\ttest_commit pop-exit-code-upstream pop-file upstream-content &&\n     -+\ttest_expect_code 2 git stash pop &&\n     ++\techo stashed >file &&\n     ++\tgit stash push -m branch-stashed &&\n     ++\ttest_expect_code 128 git stash branch conflicting-branch refs/heads/does-not-exist &&\n      +\tgit stash list >list &&\n     -+\ttest_grep pop-stashed list\n     ++\ttest_grep branch-stashed list\n      +'\n      +\n       test_expect_success 'stash show --include-untracked includes untracked files' '\n       \tgit reset --hard &&\n       \n     -\n     - ## t/t7201-co.sh ##\n     -@@ t/t7201-co.sh: test_expect_success 'checkout -m creates a recoverable stash on conflict' '\n     - \ttest_must_fail git checkout side 2>stderr &&\n     - \ttest_grep \"Your local changes\" stderr &&\n     - \tgit checkout -m side >actual 2>&1 &&\n     --\ttest_grep \"resulted in conflicts\" actual &&\n     --\ttest_grep \"git stash drop\" actual &&\n     --\ttest_grep \"git stash pop\" actual &&\n     --\ttest_grep \"The following paths have local changes\" actual &&\n     -+\tcat >expect <<-EOF &&\n     -+\tYour local changes are stashed, however applying them\n     -+\tresulted in conflicts.  You can either resolve the conflicts\n     -+\tand then discard the stash with \"git stash drop\", or, if you\n     -+\tdo not want to resolve them now, run \"git reset --hard\" and\n     -+\tapply the local changes later by running \"git stash pop\".\n     -+\n     -+\tSwitched to branch ${SQ}side${SQ}\n     -+\tThe following paths have local changes:\n     -+\tM\tone\n     -+\tEOF\n     -+\ttest_cmp expect actual &&\n     - \tgit log -p -1 --format=\"%gs%n%B\" -g --diff-merges=1 refs/stash >actual &&\n     - \tsed /^index/d actual >actual.trimmed &&\n     - \tcat >expect <<-EOF &&\n -:  ---------- > 2:  935fa0a9ae checkout: separate autostash conflict advice from branch-switch message\n\n-- \ngitgitgadget\n"},{"id":"551796","messageId":"ff4322180294c784bcd5f4e92b35e4b334324ddc.1788373743.git.gitgitgadget@gmail.com","threadId":"66064","inReplyTo":"pull.2364.v4.git.git.1788373743.gitgitgadget@gmail.com","subject":"[PATCH v4 1/2] stash: reserve exit status 1 for conflicts","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-02T18:29:02Z","receivedAt":"2026-09-02T18:29:06Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\n\"git stash apply\", \"pop\" and \"branch\" exit with status 1 both when\napplying the stash entry resulted in conflicts and when they fail for\nother reasons, so callers cannot tell the two apart.\n\nFollow the convention of \"git merge-tree\" and the merge strategies,\nwhich exit with status 1 to indicate conflicts and with a different\nnon-zero status for errors: those subcommands now exit with status 1\nonly when applying the stash entry resulted in conflicts, in which\ncase the stash entry is left in place, and exit with status 128, the\nstatus die() uses, when they fail for other reasons.  Document the\nexit statuses.\n\ncmd_stash() used to collapse the return values of the subcommand\nimplementations to a boolean.  It now maps negative values, which\nsignal a failure, to 128 and passes everything else through as-is.\nThe only implementations that return a positive value are \"apply\",\n\"pop\" and \"branch\", which return the value of do_apply_stash():\n\"apply\" returns it directly, and \"pop\" and \"branch\" drop the stash\nentry, via do_drop_stash(), which always returns 0, only when the\napplication succeeded.  The positive value is always 1, as\ndo_apply_stash() only returns a positive value when the three-way\nmerge was unclean.\n\nMake the convention explicit by introducing enum stash_apply_result\nwith the values STASH_APPLY_CLEAN, STASH_APPLY_CONFLICT and\nSTASH_APPLY_ERROR, and use it for the in-process autostash helpers,\ntoo.  They spawn \"git stash apply\" and can now tell conflicts apart\nfrom other failures, e.g. a crash or death by signal of the child,\nwhich map to exit statuses above 1.  Since we know the stash entry\nwas saved, tell users so in the error message instead of leaving them\nwondering what happened to their stashed changes.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n Documentation/git-stash.adoc |  9 +++++\n builtin/stash.c              | 32 ++++++++++++-----\n sequencer.c                  | 66 ++++++++++++++++++++++--------------\n sequencer.h                  | 19 +++++++----\n stash.h                      | 21 ++++++++++++\n t/t3903-stash.sh             | 25 ++++++++++++--\n 6 files changed, 128 insertions(+), 44 deletions(-)\n create mode 100644 stash.h\n\ndiff --git a/Documentation/git-stash.adoc b/Documentation/git-stash.adoc\nindex 50bb89f483..fc6a9a008c 100644\n--- a/Documentation/git-stash.adoc\n+++ b/Documentation/git-stash.adoc\n@@ -426,6 +426,15 @@ include::includes/cmd-config-section-all.adoc[]\n :git-stash: 1\n include::config/stash.adoc[]\n \n+EXIT STATUS\n+-----------\n+\n+The `git stash` subcommands exit with status 0 on success.  The\n+subcommands that apply a stash entry, i.e. `apply`, `pop` and `branch`,\n+exit with status 1 when applying the stash entry resulted in conflicts,\n+in which case the stash entry is left in place, and with a non-zero\n+status other than 1 when they fail for other reasons.\n+\n \n SEE ALSO\n --------\ndiff --git a/builtin/stash.c b/builtin/stash.c\nindex 72c52571f8..d858b7603f 100644\n--- a/builtin/stash.c\n+++ b/builtin/stash.c\n@@ -10,6 +10,7 @@\n #include \"object-name.h\"\n #include \"parse-options.h\"\n #include \"refs.h\"\n+#include \"stash.h\"\n #include \"lockfile.h\"\n #include \"cache-tree.h\"\n #include \"unpack-trees.h\"\n@@ -640,10 +641,12 @@ static void unstage_changes_unless_new(struct object_id *orig_tree)\n \t\tdie(_(\"could not write index\"));\n }\n \n-static int do_apply_stash(const char *prefix, struct stash_info *info,\n-\t\t\t  int index, int quiet,\n-\t\t\t  const char *label_ours, const char *label_theirs,\n-\t\t\t  const char *label_base)\n+static enum stash_apply_result do_apply_stash(const char *prefix,\n+\t\t\t\t\t      struct stash_info *info,\n+\t\t\t\t\t      int index, int quiet,\n+\t\t\t\t\t      const char *label_ours,\n+\t\t\t\t\t      const char *label_theirs,\n+\t\t\t\t\t      const char *label_base)\n {\n \tint clean, ret;\n \tint has_index = index;\n@@ -717,8 +720,8 @@ static int do_apply_stash(const char *prefix, struct stash_info *info,\n \n \t/*\n \t * If 'clean' >= 0, reverse the value for 'ret' so 'ret' is 0 when the\n-\t * merge was clean, and nonzero if the merge was unclean or encountered\n-\t * an error.\n+\t * merge was clean, and 1 if the merge was unclean or a negative value\n+\t * if it encountered an error.\n \t */\n \tret = clean >= 0 ? !clean : clean;\n \n@@ -2492,9 +2495,20 @@ int cmd_stash(int argc,\n \tstrbuf_addf(&stash_index_path, \"%s.stash.%\" PRIuMAX, index_file,\n \t\t    (uintmax_t)pid);\n \n-\tif (fn)\n-\t\treturn !!fn(argc, argv, prefix, repo);\n-\telse if (!argc)\n+\tif (fn) {\n+\t\tret = fn(argc, argv, prefix, repo);\n+\n+\t\t/*\n+\t\t * The subcommand implementations return 0 on success, a\n+\t\t * negative value on failure, and STASH_APPLY_CONFLICT\n+\t\t * when applying a stash entry resulted in conflicts.\n+\t\t * Map failures to 128, the status die() uses, so that\n+\t\t * exit status 1 unambiguously indicates conflicts.\n+\t\t */\n+\t\tif (ret < 0)\n+\t\t\treturn 128;\n+\t\treturn ret;\n+\t} else if (!argc)\n \t\treturn !!push_stash_unassumed(0, NULL, prefix, repo);\n \n \t/* Assume 'stash push' */\ndiff --git a/sequencer.c b/sequencer.c\nindex 65afd100d9..b5dd855084 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -19,6 +19,7 @@\n #include \"commit.h\"\n #include \"sequencer.h\"\n #include \"run-command.h\"\n+#include \"stash.h\"\n #include \"hook.h\"\n #include \"utf8.h\"\n #include \"cache-tree.h\"\n@@ -4794,13 +4795,15 @@ void create_autostash_ref(struct repository *r, const char *refname,\n \tcreate_autostash_internal(r, NULL, refname, message, silent);\n }\n \n-static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply,\n-\t\t\t\t    const char *label_ours, const char *label_theirs,\n-\t\t\t\t    const char *label_base,\n-\t\t\t\t    const char *stash_msg)\n+static enum stash_apply_result apply_save_autostash_oid(const char *stash_oid,\n+\t\t\t\t\t\t\tint attempt_apply,\n+\t\t\t\t\t\t\tconst char *label_ours,\n+\t\t\t\t\t\t\tconst char *label_theirs,\n+\t\t\t\t\t\t\tconst char *label_base,\n+\t\t\t\t\t\t\tconst char *stash_msg)\n {\n \tstruct child_process child = CHILD_PROCESS_INIT;\n-\tint ret = 0;\n+\tenum stash_apply_result ret = STASH_APPLY_CLEAN;\n \n \tif (attempt_apply) {\n \t\tchild.git_cmd = 1;\n@@ -4816,9 +4819,11 @@ static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply,\n \t\t\tstrvec_pushf(&child.args, \"--label-base=%s\", label_base);\n \t\tstrvec_push(&child.args, stash_oid);\n \t\tret = run_command(&child);\n+\t\tif (ret > 1)\n+\t\t\tret = STASH_APPLY_ERROR;\n \t}\n \n-\tif (attempt_apply && !ret)\n+\tif (attempt_apply && ret == STASH_APPLY_CLEAN)\n \t\tfprintf(stderr, _(\"Applied autostash.\\n\"));\n \telse {\n \t\tstruct child_process store = CHILD_PROCESS_INIT;\n@@ -4832,13 +4837,16 @@ static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply,\n \t\tstrvec_push(&store.args, stash_oid);\n \t\tif (run_command(&store))\n \t\t\tret = error(_(\"cannot store %s\"), stash_oid);\n-\t\telse if (attempt_apply)\n+\t\telse if (attempt_apply && ret == STASH_APPLY_CONFLICT)\n \t\t\tfprintf(stderr,\n \t\t\t\t_(\"Your local changes are stashed, however applying them\\n\"\n \t\t\t\t  \"resulted in conflicts.  You can either resolve the conflicts\\n\"\n \t\t\t\t  \"and then discard the stash with \\\"git stash drop\\\", or, if you\\n\"\n \t\t\t\t  \"do not want to resolve them now, run \\\"git reset --hard\\\" and\\n\"\n \t\t\t\t  \"apply the local changes later by running \\\"git stash pop\\\".\\n\"));\n+\t\telse if (attempt_apply)\n+\t\t\tret = error(_(\"could not apply autostash; \"\n+\t\t\t\t      \"your changes are safe in the stash\"));\n \t\telse\n \t\t\tfprintf(stderr,\n \t\t\t\t_(\"Autostash exists; creating a new stash entry.\\n\"\n@@ -4850,15 +4858,16 @@ static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply,\n \treturn ret;\n }\n \n-static int apply_save_autostash(const char *path, int attempt_apply)\n+static enum stash_apply_result apply_save_autostash(const char *path,\n+\t\t\t\t\t\t    int attempt_apply)\n {\n \tstruct strbuf stash_oid = STRBUF_INIT;\n-\tint ret = 0;\n+\tenum stash_apply_result ret = STASH_APPLY_CLEAN;\n \n \tif (!read_oneliner(&stash_oid, path,\n \t\t\t   READ_ONELINER_SKIP_IF_EMPTY)) {\n \t\tstrbuf_release(&stash_oid);\n-\t\treturn 0;\n+\t\treturn STASH_APPLY_CLEAN;\n \t}\n \tstrbuf_trim(&stash_oid);\n \n@@ -4870,37 +4879,40 @@ static int apply_save_autostash(const char *path, int attempt_apply)\n \treturn ret;\n }\n \n-int save_autostash(const char *path)\n+enum stash_apply_result save_autostash(const char *path)\n {\n \treturn apply_save_autostash(path, 0);\n }\n \n-int apply_autostash(const char *path)\n+enum stash_apply_result apply_autostash(const char *path)\n {\n \treturn apply_save_autostash(path, 1);\n }\n \n-int apply_autostash_oid(const char *stash_oid)\n+enum stash_apply_result apply_autostash_oid(const char *stash_oid)\n {\n \treturn apply_save_autostash_oid(stash_oid, 1, NULL, NULL, NULL, NULL);\n }\n \n-static int apply_save_autostash_ref(struct repository *r, const char *refname,\n-\t\t\t\t    int attempt_apply,\n-\t\t\t\t    const char *label_ours, const char *label_theirs,\n-\t\t\t\t    const char *label_base,\n-\t\t\t\t    const char *stash_msg)\n+static enum stash_apply_result apply_save_autostash_ref(struct repository *r,\n+\t\t\t\t\t\t\tconst char *refname,\n+\t\t\t\t\t\t\tint attempt_apply,\n+\t\t\t\t\t\t\tconst char *label_ours,\n+\t\t\t\t\t\t\tconst char *label_theirs,\n+\t\t\t\t\t\t\tconst char *label_base,\n+\t\t\t\t\t\t\tconst char *stash_msg)\n {\n \tstruct object_id stash_oid;\n \tchar stash_oid_hex[GIT_MAX_HEXSZ + 1];\n-\tint flag, ret;\n+\tint flag;\n+\tenum stash_apply_result ret;\n \n \tif (!refs_ref_exists(get_main_ref_store(r), refname))\n-\t\treturn 0;\n+\t\treturn STASH_APPLY_CLEAN;\n \n \tif (!refs_resolve_ref_unsafe(get_main_ref_store(r), refname,\n \t\t\t\t     RESOLVE_REF_READING, &stash_oid, &flag))\n-\t\treturn -1;\n+\t\treturn STASH_APPLY_ERROR;\n \tif (flag & REF_ISSYMREF)\n \t\treturn error(_(\"autostash reference is a symref\"));\n \n@@ -4915,15 +4927,19 @@ static int apply_save_autostash_ref(struct repository *r, const char *refname,\n \treturn ret;\n }\n \n-int save_autostash_ref(struct repository *r, const char *refname)\n+enum stash_apply_result save_autostash_ref(struct repository *r,\n+\t\t\t\t\t   const char *refname)\n {\n \treturn apply_save_autostash_ref(r, refname, 0,\n \t\t\t\t\tNULL, NULL, NULL, NULL);\n }\n \n-int apply_autostash_ref(struct repository *r, const char *refname,\n-\t\t\tconst char *label_ours, const char *label_theirs,\n-\t\t\tconst char *label_base, const char *stash_msg)\n+enum stash_apply_result apply_autostash_ref(struct repository *r,\n+\t\t\t\t\t    const char *refname,\n+\t\t\t\t\t    const char *label_ours,\n+\t\t\t\t\t    const char *label_theirs,\n+\t\t\t\t\t    const char *label_base,\n+\t\t\t\t\t    const char *stash_msg)\n {\n \treturn apply_save_autostash_ref(r, refname, 1,\n \t\t\t\t\tlabel_ours, label_theirs, label_base,\ndiff --git a/sequencer.h b/sequencer.h\nindex 64a9c7fb1b..804501b64c 100644\n--- a/sequencer.h\n+++ b/sequencer.h\n@@ -3,6 +3,7 @@\n \n #include \"strbuf.h\"\n #include \"strvec.h\"\n+#include \"stash.h\"\n #include \"wt-status.h\"\n \n struct commit;\n@@ -231,13 +232,17 @@ void commit_post_rewrite(struct repository *r,\n void create_autostash(struct repository *r, const char *path);\n void create_autostash_ref(struct repository *r, const char *refname,\n \t\t\t  const char *message, bool silent);\n-int save_autostash(const char *path);\n-int save_autostash_ref(struct repository *r, const char *refname);\n-int apply_autostash(const char *path);\n-int apply_autostash_oid(const char *stash_oid);\n-int apply_autostash_ref(struct repository *r, const char *refname,\n-\t\t\tconst char *label_ours, const char *label_theirs,\n-\t\t\tconst char *label_base, const char *stash_msg);\n+enum stash_apply_result save_autostash(const char *path);\n+enum stash_apply_result save_autostash_ref(struct repository *r,\n+\t\t\t\t\t   const char *refname);\n+enum stash_apply_result apply_autostash(const char *path);\n+enum stash_apply_result apply_autostash_oid(const char *stash_oid);\n+enum stash_apply_result apply_autostash_ref(struct repository *r,\n+\t\t\t\t\t    const char *refname,\n+\t\t\t\t\t    const char *label_ours,\n+\t\t\t\t\t    const char *label_theirs,\n+\t\t\t\t\t    const char *label_base,\n+\t\t\t\t\t    const char *stash_msg);\n \n #define SUMMARY_INITIAL_COMMIT   (1 << 0)\n #define SUMMARY_SHOW_AUTHOR_DATE (1 << 1)\ndiff --git a/stash.h b/stash.h\nnew file mode 100644\nindex 0000000000..14ba4f946d\n--- /dev/null\n+++ b/stash.h\n@@ -0,0 +1,21 @@\n+#ifndef STASH_H\n+#define STASH_H\n+\n+enum stash_apply_result {\n+\t/* The stash was applied cleanly, or there was nothing to apply. */\n+\tSTASH_APPLY_CLEAN = 0,\n+\n+\t/*\n+\t * The stash could not be applied because it resulted in\n+\t * conflicts.  The stash entry is left in place.  The \"git stash\n+\t * apply\", \"pop\" and \"branch\" subcommands exit with this status\n+\t * in this case, mirroring the convention of \"git merge-tree\" and\n+\t * the merge strategies.\n+\t */\n+\tSTASH_APPLY_CONFLICT = 1,\n+\n+\t/* Something went wrong. */\n+\tSTASH_APPLY_ERROR = -1,\n+};\n+\n+#endif /* STASH_H */\ndiff --git a/t/t3903-stash.sh b/t/t3903-stash.sh\nindex da27a6599a..6529508b06 100755\n--- a/t/t3903-stash.sh\n+++ b/t/t3903-stash.sh\n@@ -1791,13 +1791,13 @@ test_expect_success 'stash.index=false overridden by --index' '\n \ttest_cmp expect file\n '\n \n-test_expect_success 'apply with custom conflict labels' '\n+test_expect_success 'apply exits 1 on conflicts' '\n \tgit reset --hard initial &&\n \ttest_commit label-base conflict-file base-content &&\n \techo stashed >conflict-file &&\n \tgit stash push -m \"stashed\" &&\n \ttest_commit label-upstream conflict-file upstream-content &&\n-\ttest_must_fail git -c merge.conflictStyle=diff3 stash apply --label-ours=UP --label-theirs=STASH &&\n+\ttest_expect_code 1 git -c merge.conflictStyle=diff3 stash apply --label-ours=UP --label-theirs=STASH &&\n \ttest_grep \"^<<<<<<< UP\" conflict-file &&\n \ttest_grep \"^||||||| Stash base\" conflict-file &&\n \ttest_grep \"^>>>>>>> STASH\" conflict-file\n@@ -1809,11 +1809,30 @@ test_expect_success 'apply with empty conflict labels' '\n \techo stashed >conflict-file &&\n \tgit stash push -m \"stashed\" &&\n \ttest_commit empty-label-upstream conflict-file upstream-content &&\n-\ttest_must_fail git stash apply --label-ours= --label-theirs= &&\n+\ttest_expect_code 1 git stash apply --label-ours= --label-theirs= &&\n \ttest_grep \"^<<<<<<<$\" conflict-file &&\n \ttest_grep \"^>>>>>>>$\" conflict-file\n '\n \n+test_expect_success 'pop exits 1 on conflicts and keeps the stash entry' '\n+\tgit reset --hard initial &&\n+\techo stashed >file &&\n+\tgit stash push -m pop-stashed &&\n+\ttest_commit pop-upstream file upstream-content &&\n+\ttest_expect_code 1 git stash pop &&\n+\tgit stash list >list &&\n+\ttest_grep pop-stashed list\n+'\n+\n+test_expect_success 'stash branch exits with a non-1 status on errors' '\n+\tgit reset --hard initial &&\n+\techo stashed >file &&\n+\tgit stash push -m branch-stashed &&\n+\ttest_expect_code 128 git stash branch conflicting-branch refs/heads/does-not-exist &&\n+\tgit stash list >list &&\n+\ttest_grep branch-stashed list\n+'\n+\n test_expect_success 'stash show --include-untracked includes untracked files' '\n \tgit reset --hard &&\n \n-- \ngitgitgadget\n\n"},{"id":"551797","messageId":"935fa0a9ae69f269a8a79f213f02aa4ed1e8279b.1788373743.git.gitgitgadget@gmail.com","threadId":"66064","inReplyTo":"pull.2364.v4.git.git.1788373743.gitgitgadget@gmail.com","subject":"[PATCH v4 2/2] checkout: separate autostash conflict advice from branch-switch message","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-02T18:29:03Z","receivedAt":"2026-09-02T18:29:07Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\n\"git checkout -m\" stashes the user's local changes when it cannot\nperform the checkout, and then applies the stash.  When applying the\nstash results in conflicts, the advice on how to deal with them is\nprinted directly on top of the branch-switch message (\"Switched to\nbranch ...\"), making the two hard to tell apart.  Print a blank line\nin between so that the advice and the branch-switch message are\nvisually distinct.\n\napply_autostash_ref() reports whether applying the stash resulted in\nconflicts via its enum stash_apply_result return value, so only print\nthe blank line in the conflicted case.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n builtin/checkout.c | 15 +++++++++------\n t/t7201-co.sh      | 16 ++++++++++++----\n 2 files changed, 21 insertions(+), 10 deletions(-)\n\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex 55e3a89a85..c960f98287 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -1166,6 +1166,7 @@ static int switch_branches(const struct checkout_opts *opts,\n \tint flag, writeout_error = 0;\n \tint do_merge = 1;\n \tint created_autostash = 0;\n+\tenum stash_apply_result autostash_res = STASH_APPLY_CLEAN;\n \tstruct strbuf old_commit_shortname = STRBUF_INIT;\n \tstruct strbuf autostash_msg = STRBUF_INIT;\n \tconst char *stash_label_base = NULL;\n@@ -1237,12 +1238,12 @@ static int switch_branches(const struct checkout_opts *opts,\n \t\t\t\tgit_config_push_parameter(cfg.buf);\n \t\t\t\tstrbuf_release(&cfg);\n \t\t\t}\n-\t\t\tapply_autostash_ref(the_repository,\n-\t\t\t\t\t    \"CHECKOUT_AUTOSTASH_HEAD\",\n-\t\t\t\t\t    new_branch_info->name,\n-\t\t\t\t\t    \"local\",\n-\t\t\t\t\t    stash_label_base,\n-\t\t\t\t\t    autostash_msg.buf);\n+\t\t\tautostash_res = apply_autostash_ref(the_repository,\n+\t\t\t\t    \"CHECKOUT_AUTOSTASH_HEAD\",\n+\t\t\t\t    new_branch_info->name,\n+\t\t\t\t    \"local\",\n+\t\t\t\t    stash_label_base,\n+\t\t\t\t    autostash_msg.buf);\n \t\t}\n \t\tif (ret) {\n \t\t\tbranch_info_release(&old_branch_info);\n@@ -1255,6 +1256,8 @@ static int switch_branches(const struct checkout_opts *opts,\n \tif (!opts->quiet && !old_branch_info.path && old_branch_info.commit && new_branch_info->commit != old_branch_info.commit)\n \t\torphaned_commit_warning(old_branch_info.commit, new_branch_info->commit);\n \n+\tif (autostash_res == STASH_APPLY_CONFLICT && !opts->quiet)\n+\t\tfputc('\\n', stderr);\n \tupdate_refs_for_switch(opts, &old_branch_info, new_branch_info);\n \n \tif (created_autostash) {\ndiff --git a/t/t7201-co.sh b/t/t7201-co.sh\nindex 0ddd1ad7aa..9ea9462914 100755\n--- a/t/t7201-co.sh\n+++ b/t/t7201-co.sh\n@@ -236,10 +236,18 @@ test_expect_success 'checkout -m creates a recoverable stash on conflict' '\n \ttest_must_fail git checkout side 2>stderr &&\n \ttest_grep \"Your local changes\" stderr &&\n \tgit checkout -m side >actual 2>&1 &&\n-\ttest_grep \"resulted in conflicts\" actual &&\n-\ttest_grep \"git stash drop\" actual &&\n-\ttest_grep \"git stash pop\" actual &&\n-\ttest_grep \"The following paths have local changes\" actual &&\n+\tcat >expect <<-EOF &&\n+\tYour local changes are stashed, however applying them\n+\tresulted in conflicts.  You can either resolve the conflicts\n+\tand then discard the stash with \"git stash drop\", or, if you\n+\tdo not want to resolve them now, run \"git reset --hard\" and\n+\tapply the local changes later by running \"git stash pop\".\n+\n+\tSwitched to branch ${SQ}side${SQ}\n+\tThe following paths have local changes:\n+\tM\tone\n+\tEOF\n+\ttest_cmp expect actual &&\n \tgit log -p -1 --format=\"%gs%n%B\" -g --diff-merges=1 refs/stash >actual &&\n \tsed /^index/d actual >actual.trimmed &&\n \tcat >expect <<-EOF &&\n-- \ngitgitgadget\n"},{"id":"551805","messageId":"xmqqwlt3h1oc.fsf@gitster.g","threadId":"66064","inReplyTo":"ff4322180294c784bcd5f4e92b35e4b334324ddc.1788373743.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v4 1/2] stash: reserve exit status 1 for conflicts","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-09-02T19:51:31Z","receivedAt":"2026-09-02T19:51:35Z","isPatch":true,"body":"\"Harald Nordgren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Harald Nordgren <haraldnordgren@gmail.com>\n>\n> \"git stash apply\", \"pop\" and \"branch\" exit with status 1 both when\n> applying the stash entry resulted in conflicts and when they fail for\n> other reasons, so callers cannot tell the two apart.\n>\n> Follow the convention of \"git merge-tree\" and the merge strategies,\n> which exit with status 1 to indicate conflicts and with a different\n> non-zero status for errors: those subcommands now exit with status 1\n> only when applying the stash entry resulted in conflicts, in which\n> case the stash entry is left in place, and exit with status 128, the\n> status die() uses, when they fail for other reasons.  Document the\n> exit statuses.\n>\n> cmd_stash() used to collapse the return values of the subcommand\n> implementations to a boolean.  It now maps negative values, which\n> signal a failure, to 128 and passes everything else through as-is.\n> The only implementations that return a positive value are \"apply\",\n> \"pop\" and \"branch\", which return the value of do_apply_stash():\n> \"apply\" returns it directly, and \"pop\" and \"branch\" drop the stash\n> entry, via do_drop_stash(), which always returns 0, only when the\n> application succeeded.  The positive value is always 1, as\n> do_apply_stash() only returns a positive value when the three-way\n> merge was unclean.\n>\n> Make the convention explicit by introducing enum stash_apply_result\n> with the values STASH_APPLY_CLEAN, STASH_APPLY_CONFLICT and\n> STASH_APPLY_ERROR, and use it for the in-process autostash helpers,\n> too.  They spawn \"git stash apply\" and can now tell conflicts apart\n> from other failures, e.g. a crash or death by signal of the child,\n> which map to exit statuses above 1.  Since we know the stash entry\n> was saved, tell users so in the error message instead of leaving them\n> wondering what happened to their stashed changes.\n>\n> Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n> ---\n\nThe above is on the overly verbose side.  The first two paragraphs\ngive enough discussion and the remainder mostly repeats with small\ndetails sprinkled in, which can probably be shortened to 1/4 of the\namount of text, but it is OK.\n\n> diff --git a/Documentation/git-stash.adoc b/Documentation/git-stash.adoc\n> index 50bb89f483..fc6a9a008c 100644\n> --- a/Documentation/git-stash.adoc\n> +++ b/Documentation/git-stash.adoc\n> @@ -426,6 +426,15 @@ include::includes/cmd-config-section-all.adoc[]\n>  :git-stash: 1\n>  include::config/stash.adoc[]\n>  \n> +EXIT STATUS\n> +-----------\n> +\n> +The `git stash` subcommands exit with status 0 on success.  The\n> +subcommands that apply a stash entry, i.e. `apply`, `pop` and `branch`,\n> +exit with status 1 when applying the stash entry resulted in conflicts,\n> +in which case the stash entry is left in place, and with a non-zero\n> +status other than 1 when they fail for other reasons.\n> +\n\nGreat.\n\n> +static enum stash_apply_result do_apply_stash(const char *prefix,\n> +\t\t\t\t\t      struct stash_info *info,\n> +\t\t\t\t\t      int index, int quiet,\n> +\t\t\t\t\t      const char *label_ours,\n> +\t\t\t\t\t      const char *label_theirs,\n> +\t\t\t\t\t      const char *label_base)\n>  {\n>  \tint clean, ret;\n>  \tint has_index = index;\n> @@ -717,8 +720,8 @@ static int do_apply_stash(const char *prefix, struct stash_info *info,\n>  \n>  \t/*\n>  \t * If 'clean' >= 0, reverse the value for 'ret' so 'ret' is 0 when the\n> -\t * merge was clean, and nonzero if the merge was unclean or encountered\n> -\t * an error.\n> +\t * merge was clean, and 1 if the merge was unclean or a negative value\n> +\t * if it encountered an error.\n>  \t */\n>  \tret = clean >= 0 ? !clean : clean;\n\nOK.\n\n> +\tif (fn) {\n> +\t\tret = fn(argc, argv, prefix, repo);\n> +\n> +\t\t/*\n> +\t\t * The subcommand implementations return 0 on success, a\n> +\t\t * negative value on failure, and STASH_APPLY_CONFLICT\n> +\t\t * when applying a stash entry resulted in conflicts.\n> +\t\t * Map failures to 128, the status die() uses, so that\n> +\t\t * exit status 1 unambiguously indicates conflicts.\n> +\t\t */\n> +\t\tif (ret < 0)\n> +\t\t\treturn 128;\n> +\t\treturn ret;\n> +\t} else if (!argc)\n>  \t\treturn !!push_stash_unassumed(0, NULL, prefix, repo);\n\nStyle.  Once one of \"if\", \"else if\" and \"else\" cascade gains\n{braches}, others should do so as well.\n\n> +static enum stash_apply_result apply_save_autostash_oid(const char *stash_oid,\n> +\t\t\t\t\t\t\tint attempt_apply,\n> +\t\t\t\t\t\t\tconst char *label_ours,\n> +\t\t\t\t\t\t\tconst char *label_theirs,\n> +\t\t\t\t\t\t\tconst char *label_base,\n> +\t\t\t\t\t\t\tconst char *stash_msg)\n>  {\n>  \tstruct child_process child = CHILD_PROCESS_INIT;\n> -\tint ret = 0;\n> +\tenum stash_apply_result ret = STASH_APPLY_CLEAN;\n>  \n>  \tif (attempt_apply) {\n>  \t\tchild.git_cmd = 1;\n> @@ -4816,9 +4819,11 @@ static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply,\n>  \t\t\tstrvec_pushf(&child.args, \"--label-base=%s\", label_base);\n>  \t\tstrvec_push(&child.args, stash_oid);\n>  \t\tret = run_command(&child);\n> +\t\tif (ret > 1)\n> +\t\t\tret = STASH_APPLY_ERROR;\n\nThis kind of code that assigns any random \"int\" that is returned by\nrun_command() to \"enum ret\" that has much narrower valid value range\nand then makes corrections annoys me a bit.\n\nOne way to do this cleanly might be to make a small helper function\ndo_stash_apply(), and use it like so:\n\n\tif (attempt_apply)\n\t\tret = do_stash_apply(stash_oid, label_ours, label_theirs,\n\t\t\t\t     label_base);\n\nThe implementation of do_stash_apply() would be like what you have\nin \"if (attempt_apply) {...}\" block, perhaps like:\n\n\tstatic enum stash_apply_result do_stash_apply(const char *stash_oid,\n\t\t\t\t\t\t      const char *label_ours,\n\t\t\t\t\t\t      const char *label_theirs,\n\t\t\t\t\t\t      const char *label_base)\n\t{\n\t\tstruct child_process child = CHILD_PROCESS_INIT;\n\n\t\tchild.git_cmd = 1;\n\t\t...\n                strvec_push(&child.args, stash_oid);\n                switch (run_command(&child)) {\n\t\tcase 0: return STASH_APPLY_CLEAN;\n\t\tcase 1: return STASH_APPLY_CONFLICT;\n\t\tdefault: return STASH_APPLY_ERROR;\n\t\t}\n\t}\n\n> -\tif (attempt_apply && !ret)\n> +\tif (attempt_apply && ret == STASH_APPLY_CLEAN)\n>  \t\tfprintf(stderr, _(\"Applied autostash.\\n\"));\n>  \telse {\n>  \t\tstruct child_process store = CHILD_PROCESS_INIT;\n\nGood, and the rest of this function is good.\n"},{"id":"551806","messageId":"xmqqse3rh1mp.fsf@gitster.g","threadId":"66064","inReplyTo":"935fa0a9ae69f269a8a79f213f02aa4ed1e8279b.1788373743.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v4 2/2] checkout: separate autostash conflict advice from branch-switch message","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-09-02T19:52:30Z","receivedAt":"2026-09-02T19:52:34Z","isPatch":true,"body":"\"Harald Nordgren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Harald Nordgren <haraldnordgren@gmail.com>\n>\n> \"git checkout -m\" stashes the user's local changes when it cannot\n> perform the checkout, and then applies the stash.  When applying the\n> stash results in conflicts, the advice on how to deal with them is\n> printed directly on top of the branch-switch message (\"Switched to\n> branch ...\"), making the two hard to tell apart.  Print a blank line\n> in between so that the advice and the branch-switch message are\n> visually distinct.\n>\n> apply_autostash_ref() reports whether applying the stash resulted in\n> conflicts via its enum stash_apply_result return value, so only print\n> the blank line in the conflicted case.\n>\n> Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n> ---\n>  builtin/checkout.c | 15 +++++++++------\n\nThis iteration looks good to me.\n"},{"id":"551807","messageId":"xmqqmrtzh0vx.fsf@gitster.g","threadId":"66064","inReplyTo":"xmqqwlt3h1oc.fsf@gitster.g","subject":"Re: [PATCH v4 1/2] stash: reserve exit status 1 for conflicts","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-09-02T20:08:34Z","receivedAt":"2026-09-02T20:08:36Z","isPatch":true,"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> \"Harald Nordgren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>\n>> From: Harald Nordgren <haraldnordgren@gmail.com>\n>>\n>> \"git stash apply\", \"pop\" and \"branch\" exit with status 1 both when\n>> applying the stash entry resulted in conflicts and when they fail for\n>> other reasons, so callers cannot tell the two apart.\n>>\n>> Follow the convention of \"git merge-tree\" and the merge strategies,\n>> which exit with status 1 to indicate conflicts and with a different\n>> non-zero status for errors: those subcommands now exit with status 1\n>> only when applying the stash entry resulted in conflicts, in which\n>> case the stash entry is left in place, and exit with status 128, the\n>> status die() uses, when they fail for other reasons.  Document the\n>> exit statuses.\n>>\n>> cmd_stash() used to collapse the return values of the subcommand\n>> implementations to a boolean.  It now maps negative values, which\n>> signal a failure, to 128 and passes everything else through as-is.\n>> The only implementations that return a positive value are \"apply\",\n>> \"pop\" and \"branch\", which return the value of do_apply_stash():\n>> \"apply\" returns it directly, and \"pop\" and \"branch\" drop the stash\n>> entry, via do_drop_stash(), which always returns 0, only when the\n>> application succeeded.  The positive value is always 1, as\n>> do_apply_stash() only returns a positive value when the three-way\n>> merge was unclean.\n>>\n>> Make the convention explicit by introducing enum stash_apply_result\n>> with the values STASH_APPLY_CLEAN, STASH_APPLY_CONFLICT and\n>> STASH_APPLY_ERROR, and use it for the in-process autostash helpers,\n>> too.  They spawn \"git stash apply\" and can now tell conflicts apart\n>> from other failures, e.g. a crash or death by signal of the child,\n>> which map to exit statuses above 1.  Since we know the stash entry\n>> was saved, tell users so in the error message instead of leaving them\n>> wondering what happened to their stashed changes.\n>>\n>> Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n>> ---\n>\n> The above is on the overly verbose side.  The first two paragraphs\n> give enough discussion and the remainder mostly repeats with small\n> details sprinkled in, which can probably be shortened to 1/4 of the\n> amount of text, but it is OK.\n\nJust for fun, I complained to an AI agent I had nearby with the\nabove four lines of critique, which spit back the following as a\nreplacement for the last two paragraphs.\n\n    Update cmd_stash() to map negative return values to 128 while\n    passing positive values through.  Formalize this with enum\n    stash_apply_result and update the autostash helpers accordingly\n    to distinguish conflicts from errors and inform users that their\n    stash was preserved.\n\nAs I said it is OK already, you do not have to adopt this shortened\nversion, but I personally think that this level of detail is enough.\n"},{"id":"551868","messageId":"002b1324-0f7f-45ec-9f67-624a41801ff7@gmail.com","threadId":"66064","inReplyTo":"xmqqwlt3h1oc.fsf@gitster.g","subject":"Re: [PATCH v4 1/2] stash: reserve exit status 1 for conflicts","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-09-03T13:57:43Z","receivedAt":"2026-09-03T13:57:49Z","isPatch":true,"body":"On 02/09/2026 20:51, Junio C Hamano wrote:\n> \"Harald Nordgren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n> \n>> From: Harald Nordgren <haraldnordgren@gmail.com>\n>>\n>> \"git stash apply\", \"pop\" and \"branch\" exit with status 1 both when\n>> applying the stash entry resulted in conflicts and when they fail for\n>> other reasons, so callers cannot tell the two apart.\n>>\n>> Follow the convention of \"git merge-tree\" and the merge strategies,\n>> which exit with status 1 to indicate conflicts and with a different\n>> non-zero status for errors: those subcommands now exit with status 1\n>> only when applying the stash entry resulted in conflicts, in which\n>> case the stash entry is left in place, and exit with status 128, the\n>> status die() uses, when they fail for other reasons.  Document the\n>> exit statuses.\n>>\n>> cmd_stash() used to collapse the return values of the subcommand\n>> implementations to a boolean.  It now maps negative values, which\n>> signal a failure, to 128 and passes everything else through as-is.\n>> The only implementations that return a positive value are \"apply\",\n>> \"pop\" and \"branch\", which return the value of do_apply_stash():\n>> \"apply\" returns it directly, and \"pop\" and \"branch\" drop the stash\n>> entry, via do_drop_stash(), which always returns 0, only when the\n>> application succeeded.  The positive value is always 1, as\n>> do_apply_stash() only returns a positive value when the three-way\n>> merge was unclean.\n>>\n>> Make the convention explicit by introducing enum stash_apply_result\n>> with the values STASH_APPLY_CLEAN, STASH_APPLY_CONFLICT and\n>> STASH_APPLY_ERROR, and use it for the in-process autostash helpers,\n>> too.  They spawn \"git stash apply\" and can now tell conflicts apart\n>> from other failures, e.g. a crash or death by signal of the child,\n>> which map to exit statuses above 1.  Since we know the stash entry\n>> was saved, tell users so in the error message instead of leaving them\n>> wondering what happened to their stashed changes.\n>>\n>> Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n>> ---\n> \n> The above is on the overly verbose side.  The first two paragraphs\n> give enough discussion and the remainder mostly repeats with small\n> details sprinkled in, which can probably be shortened to 1/4 of the\n> amount of text, but it is OK.\n\nI think the analysis in the middle of the third paragraph is useful to \nmake it clear that the return paths have been audited correctly. I agree \nthe rest could be condensed or cut.\n\nThanks\n\nPhillip\n\n> \n>> diff --git a/Documentation/git-stash.adoc b/Documentation/git-stash.adoc\n>> index 50bb89f483..fc6a9a008c 100644\n>> --- a/Documentation/git-stash.adoc\n>> +++ b/Documentation/git-stash.adoc\n>> @@ -426,6 +426,15 @@ include::includes/cmd-config-section-all.adoc[]\n>>   :git-stash: 1\n>>   include::config/stash.adoc[]\n>>   \n>> +EXIT STATUS\n>> +-----------\n>> +\n>> +The `git stash` subcommands exit with status 0 on success.  The\n>> +subcommands that apply a stash entry, i.e. `apply`, `pop` and `branch`,\n>> +exit with status 1 when applying the stash entry resulted in conflicts,\n>> +in which case the stash entry is left in place, and with a non-zero\n>> +status other than 1 when they fail for other reasons.\n>> +\n> \n> Great.\n> \n>> +static enum stash_apply_result do_apply_stash(const char *prefix,\n>> +\t\t\t\t\t      struct stash_info *info,\n>> +\t\t\t\t\t      int index, int quiet,\n>> +\t\t\t\t\t      const char *label_ours,\n>> +\t\t\t\t\t      const char *label_theirs,\n>> +\t\t\t\t\t      const char *label_base)\n>>   {\n>>   \tint clean, ret;\n>>   \tint has_index = index;\n>> @@ -717,8 +720,8 @@ static int do_apply_stash(const char *prefix, struct stash_info *info,\n>>   \n>>   \t/*\n>>   \t * If 'clean' >= 0, reverse the value for 'ret' so 'ret' is 0 when the\n>> -\t * merge was clean, and nonzero if the merge was unclean or encountered\n>> -\t * an error.\n>> +\t * merge was clean, and 1 if the merge was unclean or a negative value\n>> +\t * if it encountered an error.\n>>   \t */\n>>   \tret = clean >= 0 ? !clean : clean;\n> \n> OK.\n> \n>> +\tif (fn) {\n>> +\t\tret = fn(argc, argv, prefix, repo);\n>> +\n>> +\t\t/*\n>> +\t\t * The subcommand implementations return 0 on success, a\n>> +\t\t * negative value on failure, and STASH_APPLY_CONFLICT\n>> +\t\t * when applying a stash entry resulted in conflicts.\n>> +\t\t * Map failures to 128, the status die() uses, so that\n>> +\t\t * exit status 1 unambiguously indicates conflicts.\n>> +\t\t */\n>> +\t\tif (ret < 0)\n>> +\t\t\treturn 128;\n>> +\t\treturn ret;\n>> +\t} else if (!argc)\n>>   \t\treturn !!push_stash_unassumed(0, NULL, prefix, repo);\n> \n> Style.  Once one of \"if\", \"else if\" and \"else\" cascade gains\n> {braches}, others should do so as well.\n> \n>> +static enum stash_apply_result apply_save_autostash_oid(const char *stash_oid,\n>> +\t\t\t\t\t\t\tint attempt_apply,\n>> +\t\t\t\t\t\t\tconst char *label_ours,\n>> +\t\t\t\t\t\t\tconst char *label_theirs,\n>> +\t\t\t\t\t\t\tconst char *label_base,\n>> +\t\t\t\t\t\t\tconst char *stash_msg)\n>>   {\n>>   \tstruct child_process child = CHILD_PROCESS_INIT;\n>> -\tint ret = 0;\n>> +\tenum stash_apply_result ret = STASH_APPLY_CLEAN;\n>>   \n>>   \tif (attempt_apply) {\n>>   \t\tchild.git_cmd = 1;\n>> @@ -4816,9 +4819,11 @@ static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply,\n>>   \t\t\tstrvec_pushf(&child.args, \"--label-base=%s\", label_base);\n>>   \t\tstrvec_push(&child.args, stash_oid);\n>>   \t\tret = run_command(&child);\n>> +\t\tif (ret > 1)\n>> +\t\t\tret = STASH_APPLY_ERROR;\n> \n> This kind of code that assigns any random \"int\" that is returned by\n> run_command() to \"enum ret\" that has much narrower valid value range\n> and then makes corrections annoys me a bit.\n> \n> One way to do this cleanly might be to make a small helper function\n> do_stash_apply(), and use it like so:\n> \n> \tif (attempt_apply)\n> \t\tret = do_stash_apply(stash_oid, label_ours, label_theirs,\n> \t\t\t\t     label_base);\n> \n> The implementation of do_stash_apply() would be like what you have\n> in \"if (attempt_apply) {...}\" block, perhaps like:\n> \n> \tstatic enum stash_apply_result do_stash_apply(const char *stash_oid,\n> \t\t\t\t\t\t      const char *label_ours,\n> \t\t\t\t\t\t      const char *label_theirs,\n> \t\t\t\t\t\t      const char *label_base)\n> \t{\n> \t\tstruct child_process child = CHILD_PROCESS_INIT;\n> \n> \t\tchild.git_cmd = 1;\n> \t\t...\n>                  strvec_push(&child.args, stash_oid);\n>                  switch (run_command(&child)) {\n> \t\tcase 0: return STASH_APPLY_CLEAN;\n> \t\tcase 1: return STASH_APPLY_CONFLICT;\n> \t\tdefault: return STASH_APPLY_ERROR;\n> \t\t}\n> \t}\n> \n>> -\tif (attempt_apply && !ret)\n>> +\tif (attempt_apply && ret == STASH_APPLY_CLEAN)\n>>   \t\tfprintf(stderr, _(\"Applied autostash.\\n\"));\n>>   \telse {\n>>   \t\tstruct child_process store = CHILD_PROCESS_INIT;\n> \n> Good, and the rest of this function is good.\n\n"},{"id":"551869","messageId":"66283358-9b87-4485-a036-ba91e6d7fcad@gmail.com","threadId":"66064","inReplyTo":"pull.2364.v4.git.git.1788373743.gitgitgadget@gmail.com","subject":"Re: [PATCH v4 0/2] checkout -m: refine autostash fallback","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-09-03T14:00:27Z","receivedAt":"2026-09-03T14:00:40Z","isPatch":true,"body":"Hi Harald\n\nOn 02/09/2026 19:29, Harald Nordgren via GitGitGadget wrote:\n> Avoiding checkout -m autostash retries when no tracked local changes exist\n> and visually separating autostash conflict advice from the subsequent\n> branch-switch message. Addresses #leftoverbits from here:\n> https://lore.kernel.org/git/cfd09dbf-8d77-4464-8030-3a0ffb4aeae7@gmail.com/\n> \n> Changes in v4:\n> \n>   * Conflicts now exit with status 1 like merge-tree, other failures exit 128\n>     so exit 1 unambiguously means conflicts. Stash changes split into their\n>     own commit.\n\nThanks for changing the exit codes, I don't have anything to add to \nJunio's comments.\n\nThanks\n\nPhillip\n\n>   * The autostash apply helpers use the return value (enum\n>     stash_apply_result) instead of an out-parameter, and only claim conflicts\n>     when git stash apply actually reported them.\n> \n> Changes in v3:\n> \n>   * Use enum for git stash return values, to separate conflict from generic\n>     error.\n> \n> Changes in v2:\n> \n>   * Simplify logic and combine to one commit.\n>   * Test full output with test_cmp.\n> \n> Harald Nordgren (2):\n>    stash: reserve exit status 1 for conflicts\n>    checkout: separate autostash conflict advice from branch-switch\n>      message\n> \n>   Documentation/git-stash.adoc |  9 +++++\n>   builtin/checkout.c           | 15 ++++----\n>   builtin/stash.c              | 32 ++++++++++++-----\n>   sequencer.c                  | 66 ++++++++++++++++++++++--------------\n>   sequencer.h                  | 19 +++++++----\n>   stash.h                      | 21 ++++++++++++\n>   t/t3903-stash.sh             | 25 ++++++++++++--\n>   t/t7201-co.sh                | 16 ++++++---\n>   8 files changed, 149 insertions(+), 54 deletions(-)\n>   create mode 100644 stash.h\n> \n> \n> base-commit: 1630431f326e15fcde608827b5ff38422528eb59\n> Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2364%2FHaraldNordgren%2Fhn%2Fgit-checkout-m-leftoverbits-v4\n> Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2364/HaraldNordgren/hn/git-checkout-m-leftoverbits-v4\n> Pull-Request: https://github.com/git/git/pull/2364\n> \n> Range-diff vs v3:\n> \n>   1:  8e1979dd6c ! 1:  ff43221802 checkout: separate autostash conflict advice from branch-switch message\n>       @@ Metadata\n>        Author: Harald Nordgren <haraldnordgren@gmail.com>\n>        \n>         ## Commit message ##\n>       -    checkout: separate autostash conflict advice from branch-switch message\n>       +    stash: reserve exit status 1 for conflicts\n>        \n>       -    \"git checkout -m\" stashes the user's local changes when it cannot\n>       -    perform the checkout, and then applies the stash.  When applying the\n>       -    stash results in conflicts, the advice on how to deal with them is\n>       -    printed directly on top of the branch-switch message (\"Switched to\n>       -    branch ...\"), making the two hard to tell apart.  Print a blank line\n>       -    in between so that the advice and the branch-switch message are\n>       -    visually distinct.\n>       +    \"git stash apply\", \"pop\" and \"branch\" exit with status 1 both when\n>       +    applying the stash entry resulted in conflicts and when they fail for\n>       +    other reasons, so callers cannot tell the two apart.\n>        \n>       -    To make this possible, \"git stash apply\", \"pop\" and \"branch\" now exit\n>       -    with status 2 when applying the stash entry resulted in conflicts, in\n>       -    which case the stash entry is left in place; other failures exit with\n>       -    status 1, as before.  The exit statuses are documented in the \"git\n>       -    stash\" documentation.\n>       +    Follow the convention of \"git merge-tree\" and the merge strategies,\n>       +    which exit with status 1 to indicate conflicts and with a different\n>       +    non-zero status for errors: those subcommands now exit with status 1\n>       +    only when applying the stash entry resulted in conflicts, in which\n>       +    case the stash entry is left in place, and exit with status 128, the\n>       +    status die() uses, when they fail for other reasons.  Document the\n>       +    exit statuses.\n>       +\n>       +    cmd_stash() used to collapse the return values of the subcommand\n>       +    implementations to a boolean.  It now maps negative values, which\n>       +    signal a failure, to 128 and passes everything else through as-is.\n>       +    The only implementations that return a positive value are \"apply\",\n>       +    \"pop\" and \"branch\", which return the value of do_apply_stash():\n>       +    \"apply\" returns it directly, and \"pop\" and \"branch\" drop the stash\n>       +    entry, via do_drop_stash(), which always returns 0, only when the\n>       +    application succeeded.  The positive value is always 1, as\n>       +    do_apply_stash() only returns a positive value when the three-way\n>       +    merge was unclean.\n>       +\n>       +    Make the convention explicit by introducing enum stash_apply_result\n>       +    with the values STASH_APPLY_CLEAN, STASH_APPLY_CONFLICT and\n>       +    STASH_APPLY_ERROR, and use it for the in-process autostash helpers,\n>       +    too.  They spawn \"git stash apply\" and can now tell conflicts apart\n>       +    from other failures, e.g. a crash or death by signal of the child,\n>       +    which map to exit statuses above 1.  Since we know the stash entry\n>       +    was saved, tell users so in the error message instead of leaving them\n>       +    wondering what happened to their stashed changes.\n>        \n>            Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n>        \n>       @@ Documentation/git-stash.adoc: include::includes/cmd-config-section-all.adoc[]\n>        +EXIT STATUS\n>        +-----------\n>        +\n>       -+The `git stash` subcommands exit with status 0 on success and non-zero\n>       -+on failure.  The subcommands that apply a stash entry, i.e. `apply`,\n>       -+`pop` and `branch`, exit with status 2 when applying the stash entry\n>       -+resulted in conflicts, in which case the stash entry is left in place.\n>       -+Other failures exit with status 1 (usage errors exit with status 129).\n>       ++The `git stash` subcommands exit with status 0 on success.  The\n>       ++subcommands that apply a stash entry, i.e. `apply`, `pop` and `branch`,\n>       ++exit with status 1 when applying the stash entry resulted in conflicts,\n>       ++in which case the stash entry is left in place, and with a non-zero\n>       ++status other than 1 when they fail for other reasons.\n>        +\n>         \n>         SEE ALSO\n>         --------\n>        \n>       - ## builtin/checkout.c ##\n>       -@@ builtin/checkout.c: static int switch_branches(const struct checkout_opts *opts,\n>       - \tint flag, writeout_error = 0;\n>       - \tint do_merge = 1;\n>       - \tint created_autostash = 0;\n>       -+\tenum stash_apply_result autostash_res = STASH_APPLY_CLEAN;\n>       - \tstruct strbuf old_commit_shortname = STRBUF_INIT;\n>       - \tstruct strbuf autostash_msg = STRBUF_INIT;\n>       - \tconst char *stash_label_base = NULL;\n>       -@@ builtin/checkout.c: static int switch_branches(const struct checkout_opts *opts,\n>       - \t\t\t\tgit_config_push_parameter(cfg.buf);\n>       - \t\t\t\tstrbuf_release(&cfg);\n>       - \t\t\t}\n>       --\t\t\tapply_autostash_ref(the_repository,\n>       --\t\t\t\t\t    \"CHECKOUT_AUTOSTASH_HEAD\",\n>       --\t\t\t\t\t    new_branch_info->name,\n>       --\t\t\t\t\t    \"local\",\n>       --\t\t\t\t\t    stash_label_base,\n>       --\t\t\t\t\t    autostash_msg.buf);\n>       -+\t\t\tautostash_res = apply_autostash_ref(the_repository,\n>       -+\t\t\t\t    \"CHECKOUT_AUTOSTASH_HEAD\",\n>       -+\t\t\t\t    new_branch_info->name,\n>       -+\t\t\t\t    \"local\",\n>       -+\t\t\t\t    stash_label_base,\n>       -+\t\t\t\t    autostash_msg.buf);\n>       - \t\t}\n>       - \t\tif (ret) {\n>       - \t\t\tbranch_info_release(&old_branch_info);\n>       -@@ builtin/checkout.c: static int switch_branches(const struct checkout_opts *opts,\n>       - \tif (!opts->quiet && !old_branch_info.path && old_branch_info.commit && new_branch_info->commit != old_branch_info.commit)\n>       - \t\torphaned_commit_warning(old_branch_info.commit, new_branch_info->commit);\n>       -\n>       -+\tif (autostash_res == STASH_APPLY_CONFLICT && !opts->quiet)\n>       -+\t\tfputc('\\n', stderr);\n>       - \tupdate_refs_for_switch(opts, &old_branch_info, new_branch_info);\n>       -\n>       - \tif (created_autostash) {\n>       -\n>         ## builtin/stash.c ##\n>        @@\n>         #include \"object-name.h\"\n>       @@ builtin/stash.c: static void unstage_changes_unless_new(struct object_id *orig_t\n>         \n>        -static int do_apply_stash(const char *prefix, struct stash_info *info,\n>        -\t\t\t  int index, int quiet,\n>       +-\t\t\t  const char *label_ours, const char *label_theirs,\n>       +-\t\t\t  const char *label_base)\n>        +static enum stash_apply_result do_apply_stash(const char *prefix,\n>        +\t\t\t\t\t      struct stash_info *info,\n>        +\t\t\t\t\t      int index, int quiet,\n>       - \t\t\t  const char *label_ours, const char *label_theirs,\n>       - \t\t\t  const char *label_base)\n>       ++\t\t\t\t\t      const char *label_ours,\n>       ++\t\t\t\t\t      const char *label_theirs,\n>       ++\t\t\t\t\t      const char *label_base)\n>         {\n>       + \tint clean, ret;\n>       + \tint has_index = index;\n>        @@ builtin/stash.c: static int do_apply_stash(const char *prefix, struct stash_info *info,\n>       - \tclean = merge_ort_nonrecursive(&o, head, merge, merge_base);\n>         \n>         \t/*\n>       --\t * If 'clean' >= 0, reverse the value for 'ret' so 'ret' is 0 when the\n>       + \t * If 'clean' >= 0, reverse the value for 'ret' so 'ret' is 0 when the\n>        -\t * merge was clean, and nonzero if the merge was unclean or encountered\n>        -\t * an error.\n>       -+\t * Translate the value of 'clean' so 'ret' is STASH_APPLY_CLEAN\n>       -+\t * when the merge was clean, STASH_APPLY_CONFLICT when it was\n>       -+\t * unclean, and a negative value if it encountered an error.\n>       ++\t * merge was clean, and 1 if the merge was unclean or a negative value\n>       ++\t * if it encountered an error.\n>         \t */\n>       --\tret = clean >= 0 ? !clean : clean;\n>       -+\tret = clean >= 0 ? (clean ? STASH_APPLY_CLEAN : STASH_APPLY_CONFLICT)\n>       -+\t\t\t : clean;\n>       + \tret = clean >= 0 ? !clean : clean;\n>         \n>       - \tif (ret < 0)\n>       - \t\trollback_lock_file(&lock);\n>       -@@ builtin/stash.c: static int do_apply_stash(const char *prefix, struct stash_info *info,\n>       -\n>       - \tif (has_index) {\n>       - \t\tif (reset_tree(&index_tree, 0, 0))\n>       --\t\t\tret = -1;\n>       -+\t\t\tret = STASH_APPLY_ERROR;\n>       - \t} else {\n>       - \t\tunstage_changes_unless_new(&c_tree);\n>       - \t}\n>        @@ builtin/stash.c: int cmd_stash(int argc,\n>         \tstrbuf_addf(&stash_index_path, \"%s.stash.%\" PRIuMAX, index_file,\n>         \t\t    (uintmax_t)pid);\n>       @@ builtin/stash.c: int cmd_stash(int argc,\n>        +\tif (fn) {\n>        +\t\tret = fn(argc, argv, prefix, repo);\n>        +\n>       ++\t\t/*\n>       ++\t\t * The subcommand implementations return 0 on success, a\n>       ++\t\t * negative value on failure, and STASH_APPLY_CONFLICT\n>       ++\t\t * when applying a stash entry resulted in conflicts.\n>       ++\t\t * Map failures to 128, the status die() uses, so that\n>       ++\t\t * exit status 1 unambiguously indicates conflicts.\n>       ++\t\t */\n>        +\t\tif (ret < 0)\n>       -+\t\t\treturn 1;\n>       ++\t\t\treturn 128;\n>        +\t\treturn ret;\n>        +\t} else if (!argc)\n>         \t\treturn !!push_stash_unassumed(0, NULL, prefix, repo);\n>       @@ sequencer.c: static int apply_save_autostash_oid(const char *stash_oid, int atte\n>         \t\t\tstrvec_pushf(&child.args, \"--label-base=%s\", label_base);\n>         \t\tstrvec_push(&child.args, stash_oid);\n>         \t\tret = run_command(&child);\n>       -+\t\tif (ret && ret != STASH_APPLY_CONFLICT)\n>       ++\t\tif (ret > 1)\n>        +\t\t\tret = STASH_APPLY_ERROR;\n>         \t}\n>         \n>       @@ sequencer.c: static int apply_save_autostash_oid(const char *stash_oid, int atte\n>         \t\t\t\t  \"do not want to resolve them now, run \\\"git reset --hard\\\" and\\n\"\n>         \t\t\t\t  \"apply the local changes later by running \\\"git stash pop\\\".\\n\"));\n>        +\t\telse if (attempt_apply)\n>       -+\t\t\tret = error(_(\"could not apply autostash\"));\n>       ++\t\t\tret = error(_(\"could not apply autostash; \"\n>       ++\t\t\t\t      \"your changes are safe in the stash\"));\n>         \t\telse\n>         \t\t\tfprintf(stderr,\n>         \t\t\t\t_(\"Autostash exists; creating a new stash entry.\\n\"\n>       @@ stash.h (new)\n>        +\t * The stash could not be applied because it resulted in\n>        +\t * conflicts.  The stash entry is left in place.  The \"git stash\n>        +\t * apply\", \"pop\" and \"branch\" subcommands exit with this status\n>       -+\t * in this case.\n>       ++\t * in this case, mirroring the convention of \"git merge-tree\" and\n>       ++\t * the merge strategies.\n>        +\t */\n>       -+\tSTASH_APPLY_CONFLICT = 2,\n>       ++\tSTASH_APPLY_CONFLICT = 1,\n>        +\n>        +\t/* Something went wrong. */\n>        +\tSTASH_APPLY_ERROR = -1,\n>       @@ stash.h (new)\n>        +#endif /* STASH_H */\n>        \n>         ## t/t3903-stash.sh ##\n>       -@@ t/t3903-stash.sh: test_expect_success 'apply with custom conflict labels' '\n>       +@@ t/t3903-stash.sh: test_expect_success 'stash.index=false overridden by --index' '\n>       + \ttest_cmp expect file\n>       + '\n>       +\n>       +-test_expect_success 'apply with custom conflict labels' '\n>       ++test_expect_success 'apply exits 1 on conflicts' '\n>       + \tgit reset --hard initial &&\n>       + \ttest_commit label-base conflict-file base-content &&\n>         \techo stashed >conflict-file &&\n>         \tgit stash push -m \"stashed\" &&\n>         \ttest_commit label-upstream conflict-file upstream-content &&\n>        -\ttest_must_fail git -c merge.conflictStyle=diff3 stash apply --label-ours=UP --label-theirs=STASH &&\n>       -+\ttest_expect_code 2 git -c merge.conflictStyle=diff3 stash apply --label-ours=UP --label-theirs=STASH &&\n>       ++\ttest_expect_code 1 git -c merge.conflictStyle=diff3 stash apply --label-ours=UP --label-theirs=STASH &&\n>         \ttest_grep \"^<<<<<<< UP\" conflict-file &&\n>         \ttest_grep \"^||||||| Stash base\" conflict-file &&\n>         \ttest_grep \"^>>>>>>> STASH\" conflict-file\n>       @@ t/t3903-stash.sh: test_expect_success 'apply with empty conflict labels' '\n>         \tgit stash push -m \"stashed\" &&\n>         \ttest_commit empty-label-upstream conflict-file upstream-content &&\n>        -\ttest_must_fail git stash apply --label-ours= --label-theirs= &&\n>       -+\ttest_expect_code 2 git stash apply --label-ours= --label-theirs= &&\n>       ++\ttest_expect_code 1 git stash apply --label-ours= --label-theirs= &&\n>         \ttest_grep \"^<<<<<<<$\" conflict-file &&\n>         \ttest_grep \"^>>>>>>>$\" conflict-file\n>         '\n>         \n>       -+test_expect_success 'apply exits 2 on conflicts and keeps the stash entry' '\n>       ++test_expect_success 'pop exits 1 on conflicts and keeps the stash entry' '\n>        +\tgit reset --hard initial &&\n>       -+\ttest_commit exit-code-base conflict-file base-content &&\n>       -+\techo stashed >conflict-file &&\n>       -+\tgit stash push -m stashed &&\n>       -+\ttest_commit exit-code-upstream conflict-file upstream-content &&\n>       -+\ttest_expect_code 2 git stash apply &&\n>       ++\techo stashed >file &&\n>       ++\tgit stash push -m pop-stashed &&\n>       ++\ttest_commit pop-upstream file upstream-content &&\n>       ++\ttest_expect_code 1 git stash pop &&\n>        +\tgit stash list >list &&\n>       -+\ttest_grep stashed list\n>       ++\ttest_grep pop-stashed list\n>        +'\n>        +\n>       -+test_expect_success 'pop exits 2 on conflicts and keeps the stash entry' '\n>       ++test_expect_success 'stash branch exits with a non-1 status on errors' '\n>        +\tgit reset --hard initial &&\n>       -+\ttest_commit pop-exit-code-base pop-file base-content &&\n>       -+\techo stashed >pop-file &&\n>       -+\tgit stash push -m pop-stashed &&\n>       -+\ttest_commit pop-exit-code-upstream pop-file upstream-content &&\n>       -+\ttest_expect_code 2 git stash pop &&\n>       ++\techo stashed >file &&\n>       ++\tgit stash push -m branch-stashed &&\n>       ++\ttest_expect_code 128 git stash branch conflicting-branch refs/heads/does-not-exist &&\n>        +\tgit stash list >list &&\n>       -+\ttest_grep pop-stashed list\n>       ++\ttest_grep branch-stashed list\n>        +'\n>        +\n>         test_expect_success 'stash show --include-untracked includes untracked files' '\n>         \tgit reset --hard &&\n>         \n>       -\n>       - ## t/t7201-co.sh ##\n>       -@@ t/t7201-co.sh: test_expect_success 'checkout -m creates a recoverable stash on conflict' '\n>       - \ttest_must_fail git checkout side 2>stderr &&\n>       - \ttest_grep \"Your local changes\" stderr &&\n>       - \tgit checkout -m side >actual 2>&1 &&\n>       --\ttest_grep \"resulted in conflicts\" actual &&\n>       --\ttest_grep \"git stash drop\" actual &&\n>       --\ttest_grep \"git stash pop\" actual &&\n>       --\ttest_grep \"The following paths have local changes\" actual &&\n>       -+\tcat >expect <<-EOF &&\n>       -+\tYour local changes are stashed, however applying them\n>       -+\tresulted in conflicts.  You can either resolve the conflicts\n>       -+\tand then discard the stash with \"git stash drop\", or, if you\n>       -+\tdo not want to resolve them now, run \"git reset --hard\" and\n>       -+\tapply the local changes later by running \"git stash pop\".\n>       -+\n>       -+\tSwitched to branch ${SQ}side${SQ}\n>       -+\tThe following paths have local changes:\n>       -+\tM\tone\n>       -+\tEOF\n>       -+\ttest_cmp expect actual &&\n>       - \tgit log -p -1 --format=\"%gs%n%B\" -g --diff-merges=1 refs/stash >actual &&\n>       - \tsed /^index/d actual >actual.trimmed &&\n>       - \tcat >expect <<-EOF &&\n>   -:  ---------- > 2:  935fa0a9ae checkout: separate autostash conflict advice from branch-switch message\n> \n\n"},{"id":"551873","messageId":"pull.2364.v5.git.git.1788446398.gitgitgadget@gmail.com","threadId":"66064","inReplyTo":"pull.2364.git.git.1784993669.gitgitgadget@gmail.com","subject":"[PATCH v5 0/2] checkout -m: refine autostash fallback","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-03T14:39:56Z","receivedAt":"2026-09-03T14:40:06Z","isPatch":true,"body":"Avoiding checkout -m autostash retries when no tracked local changes exist\nand visually separating autostash conflict advice from the subsequent\nbranch-switch message. Addresses #leftoverbits from here:\nhttps://lore.kernel.org/git/cfd09dbf-8d77-4464-8030-3a0ffb4aeae7@gmail.com/\n\nChanges in v5:\n\n * Improve commit messages.\n * Create helper do_stash_apply.\n\nChanges in v4:\n\n * Conflicts now exit with status 1 like merge-tree, other failures exit 128\n   so exit 1 unambiguously means conflicts. Stash changes split into their\n   own commit.\n * The autostash apply helpers use the return value (enum\n   stash_apply_result) instead of an out-parameter, and only claim conflicts\n   when git stash apply actually reported them.\n\nChanges in v3:\n\n * Use enum for git stash return values, to separate conflict from generic\n   error.\n\nChanges in v2:\n\n * Simplify logic and combine to one commit.\n * Test full output with test_cmp.\n\nHarald Nordgren (2):\n  stash: reserve exit status 1 for conflicts\n  checkout: separate autostash conflict advice from branch-switch\n    message\n\n Documentation/git-stash.adoc |   9 +++\n builtin/checkout.c           |  15 +++--\n builtin/stash.c              |  33 +++++++---\n sequencer.c                  | 113 ++++++++++++++++++++++-------------\n sequencer.h                  |  19 +++---\n stash.h                      |  21 +++++++\n t/t3903-stash.sh             |  25 +++++++-\n t/t7201-co.sh                |  16 +++--\n 8 files changed, 181 insertions(+), 70 deletions(-)\n create mode 100644 stash.h\n\n\nbase-commit: 3cb9185f65410273787f74333cc027d2ea5daada\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2364%2FHaraldNordgren%2Fhn%2Fgit-checkout-m-leftoverbits-v5\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2364/HaraldNordgren/hn/git-checkout-m-leftoverbits-v5\nPull-Request: https://github.com/git/git/pull/2364\n\nRange-diff vs v4:\n\n 1:  ff43221802 ! 1:  fe22b1bfa6 stash: reserve exit status 1 for conflicts\n     @@ Commit message\n          status die() uses, when they fail for other reasons.  Document the\n          exit statuses.\n      \n     -    cmd_stash() used to collapse the return values of the subcommand\n     -    implementations to a boolean.  It now maps negative values, which\n     -    signal a failure, to 128 and passes everything else through as-is.\n     -    The only implementations that return a positive value are \"apply\",\n     -    \"pop\" and \"branch\", which return the value of do_apply_stash():\n     -    \"apply\" returns it directly, and \"pop\" and \"branch\" drop the stash\n     -    entry, via do_drop_stash(), which always returns 0, only when the\n     -    application succeeded.  The positive value is always 1, as\n     -    do_apply_stash() only returns a positive value when the three-way\n     -    merge was unclean.\n     -\n     -    Make the convention explicit by introducing enum stash_apply_result\n     -    with the values STASH_APPLY_CLEAN, STASH_APPLY_CONFLICT and\n     -    STASH_APPLY_ERROR, and use it for the in-process autostash helpers,\n     -    too.  They spawn \"git stash apply\" and can now tell conflicts apart\n     -    from other failures, e.g. a crash or death by signal of the child,\n     -    which map to exit statuses above 1.  Since we know the stash entry\n     -    was saved, tell users so in the error message instead of leaving them\n     -    wondering what happened to their stashed changes.\n     +    The only subcommand implementations that can return a positive value\n     +    are \"apply\", \"pop\" and \"branch\", which return the value of\n     +    do_apply_stash(): \"apply\" returns it directly, and \"pop\" and \"branch\"\n     +    drop the stash entry, via do_drop_stash(), which always returns 0,\n     +    only when the application succeeded.  do_apply_stash() only returns a\n     +    positive value when the three-way merge was unclean.  cmd_stash() now\n     +    maps negative values to 128 and passes positive values through as the\n     +    exit status, so exit status 1 unambiguously indicates conflicts.\n     +    enum stash_apply_result makes the convention explicit, and the\n     +    autostash helpers use it to tell users that their stashed changes\n     +    were saved when applying them fails.\n      \n          Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n      \n     @@ builtin/stash.c: int cmd_stash(int argc,\n      +\t\tif (ret < 0)\n      +\t\t\treturn 128;\n      +\t\treturn ret;\n     -+\t} else if (!argc)\n     ++\t} else if (!argc) {\n       \t\treturn !!push_stash_unassumed(0, NULL, prefix, repo);\n     ++\t}\n       \n       \t/* Assume 'stash push' */\n     + \tstrvec_push(&args, \"push\");\n      \n       ## sequencer.c ##\n      @@\n     @@ sequencer.c: void create_autostash_ref(struct repository *r, const char *refname\n      -\t\t\t\t    const char *label_ours, const char *label_theirs,\n      -\t\t\t\t    const char *label_base,\n      -\t\t\t\t    const char *stash_msg)\n     ++static enum stash_apply_result do_stash_apply(const char *stash_oid,\n     ++\t\t\t\t\t      const char *label_ours,\n     ++\t\t\t\t\t      const char *label_theirs,\n     ++\t\t\t\t\t      const char *label_base)\n     + {\n     + \tstruct child_process child = CHILD_PROCESS_INIT;\n     +-\tint ret = 0;\n     + \n     +-\tif (attempt_apply) {\n     +-\t\tchild.git_cmd = 1;\n     +-\t\tchild.no_stdout = 1;\n     +-\t\tchild.no_stderr = 1;\n     +-\t\tstrvec_push(&child.args, \"stash\");\n     +-\t\tstrvec_push(&child.args, \"apply\");\n     +-\t\tif (label_ours)\n     +-\t\t\tstrvec_pushf(&child.args, \"--label-ours=%s\", label_ours);\n     +-\t\tif (label_theirs)\n     +-\t\t\tstrvec_pushf(&child.args, \"--label-theirs=%s\", label_theirs);\n     +-\t\tif (label_base)\n     +-\t\t\tstrvec_pushf(&child.args, \"--label-base=%s\", label_base);\n     +-\t\tstrvec_push(&child.args, stash_oid);\n     +-\t\tret = run_command(&child);\n     +-\t}\n     +-\n     +-\tif (attempt_apply && !ret)\n     ++\tchild.git_cmd = 1;\n     ++\tchild.no_stdout = 1;\n     ++\tchild.no_stderr = 1;\n     ++\tstrvec_push(&child.args, \"stash\");\n     ++\tstrvec_push(&child.args, \"apply\");\n     ++\tif (label_ours)\n     ++\t\tstrvec_pushf(&child.args, \"--label-ours=%s\", label_ours);\n     ++\tif (label_theirs)\n     ++\t\tstrvec_pushf(&child.args, \"--label-theirs=%s\", label_theirs);\n     ++\tif (label_base)\n     ++\t\tstrvec_pushf(&child.args, \"--label-base=%s\", label_base);\n     ++\tstrvec_push(&child.args, stash_oid);\n     ++\n     ++\tswitch (run_command(&child)) {\n     ++\tcase 0:\n     ++\t\treturn STASH_APPLY_CLEAN;\n     ++\tcase STASH_APPLY_CONFLICT:\n     ++\t\treturn STASH_APPLY_CONFLICT;\n     ++\tdefault:\n     ++\t\treturn STASH_APPLY_ERROR;\n     ++\t}\n     ++}\n     ++\n      +static enum stash_apply_result apply_save_autostash_oid(const char *stash_oid,\n      +\t\t\t\t\t\t\tint attempt_apply,\n      +\t\t\t\t\t\t\tconst char *label_ours,\n      +\t\t\t\t\t\t\tconst char *label_theirs,\n      +\t\t\t\t\t\t\tconst char *label_base,\n      +\t\t\t\t\t\t\tconst char *stash_msg)\n     - {\n     - \tstruct child_process child = CHILD_PROCESS_INIT;\n     --\tint ret = 0;\n     ++{\n      +\tenum stash_apply_result ret = STASH_APPLY_CLEAN;\n     - \n     - \tif (attempt_apply) {\n     - \t\tchild.git_cmd = 1;\n     -@@ sequencer.c: static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply,\n     - \t\t\tstrvec_pushf(&child.args, \"--label-base=%s\", label_base);\n     - \t\tstrvec_push(&child.args, stash_oid);\n     - \t\tret = run_command(&child);\n     -+\t\tif (ret > 1)\n     -+\t\t\tret = STASH_APPLY_ERROR;\n     - \t}\n     - \n     --\tif (attempt_apply && !ret)\n     ++\n     ++\tif (attempt_apply)\n     ++\t\tret = do_stash_apply(stash_oid, label_ours, label_theirs,\n     ++\t\t\t\t     label_base);\n     ++\n      +\tif (attempt_apply && ret == STASH_APPLY_CLEAN)\n       \t\tfprintf(stderr, _(\"Applied autostash.\\n\"));\n       \telse {\n 2:  935fa0a9ae = 2:  d18ff3ea9a checkout: separate autostash conflict advice from branch-switch message\n\n-- \ngitgitgadget\n"},{"id":"551874","messageId":"fe22b1bfa6b04b71ca2df6f524ad8d18663b9a17.1788446398.git.gitgitgadget@gmail.com","threadId":"66064","inReplyTo":"pull.2364.v5.git.git.1788446398.gitgitgadget@gmail.com","subject":"[PATCH v5 1/2] stash: reserve exit status 1 for conflicts","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-03T14:39:57Z","receivedAt":"2026-09-03T14:40:12Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\n\"git stash apply\", \"pop\" and \"branch\" exit with status 1 both when\napplying the stash entry resulted in conflicts and when they fail for\nother reasons, so callers cannot tell the two apart.\n\nFollow the convention of \"git merge-tree\" and the merge strategies,\nwhich exit with status 1 to indicate conflicts and with a different\nnon-zero status for errors: those subcommands now exit with status 1\nonly when applying the stash entry resulted in conflicts, in which\ncase the stash entry is left in place, and exit with status 128, the\nstatus die() uses, when they fail for other reasons.  Document the\nexit statuses.\n\nThe only subcommand implementations that can return a positive value\nare \"apply\", \"pop\" and \"branch\", which return the value of\ndo_apply_stash(): \"apply\" returns it directly, and \"pop\" and \"branch\"\ndrop the stash entry, via do_drop_stash(), which always returns 0,\nonly when the application succeeded.  do_apply_stash() only returns a\npositive value when the three-way merge was unclean.  cmd_stash() now\nmaps negative values to 128 and passes positive values through as the\nexit status, so exit status 1 unambiguously indicates conflicts.\nenum stash_apply_result makes the convention explicit, and the\nautostash helpers use it to tell users that their stashed changes\nwere saved when applying them fails.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n Documentation/git-stash.adoc |   9 +++\n builtin/stash.c              |  33 +++++++---\n sequencer.c                  | 113 ++++++++++++++++++++++-------------\n sequencer.h                  |  19 +++---\n stash.h                      |  21 +++++++\n t/t3903-stash.sh             |  25 +++++++-\n 6 files changed, 160 insertions(+), 60 deletions(-)\n create mode 100644 stash.h\n\ndiff --git a/Documentation/git-stash.adoc b/Documentation/git-stash.adoc\nindex 50bb89f483..fc6a9a008c 100644\n--- a/Documentation/git-stash.adoc\n+++ b/Documentation/git-stash.adoc\n@@ -426,6 +426,15 @@ include::includes/cmd-config-section-all.adoc[]\n :git-stash: 1\n include::config/stash.adoc[]\n \n+EXIT STATUS\n+-----------\n+\n+The `git stash` subcommands exit with status 0 on success.  The\n+subcommands that apply a stash entry, i.e. `apply`, `pop` and `branch`,\n+exit with status 1 when applying the stash entry resulted in conflicts,\n+in which case the stash entry is left in place, and with a non-zero\n+status other than 1 when they fail for other reasons.\n+\n \n SEE ALSO\n --------\ndiff --git a/builtin/stash.c b/builtin/stash.c\nindex 72c52571f8..7a9843413b 100644\n--- a/builtin/stash.c\n+++ b/builtin/stash.c\n@@ -10,6 +10,7 @@\n #include \"object-name.h\"\n #include \"parse-options.h\"\n #include \"refs.h\"\n+#include \"stash.h\"\n #include \"lockfile.h\"\n #include \"cache-tree.h\"\n #include \"unpack-trees.h\"\n@@ -640,10 +641,12 @@ static void unstage_changes_unless_new(struct object_id *orig_tree)\n \t\tdie(_(\"could not write index\"));\n }\n \n-static int do_apply_stash(const char *prefix, struct stash_info *info,\n-\t\t\t  int index, int quiet,\n-\t\t\t  const char *label_ours, const char *label_theirs,\n-\t\t\t  const char *label_base)\n+static enum stash_apply_result do_apply_stash(const char *prefix,\n+\t\t\t\t\t      struct stash_info *info,\n+\t\t\t\t\t      int index, int quiet,\n+\t\t\t\t\t      const char *label_ours,\n+\t\t\t\t\t      const char *label_theirs,\n+\t\t\t\t\t      const char *label_base)\n {\n \tint clean, ret;\n \tint has_index = index;\n@@ -717,8 +720,8 @@ static int do_apply_stash(const char *prefix, struct stash_info *info,\n \n \t/*\n \t * If 'clean' >= 0, reverse the value for 'ret' so 'ret' is 0 when the\n-\t * merge was clean, and nonzero if the merge was unclean or encountered\n-\t * an error.\n+\t * merge was clean, and 1 if the merge was unclean or a negative value\n+\t * if it encountered an error.\n \t */\n \tret = clean >= 0 ? !clean : clean;\n \n@@ -2492,10 +2495,22 @@ int cmd_stash(int argc,\n \tstrbuf_addf(&stash_index_path, \"%s.stash.%\" PRIuMAX, index_file,\n \t\t    (uintmax_t)pid);\n \n-\tif (fn)\n-\t\treturn !!fn(argc, argv, prefix, repo);\n-\telse if (!argc)\n+\tif (fn) {\n+\t\tret = fn(argc, argv, prefix, repo);\n+\n+\t\t/*\n+\t\t * The subcommand implementations return 0 on success, a\n+\t\t * negative value on failure, and STASH_APPLY_CONFLICT\n+\t\t * when applying a stash entry resulted in conflicts.\n+\t\t * Map failures to 128, the status die() uses, so that\n+\t\t * exit status 1 unambiguously indicates conflicts.\n+\t\t */\n+\t\tif (ret < 0)\n+\t\t\treturn 128;\n+\t\treturn ret;\n+\t} else if (!argc) {\n \t\treturn !!push_stash_unassumed(0, NULL, prefix, repo);\n+\t}\n \n \t/* Assume 'stash push' */\n \tstrvec_push(&args, \"push\");\ndiff --git a/sequencer.c b/sequencer.c\nindex 65afd100d9..e4a6ddfd01 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -19,6 +19,7 @@\n #include \"commit.h\"\n #include \"sequencer.h\"\n #include \"run-command.h\"\n+#include \"stash.h\"\n #include \"hook.h\"\n #include \"utf8.h\"\n #include \"cache-tree.h\"\n@@ -4794,31 +4795,50 @@ void create_autostash_ref(struct repository *r, const char *refname,\n \tcreate_autostash_internal(r, NULL, refname, message, silent);\n }\n \n-static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply,\n-\t\t\t\t    const char *label_ours, const char *label_theirs,\n-\t\t\t\t    const char *label_base,\n-\t\t\t\t    const char *stash_msg)\n+static enum stash_apply_result do_stash_apply(const char *stash_oid,\n+\t\t\t\t\t      const char *label_ours,\n+\t\t\t\t\t      const char *label_theirs,\n+\t\t\t\t\t      const char *label_base)\n {\n \tstruct child_process child = CHILD_PROCESS_INIT;\n-\tint ret = 0;\n \n-\tif (attempt_apply) {\n-\t\tchild.git_cmd = 1;\n-\t\tchild.no_stdout = 1;\n-\t\tchild.no_stderr = 1;\n-\t\tstrvec_push(&child.args, \"stash\");\n-\t\tstrvec_push(&child.args, \"apply\");\n-\t\tif (label_ours)\n-\t\t\tstrvec_pushf(&child.args, \"--label-ours=%s\", label_ours);\n-\t\tif (label_theirs)\n-\t\t\tstrvec_pushf(&child.args, \"--label-theirs=%s\", label_theirs);\n-\t\tif (label_base)\n-\t\t\tstrvec_pushf(&child.args, \"--label-base=%s\", label_base);\n-\t\tstrvec_push(&child.args, stash_oid);\n-\t\tret = run_command(&child);\n-\t}\n-\n-\tif (attempt_apply && !ret)\n+\tchild.git_cmd = 1;\n+\tchild.no_stdout = 1;\n+\tchild.no_stderr = 1;\n+\tstrvec_push(&child.args, \"stash\");\n+\tstrvec_push(&child.args, \"apply\");\n+\tif (label_ours)\n+\t\tstrvec_pushf(&child.args, \"--label-ours=%s\", label_ours);\n+\tif (label_theirs)\n+\t\tstrvec_pushf(&child.args, \"--label-theirs=%s\", label_theirs);\n+\tif (label_base)\n+\t\tstrvec_pushf(&child.args, \"--label-base=%s\", label_base);\n+\tstrvec_push(&child.args, stash_oid);\n+\n+\tswitch (run_command(&child)) {\n+\tcase 0:\n+\t\treturn STASH_APPLY_CLEAN;\n+\tcase STASH_APPLY_CONFLICT:\n+\t\treturn STASH_APPLY_CONFLICT;\n+\tdefault:\n+\t\treturn STASH_APPLY_ERROR;\n+\t}\n+}\n+\n+static enum stash_apply_result apply_save_autostash_oid(const char *stash_oid,\n+\t\t\t\t\t\t\tint attempt_apply,\n+\t\t\t\t\t\t\tconst char *label_ours,\n+\t\t\t\t\t\t\tconst char *label_theirs,\n+\t\t\t\t\t\t\tconst char *label_base,\n+\t\t\t\t\t\t\tconst char *stash_msg)\n+{\n+\tenum stash_apply_result ret = STASH_APPLY_CLEAN;\n+\n+\tif (attempt_apply)\n+\t\tret = do_stash_apply(stash_oid, label_ours, label_theirs,\n+\t\t\t\t     label_base);\n+\n+\tif (attempt_apply && ret == STASH_APPLY_CLEAN)\n \t\tfprintf(stderr, _(\"Applied autostash.\\n\"));\n \telse {\n \t\tstruct child_process store = CHILD_PROCESS_INIT;\n@@ -4832,13 +4852,16 @@ static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply,\n \t\tstrvec_push(&store.args, stash_oid);\n \t\tif (run_command(&store))\n \t\t\tret = error(_(\"cannot store %s\"), stash_oid);\n-\t\telse if (attempt_apply)\n+\t\telse if (attempt_apply && ret == STASH_APPLY_CONFLICT)\n \t\t\tfprintf(stderr,\n \t\t\t\t_(\"Your local changes are stashed, however applying them\\n\"\n \t\t\t\t  \"resulted in conflicts.  You can either resolve the conflicts\\n\"\n \t\t\t\t  \"and then discard the stash with \\\"git stash drop\\\", or, if you\\n\"\n \t\t\t\t  \"do not want to resolve them now, run \\\"git reset --hard\\\" and\\n\"\n \t\t\t\t  \"apply the local changes later by running \\\"git stash pop\\\".\\n\"));\n+\t\telse if (attempt_apply)\n+\t\t\tret = error(_(\"could not apply autostash; \"\n+\t\t\t\t      \"your changes are safe in the stash\"));\n \t\telse\n \t\t\tfprintf(stderr,\n \t\t\t\t_(\"Autostash exists; creating a new stash entry.\\n\"\n@@ -4850,15 +4873,16 @@ static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply,\n \treturn ret;\n }\n \n-static int apply_save_autostash(const char *path, int attempt_apply)\n+static enum stash_apply_result apply_save_autostash(const char *path,\n+\t\t\t\t\t\t    int attempt_apply)\n {\n \tstruct strbuf stash_oid = STRBUF_INIT;\n-\tint ret = 0;\n+\tenum stash_apply_result ret = STASH_APPLY_CLEAN;\n \n \tif (!read_oneliner(&stash_oid, path,\n \t\t\t   READ_ONELINER_SKIP_IF_EMPTY)) {\n \t\tstrbuf_release(&stash_oid);\n-\t\treturn 0;\n+\t\treturn STASH_APPLY_CLEAN;\n \t}\n \tstrbuf_trim(&stash_oid);\n \n@@ -4870,37 +4894,40 @@ static int apply_save_autostash(const char *path, int attempt_apply)\n \treturn ret;\n }\n \n-int save_autostash(const char *path)\n+enum stash_apply_result save_autostash(const char *path)\n {\n \treturn apply_save_autostash(path, 0);\n }\n \n-int apply_autostash(const char *path)\n+enum stash_apply_result apply_autostash(const char *path)\n {\n \treturn apply_save_autostash(path, 1);\n }\n \n-int apply_autostash_oid(const char *stash_oid)\n+enum stash_apply_result apply_autostash_oid(const char *stash_oid)\n {\n \treturn apply_save_autostash_oid(stash_oid, 1, NULL, NULL, NULL, NULL);\n }\n \n-static int apply_save_autostash_ref(struct repository *r, const char *refname,\n-\t\t\t\t    int attempt_apply,\n-\t\t\t\t    const char *label_ours, const char *label_theirs,\n-\t\t\t\t    const char *label_base,\n-\t\t\t\t    const char *stash_msg)\n+static enum stash_apply_result apply_save_autostash_ref(struct repository *r,\n+\t\t\t\t\t\t\tconst char *refname,\n+\t\t\t\t\t\t\tint attempt_apply,\n+\t\t\t\t\t\t\tconst char *label_ours,\n+\t\t\t\t\t\t\tconst char *label_theirs,\n+\t\t\t\t\t\t\tconst char *label_base,\n+\t\t\t\t\t\t\tconst char *stash_msg)\n {\n \tstruct object_id stash_oid;\n \tchar stash_oid_hex[GIT_MAX_HEXSZ + 1];\n-\tint flag, ret;\n+\tint flag;\n+\tenum stash_apply_result ret;\n \n \tif (!refs_ref_exists(get_main_ref_store(r), refname))\n-\t\treturn 0;\n+\t\treturn STASH_APPLY_CLEAN;\n \n \tif (!refs_resolve_ref_unsafe(get_main_ref_store(r), refname,\n \t\t\t\t     RESOLVE_REF_READING, &stash_oid, &flag))\n-\t\treturn -1;\n+\t\treturn STASH_APPLY_ERROR;\n \tif (flag & REF_ISSYMREF)\n \t\treturn error(_(\"autostash reference is a symref\"));\n \n@@ -4915,15 +4942,19 @@ static int apply_save_autostash_ref(struct repository *r, const char *refname,\n \treturn ret;\n }\n \n-int save_autostash_ref(struct repository *r, const char *refname)\n+enum stash_apply_result save_autostash_ref(struct repository *r,\n+\t\t\t\t\t   const char *refname)\n {\n \treturn apply_save_autostash_ref(r, refname, 0,\n \t\t\t\t\tNULL, NULL, NULL, NULL);\n }\n \n-int apply_autostash_ref(struct repository *r, const char *refname,\n-\t\t\tconst char *label_ours, const char *label_theirs,\n-\t\t\tconst char *label_base, const char *stash_msg)\n+enum stash_apply_result apply_autostash_ref(struct repository *r,\n+\t\t\t\t\t    const char *refname,\n+\t\t\t\t\t    const char *label_ours,\n+\t\t\t\t\t    const char *label_theirs,\n+\t\t\t\t\t    const char *label_base,\n+\t\t\t\t\t    const char *stash_msg)\n {\n \treturn apply_save_autostash_ref(r, refname, 1,\n \t\t\t\t\tlabel_ours, label_theirs, label_base,\ndiff --git a/sequencer.h b/sequencer.h\nindex 64a9c7fb1b..804501b64c 100644\n--- a/sequencer.h\n+++ b/sequencer.h\n@@ -3,6 +3,7 @@\n \n #include \"strbuf.h\"\n #include \"strvec.h\"\n+#include \"stash.h\"\n #include \"wt-status.h\"\n \n struct commit;\n@@ -231,13 +232,17 @@ void commit_post_rewrite(struct repository *r,\n void create_autostash(struct repository *r, const char *path);\n void create_autostash_ref(struct repository *r, const char *refname,\n \t\t\t  const char *message, bool silent);\n-int save_autostash(const char *path);\n-int save_autostash_ref(struct repository *r, const char *refname);\n-int apply_autostash(const char *path);\n-int apply_autostash_oid(const char *stash_oid);\n-int apply_autostash_ref(struct repository *r, const char *refname,\n-\t\t\tconst char *label_ours, const char *label_theirs,\n-\t\t\tconst char *label_base, const char *stash_msg);\n+enum stash_apply_result save_autostash(const char *path);\n+enum stash_apply_result save_autostash_ref(struct repository *r,\n+\t\t\t\t\t   const char *refname);\n+enum stash_apply_result apply_autostash(const char *path);\n+enum stash_apply_result apply_autostash_oid(const char *stash_oid);\n+enum stash_apply_result apply_autostash_ref(struct repository *r,\n+\t\t\t\t\t    const char *refname,\n+\t\t\t\t\t    const char *label_ours,\n+\t\t\t\t\t    const char *label_theirs,\n+\t\t\t\t\t    const char *label_base,\n+\t\t\t\t\t    const char *stash_msg);\n \n #define SUMMARY_INITIAL_COMMIT   (1 << 0)\n #define SUMMARY_SHOW_AUTHOR_DATE (1 << 1)\ndiff --git a/stash.h b/stash.h\nnew file mode 100644\nindex 0000000000..14ba4f946d\n--- /dev/null\n+++ b/stash.h\n@@ -0,0 +1,21 @@\n+#ifndef STASH_H\n+#define STASH_H\n+\n+enum stash_apply_result {\n+\t/* The stash was applied cleanly, or there was nothing to apply. */\n+\tSTASH_APPLY_CLEAN = 0,\n+\n+\t/*\n+\t * The stash could not be applied because it resulted in\n+\t * conflicts.  The stash entry is left in place.  The \"git stash\n+\t * apply\", \"pop\" and \"branch\" subcommands exit with this status\n+\t * in this case, mirroring the convention of \"git merge-tree\" and\n+\t * the merge strategies.\n+\t */\n+\tSTASH_APPLY_CONFLICT = 1,\n+\n+\t/* Something went wrong. */\n+\tSTASH_APPLY_ERROR = -1,\n+};\n+\n+#endif /* STASH_H */\ndiff --git a/t/t3903-stash.sh b/t/t3903-stash.sh\nindex da27a6599a..6529508b06 100755\n--- a/t/t3903-stash.sh\n+++ b/t/t3903-stash.sh\n@@ -1791,13 +1791,13 @@ test_expect_success 'stash.index=false overridden by --index' '\n \ttest_cmp expect file\n '\n \n-test_expect_success 'apply with custom conflict labels' '\n+test_expect_success 'apply exits 1 on conflicts' '\n \tgit reset --hard initial &&\n \ttest_commit label-base conflict-file base-content &&\n \techo stashed >conflict-file &&\n \tgit stash push -m \"stashed\" &&\n \ttest_commit label-upstream conflict-file upstream-content &&\n-\ttest_must_fail git -c merge.conflictStyle=diff3 stash apply --label-ours=UP --label-theirs=STASH &&\n+\ttest_expect_code 1 git -c merge.conflictStyle=diff3 stash apply --label-ours=UP --label-theirs=STASH &&\n \ttest_grep \"^<<<<<<< UP\" conflict-file &&\n \ttest_grep \"^||||||| Stash base\" conflict-file &&\n \ttest_grep \"^>>>>>>> STASH\" conflict-file\n@@ -1809,11 +1809,30 @@ test_expect_success 'apply with empty conflict labels' '\n \techo stashed >conflict-file &&\n \tgit stash push -m \"stashed\" &&\n \ttest_commit empty-label-upstream conflict-file upstream-content &&\n-\ttest_must_fail git stash apply --label-ours= --label-theirs= &&\n+\ttest_expect_code 1 git stash apply --label-ours= --label-theirs= &&\n \ttest_grep \"^<<<<<<<$\" conflict-file &&\n \ttest_grep \"^>>>>>>>$\" conflict-file\n '\n \n+test_expect_success 'pop exits 1 on conflicts and keeps the stash entry' '\n+\tgit reset --hard initial &&\n+\techo stashed >file &&\n+\tgit stash push -m pop-stashed &&\n+\ttest_commit pop-upstream file upstream-content &&\n+\ttest_expect_code 1 git stash pop &&\n+\tgit stash list >list &&\n+\ttest_grep pop-stashed list\n+'\n+\n+test_expect_success 'stash branch exits with a non-1 status on errors' '\n+\tgit reset --hard initial &&\n+\techo stashed >file &&\n+\tgit stash push -m branch-stashed &&\n+\ttest_expect_code 128 git stash branch conflicting-branch refs/heads/does-not-exist &&\n+\tgit stash list >list &&\n+\ttest_grep branch-stashed list\n+'\n+\n test_expect_success 'stash show --include-untracked includes untracked files' '\n \tgit reset --hard &&\n \n-- \ngitgitgadget\n\n"},{"id":"551875","messageId":"d18ff3ea9a018b6ae207bd4f3c7740b96b3197c0.1788446398.git.gitgitgadget@gmail.com","threadId":"66064","inReplyTo":"pull.2364.v5.git.git.1788446398.gitgitgadget@gmail.com","subject":"[PATCH v5 2/2] checkout: separate autostash conflict advice from branch-switch message","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-03T14:39:58Z","receivedAt":"2026-09-03T14:40:16Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\n\"git checkout -m\" stashes the user's local changes when it cannot\nperform the checkout, and then applies the stash.  When applying the\nstash results in conflicts, the advice on how to deal with them is\nprinted directly on top of the branch-switch message (\"Switched to\nbranch ...\"), making the two hard to tell apart.  Print a blank line\nin between so that the advice and the branch-switch message are\nvisually distinct.\n\napply_autostash_ref() reports whether applying the stash resulted in\nconflicts via its enum stash_apply_result return value, so only print\nthe blank line in the conflicted case.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n builtin/checkout.c | 15 +++++++++------\n t/t7201-co.sh      | 16 ++++++++++++----\n 2 files changed, 21 insertions(+), 10 deletions(-)\n\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex 55e3a89a85..c960f98287 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -1166,6 +1166,7 @@ static int switch_branches(const struct checkout_opts *opts,\n \tint flag, writeout_error = 0;\n \tint do_merge = 1;\n \tint created_autostash = 0;\n+\tenum stash_apply_result autostash_res = STASH_APPLY_CLEAN;\n \tstruct strbuf old_commit_shortname = STRBUF_INIT;\n \tstruct strbuf autostash_msg = STRBUF_INIT;\n \tconst char *stash_label_base = NULL;\n@@ -1237,12 +1238,12 @@ static int switch_branches(const struct checkout_opts *opts,\n \t\t\t\tgit_config_push_parameter(cfg.buf);\n \t\t\t\tstrbuf_release(&cfg);\n \t\t\t}\n-\t\t\tapply_autostash_ref(the_repository,\n-\t\t\t\t\t    \"CHECKOUT_AUTOSTASH_HEAD\",\n-\t\t\t\t\t    new_branch_info->name,\n-\t\t\t\t\t    \"local\",\n-\t\t\t\t\t    stash_label_base,\n-\t\t\t\t\t    autostash_msg.buf);\n+\t\t\tautostash_res = apply_autostash_ref(the_repository,\n+\t\t\t\t    \"CHECKOUT_AUTOSTASH_HEAD\",\n+\t\t\t\t    new_branch_info->name,\n+\t\t\t\t    \"local\",\n+\t\t\t\t    stash_label_base,\n+\t\t\t\t    autostash_msg.buf);\n \t\t}\n \t\tif (ret) {\n \t\t\tbranch_info_release(&old_branch_info);\n@@ -1255,6 +1256,8 @@ static int switch_branches(const struct checkout_opts *opts,\n \tif (!opts->quiet && !old_branch_info.path && old_branch_info.commit && new_branch_info->commit != old_branch_info.commit)\n \t\torphaned_commit_warning(old_branch_info.commit, new_branch_info->commit);\n \n+\tif (autostash_res == STASH_APPLY_CONFLICT && !opts->quiet)\n+\t\tfputc('\\n', stderr);\n \tupdate_refs_for_switch(opts, &old_branch_info, new_branch_info);\n \n \tif (created_autostash) {\ndiff --git a/t/t7201-co.sh b/t/t7201-co.sh\nindex 0ddd1ad7aa..9ea9462914 100755\n--- a/t/t7201-co.sh\n+++ b/t/t7201-co.sh\n@@ -236,10 +236,18 @@ test_expect_success 'checkout -m creates a recoverable stash on conflict' '\n \ttest_must_fail git checkout side 2>stderr &&\n \ttest_grep \"Your local changes\" stderr &&\n \tgit checkout -m side >actual 2>&1 &&\n-\ttest_grep \"resulted in conflicts\" actual &&\n-\ttest_grep \"git stash drop\" actual &&\n-\ttest_grep \"git stash pop\" actual &&\n-\ttest_grep \"The following paths have local changes\" actual &&\n+\tcat >expect <<-EOF &&\n+\tYour local changes are stashed, however applying them\n+\tresulted in conflicts.  You can either resolve the conflicts\n+\tand then discard the stash with \"git stash drop\", or, if you\n+\tdo not want to resolve them now, run \"git reset --hard\" and\n+\tapply the local changes later by running \"git stash pop\".\n+\n+\tSwitched to branch ${SQ}side${SQ}\n+\tThe following paths have local changes:\n+\tM\tone\n+\tEOF\n+\ttest_cmp expect actual &&\n \tgit log -p -1 --format=\"%gs%n%B\" -g --diff-merges=1 refs/stash >actual &&\n \tsed /^index/d actual >actual.trimmed &&\n \tcat >expect <<-EOF &&\n-- \ngitgitgadget\n"},{"id":"551876","messageId":"CAHwyqnVp6BVGx3+UCqBC_gOCuyq1c62uNi1f7Wyfpxd1KdyM9A@mail.gmail.com","threadId":"66064","inReplyTo":"002b1324-0f7f-45ec-9f67-624a41801ff7@gmail.com","subject":"Re: [PATCH v4 1/2] stash: reserve exit status 1 for conflicts","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-09-03T14:45:53Z","receivedAt":"2026-09-03T14:46:32Z","isPatch":true,"body":"> >> +    if (fn) {\n> >> +            ret = fn(argc, argv, prefix, repo);\n> >> +\n> >> +            /*\n> >> +             * The subcommand implementations return 0 on success, a\n> >> +             * negative value on failure, and STASH_APPLY_CONFLICT\n> >> +             * when applying a stash entry resulted in conflicts.\n> >> +             * Map failures to 128, the status die() uses, so that\n> >> +             * exit status 1 unambiguously indicates conflicts.\n> >> +             */\n> >> +            if (ret < 0)\n> >> +                    return 128;\n> >> +            return ret;\n> >> +    } else if (!argc)\n> >>              return !!push_stash_unassumed(0, NULL, prefix, repo);\n> >\n> > Style.  Once one of \"if\", \"else if\" and \"else\" cascade gains\n> > {braches}, others should do so as well.\n\nI would love to have a linter rule for this, since I keep forgetting\nand it's a waste of time for others to remind me.\n\nMaybe we should copy this over from Linux?\nhttps://github.com/torvalds/linux/blob/v7.2/scripts/checkpatch.pl#L6270\n\n\nHarald\n"},{"id":"551896","messageId":"xmqqqzjab2ho.fsf@gitster.g","threadId":"66064","inReplyTo":"CAHwyqnVp6BVGx3+UCqBC_gOCuyq1c62uNi1f7Wyfpxd1KdyM9A@mail.gmail.com","subject":"Re: [PATCH v4 1/2] stash: reserve exit status 1 for conflicts","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-09-03T18:42:43Z","receivedAt":"2026-09-03T18:42:46Z","isPatch":true,"body":"Harald Nordgren <haraldnordgren@gmail.com> writes:\n\n>> >> +    if (fn) {\n>> >> +            ret = fn(argc, argv, prefix, repo);\n>> >> +\n>> >> +            /*\n>> >> +             * The subcommand implementations return 0 on success, a\n>> >> +             * negative value on failure, and STASH_APPLY_CONFLICT\n>> >> +             * when applying a stash entry resulted in conflicts.\n>> >> +             * Map failures to 128, the status die() uses, so that\n>> >> +             * exit status 1 unambiguously indicates conflicts.\n>> >> +             */\n>> >> +            if (ret < 0)\n>> >> +                    return 128;\n>> >> +            return ret;\n>> >> +    } else if (!argc)\n>> >>              return !!push_stash_unassumed(0, NULL, prefix, repo);\n>> >\n>> > Style.  Once one of \"if\", \"else if\" and \"else\" cascade gains\n>> > {braches}, others should do so as well.\n>\n> I would love to have a linter rule for this, since I keep forgetting\n> and it's a waste of time for others to remind me.\n\nDoes our .clang-format file already cover this?\n\n"},{"id":"551903","messageId":"xmqq7bl2b1zs.fsf@gitster.g","threadId":"66064","inReplyTo":"pull.2364.v5.git.git.1788446398.gitgitgadget@gmail.com","subject":"Re: [PATCH v5 0/2] checkout -m: refine autostash fallback","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-09-03T18:53:27Z","receivedAt":"2026-09-03T18:53:30Z","isPatch":true,"body":"\"Harald Nordgren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> Avoiding checkout -m autostash retries when no tracked local changes exist\n> and visually separating autostash conflict advice from the subsequent\n> branch-switch message. Addresses #leftoverbits from here:\n> https://lore.kernel.org/git/cfd09dbf-8d77-4464-8030-3a0ffb4aeae7@gmail.com/\n>\n> Changes in v5:\n>\n>  * Improve commit messages.\n>  * Create helper do_stash_apply.\n\nBoth patches look good to me.  Thanks.  Will queue.\n"},{"id":"551905","messageId":"CAHwyqnW8sdnDcncpZpaMD8fmWaj+WJQxhA4G-Y+L5PTW=h3k6Q@mail.gmail.com","threadId":"66064","inReplyTo":"xmqqqzjab2ho.fsf@gitster.g","subject":"Re: [PATCH v4 1/2] stash: reserve exit status 1 for conflicts","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-09-03T19:09:52Z","receivedAt":"2026-09-03T19:10:34Z","isPatch":true,"body":"> >> > Style.  Once one of \"if\", \"else if\" and \"else\" cascade gains\n> >> > {braches}, others should do so as well.\n> >\n> > I would love to have a linter rule for this, since I keep forgetting\n> > and it's a waste of time for others to remind me.\n>\n> Does our .clang-format file already cover this?\n\nI don't think so. When GitHub CI runs, it doesn't complain about\nmismatched braces.\n\n\nHarald\n"},{"id":"551906","messageId":"xmqqwlt29l01.fsf@gitster.g","threadId":"66064","inReplyTo":"CAHwyqnW8sdnDcncpZpaMD8fmWaj+WJQxhA4G-Y+L5PTW=h3k6Q@mail.gmail.com","subject":"Re: [PATCH v4 1/2] stash: reserve exit status 1 for conflicts","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-09-03T19:45:50Z","receivedAt":"2026-09-03T19:45:56Z","isPatch":true,"body":"Harald Nordgren <haraldnordgren@gmail.com> writes:\n\n>> >> > Style.  Once one of \"if\", \"else if\" and \"else\" cascade gains\n>> >> > {braches}, others should do so as well.\n>> >\n>> > I would love to have a linter rule for this, since I keep forgetting\n>> > and it's a waste of time for others to remind me.\n>>\n>> Does our .clang-format file already cover this?\n>\n> I don't think so. When GitHub CI runs, it doesn't complain about\n> mismatched braces.\n\nA local \"make style\" while you develop runs\n\n\tgit clang-format --style file --diff --extensions c,h\n\nwhich tells the command to check only the parts of the system that\nyou touched.\n\nI do not think it is used in CI.  \n"},{"id":"551936","messageId":"CAHwyqnU2c3T_i0gvkqiPPM7UYr8t5bzynr66iG4Z6YUKqZk1Cg@mail.gmail.com","threadId":"66064","inReplyTo":"xmqqwlt29l01.fsf@gitster.g","subject":"Re: [PATCH v4 1/2] stash: reserve exit status 1 for conflicts","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-09-04T08:16:17Z","receivedAt":"2026-09-04T08:16:55Z","isPatch":true,"body":"> A local \"make style\" while you develop runs\n>\n>         git clang-format --style file --diff --extensions c,h\n>\n> which tells the command to check only the parts of the system that\n> you touched.\n>\n> I do not think it is used in CI.\n\nIt seems to run as part of CI and doesn't catch it then:\n'.github/workflows/check-style.yml'\n\n\nHarald\n"},{"id":"551970","messageId":"99b6da14-e03a-472b-b7a3-81162dc74166@gmail.com","threadId":"66064","inReplyTo":"CAHwyqnU2c3T_i0gvkqiPPM7UYr8t5bzynr66iG4Z6YUKqZk1Cg@mail.gmail.com","subject":"Re: [PATCH v4 1/2] stash: reserve exit status 1 for conflicts","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-09-04T15:09:50Z","receivedAt":"2026-09-04T15:09:55Z","isPatch":true,"body":"[Cc'd Karthik for a view on the CI style job]\n\nOn 04/09/2026 09:16, Harald Nordgren wrote:\n>> A local \"make style\" while you develop runs\n>>\n>>          git clang-format --style file --diff --extensions c,h\n>>\n>> which tells the command to check only the parts of the system that\n>> you touched.\n>>\n>> I do not think it is used in CI.\n> \n> It seems to run as part of CI and doesn't catch it then:\n> '.github/workflows/check-style.yml'\n\nMy recollection is that we made that job never fail because clang-format \ndoes not always do a good job of following our style guide and the \noutput is not guaranteed to be stable across different versions. That \nmakes its value debatable as I don't think many people (anyone?) bother \nchecking the output to see what suggestions it made. It would be a lot \nsimpler for contributors if we just devolved style decisions to \nclang-format so no one had to think about or comment on the style. \nWhether that tradeoff is worth it depends on how far the output of \nclang-format deviates from our preferred style.\n\nThanks\n\nPhillip\n\n"},{"id":"551972","messageId":"xmqq7bl16nzl.fsf@gitster.g","threadId":"66064","inReplyTo":"CAHwyqnU2c3T_i0gvkqiPPM7UYr8t5bzynr66iG4Z6YUKqZk1Cg@mail.gmail.com","subject":"Re: [PATCH v4 1/2] stash: reserve exit status 1 for conflicts","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-09-04T15:21:50Z","receivedAt":"2026-09-04T15:21:53Z","isPatch":true,"body":"Harald Nordgren <haraldnordgren@gmail.com> writes:\n\n>> A local \"make style\" while you develop runs\n>>\n>>         git clang-format --style file --diff --extensions c,h\n>>\n>> which tells the command to check only the parts of the system that\n>> you touched.\n>>\n>> I do not think it is used in CI.\n>\n> It seems to run as part of CI and doesn't catch it then:\n> '.github/workflows/check-style.yml'\n\nI never make any pull request to this project, so I didn't even\nnotice its existence.\n\nMy question was more about what is in .clang-format file we already\nhave and if it would have caught this.  I was actually hoping that\n\"RemoveBracesLLVM: true\" we have at the end, which is explained like\nso\n\n    # Remove optional braces of control statements (if, else, for, and while)\n    # according to the LLVM coding style. This avoids braces on simple\n    # single-statement bodies of statements but keeps braces if one side of\n    # if/else if/.../else cascade has multi-statement body.\n\nwould help us, but it only can do a small subset of what we want,\ni.e., excess braces around both or either if/else body are removed\n(see [*] below), and it cannot add braces around all the bodies of\nif/else if.../else cascade only when one of them require them, it\nseems.\n\n\n[*] effect of RemoveBracesLLVM.\n\ngit clang-format --style file --diff --extensions c,h\ndiff --git a/git.c b/git.c\nindex dfd62d4010..c8df287d70 100644\n--- a/git.c\n+++ b/git.c\n@@ -61,11 +61,10 @@ static void exclude_helpers_from_list(struct string_list *list)\n \tsize_t i = 0;\n \n \twhile (i < list->nr) {\n-\t\tif (strstr(list->items[i].string, \"--\")) {\n+\t\tif (strstr(list->items[i].string, \"--\"))\n \t\t\tunsorted_string_list_delete_item(list, i, 0);\n-\t\t} else {\n+\t\telse\n \t\t\ti++;\n-\t\t}\n \t}\n }\n \nmake: *** [Makefile:3503: style] Error 1\n"},{"id":"551985","messageId":"CAHwyqnXoxmgstsczTkkFDQbi2oN5aH8sg57zBtj1WEvqWy_zRg@mail.gmail.com","threadId":"66064","inReplyTo":"99b6da14-e03a-472b-b7a3-81162dc74166@gmail.com","subject":"Re: [PATCH v4 1/2] stash: reserve exit status 1 for conflicts","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-09-04T16:42:36Z","receivedAt":"2026-09-04T16:43:14Z","isPatch":true,"body":"On Fri, Sep 4, 2026 at 5:09 PM Phillip Wood <phillip.wood123@gmail.com> wrote:\n>\n> [Cc'd Karthik for a view on the CI style job]\n>\n> On 04/09/2026 09:16, Harald Nordgren wrote:\n> >> A local \"make style\" while you develop runs\n> >>\n> >>          git clang-format --style file --diff --extensions c,h\n> >>\n> >> which tells the command to check only the parts of the system that\n> >> you touched.\n> >>\n> >> I do not think it is used in CI.\n> >\n> > It seems to run as part of CI and doesn't catch it then:\n> > '.github/workflows/check-style.yml'\n>\n> My recollection is that we made that job never fail because clang-format\n> does not always do a good job of following our style guide and the\n> output is not guaranteed to be stable across different versions.\n\nThanks for the history! Seems that when linter output is not\nconsistent between versions, then version pinning is necessary.\n\n> That\n> makes its value debatable as I don't think many people (anyone?) bother\n> checking the output to see what suggestions it made. It would be a lot\n> simpler for contributors if we just devolved style decisions to\n> clang-format so no one had to think about or comment on the style.\n\nMy professional opinion is that a consistent style, auto-enforceable +\nauto-fixable, is extremely valuable, because it removes an entire\nclass of code review discussions. I almost never care which style is\nchosen, as long as it's the same everywhere.\n\nI tried today to apply the full clang-format output and it was A LOT.\nDoesn't mean it shouldn't be done, but it would need to be done at a\ncarefully chosen time because there will be lots of conflicts on all\nin-flight topics.\n\n> Whether that tradeoff is worth it depends on how far the output of\n> clang-format deviates from our preferred style.\n\nIn the best of worlds, the preferred style is exactly what the linter\ndictates, then these two are equivalent.\n\n\nHarald\n"}]}