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 &&