checkout -m: refine autostash fallback

When unpack_trees() fails under "git checkout -m", only create an autostash and retry if there are tracked local changes. Without such changes, the fallback cannot help and merely repeats the same failure. Use the conflict result from apply_autostash_ref() to print a blank line before the branch-switch message, visually separating it from the conflict advice. Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Harald Nordgren committed Jul 25, 2026 at 15:34 UTC f4996ed4619836f4108520a4721a0a781298b644
2 files changed +29 -5
builtin/checkout.c
+13 -4
@@ -838,10 +838,11 @@ static void init_topts(struct unpack_trees_options *topts,
838 static int merge_working_tree(const struct checkout_opts *opts,
839 struct branch_info *old_branch_info,
840 struct branch_info *new_branch_info,
841 - bool quiet,
841 + bool allow_autostash,
842 int *writeout_error)
843 {
844 int ret;
845 + bool can_autostash = false;
846 struct lock_file lock_file = LOCK_INIT;
847 struct tree *new_tree;
848
@@ -888,9 +889,13 @@ static int merge_working_tree(const struct checkout_opts *opts,
889 return 1;
890 }
891
892 + if (allow_autostash)
893 + can_autostash = has_unstaged_changes(the_repository, 1) ||
894 + has_uncommitted_changes(the_repository, 1);
895 +
896 /* 2-way merge to the new branch */
897 init_topts(&topts, opts->show_progress,
893 - opts->overwrite_ignore, quiet);
898 + opts->overwrite_ignore, can_autostash);
899 init_checkout_metadata(&topts.meta, new_branch_info->refname,
900 new_branch_info->commit ?
901 &new_branch_info->commit->object.oid :
@@ -917,7 +922,8 @@ static int merge_working_tree(const struct checkout_opts *opts,
922 clear_unpack_trees_porcelain(&topts);
923 if (ret == -1) {
924 rollback_lock_file(&lock_file);
920 - return MERGE_WORKING_TREE_UNPACK_FAILED;
925 + return can_autostash ?
926 + MERGE_WORKING_TREE_UNPACK_FAILED : 1;
927 }
928 }
929
@@ -1163,6 +1169,7 @@ static int switch_branches(const struct checkout_opts *opts,
1169 int flag, writeout_error = 0;
1170 int do_merge = 1;
1171 int created_autostash = 0;
1172 + bool autostash_conflicted = false;
1173 struct strbuf old_commit_shortname = STRBUF_INIT;
1174 struct strbuf autostash_msg = STRBUF_INIT;
1175 const char *stash_label_base = NULL;
@@ -1240,7 +1247,7 @@ static int switch_branches(const struct checkout_opts *opts,
1247 "local",
1248 stash_label_base,
1249 autostash_msg.buf,
1243 - NULL);
1250 + &autostash_conflicted);
1251 }
1252 if (ret) {
1253 branch_info_release(&old_branch_info);
@@ -1253,6 +1260,8 @@ static int switch_branches(const struct checkout_opts *opts,
1260 if (!opts->quiet && !old_branch_info.path && old_branch_info.commit && new_branch_info->commit != old_branch_info.commit)
1261 orphaned_commit_warning(old_branch_info.commit, new_branch_info->commit);
1262
1263 + if (autostash_conflicted && !opts->quiet)
1264 + fputc('\n', stderr);
1265 update_refs_for_switch(opts, &old_branch_info, new_branch_info);
1266
1267 if (created_autostash) {
t/t7201-co.sh
+16 -1
@@ -240,6 +240,14 @@ test_expect_success 'checkout -m creates a recoverable stash on conflict' '
240 test_grep "git stash drop" actual &&
241 test_grep "git stash pop" actual &&
242 test_grep "The following paths have local changes" actual &&
243 + sed -n "/apply the local changes later/,/Switched to branch/p" \
244 + actual >separator.actual &&
245 + cat >separator.expect <<-EOF &&
246 + apply the local changes later by running "git stash pop".
247 +
248 + Switched to branch ${SQ}side${SQ}
249 + EOF
250 + test_cmp separator.expect separator.actual &&
251 git log -p -1 --format="%gs%n%B" -g --diff-merges=1 refs/stash >actual &&
252 sed /^index/d actual >actual.trimmed &&
253 cat >expect <<-EOF &&
@@ -262,11 +270,18 @@ test_expect_success 'checkout -m creates a recoverable stash on conflict' '
270 git reset --hard
271 '
272
265 -test_expect_success 'checkout -m which would overwrite untracked file' '
273 +test_expect_success 'checkout -m only retries untracked-file failure with local changes' '
274 git checkout -f --detach main &&
275 test_commit another-file &&
276 git checkout HEAD^ &&
277 >another-file.t &&
278 + test_must_fail env GIT_TRACE2_EVENT="$(pwd)/trace" \
279 + git checkout -m @{-1} 2>err &&
280 + test_grep "untracked working tree files" err &&
281 + grep "\"region_enter\".*\"category\":\"index\",\"label\":\"refresh\"" \
282 + trace >refresh.events &&
283 + test_line_count = 1 refresh.events &&
284 +
285 fill 1 2 3 4 5 >one &&
286 test_must_fail git checkout -m @{-1} 2>err &&
287 q_to_tab >expect <<-\EOF &&