bisect: add --reset-when-found to leave when done

When a bisection finishes, "git bisect" reports the first bad commit but leaves the session active until "git bisect reset" is run by hand. Add a "--reset-when-found[=<where>]" option, accepted by both "git bisect start" and "git bisect run", that resets as soon as the first bad commit is found. The "original" value returns to the commit checked out before "git bisect start", while "found" leaves the first bad commit checked out; omitting the value defaults to "original". Persist the selected target in a BISECT_RESET_WHEN_FOUND state file and perform the reset quietly. For "git bisect run", defer the reset until after the captured output is printed and BISECT_RUN is closed. This lets cleanup remove the file on systems that cannot unlink an open file. Reject this option together with "--no-checkout", since that mode must not check out either target. Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Harald Nordgren committed Jul 20, 2026 at 09:10 UTC 2f0dacd385420a81f78d3976c24f6820dcc8b241
4 files changed +243 -16
Documentation/git-bisect.adoc
+12 -2
@@ -10,7 +10,7 @@ SYNOPSIS
10 --------
11 [synopsis]
12 git bisect start [--term-(bad|new)=<term-new> --term-(good|old)=<term-old>]
13 - [--no-checkout] [--first-parent] [<bad> [<good>...]] [--] [<pathspec>...]
13 + [--no-checkout] [--first-parent] [--reset-when-found[=<where>]] [<bad> [<good>...]] [--] [<pathspec>...]
14 git bisect (bad|new|<term-new>) [<rev>]
15 git bisect (good|old|<term-old>) [<rev>...]
16 git bisect terms [--term-(good|old) | --term-(bad|new)]
@@ -20,7 +20,7 @@ git bisect reset [<commit>]
20 git bisect (visualize|view)
21 git bisect replay <logfile>
22 git bisect log
23 -git bisect run <cmd> [<arg>...]
23 +git bisect run [--reset-when-found[=<where>]] <cmd> [<arg>...]
24 git bisect help
25
26 DESCRIPTION
@@ -385,6 +385,16 @@ ignored.
385 This option is particularly useful in avoiding false positives when a merged
386 branch contained broken or non-buildable commits, but the merge itself was OK.
387
388 +`--reset-when-found[=<where>]`::
389 + Once the first bad commit is found, report it and clean up the
390 + bisection state. `<where>` may be `original` to return to the commit
391 + checked out before `git bisect start`, or `found` to leave the first
392 + bad commit checked out. If `<where>` is omitted, it defaults to
393 + `original`.
394 ++
395 +This option may be given to `git bisect start` or to `git bisect run`. It
396 +cannot be used for a bisection started with `--no-checkout`.
397 +
398 EXAMPLES
399 --------
400
bisect.c
+2
@@ -488,6 +488,7 @@ static GIT_PATH_FUNC(git_path_bisect_start, "BISECT_START")
488 static GIT_PATH_FUNC(git_path_bisect_log, "BISECT_LOG")
489 static GIT_PATH_FUNC(git_path_bisect_terms, "BISECT_TERMS")
490 static GIT_PATH_FUNC(git_path_bisect_first_parent, "BISECT_FIRST_PARENT")
491 +static GIT_PATH_FUNC(git_path_bisect_reset_when_found, "BISECT_RESET_WHEN_FOUND")
492
493 static void read_bisect_paths(struct strvec *array)
494 {
@@ -1211,6 +1212,7 @@ int bisect_clean_state(void)
1212 unlink_or_warn(git_path_bisect_run());
1213 unlink_or_warn(git_path_bisect_terms());
1214 unlink_or_warn(git_path_bisect_first_parent());
1215 + unlink_or_warn(git_path_bisect_reset_when_found());
1216 /*
1217 * Cleanup BISECT_START last to support the --no-checkout option
1218 * introduced in the commit 4796e823a.
builtin/bisect.c
+120 -14
@@ -24,11 +24,12 @@ static GIT_PATH_FUNC(git_path_bisect_start, "BISECT_START")
24 static GIT_PATH_FUNC(git_path_bisect_log, "BISECT_LOG")
25 static GIT_PATH_FUNC(git_path_bisect_names, "BISECT_NAMES")
26 static GIT_PATH_FUNC(git_path_bisect_first_parent, "BISECT_FIRST_PARENT")
27 +static GIT_PATH_FUNC(git_path_bisect_reset_when_found, "BISECT_RESET_WHEN_FOUND")
28 static GIT_PATH_FUNC(git_path_bisect_run, "BISECT_RUN")
29
30 #define BUILTIN_GIT_BISECT_START_USAGE \
31 N_("git bisect start [--term-(bad|new)=<term-new> --term-(good|old)=<term-old>]\n" \
31 - " [--no-checkout] [--first-parent] [<bad> [<good>...]] [--] [<pathspec>...]")
32 + " [--no-checkout] [--first-parent] [--reset-when-found[=<where>]] [<bad> [<good>...]] [--] [<pathspec>...]")
33 #define BUILTIN_GIT_BISECT_BAD_USAGE \
34 N_("git bisect (bad|new|<term-new>) [<rev>]")
35 #define BUILTIN_GIT_BISECT_GOOD_USAGE \
@@ -48,7 +49,7 @@ static GIT_PATH_FUNC(git_path_bisect_run, "BISECT_RUN")
49 #define BUILTIN_GIT_BISECT_LOG_USAGE \
50 "git bisect log"
51 #define BUILTIN_GIT_BISECT_RUN_USAGE \
51 - N_("git bisect run <cmd> [<arg>...]")
52 + N_("git bisect run [--reset-when-found[=<where>]] <cmd> [<arg>...]")
53 #define BUILTIN_GIT_BISECT_HELP_USAGE \
54 "git bisect help"
55
@@ -68,6 +69,12 @@ static const char * const git_bisect_usage[] = {
69 NULL
70 };
71
72 +enum reset_when_found_mode {
73 + RESET_WHEN_FOUND_NONE,
74 + RESET_WHEN_FOUND_TO_ORIGINAL,
75 + RESET_WHEN_FOUND_TO_FOUND,
76 +};
77 +
78 struct add_bisect_ref_data {
79 struct rev_info *revs;
80 unsigned int object_flags;
@@ -272,6 +279,61 @@ static int bisect_reset(const char *commit, int quiet)
279 return bisect_clean_state();
280 }
281
282 +static int parse_reset_when_found(const char *value,
283 + enum reset_when_found_mode *mode)
284 +{
285 + if (!strcmp(value, "original"))
286 + *mode = RESET_WHEN_FOUND_TO_ORIGINAL;
287 + else if (!strcmp(value, "found"))
288 + *mode = RESET_WHEN_FOUND_TO_FOUND;
289 + else
290 + return error(_("invalid value for '--reset-when-found': '%s'"),
291 + value);
292 +
293 + return 0;
294 +}
295 +
296 +static const char *reset_when_found_mode_name(enum reset_when_found_mode mode)
297 +{
298 + switch (mode) {
299 + case RESET_WHEN_FOUND_TO_ORIGINAL:
300 + return "original";
301 + case RESET_WHEN_FOUND_TO_FOUND:
302 + return "found";
303 + case RESET_WHEN_FOUND_NONE:
304 + BUG("no name for unset reset-when-found mode");
305 + }
306 + BUG("unknown reset-when-found mode %d", mode);
307 +}
308 +
309 +static int bisect_reset_when_found(struct bisect_terms *terms)
310 +{
311 + struct strbuf value = STRBUF_INIT;
312 + enum reset_when_found_mode mode;
313 + char *commit = NULL;
314 + int res;
315 +
316 + if (strbuf_read_file(&value, git_path_bisect_reset_when_found(), 0) < 0) {
317 + res = error_errno(_("could not read '%s'"),
318 + git_path_bisect_reset_when_found());
319 + goto cleanup;
320 + }
321 + strbuf_trim(&value);
322 + if (parse_reset_when_found(value.buf, &mode)) {
323 + res = -1;
324 + goto cleanup;
325 + }
326 +
327 + if (mode == RESET_WHEN_FOUND_TO_FOUND)
328 + commit = xstrfmt("refs/bisect/%s", terms->term_bad);
329 + res = bisect_reset(commit, 1);
330 +
331 +cleanup:
332 + free(commit);
333 + strbuf_release(&value);
334 + return res;
335 +}
336 +
337 static void log_commit(FILE *fp,
338 const char *fmt, const char *state,
339 struct commit *commit)
@@ -677,7 +739,8 @@ static int bisect_successful(struct bisect_terms *terms)
739 return res;
740 }
741
680 -static enum bisect_error bisect_next(struct bisect_terms *terms, const char *prefix)
742 +static enum bisect_error bisect_next(struct bisect_terms *terms,
743 + const char *prefix, bool defer_reset)
744 {
745 enum bisect_error res;
746
@@ -692,6 +755,9 @@ static enum bisect_error bisect_next(struct bisect_terms *terms, const char *pre
755
756 if (res == BISECT_INTERNAL_SUCCESS_1ST_BAD_FOUND) {
757 res = bisect_successful(terms);
758 + if (!res && !defer_reset &&
759 + !is_empty_or_missing_file(git_path_bisect_reset_when_found()))
760 + res = bisect_reset_when_found(terms);
761 return res ? res : BISECT_INTERNAL_SUCCESS_1ST_BAD_FOUND;
762 } else if (res == BISECT_ONLY_SKIPPED_LEFT) {
763 res = bisect_skipped_commits(terms);
@@ -700,14 +766,15 @@ static enum bisect_error bisect_next(struct bisect_terms *terms, const char *pre
766 return res;
767 }
768
703 -static enum bisect_error bisect_auto_next(struct bisect_terms *terms, const char *prefix)
769 +static enum bisect_error bisect_auto_next(struct bisect_terms *terms,
770 + const char *prefix, bool defer_reset)
771 {
772 if (bisect_next_check(terms, NULL)) {
773 bisect_print_status(terms);
774 return BISECT_OK;
775 }
776
710 - return bisect_next(terms, prefix);
777 + return bisect_next(terms, prefix, defer_reset);
778 }
779
780 static enum bisect_error bisect_start(struct bisect_terms *terms, int argc,
@@ -715,6 +782,7 @@ static enum bisect_error bisect_start(struct bisect_terms *terms, int argc,
782 {
783 int no_checkout = 0;
784 int first_parent_only = 0;
785 + enum reset_when_found_mode reset_when_found = RESET_WHEN_FOUND_NONE;
786 int i, has_double_dash = 0, must_write_terms = 0, bad_seen = 0;
787 int flags, pathspec_pos;
788 enum bisect_error res = BISECT_OK;
@@ -747,6 +815,13 @@ static enum bisect_error bisect_start(struct bisect_terms *terms, int argc,
815 no_checkout = 1;
816 } else if (!strcmp(arg, "--first-parent")) {
817 first_parent_only = 1;
818 + } else if (!strcmp(arg, "--reset-when-found")) {
819 + reset_when_found = RESET_WHEN_FOUND_TO_ORIGINAL;
820 + } else if (skip_prefix(arg, "--reset-when-found=", &arg)) {
821 + if (parse_reset_when_found(arg, &reset_when_found)) {
822 + res = BISECT_FAILED;
823 + goto finish;
824 + }
825 } else if (!strcmp(arg, "--term-good") ||
826 !strcmp(arg, "--term-old")) {
827 i++;
@@ -784,6 +859,10 @@ static enum bisect_error bisect_start(struct bisect_terms *terms, int argc,
859 break;
860 }
861 }
862 + if (reset_when_found != RESET_WHEN_FOUND_NONE && no_checkout) {
863 + res = error(_("'--reset-when-found' cannot be used with '--no-checkout'"));
864 + goto finish;
865 + }
866 pathspec_pos = i;
867
868 /*
@@ -861,6 +940,10 @@ static enum bisect_error bisect_start(struct bisect_terms *terms, int argc,
940 if (first_parent_only)
941 write_file(git_path_bisect_first_parent(), "\n");
942
943 + if (reset_when_found != RESET_WHEN_FOUND_NONE)
944 + write_file(git_path_bisect_reset_when_found(), "%s\n",
945 + reset_when_found_mode_name(reset_when_found));
946 +
947 if (no_checkout) {
948 if (repo_get_oid(the_repository, start_head.buf, &oid) < 0) {
949 res = error(_("invalid ref: '%s'"), start_head.buf);
@@ -902,7 +985,7 @@ finish:
985 if (res)
986 return res;
987
905 - res = bisect_auto_next(terms, NULL);
988 + res = bisect_auto_next(terms, NULL, false);
989 if (!is_bisect_success(res))
990 bisect_clean_state();
991 return res;
@@ -941,7 +1024,7 @@ static int bisect_autostart(struct bisect_terms *terms)
1024 }
1025
1026 static enum bisect_error bisect_state(struct bisect_terms *terms, int argc,
944 - const char **argv)
1027 + const char **argv, bool defer_reset)
1028 {
1029 const char *state;
1030 int i, verify_expected = 1;
@@ -1018,7 +1101,7 @@ static enum bisect_error bisect_state(struct bisect_terms *terms, int argc,
1101 }
1102
1103 oid_array_clear(&revs);
1021 - return bisect_auto_next(terms, NULL);
1104 + return bisect_auto_next(terms, NULL, defer_reset);
1105 }
1106
1107 static enum bisect_error bisect_log(void)
@@ -1107,7 +1190,7 @@ static enum bisect_error bisect_replay(struct bisect_terms *terms, const char *f
1190 if (res)
1191 return BISECT_FAILED;
1192
1110 - return bisect_auto_next(terms, NULL);
1193 + return bisect_auto_next(terms, NULL, false);
1194 }
1195
1196 static enum bisect_error bisect_skip(struct bisect_terms *terms, int argc,
@@ -1141,7 +1224,7 @@ static enum bisect_error bisect_skip(struct bisect_terms *terms, int argc,
1224 strvec_push(&argv_state, argv[i]);
1225 }
1226 }
1144 - res = bisect_state(terms, argv_state.nr, argv_state.v);
1227 + res = bisect_state(terms, argv_state.nr, argv_state.v, false);
1228
1229 strvec_clear(&argv_state);
1230 return res;
@@ -1239,6 +1322,8 @@ static int bisect_run(struct bisect_terms *terms, int argc, const char **argv)
1322 {
1323 int res = BISECT_OK;
1324 struct strbuf command = STRBUF_INIT;
1325 + enum reset_when_found_mode reset_when_found = RESET_WHEN_FOUND_NONE;
1326 + const char *reset_when_found_arg;
1327 const char *new_state;
1328 int temporary_stdout_fd, saved_stdout;
1329 int is_first_run = 1;
@@ -1246,6 +1331,23 @@ static int bisect_run(struct bisect_terms *terms, int argc, const char **argv)
1331 if (bisect_next_check(terms, NULL))
1332 return BISECT_FAILED;
1333
1334 + if (argc && !strcmp(argv[0], "--reset-when-found"))
1335 + reset_when_found = RESET_WHEN_FOUND_TO_ORIGINAL;
1336 + else if (argc && skip_prefix(argv[0], "--reset-when-found=",
1337 + &reset_when_found_arg)) {
1338 + if (parse_reset_when_found(reset_when_found_arg, &reset_when_found))
1339 + return BISECT_FAILED;
1340 + }
1341 +
1342 + if (reset_when_found != RESET_WHEN_FOUND_NONE) {
1343 + if (refs_ref_exists(get_main_ref_store(the_repository), "BISECT_HEAD"))
1344 + return error(_("'--reset-when-found' cannot be used with '--no-checkout'"));
1345 + write_file(git_path_bisect_reset_when_found(), "%s\n",
1346 + reset_when_found_mode_name(reset_when_found));
1347 + argc--;
1348 + argv++;
1349 + }
1350 +
1351 if (!argc) {
1352 error(_("bisect run failed: no command provided."));
1353 return BISECT_FAILED;
@@ -1304,7 +1406,7 @@ static int bisect_run(struct bisect_terms *terms, int argc, const char **argv)
1406 saved_stdout = dup(1);
1407 dup2(temporary_stdout_fd, 1);
1408
1307 - res = bisect_state(terms, 1, &new_state);
1409 + res = bisect_state(terms, 1, &new_state, true);
1410
1411 fflush(stdout);
1412 dup2(saved_stdout, 1);
@@ -1320,7 +1422,11 @@ static int bisect_run(struct bisect_terms *terms, int argc, const char **argv)
1422 res = BISECT_OK;
1423 } else if (res == BISECT_INTERNAL_SUCCESS_1ST_BAD_FOUND) {
1424 printf(_("bisect found first '%s' commit\n"), terms->term_bad);
1323 - res = BISECT_OK;
1425 + if (!is_empty_or_missing_file(git_path_bisect_reset_when_found()) &&
1426 + bisect_reset_when_found(terms))
1427 + res = BISECT_FAILED;
1428 + else
1429 + res = BISECT_OK;
1430 } else if (res) {
1431 error(_("bisect run failed: 'git bisect %s'"
1432 " exited with error code %d"), new_state, res);
@@ -1379,7 +1485,7 @@ static int cmd_bisect__next(int argc, const char **argv UNUSED, const char *pref
1485 return error(_("'%s' requires 0 arguments"),
1486 "git bisect next");
1487 get_terms(&terms);
1382 - res = bisect_next(&terms, prefix);
1488 + res = bisect_next(&terms, prefix, false);
1489 free_terms(&terms);
1490 return res;
1491 }
@@ -1482,7 +1588,7 @@ int cmd_bisect(int argc,
1588 !one_of(argv[0], terms.term_good, terms.term_bad, NULL))
1589 usage_msg_optf(_("unknown command: '%s'"), git_bisect_usage,
1590 options, argv[0]);
1485 - res = bisect_state(&terms, argc, argv);
1591 + res = bisect_state(&terms, argc, argv, false);
1592 free_terms(&terms);
1593 } else {
1594 argc--;
t/t6030-bisect-porcelain.sh
+109
@@ -43,6 +43,38 @@ test_bisect_usage () {
43 test_cmp expect actual
44 }
45
46 +test_bisect_state_file () {
47 + test_path_is_file "$(git rev-parse --git-path "$1")"
48 +}
49 +
50 +test_bisect_state_missing () {
51 + test_path_is_missing "$(git rev-parse --git-path "$1")"
52 +}
53 +
54 +bisect_start_and_finish () {
55 + git bisect start "$1" $HASH4 $HASH2 &&
56 + git bisect bad
57 +}
58 +
59 +bisect_run_reset_when_found () {
60 + write_script test_script.sh <<-\EOF &&
61 + ! grep Another hello >/dev/null
62 + EOF
63 + git bisect start $HASH4 $HASH2 &&
64 + git bisect run "$1" ./test_script.sh >my_bisect_log.txt &&
65 + test_grep "$HASH3 is the first .bad. commit" my_bisect_log.txt &&
66 + test_bisect_state_missing BISECT_RUN
67 +}
68 +
69 +test_reset_when_found_fails () {
70 + local pattern="$1" &&
71 + local state_file="$2" &&
72 + shift 2 &&
73 + test_must_fail "$@" 2>err &&
74 + test_grep -- "$pattern" err &&
75 + test_bisect_state_missing "$state_file"
76 +}
77 +
78 test_expect_success 'bisect usage' "
79 test_bisect_usage 1 git bisect reset extra1 extra2 <<-\EOF &&
80 error: 'git bisect reset' requires either no argument or a commit
@@ -453,6 +485,83 @@ test_expect_success '"git bisect run" simple case' '
485 git bisect reset
486 '
487
488 +test_expect_success '"git bisect start --reset-when-found" defaults to original' '
489 + test_when_finished "git bisect reset; git checkout main" &&
490 + git checkout main &&
491 + bisect_start_and_finish --reset-when-found &&
492 + test "$HASH4" = "$(git rev-parse HEAD)" &&
493 + test main = "$(git branch --show-current)" &&
494 + test_bisect_state_missing BISECT_START &&
495 +
496 + bisect_start_and_finish --reset-when-found=original &&
497 + test "$HASH4" = "$(git rev-parse HEAD)" &&
498 + test main = "$(git branch --show-current)" &&
499 + test_bisect_state_missing BISECT_START
500 +'
501 +
502 +test_expect_success '"git bisect start --reset-when-found=found" leaves first bad checked out' '
503 + test_when_finished "git bisect reset; git checkout main" &&
504 + bisect_start_and_finish --reset-when-found=found &&
505 + test "$HASH3" = "$(git rev-parse HEAD)" &&
506 + test_bisect_state_missing BISECT_START
507 +'
508 +
509 +test_expect_success '"git bisect run --reset-when-found" defaults to original' '
510 + test_when_finished "git bisect reset; git checkout main" &&
511 + bisect_run_reset_when_found --reset-when-found &&
512 + test "$HASH4" = "$(git rev-parse HEAD)" &&
513 + test main = "$(git branch --show-current)" &&
514 + test_bisect_state_missing BISECT_START
515 +'
516 +
517 +test_expect_success '"git bisect run --reset-when-found=found" leaves first bad checked out' '
518 + test_when_finished "git bisect reset; git checkout main" &&
519 + bisect_run_reset_when_found --reset-when-found=found &&
520 + test "$HASH3" = "$(git rev-parse HEAD)" &&
521 + test_bisect_state_missing BISECT_START
522 +'
523 +
524 +test_expect_success '--reset-when-found rejects an unknown reset target' '
525 + test_when_finished "git bisect reset; git checkout main" &&
526 + test_reset_when_found_fails \
527 + "invalid value for.*--reset-when-found.*unknown" BISECT_START \
528 + git bisect start --reset-when-found=unknown $HASH4 $HASH2 &&
529 +
530 + git bisect start $HASH4 $HASH2 &&
531 + test_reset_when_found_fails \
532 + "invalid value for.*--reset-when-found.*unknown" \
533 + BISECT_RESET_WHEN_FOUND \
534 + git bisect run --reset-when-found=unknown true
535 +'
536 +
537 +test_expect_success '--reset-when-found cannot be used with --no-checkout' '
538 + test_when_finished "git bisect reset" &&
539 + test_reset_when_found_fails \
540 + "cannot be used with.*--no-checkout" BISECT_START \
541 + git bisect start --reset-when-found=original --no-checkout $HASH4 $HASH2 &&
542 +
543 + git bisect start --no-checkout $HASH4 $HASH2 &&
544 + test_reset_when_found_fails \
545 + "cannot be used with.*--no-checkout" BISECT_RESET_WHEN_FOUND \
546 + git bisect run --reset-when-found=found true
547 +'
548 +
549 +test_expect_success 'without --reset-when-found the bisection state is kept' '
550 + test_when_finished "git bisect reset" &&
551 + git bisect start $HASH4 $HASH2 &&
552 + git bisect bad &&
553 + test_bisect_state_file BISECT_START
554 +'
555 +
556 +test_expect_success '--reset-when-found does not leak into a later bisection' '
557 + test_when_finished "git bisect reset; git checkout main" &&
558 + bisect_start_and_finish --reset-when-found &&
559 +
560 + git bisect start $HASH4 $HASH2 &&
561 + git bisect bad &&
562 + test_bisect_state_file BISECT_START
563 +'
564 +
565 # We want to automatically find the commit that
566 # added "Ciao" into hello.
567 test_expect_success '"git bisect run" with more complex "git bisect start"' '