Skip to content

Commit

Permalink
bisect: add --force flag to force checkout
Browse files Browse the repository at this point in the history
Adds a `--force`/`-f` flag to `git bisect good/bad` and `git bisect run` to
force a checkout. Currently, if the repository state adds any local changes
the user must manually reset the repository state before moving to the next
bisection step. This can happen with package lock files or log output data,
for example. With this change, a developer can run `git bisect run --force`
to automatically reset the repository state after each evaluation. The flag
is also supported as `git bisect (good|bad) --force` as well.

Signed-off-by: Kevin Wang <[email protected]>
  • Loading branch information
kevmo314 committed Jan 15, 2024
1 parent a26002b commit 4723911
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 26 deletions.
13 changes: 11 additions & 2 deletions Documentation/git-bisect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ on the subcommand:
git bisect start [--term-(new|bad)=<term-new> --term-(old|good)=<term-old>]
[--no-checkout] [--first-parent] [<bad> [<good>...]] [--] [<paths>...]
git bisect (bad|new|<term-new>) [<rev>]
git bisect (good|old|<term-old>) [<rev>...]
git bisect (good|old|<term-old>) [-f] [<rev>...]
git bisect terms [--term-good | --term-bad]
git bisect skip [(<rev>|<range>)...]
git bisect reset [<commit>]
git bisect (visualize|view)
git bisect replay <logfile>
git bisect log
git bisect run <cmd> [<arg>...]
git bisect run [-f] <cmd> [<arg>...]
git bisect help

This command uses a binary search algorithm to find which commit in
Expand Down Expand Up @@ -381,6 +381,15 @@ 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.

-f::
--force::
+
Throw away any local changes and untracked files before moving to the next
bisection step.
+
This option may be useful if the repository state changes when testing a
revision.

EXAMPLES
--------

Expand Down
17 changes: 11 additions & 6 deletions bisect.c
Original file line number Diff line number Diff line change
Expand Up @@ -713,7 +713,7 @@ static int is_expected_rev(const struct object_id *oid)
}

enum bisect_error bisect_checkout(const struct object_id *bisect_rev,
int no_checkout)
int no_checkout, int force_checkout)
{
struct commit *commit;
struct pretty_print_context pp = {0};
Expand All @@ -728,8 +728,13 @@ enum bisect_error bisect_checkout(const struct object_id *bisect_rev,
struct child_process cmd = CHILD_PROCESS_INIT;

cmd.git_cmd = 1;
strvec_pushl(&cmd.args, "checkout", "-q",
oid_to_hex(bisect_rev), "--", NULL);
if (force_checkout) {
strvec_pushl(&cmd.args, "checkout", "-f", "-q",
oid_to_hex(bisect_rev), "--", NULL);
} else {
strvec_pushl(&cmd.args, "checkout", "-q",
oid_to_hex(bisect_rev), "--", NULL);
}
if (run_command(&cmd))
/*
* Errors in `run_command()` itself, signaled by res < 0,
Expand Down Expand Up @@ -850,7 +855,7 @@ static enum bisect_error check_merge_bases(int rev_nr, struct commit **rev, int
handle_skipped_merge_base(mb);
} else {
printf(_("Bisecting: a merge base must be tested\n"));
res = bisect_checkout(mb, no_checkout);
res = bisect_checkout(mb, no_checkout, 0);
if (!res)
/* indicate early success */
res = BISECT_INTERNAL_SUCCESS_MERGE_BASE;
Expand Down Expand Up @@ -1002,7 +1007,7 @@ void read_bisect_terms(const char **read_bad, const char **read_good)
* the end of bisect_helper::cmd_bisect__helper() helps bypassing
* all the code related to finding a commit to test.
*/
enum bisect_error bisect_next_all(struct repository *r, const char *prefix)
enum bisect_error bisect_next_all(struct repository *r, const char *prefix, int force_checkout)
{
struct strvec rev_argv = STRVEC_INIT;
struct rev_info revs = REV_INFO_INIT;
Expand Down Expand Up @@ -1104,7 +1109,7 @@ enum bisect_error bisect_next_all(struct repository *r, const char *prefix)
/* Clean up objects used, as they will be reused. */
repo_clear_commit_marks(r, ALL_REV_FLAGS);

res = bisect_checkout(bisect_rev, no_checkout);
res = bisect_checkout(bisect_rev, no_checkout, force_checkout);
cleanup:
release_revisions(&revs);
strvec_clear(&rev_argv);
Expand Down
4 changes: 2 additions & 2 deletions bisect.h
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ struct bisect_state {
unsigned int nr_bad;
};

enum bisect_error bisect_next_all(struct repository *r, const char *prefix);
enum bisect_error bisect_next_all(struct repository *r, const char *prefix, int force_checkout);

int estimate_bisect_steps(int all);

Expand All @@ -80,6 +80,6 @@ void read_bisect_terms(const char **bad, const char **good);
int bisect_clean_state(void);

enum bisect_error bisect_checkout(const struct object_id *bisect_rev,
int no_checkout);
int no_checkout, int force_checkout);

#endif
50 changes: 34 additions & 16 deletions builtin/bisect.c
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ static GIT_PATH_FUNC(git_path_bisect_run, "BISECT_RUN")
" [--no-checkout] [--first-parent] [<bad> [<good>...]] [--]" \
" [<pathspec>...]")
#define BUILTIN_GIT_BISECT_STATE_USAGE \
N_("git bisect (good|bad) [<rev>...]")
N_("git bisect (good|bad) [-f] [<rev>...]")
#define BUILTIN_GIT_BISECT_TERMS_USAGE \
"git bisect terms [--term-good | --term-bad]"
#define BUILTIN_GIT_BISECT_SKIP_USAGE \
Expand All @@ -45,7 +45,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 <cmd> [<arg>...]")
N_("git bisect run [-f] <cmd> [<arg>...]")

static const char * const git_bisect_usage[] = {
BUILTIN_GIT_BISECT_START_USAGE,
Expand Down Expand Up @@ -651,7 +651,7 @@ 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, int force_checkout)
{
enum bisect_error res;

Expand All @@ -662,7 +662,7 @@ static enum bisect_error bisect_next(struct bisect_terms *terms, const char *pre
return BISECT_FAILED;

/* Perform all bisection computation */
res = bisect_next_all(the_repository, prefix);
res = bisect_next_all(the_repository, prefix, force_checkout);

if (res == BISECT_INTERNAL_SUCCESS_1ST_BAD_FOUND) {
res = bisect_successful(terms);
Expand All @@ -674,14 +674,14 @@ 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, int force_checkout)
{
if (bisect_next_check(terms, NULL)) {
bisect_print_status(terms);
return BISECT_OK;
}

return bisect_next(terms, prefix);
return bisect_next(terms, prefix, force_checkout);
}

static enum bisect_error bisect_start(struct bisect_terms *terms, int argc,
Expand Down Expand Up @@ -875,7 +875,7 @@ static enum bisect_error bisect_start(struct bisect_terms *terms, int argc,
if (res)
return res;

res = bisect_auto_next(terms, NULL);
res = bisect_auto_next(terms, NULL, 0);
if (!is_bisect_success(res))
bisect_clean_state();
return res;
Expand Down Expand Up @@ -917,7 +917,7 @@ static enum bisect_error bisect_state(struct bisect_terms *terms, int argc,
const char **argv)
{
const char *state;
int i, verify_expected = 1;
int i, force_checkout = 0, verify_expected = 1;
struct object_id oid, expected;
struct oid_array revs = OID_ARRAY_INIT;

Expand All @@ -934,6 +934,13 @@ static enum bisect_error bisect_state(struct bisect_terms *terms, int argc,

argv++;
argc--;

if (argc > 0 && (!strcmp(argv[0], "--force") || !strcmp(argv[0], "-f"))) {
force_checkout = 1;
argv++;
argc--;
}

if (argc > 1 && !strcmp(state, terms->term_bad))
return error(_("'git bisect %s' can take only one argument."), terms->term_bad);

Expand Down Expand Up @@ -989,7 +996,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, force_checkout);
}

static enum bisect_error bisect_log(void)
Expand Down Expand Up @@ -1078,7 +1085,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, 0);
}

static enum bisect_error bisect_skip(struct bisect_terms *terms, int argc,
Expand Down Expand Up @@ -1173,7 +1180,7 @@ static int do_bisect_run(const char *command)
return run_command(&cmd);
}

static int verify_good(const struct bisect_terms *terms, const char *command)
static int verify_good(const struct bisect_terms *terms, const char *command, int force_checkout)
{
int rc;
enum bisect_error res;
Expand All @@ -1189,13 +1196,13 @@ static int verify_good(const struct bisect_terms *terms, const char *command)
if (read_ref(no_checkout ? "BISECT_HEAD" : "HEAD", &current_rev))
return -1;

res = bisect_checkout(&good_rev, no_checkout);
res = bisect_checkout(&good_rev, no_checkout, force_checkout);
if (res != BISECT_OK)
return -1;

rc = do_bisect_run(command);

res = bisect_checkout(&current_rev, no_checkout);
res = bisect_checkout(&current_rev, no_checkout, force_checkout);
if (res != BISECT_OK)
return -1;

Expand All @@ -1209,6 +1216,7 @@ static int bisect_run(struct bisect_terms *terms, int argc, const char **argv)
const char *new_state;
int temporary_stdout_fd, saved_stdout;
int is_first_run = 1;
int force_checkout = 0;

if (bisect_next_check(terms, NULL))
return BISECT_FAILED;
Expand All @@ -1218,8 +1226,14 @@ static int bisect_run(struct bisect_terms *terms, int argc, const char **argv)
return BISECT_FAILED;
}

if (argc > 0 && (!strcmp(argv[0], "--force") || !strcmp(argv[0], "-f"))) {
force_checkout = 1;
argv++;
argc--;
}
sq_quote_argv(&command, argv);
strbuf_ltrim(&command);

while (1) {
res = do_bisect_run(command.buf);

Expand All @@ -1231,7 +1245,7 @@ static int bisect_run(struct bisect_terms *terms, int argc, const char **argv)
* missing or non-executable script.
*/
if (is_first_run && (res == 126 || res == 127)) {
int rc = verify_good(terms, command.buf);
int rc = verify_good(terms, command.buf, force_checkout);
is_first_run = 0;
if (rc < 0 || 128 <= rc) {
error(_("unable to verify %s on good"
Expand Down Expand Up @@ -1271,7 +1285,11 @@ 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);
if (force_checkout) {
res = bisect_state(terms, 2, (const char *[]){ new_state, "--force" });
} else {
res = bisect_state(terms, 1, &new_state);
}

fflush(stdout);
dup2(saved_stdout, 1);
Expand Down Expand Up @@ -1342,7 +1360,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, 0);
free_terms(&terms);
return res;
}
Expand Down
109 changes: 109 additions & 0 deletions t/t6030-bisect-porcelain.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1259,4 +1259,113 @@ test_expect_success 'verify correct error message' '
grep "git bisect good.*exited with error code" error
'

test_expect_success 'bisect dirty, explicit ref' '
test_when_finished "git bisect reset" &&
add_line_into_file "Commit 1" file &&
add_line_into_file "Commit 2" file &&
add_line_into_file "Commit 3" file &&
git bisect start &&
git bisect bad HEAD &&
echo "modified state" >file &&
test_when_finished "git checkout -- file" &&
test_must_fail git bisect good HEAD~2
'

test_expect_success 'bisect dirty good force, explicit ref' '
test_when_finished "git bisect reset" &&
add_line_into_file "Commit 1" file &&
add_line_into_file "Commit 2" file &&
add_line_into_file "Commit 3" file &&
git bisect start &&
git bisect bad HEAD &&
echo "modified state" >file &&
test_when_finished "git checkout -- file" &&
git bisect good --force HEAD~2
'

test_expect_success 'bisect dirty, implicit ref' '
test_when_finished "git bisect reset" &&
add_line_into_file "Commit 1" file &&
add_line_into_file "Commit 2" file &&
add_line_into_file "Commit 3" file &&
add_line_into_file "Commit 4" file &&
add_line_into_file "Commit 5" file &&
git bisect start &&
git bisect bad HEAD &&
git bisect good HEAD~4 &&
echo "modified state" >file &&
test_when_finished "git checkout -- file" &&
test_must_fail git bisect good &&
test_must_fail git bisect bad
'

test_expect_success 'bisect dirty good force, implicit ref' '
test_when_finished "git bisect reset" &&
add_line_into_file "Commit 1" file &&
add_line_into_file "Commit 2" file &&
add_line_into_file "Commit 3" file &&
add_line_into_file "Commit 4" file &&
add_line_into_file "Commit 5" file &&
git bisect start &&
git bisect bad HEAD &&
git bisect good HEAD~4 &&
echo "modified state" >file &&
test_when_finished "git checkout -- file" &&
git bisect good --force
'

test_expect_success 'bisect dirty bad force, implicit ref' '
test_when_finished "git bisect reset" &&
add_line_into_file "Commit 1" file &&
add_line_into_file "Commit 2" file &&
add_line_into_file "Commit 3" file &&
add_line_into_file "Commit 4" file &&
add_line_into_file "Commit 5" file &&
git bisect start &&
git bisect bad HEAD &&
git bisect good HEAD~4 &&
echo "modified state" >file &&
test_when_finished "git checkout -- file" &&
git bisect bad --force
'

test_expect_success 'bisect run dirty' '
test_when_finished "git bisect reset" &&
test_when_finished "git checkout -- file" &&
add_line_into_file "Bisect run 1" file &&
add_line_into_file "Bisect run 2" file &&
add_line_into_file "Bisect run 3" file &&
add_line_into_file "Bisect run 4" file &&
add_line_into_file "Bisect run 5" file &&
write_script test_script.sh <<-\EOF &&
! echo "modified state" >file
! grep "Bisect run 3" || exit 126 >/dev/null
EOF
git bisect start &&
git bisect bad HEAD &&
git bisect good HEAD~4 &&
test_must_fail git bisect run ./test_script.sh
'

test_expect_success 'bisect run dirty force' '
test_when_finished "git bisect reset" &&
test_when_finished "git checkout -- file" &&
add_line_into_file "Bisect run 1" file &&
add_line_into_file "Bisect run 2" file &&
add_line_into_file "Bisect run 3" file &&
add_line_into_file "Bisect run 4" file &&
add_line_into_file "Bisect run 5" file &&
write_script test_script.sh <<-\EOF &&
! echo "modified state" >file
! grep "Bisect run 3" || exit 126 >/dev/null
EOF
HASH5=$(git rev-parse --verify HEAD) &&
git bisect start &&
git bisect bad HEAD &&
git bisect good HEAD~4 &&
echo "doing run\n" &&
git bisect run --force ./test_script.sh >my_bisect_log.txt &&
grep "$HASH5 is the first bad commit" my_bisect_log.txt
'

test_done

0 comments on commit 4723911

Please sign in to comment.