Merge branch 'hn/checkout-m-autostash-refine' into jch

The autostash fallback in 'git checkout -m' has been refined to only
retry when there are local changes.  Additionally, a blank line now
visually separates autostash conflict advice from the subsequent
branch-switch message.

* hn/checkout-m-autostash-refine:
  checkout: separate autostash conflict advice from branch-switch message
  stash: reserve exit status 1 for conflicts
Junio C Hamano 2026-09-02 13:20:48 -07:00
commit 89009a6f16
8 changed files with 149 additions and 54 deletions

View File

@ -426,6 +426,15 @@ include::includes/cmd-config-section-all.adoc[]
:git-stash: 1
include::config/stash.adoc[]

EXIT STATUS
-----------

The `git stash` subcommands exit with status 0 on success. The
subcommands that apply a stash entry, i.e. `apply`, `pop` and `branch`,
exit with status 1 when applying the stash entry resulted in conflicts,
in which case the stash entry is left in place, and with a non-zero
status other than 1 when they fail for other reasons.


SEE ALSO
--------

View File

@ -1166,6 +1166,7 @@ static int switch_branches(const struct checkout_opts *opts,
int flag, writeout_error = 0;
int do_merge = 1;
int created_autostash = 0;
enum stash_apply_result autostash_res = STASH_APPLY_CLEAN;
struct strbuf old_commit_shortname = STRBUF_INIT;
struct strbuf autostash_msg = STRBUF_INIT;
const char *stash_label_base = NULL;
@ -1237,12 +1238,12 @@ static int switch_branches(const struct checkout_opts *opts,
git_config_push_parameter(cfg.buf);
strbuf_release(&cfg);
}
apply_autostash_ref(the_repository,
"CHECKOUT_AUTOSTASH_HEAD",
new_branch_info->name,
"local",
stash_label_base,
autostash_msg.buf);
autostash_res = apply_autostash_ref(the_repository,
"CHECKOUT_AUTOSTASH_HEAD",
new_branch_info->name,
"local",
stash_label_base,
autostash_msg.buf);
}
if (ret) {
branch_info_release(&old_branch_info);
@ -1255,6 +1256,8 @@ static int switch_branches(const struct checkout_opts *opts,
if (!opts->quiet && !old_branch_info.path && old_branch_info.commit && new_branch_info->commit != old_branch_info.commit)
orphaned_commit_warning(old_branch_info.commit, new_branch_info->commit);

if (autostash_res == STASH_APPLY_CONFLICT && !opts->quiet)
fputc('\n', stderr);
update_refs_for_switch(opts, &old_branch_info, new_branch_info);

if (created_autostash) {

View File

@ -10,6 +10,7 @@
#include "object-name.h"
#include "parse-options.h"
#include "refs.h"
#include "stash.h"
#include "lockfile.h"
#include "cache-tree.h"
#include "unpack-trees.h"
@ -640,10 +641,12 @@ static void unstage_changes_unless_new(struct object_id *orig_tree)
die(_("could not write index"));
}

static int do_apply_stash(const char *prefix, struct stash_info *info,
int index, int quiet,
const char *label_ours, const char *label_theirs,
const char *label_base)
static enum stash_apply_result do_apply_stash(const char *prefix,
struct stash_info *info,
int index, int quiet,
const char *label_ours,
const char *label_theirs,
const char *label_base)
{
int clean, ret;
int has_index = index;
@ -717,8 +720,8 @@ static int do_apply_stash(const char *prefix, struct stash_info *info,

/*
* If 'clean' >= 0, reverse the value for 'ret' so 'ret' is 0 when the
* merge was clean, and nonzero if the merge was unclean or encountered
* an error.
* merge was clean, and 1 if the merge was unclean or a negative value
* if it encountered an error.
*/
ret = clean >= 0 ? !clean : clean;

@ -2492,9 +2495,20 @@ int cmd_stash(int argc,
strbuf_addf(&stash_index_path, "%s.stash.%" PRIuMAX, index_file,
(uintmax_t)pid);

if (fn)
return !!fn(argc, argv, prefix, repo);
else if (!argc)
if (fn) {
ret = fn(argc, argv, prefix, repo);

/*
* The subcommand implementations return 0 on success, a
* negative value on failure, and STASH_APPLY_CONFLICT
* when applying a stash entry resulted in conflicts.
* Map failures to 128, the status die() uses, so that
* exit status 1 unambiguously indicates conflicts.
*/
if (ret < 0)
return 128;
return ret;
} else if (!argc)
return !!push_stash_unassumed(0, NULL, prefix, repo);

/* Assume 'stash push' */

View File

@ -19,6 +19,7 @@
#include "commit.h"
#include "sequencer.h"
#include "run-command.h"
#include "stash.h"
#include "hook.h"
#include "utf8.h"
#include "cache-tree.h"
@ -4794,13 +4795,15 @@ void create_autostash_ref(struct repository *r, const char *refname,
create_autostash_internal(r, NULL, refname, message, silent);
}

static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply,
const char *label_ours, const char *label_theirs,
const char *label_base,
const char *stash_msg)
static enum stash_apply_result apply_save_autostash_oid(const char *stash_oid,
int attempt_apply,
const char *label_ours,
const char *label_theirs,
const char *label_base,
const char *stash_msg)
{
struct child_process child = CHILD_PROCESS_INIT;
int ret = 0;
enum stash_apply_result ret = STASH_APPLY_CLEAN;

if (attempt_apply) {
child.git_cmd = 1;
@ -4816,9 +4819,11 @@ static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply,
strvec_pushf(&child.args, "--label-base=%s", label_base);
strvec_push(&child.args, stash_oid);
ret = run_command(&child);
if (ret > 1)
ret = STASH_APPLY_ERROR;
}

if (attempt_apply && !ret)
if (attempt_apply && ret == STASH_APPLY_CLEAN)
fprintf(stderr, _("Applied autostash.\n"));
else {
struct child_process store = CHILD_PROCESS_INIT;
@ -4832,13 +4837,16 @@ static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply,
strvec_push(&store.args, stash_oid);
if (run_command(&store))
ret = error(_("cannot store %s"), stash_oid);
else if (attempt_apply)
else if (attempt_apply && ret == STASH_APPLY_CONFLICT)
fprintf(stderr,
_("Your local changes are stashed, however applying them\n"
"resulted in conflicts. You can either resolve the conflicts\n"
"and then discard the stash with \"git stash drop\", or, if you\n"
"do not want to resolve them now, run \"git reset --hard\" and\n"
"apply the local changes later by running \"git stash pop\".\n"));
else if (attempt_apply)
ret = error(_("could not apply autostash; "
"your changes are safe in the stash"));
else
fprintf(stderr,
_("Autostash exists; creating a new stash entry.\n"
@ -4850,15 +4858,16 @@ static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply,
return ret;
}

static int apply_save_autostash(const char *path, int attempt_apply)
static enum stash_apply_result apply_save_autostash(const char *path,
int attempt_apply)
{
struct strbuf stash_oid = STRBUF_INIT;
int ret = 0;
enum stash_apply_result ret = STASH_APPLY_CLEAN;

if (!read_oneliner(&stash_oid, path,
READ_ONELINER_SKIP_IF_EMPTY)) {
strbuf_release(&stash_oid);
return 0;
return STASH_APPLY_CLEAN;
}
strbuf_trim(&stash_oid);

@ -4870,37 +4879,40 @@ static int apply_save_autostash(const char *path, int attempt_apply)
return ret;
}

int save_autostash(const char *path)
enum stash_apply_result save_autostash(const char *path)
{
return apply_save_autostash(path, 0);
}

int apply_autostash(const char *path)
enum stash_apply_result apply_autostash(const char *path)
{
return apply_save_autostash(path, 1);
}

int apply_autostash_oid(const char *stash_oid)
enum stash_apply_result apply_autostash_oid(const char *stash_oid)
{
return apply_save_autostash_oid(stash_oid, 1, NULL, NULL, NULL, NULL);
}

static int apply_save_autostash_ref(struct repository *r, const char *refname,
int attempt_apply,
const char *label_ours, const char *label_theirs,
const char *label_base,
const char *stash_msg)
static enum stash_apply_result apply_save_autostash_ref(struct repository *r,
const char *refname,
int attempt_apply,
const char *label_ours,
const char *label_theirs,
const char *label_base,
const char *stash_msg)
{
struct object_id stash_oid;
char stash_oid_hex[GIT_MAX_HEXSZ + 1];
int flag, ret;
int flag;
enum stash_apply_result ret;

if (!refs_ref_exists(get_main_ref_store(r), refname))
return 0;
return STASH_APPLY_CLEAN;

if (!refs_resolve_ref_unsafe(get_main_ref_store(r), refname,
RESOLVE_REF_READING, &stash_oid, &flag))
return -1;
return STASH_APPLY_ERROR;
if (flag & REF_ISSYMREF)
return error(_("autostash reference is a symref"));

@ -4915,15 +4927,19 @@ static int apply_save_autostash_ref(struct repository *r, const char *refname,
return ret;
}

int save_autostash_ref(struct repository *r, const char *refname)
enum stash_apply_result save_autostash_ref(struct repository *r,
const char *refname)
{
return apply_save_autostash_ref(r, refname, 0,
NULL, NULL, NULL, NULL);
}

int apply_autostash_ref(struct repository *r, const char *refname,
const char *label_ours, const char *label_theirs,
const char *label_base, const char *stash_msg)
enum stash_apply_result apply_autostash_ref(struct repository *r,
const char *refname,
const char *label_ours,
const char *label_theirs,
const char *label_base,
const char *stash_msg)
{
return apply_save_autostash_ref(r, refname, 1,
label_ours, label_theirs, label_base,

View File

@ -3,6 +3,7 @@

#include "strbuf.h"
#include "strvec.h"
#include "stash.h"
#include "wt-status.h"

struct commit;
@ -231,13 +232,17 @@ void commit_post_rewrite(struct repository *r,
void create_autostash(struct repository *r, const char *path);
void create_autostash_ref(struct repository *r, const char *refname,
const char *message, bool silent);
int save_autostash(const char *path);
int save_autostash_ref(struct repository *r, const char *refname);
int apply_autostash(const char *path);
int apply_autostash_oid(const char *stash_oid);
int apply_autostash_ref(struct repository *r, const char *refname,
const char *label_ours, const char *label_theirs,
const char *label_base, const char *stash_msg);
enum stash_apply_result save_autostash(const char *path);
enum stash_apply_result save_autostash_ref(struct repository *r,
const char *refname);
enum stash_apply_result apply_autostash(const char *path);
enum stash_apply_result apply_autostash_oid(const char *stash_oid);
enum stash_apply_result apply_autostash_ref(struct repository *r,
const char *refname,
const char *label_ours,
const char *label_theirs,
const char *label_base,
const char *stash_msg);

#define SUMMARY_INITIAL_COMMIT (1 << 0)
#define SUMMARY_SHOW_AUTHOR_DATE (1 << 1)

21
stash.h Normal file
View File

@ -0,0 +1,21 @@
#ifndef STASH_H
#define STASH_H

enum stash_apply_result {
/* The stash was applied cleanly, or there was nothing to apply. */
STASH_APPLY_CLEAN = 0,

/*
* The stash could not be applied because it resulted in
* conflicts. The stash entry is left in place. The "git stash
* apply", "pop" and "branch" subcommands exit with this status
* in this case, mirroring the convention of "git merge-tree" and
* the merge strategies.
*/
STASH_APPLY_CONFLICT = 1,

/* Something went wrong. */
STASH_APPLY_ERROR = -1,
};

#endif /* STASH_H */

View File

@ -1808,13 +1808,13 @@ test_expect_success 'stash.index=false overridden by --index' '
test_cmp expect file
'

test_expect_success 'apply with custom conflict labels' '
test_expect_success 'apply exits 1 on conflicts' '
git reset --hard initial &&
test_commit label-base conflict-file base-content &&
echo stashed >conflict-file &&
git stash push -m "stashed" &&
test_commit label-upstream conflict-file upstream-content &&
test_must_fail git -c merge.conflictStyle=diff3 stash apply --label-ours=UP --label-theirs=STASH &&
test_expect_code 1 git -c merge.conflictStyle=diff3 stash apply --label-ours=UP --label-theirs=STASH &&
test_grep "^<<<<<<< UP" conflict-file &&
test_grep "^||||||| Stash base" conflict-file &&
test_grep "^>>>>>>> STASH" conflict-file
@ -1826,11 +1826,30 @@ test_expect_success 'apply with empty conflict labels' '
echo stashed >conflict-file &&
git stash push -m "stashed" &&
test_commit empty-label-upstream conflict-file upstream-content &&
test_must_fail git stash apply --label-ours= --label-theirs= &&
test_expect_code 1 git stash apply --label-ours= --label-theirs= &&
test_grep "^<<<<<<<$" conflict-file &&
test_grep "^>>>>>>>$" conflict-file
'

test_expect_success 'pop exits 1 on conflicts and keeps the stash entry' '
git reset --hard initial &&
echo stashed >file &&
git stash push -m pop-stashed &&
test_commit pop-upstream file upstream-content &&
test_expect_code 1 git stash pop &&
git stash list >list &&
test_grep pop-stashed list
'

test_expect_success 'stash branch exits with a non-1 status on errors' '
git reset --hard initial &&
echo stashed >file &&
git stash push -m branch-stashed &&
test_expect_code 128 git stash branch conflicting-branch refs/heads/does-not-exist &&
git stash list >list &&
test_grep branch-stashed list
'

test_expect_success 'stash show --include-untracked includes untracked files' '
git reset --hard &&


View File

@ -236,10 +236,18 @@ test_expect_success 'checkout -m creates a recoverable stash on conflict' '
test_must_fail git checkout side 2>stderr &&
test_grep "Your local changes" stderr &&
git checkout -m side >actual 2>&1 &&
test_grep "resulted in conflicts" actual &&
test_grep "git stash drop" actual &&
test_grep "git stash pop" actual &&
test_grep "The following paths have local changes" actual &&
cat >expect <<-EOF &&
Your local changes are stashed, however applying them
resulted in conflicts. You can either resolve the conflicts
and then discard the stash with "git stash drop", or, if you
do not want to resolve them now, run "git reset --hard" and
apply the local changes later by running "git stash pop".

Switched to branch ${SQ}side${SQ}
The following paths have local changes:
M one
EOF
test_cmp expect actual &&
git log -p -1 --format="%gs%n%B" -g --diff-merges=1 refs/stash >actual &&
sed /^index/d actual >actual.trimmed &&
cat >expect <<-EOF &&