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 -m: refine autostash fallback
  sequencer: teach autostash apply to report conflicts
This commit is contained in:
Junio C Hamano
2026-08-03 09:31:44 -07:00
6 changed files with 55 additions and 20 deletions

View File

@@ -838,10 +838,11 @@ static void init_topts(struct unpack_trees_options *topts,
static int merge_working_tree(const struct checkout_opts *opts,
struct branch_info *old_branch_info,
struct branch_info *new_branch_info,
bool quiet,
bool allow_autostash,
int *writeout_error)
{
int ret;
bool can_autostash = false;
struct lock_file lock_file = LOCK_INIT;
struct tree *new_tree;
@@ -888,9 +889,13 @@ static int merge_working_tree(const struct checkout_opts *opts,
return 1;
}
if (allow_autostash)
can_autostash = has_unstaged_changes(the_repository, 1) ||
has_uncommitted_changes(the_repository, 1);
/* 2-way merge to the new branch */
init_topts(&topts, opts->show_progress,
opts->overwrite_ignore, quiet);
opts->overwrite_ignore, can_autostash);
init_checkout_metadata(&topts.meta, new_branch_info->refname,
new_branch_info->commit ?
&new_branch_info->commit->object.oid :
@@ -917,7 +922,8 @@ static int merge_working_tree(const struct checkout_opts *opts,
clear_unpack_trees_porcelain(&topts);
if (ret == -1) {
rollback_lock_file(&lock_file);
return MERGE_WORKING_TREE_UNPACK_FAILED;
return can_autostash ?
MERGE_WORKING_TREE_UNPACK_FAILED : 1;
}
}
@@ -1166,6 +1172,7 @@ static int switch_branches(const struct checkout_opts *opts,
int flag, writeout_error = 0;
int do_merge = 1;
int created_autostash = 0;
bool autostash_conflicted = false;
struct strbuf old_commit_shortname = STRBUF_INIT;
struct strbuf autostash_msg = STRBUF_INIT;
const char *stash_label_base = NULL;
@@ -1242,7 +1249,8 @@ static int switch_branches(const struct checkout_opts *opts,
new_branch_info->name,
"local",
stash_label_base,
autostash_msg.buf);
autostash_msg.buf,
&autostash_conflicted);
}
if (ret) {
branch_info_release(&old_branch_info);
@@ -1255,6 +1263,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_conflicted && !opts->quiet)
fputc('\n', stderr);
update_refs_for_switch(opts, &old_branch_info, new_branch_info);
if (created_autostash) {

View File

@@ -1980,7 +1980,7 @@ int cmd_commit(int argc,
}
apply_autostash_ref(the_repository, "MERGE_AUTOSTASH",
NULL, NULL, NULL, NULL);
NULL, NULL, NULL, NULL, NULL);
cleanup:
free_commit_extra_headers(extra);

View File

@@ -538,7 +538,7 @@ static void finish(struct commit *head_commit,
if (new_head)
apply_autostash_ref(the_repository, "MERGE_AUTOSTASH",
NULL, NULL, NULL, NULL);
NULL, NULL, NULL, NULL, NULL);
strbuf_release(&reflog_message);
}
@@ -1682,7 +1682,7 @@ int cmd_merge(int argc,
&commit->object.oid,
overwrite_ignore)) {
apply_autostash_ref(the_repository, "MERGE_AUTOSTASH",
NULL, NULL, NULL, NULL);
NULL, NULL, NULL, NULL, NULL);
ret = 1;
goto done;
}
@@ -1846,7 +1846,7 @@ int cmd_merge(int argc,
fprintf(stderr, _("Merge with strategy %s failed.\n"),
use_strategies[0]->name);
apply_autostash_ref(the_repository, "MERGE_AUTOSTASH",
NULL, NULL, NULL, NULL);
NULL, NULL, NULL, NULL, NULL);
ret = 2;
goto done;
} else if (best_strategy == wt_strategy)

View File

@@ -4796,7 +4796,8 @@ void create_autostash_ref(struct repository *r, const char *refname,
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)
const char *stash_msg,
bool *conflicted)
{
struct child_process child = CHILD_PROCESS_INIT;
int ret = 0;
@@ -4831,14 +4832,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) {
if (conflicted)
*conflicted = true;
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
} else
fprintf(stderr,
_("Autostash exists; creating a new stash entry.\n"
"Your changes are safe in the stash.\n"
@@ -4862,7 +4865,7 @@ static int apply_save_autostash(const char *path, int attempt_apply)
strbuf_trim(&stash_oid);
ret = apply_save_autostash_oid(stash_oid.buf, attempt_apply,
NULL, NULL, NULL, NULL);
NULL, NULL, NULL, NULL, NULL);
unlink(path);
strbuf_release(&stash_oid);
@@ -4881,19 +4884,24 @@ int apply_autostash(const char *path)
int apply_autostash_oid(const char *stash_oid)
{
return apply_save_autostash_oid(stash_oid, 1, NULL, NULL, NULL, NULL);
return apply_save_autostash_oid(stash_oid, 1, NULL, 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)
const char *stash_msg,
bool *conflicted)
{
struct object_id stash_oid;
char stash_oid_hex[GIT_MAX_HEXSZ + 1];
int flag, ret;
if (conflicted)
*conflicted = false;
if (!refs_ref_exists(get_main_ref_store(r), refname))
return 0;
@@ -4906,7 +4914,7 @@ static int apply_save_autostash_ref(struct repository *r, const char *refname,
oid_to_hex_r(stash_oid_hex, &stash_oid);
ret = apply_save_autostash_oid(stash_oid_hex, attempt_apply,
label_ours, label_theirs, label_base,
stash_msg);
stash_msg, conflicted);
refs_delete_ref(get_main_ref_store(r), "", refname,
&stash_oid, REF_NO_DEREF);
@@ -4917,16 +4925,17 @@ static int apply_save_autostash_ref(struct repository *r, const char *refname,
int save_autostash_ref(struct repository *r, const char *refname)
{
return apply_save_autostash_ref(r, refname, 0,
NULL, NULL, NULL, NULL);
NULL, 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)
const char *label_base, const char *stash_msg,
bool *conflicted)
{
return apply_save_autostash_ref(r, refname, 1,
label_ours, label_theirs, label_base,
stash_msg);
stash_msg, conflicted);
}
static int checkout_onto(struct repository *r, struct replay_opts *opts,

View File

@@ -237,7 +237,8 @@ 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);
const char *label_base, const char *stash_msg,
bool *conflicted);
#define SUMMARY_INITIAL_COMMIT (1 << 0)
#define SUMMARY_SHOW_AUTHOR_DATE (1 << 1)

View File

@@ -240,6 +240,14 @@ test_expect_success 'checkout -m creates a recoverable stash on conflict' '
test_grep "git stash drop" actual &&
test_grep "git stash pop" actual &&
test_grep "The following paths have local changes" actual &&
sed -n "/apply the local changes later/,/Switched to branch/p" \
actual >separator.actual &&
cat >separator.expect <<-EOF &&
apply the local changes later by running "git stash pop".
Switched to branch ${SQ}side${SQ}
EOF
test_cmp separator.expect separator.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 &&
@@ -262,11 +270,18 @@ test_expect_success 'checkout -m creates a recoverable stash on conflict' '
git reset --hard
'
test_expect_success 'checkout -m which would overwrite untracked file' '
test_expect_success 'checkout -m only retries untracked-file failure with local changes' '
git checkout -f --detach main &&
test_commit another-file &&
git checkout HEAD^ &&
>another-file.t &&
test_must_fail env GIT_TRACE2_EVENT="$(pwd)/trace" \
git checkout -m @{-1} 2>err &&
test_grep "untracked working tree files" err &&
grep "\"region_enter\".*\"category\":\"index\",\"label\":\"refresh\"" \
trace >refresh.events &&
test_line_count = 1 refresh.events &&
fill 1 2 3 4 5 >one &&
test_must_fail git checkout -m @{-1} 2>err &&
q_to_tab >expect <<-\EOF &&