diff --git a/builtin/bisect--helper.c b/builtin/bisect--helper.c index f184eaeac6..bc210b23c8 100644 --- a/builtin/bisect--helper.c +++ b/builtin/bisect--helper.c @@ -18,10 +18,10 @@ static GIT_PATH_FUNC(git_path_bisect_log, "BISECT_LOG") static GIT_PATH_FUNC(git_path_head_name, "head-name") 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_run, "BISECT_RUN") static const char * const git_bisect_helper_usage[] = { N_("git bisect--helper --bisect-reset []"), - N_("git bisect--helper --bisect-next-check []"), N_("git bisect--helper --bisect-terms [--term-good | --term-old | --term-bad | --term-new]"), N_("git bisect--helper --bisect-start [--term-{new,bad}= --term-{old,good}=]" " [--no-checkout] [--first-parent] [ [...]] [--] [...]"), @@ -30,6 +30,8 @@ static const char * const git_bisect_helper_usage[] = { N_("git bisect--helper --bisect-state (good|old) [...]"), N_("git bisect--helper --bisect-replay "), N_("git bisect--helper --bisect-skip [(|)...]"), + N_("git bisect--helper --bisect-visualize"), + N_("git bisect--helper --bisect-run ..."), NULL }; @@ -143,6 +145,19 @@ static int append_to_file(const char *path, const char *format, ...) return res; } +static int print_file_to_stdout(const char *path) +{ + int fd = open(path, O_RDONLY); + int ret = 0; + + if (fd < 0) + return error_errno(_("cannot open file '%s' for reading"), path); + if (copy_fd(fd, 1) < 0) + ret = error_errno(_("failed to read '%s'"), path); + close(fd); + return ret; +} + static int check_term_format(const char *term, const char *orig_term) { int res; @@ -1036,6 +1051,125 @@ static enum bisect_error bisect_skip(struct bisect_terms *terms, const char **ar return res; } +static int bisect_visualize(struct bisect_terms *terms, const char **argv, int argc) +{ + struct strvec args = STRVEC_INIT; + int flags = RUN_COMMAND_NO_STDIN, res = 0; + struct strbuf sb = STRBUF_INIT; + + if (bisect_next_check(terms, NULL) != 0) + return BISECT_FAILED; + + if (!argc) { + if ((getenv("DISPLAY") || getenv("SESSIONNAME") || getenv("MSYSTEM") || + getenv("SECURITYSESSIONID")) && exists_in_PATH("gitk")) { + strvec_push(&args, "gitk"); + } else { + strvec_push(&args, "log"); + flags |= RUN_GIT_CMD; + } + } else { + if (argv[0][0] == '-') { + strvec_push(&args, "log"); + flags |= RUN_GIT_CMD; + } else if (strcmp(argv[0], "tig") && !starts_with(argv[0], "git")) + flags |= RUN_GIT_CMD; + + strvec_pushv(&args, argv); + } + + strvec_pushl(&args, "--bisect", "--", NULL); + + strbuf_read_file(&sb, git_path_bisect_names(), 0); + sq_dequote_to_strvec(sb.buf, &args); + strbuf_release(&sb); + + res = run_command_v_opt(args.v, flags); + strvec_clear(&args); + return res; +} + +static int bisect_run(struct bisect_terms *terms, const char **argv, int argc) +{ + int res = BISECT_OK; + struct strbuf command = STRBUF_INIT; + struct strvec args = STRVEC_INIT; + struct strvec run_args = STRVEC_INIT; + const char *new_state; + int temporary_stdout_fd, saved_stdout; + + if (bisect_next_check(terms, NULL)) + return BISECT_FAILED; + + if (argc) + sq_quote_argv(&command, argv); + else { + error(_("bisect run failed: no command provided.")); + return BISECT_FAILED; + } + + strvec_push(&run_args, command.buf); + + while (1) { + strvec_clear(&args); + + printf(_("running %s\n"), command.buf); + res = run_command_v_opt(run_args.v, RUN_USING_SHELL); + + if (res < 0 || 128 <= res) { + error(_("bisect run failed: exit code %d from" + " '%s' is < 0 or >= 128"), res, command.buf); + strbuf_release(&command); + return res; + } + + if (res == 125) + new_state = "skip"; + else if (!res) + new_state = terms->term_good; + else + new_state = terms->term_bad; + + temporary_stdout_fd = open(git_path_bisect_run(), O_CREAT | O_WRONLY | O_TRUNC, 0666); + + if (temporary_stdout_fd < 0) + return error_errno(_("cannot open file '%s' for writing"), git_path_bisect_run()); + + fflush(stdout); + saved_stdout = dup(1); + dup2(temporary_stdout_fd, 1); + + res = bisect_state(terms, &new_state, 1); + + fflush(stdout); + dup2(saved_stdout, 1); + close(saved_stdout); + close(temporary_stdout_fd); + + print_file_to_stdout(git_path_bisect_run()); + + if (res == BISECT_ONLY_SKIPPED_LEFT) + error(_("bisect run cannot continue any more")); + else if (res == BISECT_INTERNAL_SUCCESS_MERGE_BASE) { + printf(_("bisect run success")); + res = BISECT_OK; + } else if (res == BISECT_INTERNAL_SUCCESS_1ST_BAD_FOUND) { + printf(_("bisect found first bad commit")); + res = BISECT_OK; + } else if (res) { + error(_("bisect run failed:'git bisect--helper --bisect-state" + " %s' exited with error code %d"), args.v[0], res); + } else { + continue; + } + + strbuf_release(&command); + strvec_clear(&args); + strvec_clear(&run_args); + return res; + } +} + int cmd_bisect__helper(int argc, const char **argv, const char *prefix) { enum { @@ -1048,7 +1182,9 @@ int cmd_bisect__helper(int argc, const char **argv, const char *prefix) BISECT_STATE, BISECT_LOG, BISECT_REPLAY, - BISECT_SKIP + BISECT_SKIP, + BISECT_VISUALIZE, + BISECT_RUN, } cmdmode = 0; int res = 0, nolog = 0; struct option options[] = { @@ -1070,6 +1206,10 @@ int cmd_bisect__helper(int argc, const char **argv, const char *prefix) N_("replay the bisection process from the given file"), BISECT_REPLAY), OPT_CMDMODE(0, "bisect-skip", &cmdmode, N_("skip some commits for checkout"), BISECT_SKIP), + OPT_CMDMODE(0, "bisect-visualize", &cmdmode, + N_("visualize the bisection"), BISECT_VISUALIZE), + OPT_CMDMODE(0, "bisect-run", &cmdmode, + N_("use ... to automatically bisect."), BISECT_RUN), OPT_BOOL(0, "no-log", &nolog, N_("no log for BISECT_WRITE")), OPT_END() @@ -1089,12 +1229,6 @@ int cmd_bisect__helper(int argc, const char **argv, const char *prefix) return error(_("--bisect-reset requires either no argument or a commit")); res = bisect_reset(argc ? argv[0] : NULL); break; - case BISECT_NEXT_CHECK: - if (argc != 2 && argc != 3) - return error(_("--bisect-next-check requires 2 or 3 arguments")); - set_terms(&terms, argv[1], argv[0]); - res = bisect_next_check(&terms, argc == 3 ? argv[2] : NULL); - break; case BISECT_TERMS: if (argc > 1) return error(_("--bisect-terms requires 0 or 1 argument")); @@ -1131,6 +1265,16 @@ int cmd_bisect__helper(int argc, const char **argv, const char *prefix) get_terms(&terms); res = bisect_skip(&terms, argv, argc); break; + case BISECT_VISUALIZE: + get_terms(&terms); + res = bisect_visualize(&terms, argv, argc); + break; + case BISECT_RUN: + if (!argc) + return error(_("bisect run failed: no command provided.")); + get_terms(&terms); + res = bisect_run(&terms, argv, argc); + break; default: BUG("unknown subcommand %d", cmdmode); } diff --git a/git-bisect.sh b/git-bisect.sh index b59f3aaad4..405cf76f2a 100755 --- a/git-bisect.sh +++ b/git-bisect.sh @@ -37,89 +37,6 @@ OPTIONS_SPEC= TERM_BAD=bad TERM_GOOD=good -bisect_visualize() { - git bisect--helper --bisect-next-check $TERM_GOOD $TERM_BAD fail || exit - - if test $# = 0 - then - if test -n "${DISPLAY+set}${SESSIONNAME+set}${MSYSTEM+set}${SECURITYSESSIONID+set}" && - type gitk >/dev/null 2>&1 - then - set gitk - else - set git log - fi - else - case "$1" in - git*|tig) ;; - -*) set git log "$@" ;; - *) set git "$@" ;; - esac - fi - - eval '"$@"' --bisect -- $(cat "$GIT_DIR/BISECT_NAMES") -} - -bisect_run () { - git bisect--helper --bisect-next-check $TERM_GOOD $TERM_BAD fail || exit - - test -n "$*" || die "$(gettext "bisect run failed: no command provided.")" - - while true - do - command="$@" - eval_gettextln "running \$command" - "$@" - res=$? - - # Check for really bad run error. - if [ $res -lt 0 -o $res -ge 128 ] - then - eval_gettextln "bisect run failed: -exit code \$res from '\$command' is < 0 or >= 128" >&2 - exit $res - fi - - # Find current state depending on run success or failure. - # A special exit code of 125 means cannot test. - if [ $res -eq 125 ] - then - state='skip' - elif [ $res -gt 0 ] - then - state="$TERM_BAD" - else - state="$TERM_GOOD" - fi - - git bisect--helper --bisect-state $state >"$GIT_DIR/BISECT_RUN" - res=$? - - cat "$GIT_DIR/BISECT_RUN" - - if sane_grep "first $TERM_BAD commit could be any of" "$GIT_DIR/BISECT_RUN" \ - >/dev/null - then - gettextln "bisect run cannot continue any more" >&2 - exit $res - fi - - if [ $res -ne 0 ] - then - eval_gettextln "bisect run failed: -'bisect-state \$state' exited with error code \$res" >&2 - exit $res - fi - - if sane_grep "is the first $TERM_BAD commit" "$GIT_DIR/BISECT_RUN" >/dev/null - then - gettextln "bisect run success" - exit 0; - fi - - done -} - get_terms () { if test -s "$GIT_DIR/BISECT_TERMS" then @@ -150,7 +67,7 @@ case "$#" in # Not sure we want "next" at the UI level anymore. git bisect--helper --bisect-next "$@" || exit ;; visualize|view) - bisect_visualize "$@" ;; + git bisect--helper --bisect-visualize "$@" || exit;; reset) git bisect--helper --bisect-reset "$@" ;; replay) @@ -158,7 +75,7 @@ case "$#" in log) git bisect--helper --bisect-log || exit ;; run) - bisect_run "$@" ;; + git bisect--helper --bisect-run "$@" || exit;; terms) git bisect--helper --bisect-terms "$@" || exit;; *) diff --git a/run-command.c b/run-command.c index 821245672d..04a07e6366 100644 --- a/run-command.c +++ b/run-command.c @@ -211,9 +211,9 @@ static char *locate_in_PATH(const char *file) return NULL; } -static int exists_in_PATH(const char *file) +int exists_in_PATH(const char *command) { - char *r = locate_in_PATH(file); + char *r = locate_in_PATH(command); int found = r != NULL; free(r); return found; diff --git a/run-command.h b/run-command.h index ad207daced..b9aff74914 100644 --- a/run-command.h +++ b/run-command.h @@ -190,6 +190,18 @@ void child_process_clear(struct child_process *); int is_executable(const char *name); +/** + * Check if the command exists on $PATH. This emulates the path search that + * execvp would perform, without actually executing the command so it + * can be used before fork() to prepare to run a command using + * execve() or after execvp() to diagnose why it failed. + * + * The caller should ensure that command contains no directory separators. + * + * Returns 1 if it is found in $PATH or 0 if the command could not be found. + */ +int exists_in_PATH(const char *command); + /** * Start a sub-process. Takes a pointer to a `struct child_process` * that specifies the details and returns pipe FDs (if requested). diff --git a/t/t6030-bisect-porcelain.sh b/t/t6030-bisect-porcelain.sh index a1baf4e451..1be85d064e 100755 --- a/t/t6030-bisect-porcelain.sh +++ b/t/t6030-bisect-porcelain.sh @@ -962,4 +962,22 @@ test_expect_success 'bisect handles annotated tags' ' grep "$bad is the first bad commit" output ' +test_expect_success 'bisect run fails with exit code equals or greater than 128' ' + write_script test_script.sh <<-\EOF && + exit 128 + EOF + test_must_fail git bisect run ./test_script.sh && + write_script test_script.sh <<-\EOF && + exit 255 + EOF + test_must_fail git bisect run ./test_script.sh +' + +test_expect_success 'bisect visualize with a filename with dash and space' ' + echo "My test line" >>"./-hello 2" && + git add -- "./-hello 2" && + git commit --quiet -m "Add test line" -- "./-hello 2" && + git bisect visualize -p -- "-hello 2" +' + test_done