diff --git a/Documentation/git-bisect.adoc b/Documentation/git-bisect.adoc index d2115b2990..aabddd42ca 100644 --- a/Documentation/git-bisect.adoc +++ b/Documentation/git-bisect.adoc @@ -10,7 +10,7 @@ SYNOPSIS -------- [synopsis] git bisect start [--term-(bad|new)= --term-(good|old)=] - [--no-checkout] [--first-parent] [ [...]] [--] [...] + [--no-checkout] [--first-parent] [--reset-when-found[=]] [ [...]] [--] [...] git bisect (bad|new|) [] git bisect (good|old|) [...] git bisect terms [--term-(good|old) | --term-(bad|new)] @@ -20,7 +20,7 @@ git bisect reset [] git bisect (visualize|view) git bisect replay git bisect log -git bisect run [...] +git bisect run [--reset-when-found[=]] [...] git bisect help DESCRIPTION @@ -385,6 +385,16 @@ ignored. This option is particularly useful in avoiding false positives when a merged branch contained broken or non-buildable commits, but the merge itself was OK. +`--reset-when-found[=]`:: + Once the first bad commit is found, report it and clean up the + bisection state. `` may be `original` to return to the commit + checked out before `git bisect start`, or `found` to leave the first + bad commit checked out. If `` is omitted, it defaults to + `original`. ++ +This option may be given to `git bisect start` or to `git bisect run`. It +cannot be used for a bisection started with `--no-checkout`. + EXAMPLES -------- diff --git a/bisect.c b/bisect.c index 94c7028d2a..d426fcd5a9 100644 --- a/bisect.c +++ b/bisect.c @@ -488,6 +488,7 @@ static GIT_PATH_FUNC(git_path_bisect_start, "BISECT_START") static GIT_PATH_FUNC(git_path_bisect_log, "BISECT_LOG") static GIT_PATH_FUNC(git_path_bisect_terms, "BISECT_TERMS") static GIT_PATH_FUNC(git_path_bisect_first_parent, "BISECT_FIRST_PARENT") +static GIT_PATH_FUNC(git_path_bisect_reset_when_found, "BISECT_RESET_WHEN_FOUND") static void read_bisect_paths(struct strvec *array) { @@ -1211,6 +1212,7 @@ int bisect_clean_state(void) unlink_or_warn(git_path_bisect_run()); unlink_or_warn(git_path_bisect_terms()); unlink_or_warn(git_path_bisect_first_parent()); + unlink_or_warn(git_path_bisect_reset_when_found()); /* * Cleanup BISECT_START last to support the --no-checkout option * introduced in the commit 4796e823a. diff --git a/builtin/bisect.c b/builtin/bisect.c index 3264e2da54..70b55d0947 100644 --- a/builtin/bisect.c +++ b/builtin/bisect.c @@ -24,11 +24,12 @@ static GIT_PATH_FUNC(git_path_bisect_start, "BISECT_START") static GIT_PATH_FUNC(git_path_bisect_log, "BISECT_LOG") static GIT_PATH_FUNC(git_path_bisect_names, "BISECT_NAMES") static GIT_PATH_FUNC(git_path_bisect_first_parent, "BISECT_FIRST_PARENT") +static GIT_PATH_FUNC(git_path_bisect_reset_when_found, "BISECT_RESET_WHEN_FOUND") static GIT_PATH_FUNC(git_path_bisect_run, "BISECT_RUN") #define BUILTIN_GIT_BISECT_START_USAGE \ N_("git bisect start [--term-(bad|new)= --term-(good|old)=]\n" \ - " [--no-checkout] [--first-parent] [ [...]] [--] [...]") + " [--no-checkout] [--first-parent] [--reset-when-found[=]] [ [...]] [--] [...]") #define BUILTIN_GIT_BISECT_BAD_USAGE \ N_("git bisect (bad|new|) []") #define BUILTIN_GIT_BISECT_GOOD_USAGE \ @@ -48,7 +49,7 @@ static GIT_PATH_FUNC(git_path_bisect_run, "BISECT_RUN") #define BUILTIN_GIT_BISECT_LOG_USAGE \ "git bisect log" #define BUILTIN_GIT_BISECT_RUN_USAGE \ - N_("git bisect run [...]") + N_("git bisect run [--reset-when-found[=]] [...]") #define BUILTIN_GIT_BISECT_HELP_USAGE \ "git bisect help" @@ -68,6 +69,12 @@ static const char * const git_bisect_usage[] = { NULL }; +enum reset_when_found_mode { + RESET_WHEN_FOUND_NONE, + RESET_WHEN_FOUND_TO_ORIGINAL, + RESET_WHEN_FOUND_TO_FOUND, +}; + struct add_bisect_ref_data { struct rev_info *revs; unsigned int object_flags; @@ -234,7 +241,7 @@ static int write_terms(const char *bad, const char *good) return res; } -static int bisect_reset(const char *commit) +static int bisect_reset(const char *commit, int quiet) { struct strbuf branch = STRBUF_INIT; @@ -255,8 +262,10 @@ static int bisect_reset(const char *commit) struct child_process cmd = CHILD_PROCESS_INIT; cmd.git_cmd = 1; - strvec_pushl(&cmd.args, "checkout", "--ignore-other-worktrees", - branch.buf, "--", NULL); + strvec_pushl(&cmd.args, "checkout", "--ignore-other-worktrees", NULL); + if (quiet) + strvec_push(&cmd.args, "--quiet"); + strvec_pushl(&cmd.args, branch.buf, "--", NULL); if (run_command(&cmd)) { error(_("could not check out original" " HEAD '%s'. Try 'git bisect" @@ -270,6 +279,61 @@ static int bisect_reset(const char *commit) return bisect_clean_state(); } +static int parse_reset_when_found(const char *value, + enum reset_when_found_mode *mode) +{ + if (!strcmp(value, "original")) + *mode = RESET_WHEN_FOUND_TO_ORIGINAL; + else if (!strcmp(value, "found")) + *mode = RESET_WHEN_FOUND_TO_FOUND; + else + return error(_("invalid value for '--reset-when-found': '%s'"), + value); + + return 0; +} + +static const char *reset_when_found_mode_name(enum reset_when_found_mode mode) +{ + switch (mode) { + case RESET_WHEN_FOUND_TO_ORIGINAL: + return "original"; + case RESET_WHEN_FOUND_TO_FOUND: + return "found"; + case RESET_WHEN_FOUND_NONE: + BUG("no name for unset reset-when-found mode"); + } + BUG("unknown reset-when-found mode %d", mode); +} + +static int bisect_reset_when_found(struct bisect_terms *terms) +{ + struct strbuf value = STRBUF_INIT; + enum reset_when_found_mode mode; + char *commit = NULL; + int res; + + if (strbuf_read_file(&value, git_path_bisect_reset_when_found(), 0) < 0) { + res = error_errno(_("could not read '%s'"), + git_path_bisect_reset_when_found()); + goto cleanup; + } + strbuf_trim(&value); + if (parse_reset_when_found(value.buf, &mode)) { + res = -1; + goto cleanup; + } + + if (mode == RESET_WHEN_FOUND_TO_FOUND) + commit = xstrfmt("refs/bisect/%s", terms->term_bad); + res = bisect_reset(commit, 1); + +cleanup: + free(commit); + strbuf_release(&value); + return res; +} + static void log_commit(FILE *fp, const char *fmt, const char *state, struct commit *commit) @@ -680,7 +744,8 @@ static int bisect_successful(struct bisect_terms *terms) return res; } -static enum bisect_error bisect_next(struct bisect_terms *terms, const char *prefix) +static enum bisect_error bisect_next(struct bisect_terms *terms, + const char *prefix, bool defer_reset) { enum bisect_error res; @@ -695,6 +760,9 @@ static enum bisect_error bisect_next(struct bisect_terms *terms, const char *pre if (res == BISECT_INTERNAL_SUCCESS_1ST_BAD_FOUND) { res = bisect_successful(terms); + if (!res && !defer_reset && + !is_empty_or_missing_file(git_path_bisect_reset_when_found())) + res = bisect_reset_when_found(terms); return res ? res : BISECT_INTERNAL_SUCCESS_1ST_BAD_FOUND; } else if (res == BISECT_ONLY_SKIPPED_LEFT) { res = bisect_skipped_commits(terms); @@ -703,14 +771,15 @@ static enum bisect_error bisect_next(struct bisect_terms *terms, const char *pre return res; } -static enum bisect_error bisect_auto_next(struct bisect_terms *terms, const char *prefix) +static enum bisect_error bisect_auto_next(struct bisect_terms *terms, + const char *prefix, bool defer_reset) { if (bisect_next_check(terms, NULL)) { bisect_print_status(terms); return BISECT_OK; } - return bisect_next(terms, prefix); + return bisect_next(terms, prefix, defer_reset); } static enum bisect_error bisect_start(struct bisect_terms *terms, int argc, @@ -718,6 +787,7 @@ static enum bisect_error bisect_start(struct bisect_terms *terms, int argc, { int no_checkout = 0; int first_parent_only = 0; + enum reset_when_found_mode reset_when_found = RESET_WHEN_FOUND_NONE; int i, has_double_dash = 0, must_write_terms = 0, bad_seen = 0; int flags, pathspec_pos; enum bisect_error res = BISECT_OK; @@ -750,6 +820,13 @@ static enum bisect_error bisect_start(struct bisect_terms *terms, int argc, no_checkout = 1; } else if (!strcmp(arg, "--first-parent")) { first_parent_only = 1; + } else if (!strcmp(arg, "--reset-when-found")) { + reset_when_found = RESET_WHEN_FOUND_TO_ORIGINAL; + } else if (skip_prefix(arg, "--reset-when-found=", &arg)) { + if (parse_reset_when_found(arg, &reset_when_found)) { + res = BISECT_FAILED; + goto finish; + } } else if (!strcmp(arg, "--term-good") || !strcmp(arg, "--term-old")) { i++; @@ -787,6 +864,10 @@ static enum bisect_error bisect_start(struct bisect_terms *terms, int argc, break; } } + if (reset_when_found != RESET_WHEN_FOUND_NONE && no_checkout) { + res = error(_("'--reset-when-found' cannot be used with '--no-checkout'")); + goto finish; + } pathspec_pos = i; /* @@ -866,6 +947,10 @@ static enum bisect_error bisect_start(struct bisect_terms *terms, int argc, if (first_parent_only) write_file(git_path_bisect_first_parent(), "\n"); + if (reset_when_found != RESET_WHEN_FOUND_NONE) + write_file(git_path_bisect_reset_when_found(), "%s\n", + reset_when_found_mode_name(reset_when_found)); + if (no_checkout) { if (repo_get_oid(the_repository, start_head.buf, &oid) < 0) { res = error(_("invalid ref: '%s'"), start_head.buf); @@ -907,7 +992,7 @@ finish: if (res) return res; - res = bisect_auto_next(terms, NULL); + res = bisect_auto_next(terms, NULL, false); if (!is_bisect_success(res)) bisect_clean_state(); return res; @@ -946,7 +1031,7 @@ static int bisect_autostart(struct bisect_terms *terms) } static enum bisect_error bisect_state(struct bisect_terms *terms, int argc, - const char **argv) + const char **argv, bool defer_reset) { const char *state; int i, verify_expected = 1; @@ -1023,7 +1108,7 @@ static enum bisect_error bisect_state(struct bisect_terms *terms, int argc, } oid_array_clear(&revs); - return bisect_auto_next(terms, NULL); + return bisect_auto_next(terms, NULL, defer_reset); } static enum bisect_error bisect_log(void) @@ -1096,7 +1181,7 @@ static enum bisect_error bisect_replay(struct bisect_terms *terms, const char *f if (is_empty_or_missing_file(filename)) return error(_("cannot read file '%s' for replaying"), filename); - if (bisect_reset(NULL)) + if (bisect_reset(NULL, 0)) return BISECT_FAILED; fp = fopen(filename, "r"); @@ -1112,7 +1197,7 @@ static enum bisect_error bisect_replay(struct bisect_terms *terms, const char *f if (res) return BISECT_FAILED; - return bisect_auto_next(terms, NULL); + return bisect_auto_next(terms, NULL, false); } static enum bisect_error bisect_skip(struct bisect_terms *terms, int argc, @@ -1146,7 +1231,7 @@ static enum bisect_error bisect_skip(struct bisect_terms *terms, int argc, strvec_push(&argv_state, argv[i]); } } - res = bisect_state(terms, argv_state.nr, argv_state.v); + res = bisect_state(terms, argv_state.nr, argv_state.v, false); strvec_clear(&argv_state); return res; @@ -1244,6 +1329,8 @@ static int bisect_run(struct bisect_terms *terms, int argc, const char **argv) { int res = BISECT_OK; struct strbuf command = STRBUF_INIT; + enum reset_when_found_mode reset_when_found = RESET_WHEN_FOUND_NONE; + const char *reset_when_found_arg; const char *new_state; int temporary_stdout_fd, saved_stdout; int is_first_run = 1; @@ -1251,6 +1338,23 @@ static int bisect_run(struct bisect_terms *terms, int argc, const char **argv) if (bisect_next_check(terms, NULL)) return BISECT_FAILED; + if (argc && !strcmp(argv[0], "--reset-when-found")) + reset_when_found = RESET_WHEN_FOUND_TO_ORIGINAL; + else if (argc && skip_prefix(argv[0], "--reset-when-found=", + &reset_when_found_arg)) { + if (parse_reset_when_found(reset_when_found_arg, &reset_when_found)) + return BISECT_FAILED; + } + + if (reset_when_found != RESET_WHEN_FOUND_NONE) { + if (refs_ref_exists(get_main_ref_store(the_repository), "BISECT_HEAD")) + return error(_("'--reset-when-found' cannot be used with '--no-checkout'")); + write_file(git_path_bisect_reset_when_found(), "%s\n", + reset_when_found_mode_name(reset_when_found)); + argc--; + argv++; + } + if (!argc) { error(_("bisect run failed: no command provided.")); return BISECT_FAILED; @@ -1309,7 +1413,7 @@ static int bisect_run(struct bisect_terms *terms, int argc, const char **argv) saved_stdout = dup(1); dup2(temporary_stdout_fd, 1); - res = bisect_state(terms, 1, &new_state); + res = bisect_state(terms, 1, &new_state, true); fflush(stdout); dup2(saved_stdout, 1); @@ -1325,7 +1429,11 @@ static int bisect_run(struct bisect_terms *terms, int argc, const char **argv) res = BISECT_OK; } else if (res == BISECT_INTERNAL_SUCCESS_1ST_BAD_FOUND) { printf(_("bisect found first '%s' commit\n"), terms->term_bad); - res = BISECT_OK; + if (!is_empty_or_missing_file(git_path_bisect_reset_when_found()) && + bisect_reset_when_found(terms)) + res = BISECT_FAILED; + else + res = BISECT_OK; } else if (res) { error(_("bisect run failed: 'git bisect %s'" " exited with error code %d"), new_state, res); @@ -1345,7 +1453,7 @@ static int cmd_bisect__reset(int argc, const char **argv, const char *prefix UNU if (argc > 1) return error(_("'%s' requires either no argument or a commit"), "git bisect reset"); - return bisect_reset(argc ? argv[0] : NULL); + return bisect_reset(argc ? argv[0] : NULL, 0); } static int cmd_bisect__terms(int argc, const char **argv, const char *prefix UNUSED, @@ -1384,7 +1492,7 @@ static int cmd_bisect__next(int argc, const char **argv UNUSED, const char *pref return error(_("'%s' requires 0 arguments"), "git bisect next"); get_terms(&terms); - res = bisect_next(&terms, prefix); + res = bisect_next(&terms, prefix, false); free_terms(&terms); return res; } @@ -1487,7 +1595,7 @@ int cmd_bisect(int argc, !one_of(argv[0], terms.term_good, terms.term_bad, NULL)) usage_msg_optf(_("unknown command: '%s'"), git_bisect_usage, options, argv[0]); - res = bisect_state(&terms, argc, argv); + res = bisect_state(&terms, argc, argv, false); free_terms(&terms); } else { argc--; diff --git a/t/t6030-bisect-porcelain.sh b/t/t6030-bisect-porcelain.sh index 338df4f886..921d9e7c05 100755 --- a/t/t6030-bisect-porcelain.sh +++ b/t/t6030-bisect-porcelain.sh @@ -43,6 +43,38 @@ test_bisect_usage () { test_cmp expect actual } +test_bisect_state_file () { + test_path_is_file "$(git rev-parse --git-path "$1")" +} + +test_bisect_state_missing () { + test_path_is_missing "$(git rev-parse --git-path "$1")" +} + +bisect_start_and_finish () { + git bisect start "$1" $HASH4 $HASH2 && + git bisect bad +} + +bisect_run_reset_when_found () { + write_script test_script.sh <<-\EOF && + ! grep Another hello >/dev/null + EOF + git bisect start $HASH4 $HASH2 && + git bisect run "$1" ./test_script.sh >my_bisect_log.txt && + test_grep "$HASH3 is the first .bad. commit" my_bisect_log.txt && + test_bisect_state_missing BISECT_RUN +} + +test_reset_when_found_fails () { + local pattern="$1" && + local state_file="$2" && + shift 2 && + test_must_fail "$@" 2>err && + test_grep -- "$pattern" err && + test_bisect_state_missing "$state_file" +} + test_expect_success 'bisect usage' " test_bisect_usage 1 git bisect reset extra1 extra2 <<-\EOF && error: 'git bisect reset' requires either no argument or a commit @@ -453,6 +485,83 @@ test_expect_success '"git bisect run" simple case' ' git bisect reset ' +test_expect_success '"git bisect start --reset-when-found" defaults to original' ' + test_when_finished "git bisect reset; git checkout main" && + git checkout main && + bisect_start_and_finish --reset-when-found && + test "$HASH4" = "$(git rev-parse HEAD)" && + test main = "$(git branch --show-current)" && + test_bisect_state_missing BISECT_START && + + bisect_start_and_finish --reset-when-found=original && + test "$HASH4" = "$(git rev-parse HEAD)" && + test main = "$(git branch --show-current)" && + test_bisect_state_missing BISECT_START +' + +test_expect_success '"git bisect start --reset-when-found=found" leaves first bad checked out' ' + test_when_finished "git bisect reset; git checkout main" && + bisect_start_and_finish --reset-when-found=found && + test "$HASH3" = "$(git rev-parse HEAD)" && + test_bisect_state_missing BISECT_START +' + +test_expect_success '"git bisect run --reset-when-found" defaults to original' ' + test_when_finished "git bisect reset; git checkout main" && + bisect_run_reset_when_found --reset-when-found && + test "$HASH4" = "$(git rev-parse HEAD)" && + test main = "$(git branch --show-current)" && + test_bisect_state_missing BISECT_START +' + +test_expect_success '"git bisect run --reset-when-found=found" leaves first bad checked out' ' + test_when_finished "git bisect reset; git checkout main" && + bisect_run_reset_when_found --reset-when-found=found && + test "$HASH3" = "$(git rev-parse HEAD)" && + test_bisect_state_missing BISECT_START +' + +test_expect_success '--reset-when-found rejects an unknown reset target' ' + test_when_finished "git bisect reset; git checkout main" && + test_reset_when_found_fails \ + "invalid value for.*--reset-when-found.*unknown" BISECT_START \ + git bisect start --reset-when-found=unknown $HASH4 $HASH2 && + + git bisect start $HASH4 $HASH2 && + test_reset_when_found_fails \ + "invalid value for.*--reset-when-found.*unknown" \ + BISECT_RESET_WHEN_FOUND \ + git bisect run --reset-when-found=unknown true +' + +test_expect_success '--reset-when-found cannot be used with --no-checkout' ' + test_when_finished "git bisect reset" && + test_reset_when_found_fails \ + "cannot be used with.*--no-checkout" BISECT_START \ + git bisect start --reset-when-found=original --no-checkout $HASH4 $HASH2 && + + git bisect start --no-checkout $HASH4 $HASH2 && + test_reset_when_found_fails \ + "cannot be used with.*--no-checkout" BISECT_RESET_WHEN_FOUND \ + git bisect run --reset-when-found=found true +' + +test_expect_success 'without --reset-when-found the bisection state is kept' ' + test_when_finished "git bisect reset" && + git bisect start $HASH4 $HASH2 && + git bisect bad && + test_bisect_state_file BISECT_START +' + +test_expect_success '--reset-when-found does not leak into a later bisection' ' + test_when_finished "git bisect reset; git checkout main" && + bisect_start_and_finish --reset-when-found && + + git bisect start $HASH4 $HASH2 && + git bisect bad && + test_bisect_state_file BISECT_START +' + # We want to automatically find the commit that # added "Ciao" into hello. test_expect_success '"git bisect run" with more complex "git bisect start"' '