diff --git a/Documentation/git-bisect.txt b/Documentation/git-bisect.txt index ab60a1847..e4f46bc18 100644 --- a/Documentation/git-bisect.txt +++ b/Documentation/git-bisect.txt @@ -17,7 +17,7 @@ The command takes various subcommands, and different options depending on the subcommand: git bisect help - git bisect start [ [...]] [--] [...] + git bisect start [--no-checkout] [ [...]] [--] [...] git bisect bad [] git bisect good [...] git bisect skip [(|)...] @@ -263,6 +263,19 @@ rewind the tree to the pristine state. Finally the script should exit with the status of the real test to let the "git bisect run" command loop determine the eventual outcome of the bisect session. +OPTIONS +------- +--no-checkout:: ++ +Do not checkout the new working tree at each iteration of the bisection +process. Instead just update a special reference named 'BISECT_HEAD' to make +it point to the commit that should be tested. ++ +This option may be useful when the test you would perform in each step +does not require a checked out tree. ++ +If the repository is bare, `--no-checkout` is assumed. + EXAMPLES -------- @@ -343,6 +356,25 @@ $ git bisect run sh -c "make || exit 125; ~/check_test_case.sh" This shows that you can do without a run script if you write the test on a single line. +* Locate a good region of the object graph in a damaged repository ++ +------------ +$ git bisect start HEAD [ ... ] --no-checkout +$ git bisect run sh -c ' + GOOD=$(git for-each-ref "--format=%(objectname)" refs/bisect/good-*) && + git rev-list --objects BISECT_HEAD --not $GOOD >tmp.$$ && + git pack-objects --stdout >/dev/null &2 + gettext "Do you want me to do it for you [Y/n]? " >&2 read yesno case "$yesno" in [Nn]*) @@ -59,6 +68,50 @@ bisect_autostart() { } bisect_start() { + # + # Check for one bad and then some good revisions. + # + has_double_dash=0 + for arg; do + case "$arg" in --) has_double_dash=1; break ;; esac + done + orig_args=$(git rev-parse --sq-quote "$@") + bad_seen=0 + eval='' + if test "z$(git rev-parse --is-bare-repository)" != zfalse + then + mode=--no-checkout + else + mode='' + fi + while [ $# -gt 0 ]; do + arg="$1" + case "$arg" in + --) + shift + break + ;; + --no-checkout) + mode=--no-checkout + shift ;; + --*) + die "$(eval_gettext "unrecognised option: '\$arg'")" ;; + *) + rev=$(git rev-parse -q --verify "$arg^{commit}") || { + test $has_double_dash -eq 1 && + die "$(eval_gettext "'\$arg' does not appear to be a valid revision")" + break + } + case $bad_seen in + 0) state='bad' ; bad_seen=1 ;; + *) state='good' ;; + esac + eval="$eval bisect_write '$state' '$rev' 'nolog' &&" + shift + ;; + esac + done + # # Verify HEAD. # @@ -74,7 +127,10 @@ bisect_start() { then # Reset to the rev from where we started. start_head=$(cat "$GIT_DIR/BISECT_START") - git checkout "$start_head" -- || exit + if test "z$mode" != "z--no-checkout" + then + git checkout "$start_head" -- + fi else # Get rev from where we start. case "$head" in @@ -97,39 +153,6 @@ bisect_start() { # bisect_clean_state || exit - # - # Check for one bad and then some good revisions. - # - has_double_dash=0 - for arg; do - case "$arg" in --) has_double_dash=1; break ;; esac - done - orig_args=$(git rev-parse --sq-quote "$@") - bad_seen=0 - eval='' - while [ $# -gt 0 ]; do - arg="$1" - case "$arg" in - --) - shift - break - ;; - *) - rev=$(git rev-parse -q --verify "$arg^{commit}") || { - test $has_double_dash -eq 1 && - die "$(eval_gettext "'\$arg' does not appear to be a valid revision")" - break - } - case $bad_seen in - 0) state='bad' ; bad_seen=1 ;; - *) state='good' ;; - esac - eval="$eval bisect_write '$state' '$rev' 'nolog'; " - shift - ;; - esac - done - # # Change state. # In case of mistaken revs or checkout error, or signals received, @@ -143,9 +166,12 @@ bisect_start() { # # Write new start state. # - echo "$start_head" >"$GIT_DIR/BISECT_START" && + echo "$start_head" >"$GIT_DIR/BISECT_START" && { + test "z$mode" != "z--no-checkout" || + git update-ref --no-deref BISECT_HEAD "$start_head" + } && git rev-parse --sq-quote "$@" >"$GIT_DIR/BISECT_NAMES" && - eval "$eval" && + eval "$eval true" && echo "git bisect start$orig_args" >>"$GIT_DIR/BISECT_LOG" || exit # # Check if we can proceed to the next bisect state. @@ -176,7 +202,8 @@ is_expected_rev() { check_expected_revs() { for _rev in "$@"; do - if ! is_expected_rev "$_rev"; then + if ! is_expected_rev "$_rev" + then rm -f "$GIT_DIR/BISECT_ANCESTORS_OK" rm -f "$GIT_DIR/BISECT_EXPECTED_REV" return @@ -185,18 +212,18 @@ check_expected_revs() { } bisect_skip() { - all='' + all='' for arg in "$@" do - case "$arg" in - *..*) - revs=$(git rev-list "$arg") || die "$(eval_gettext "Bad rev input: \$arg")" ;; - *) - revs=$(git rev-parse --sq-quote "$arg") ;; - esac - all="$all $revs" - done - eval bisect_state 'skip' $all + case "$arg" in + *..*) + revs=$(git rev-list "$arg") || die "$(eval_gettext "Bad rev input: \$arg")" ;; + *) + revs=$(git rev-parse --sq-quote "$arg") ;; + esac + all="$all $revs" + done + eval bisect_state 'skip' $all } bisect_state() { @@ -206,8 +233,8 @@ bisect_state() { 0,*) die "$(gettext "Please call 'bisect_state' with at least one argument.")" ;; 1,bad|1,good|1,skip) - rev=$(git rev-parse --verify HEAD) || - die "$(gettext "Bad rev input: HEAD")" + rev=$(git rev-parse --verify $(bisect_head)) || + die "$(gettext "Bad rev input: $(bisect_head)")" bisect_write "$state" "$rev" check_expected_revs "$rev" ;; 2,bad|*,good|*,skip) @@ -291,10 +318,10 @@ bisect_next() { bisect_next_check good # Perform all bisection computation, display and checkout - git bisect--helper --next-all + git bisect--helper --next-all $(test -f "$GIT_DIR/BISECT_HEAD" && echo --no-checkout) res=$? - # Check if we should exit because bisection is finished + # Check if we should exit because bisection is finished test $res -eq 10 && exit 0 # Check for an error in the bisection process @@ -309,7 +336,8 @@ bisect_visualize() { if test $# = 0 then if test -n "${DISPLAY+set}${SESSIONNAME+set}${MSYSTEM+set}${SECURITYSESSIONID+set}" && - type gitk >/dev/null 2>&1; then + type gitk >/dev/null 2>&1 + then set gitk else set git log @@ -333,19 +361,20 @@ bisect_reset() { case "$#" in 0) branch=$(cat "$GIT_DIR/BISECT_START") ;; 1) git rev-parse --quiet --verify "$1^{commit}" > /dev/null || { - invalid="$1" - die "$(eval_gettext "'\$invalid' is not a valid commit")" - } - branch="$1" ;; + invalid="$1" + die "$(eval_gettext "'\$invalid' is not a valid commit")" + } + branch="$1" ;; *) - usage ;; + usage ;; esac - if git checkout "$branch" -- ; then - bisect_clean_state - else + + if ! test -f "$GIT_DIR/BISECT_HEAD" && ! git checkout "$branch" -- + then die "$(eval_gettext "Could not check out original HEAD '\$branch'. Try 'git bisect reset '.")" fi + bisect_clean_state } bisect_clean_state() { @@ -362,7 +391,8 @@ bisect_clean_state() { rm -f "$GIT_DIR/BISECT_RUN" && # Cleanup head-name if it got left by an old version of git-bisect rm -f "$GIT_DIR/head-name" && - + git update-ref -d --no-deref BISECT_HEAD && + # clean up BISECT_START last rm -f "$GIT_DIR/BISECT_START" } @@ -374,7 +404,8 @@ bisect_replay () { while read git bisect command rev do test "$git $bisect" = "git bisect" -o "$git" = "git-bisect" || continue - if test "$git" = "git-bisect"; then + if test "$git" = "git-bisect" + then rev="$command" command="$bisect" fi @@ -392,65 +423,71 @@ bisect_replay () { } bisect_run () { - bisect_next_check fail - - while true - do - command="$@" - eval_gettext "running \$command"; echo - "$@" - res=$? - - # Check for really bad run error. - if [ $res -lt 0 -o $res -ge 128 ]; then - ( - eval_gettext "bisect run failed: + bisect_next_check fail + + while true + do + command="$@" + eval_gettext "running \$command"; echo + "$@" + res=$? + + # Check for really bad run error. + if [ $res -lt 0 -o $res -ge 128 ] + then + ( + eval_gettext "bisect run failed: exit code \$res from '\$command' is < 0 or >= 128" && - echo - ) >&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='bad' - else - state='good' - fi - - # We have to use a subshell because "bisect_state" can exit. - ( bisect_state $state > "$GIT_DIR/BISECT_RUN" ) - res=$? - - cat "$GIT_DIR/BISECT_RUN" - - if sane_grep "first bad commit could be any of" "$GIT_DIR/BISECT_RUN" \ - > /dev/null; then - ( - gettext "bisect run cannot continue any more" && - echo - ) >&2 - exit $res - fi - - if [ $res -ne 0 ]; then - ( - eval_gettext "bisect run failed: + echo + ) >&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='bad' + else + state='good' + fi + + # We have to use a subshell because "bisect_state" can exit. + ( bisect_state $state > "$GIT_DIR/BISECT_RUN" ) + res=$? + + cat "$GIT_DIR/BISECT_RUN" + + if sane_grep "first bad commit could be any of" "$GIT_DIR/BISECT_RUN" \ + > /dev/null + then + ( + gettext "bisect run cannot continue any more" && + echo + ) >&2 + exit $res + fi + + if [ $res -ne 0 ] + then + ( + eval_gettext "bisect run failed: 'bisect_state \$state' exited with error code \$res" && - echo - ) >&2 - exit $res - fi + echo + ) >&2 + exit $res + fi - if sane_grep "is the first bad commit" "$GIT_DIR/BISECT_RUN" > /dev/null; then - gettext "bisect run success"; echo - exit 0; - fi + if sane_grep "is the first bad commit" "$GIT_DIR/BISECT_RUN" > /dev/null + then + gettext "bisect run success"; echo + exit 0; + fi - done + done } bisect_log () { @@ -460,33 +497,33 @@ bisect_log () { case "$#" in 0) - usage ;; + usage ;; *) - cmd="$1" - shift - case "$cmd" in - help) - git bisect -h ;; - start) - bisect_start "$@" ;; - bad|good) - bisect_state "$cmd" "$@" ;; - skip) - bisect_skip "$@" ;; - next) - # Not sure we want "next" at the UI level anymore. - bisect_next "$@" ;; - visualize|view) - bisect_visualize "$@" ;; - reset) - bisect_reset "$@" ;; - replay) - bisect_replay "$@" ;; - log) - bisect_log ;; - run) - bisect_run "$@" ;; - *) - usage ;; - esac + cmd="$1" + shift + case "$cmd" in + help) + git bisect -h ;; + start) + bisect_start "$@" ;; + bad|good) + bisect_state "$cmd" "$@" ;; + skip) + bisect_skip "$@" ;; + next) + # Not sure we want "next" at the UI level anymore. + bisect_next "$@" ;; + visualize|view) + bisect_visualize "$@" ;; + reset) + bisect_reset "$@" ;; + replay) + bisect_replay "$@" ;; + log) + bisect_log ;; + run) + bisect_run "$@" ;; + *) + usage ;; + esac esac diff --git a/git.c b/git.c index 304522b53..b660e3666 100644 --- a/git.c +++ b/git.c @@ -334,7 +334,7 @@ static void handle_internal_command(int argc, const char **argv) { "annotate", cmd_annotate, RUN_SETUP }, { "apply", cmd_apply, RUN_SETUP_GENTLY }, { "archive", cmd_archive }, - { "bisect--helper", cmd_bisect__helper, RUN_SETUP | NEED_WORK_TREE }, + { "bisect--helper", cmd_bisect__helper, RUN_SETUP }, { "blame", cmd_blame, RUN_SETUP }, { "branch", cmd_branch, RUN_SETUP }, { "bundle", cmd_bundle, RUN_SETUP_GENTLY }, diff --git a/t/t6030-bisect-porcelain.sh b/t/t6030-bisect-porcelain.sh index b5063b6fe..62125eca8 100755 --- a/t/t6030-bisect-porcelain.sh +++ b/t/t6030-bisect-porcelain.sh @@ -126,6 +126,18 @@ test_expect_success 'bisect reset removes packed refs' ' test -z "$(git for-each-ref "refs/heads/bisect")" ' +test_expect_success 'bisect reset removes bisect state after --no-checkout' ' + git bisect reset && + git bisect start --no-checkout && + git bisect good $HASH1 && + git bisect bad $HASH3 && + git bisect next && + git bisect reset && + test -z "$(git for-each-ref "refs/bisect/*")" && + test -z "$(git for-each-ref "refs/heads/bisect")" && + test -z "$(git for-each-ref "BISECT_HEAD")" +' + test_expect_success 'bisect start: back in good branch' ' git branch > branch.output && grep "* other" branch.output > /dev/null && @@ -138,15 +150,23 @@ test_expect_success 'bisect start: back in good branch' ' grep "* other" branch.output > /dev/null ' -test_expect_success 'bisect start: no ".git/BISECT_START" if junk rev' ' - git bisect start $HASH4 $HASH1 -- && - git bisect good && +test_expect_success 'bisect start: no ".git/BISECT_START" created if junk rev' ' + git bisect reset && test_must_fail git bisect start $HASH4 foo -- && git branch > branch.output && grep "* other" branch.output > /dev/null && test_must_fail test -e .git/BISECT_START ' +test_expect_success 'bisect start: existing ".git/BISECT_START" not modified if junk rev' ' + git bisect start $HASH4 $HASH1 -- && + git bisect good && + cp .git/BISECT_START saved && + test_must_fail git bisect start $HASH4 foo -- && + git branch > branch.output && + grep "* (no branch)" branch.output > /dev/null && + test_cmp saved .git/BISECT_START +' test_expect_success 'bisect start: no ".git/BISECT_START" if mistaken rev' ' git bisect start $HASH4 $HASH1 -- && git bisect good && @@ -572,6 +592,155 @@ test_expect_success 'erroring out when using bad path parameters' ' grep "bad path parameters" error.txt ' +test_expect_success 'test bisection on bare repo - --no-checkout specified' ' + git clone --bare . bare.nocheckout && + ( + cd bare.nocheckout && + git bisect start --no-checkout && + git bisect good $HASH1 && + git bisect bad $HASH4 && + git bisect run eval \ + "test \$(git rev-list BISECT_HEAD ^$HASH2 --max-count=1 | wc -l) = 0" \ + >../nocheckout.log && + git bisect reset + ) && + grep "$HASH3 is the first bad commit" nocheckout.log +' + + +test_expect_success 'test bisection on bare repo - --no-checkout defaulted' ' + git clone --bare . bare.defaulted && + ( + cd bare.defaulted && + git bisect start && + git bisect good $HASH1 && + git bisect bad $HASH4 && + git bisect run eval \ + "test \$(git rev-list BISECT_HEAD ^$HASH2 --max-count=1 | wc -l) = 0" \ + >../defaulted.log && + git bisect reset + ) && + grep "$HASH3 is the first bad commit" defaulted.log +' + # +# This creates a broken branch which cannot be checked out because +# the tree created has been deleted. # +# H1-H2-H3-H4-H5-H6-H7 <--other +# \ +# S5-S6'-S7'-S8'-S9 <--broken +# +# Commits marked with ' have a missing tree. +# +test_expect_success 'broken branch creation' ' + git bisect reset && + git checkout -b broken $HASH4 && + git tag BROKEN_HASH4 $HASH4 && + add_line_into_file "5(broken): first line on a broken branch" hello2 && + git tag BROKEN_HASH5 && + mkdir missing && + :> missing/MISSING && + git add missing/MISSING && + git commit -m "6(broken): Added file that will be deleted" + git tag BROKEN_HASH6 && + add_line_into_file "7(broken): second line on a broken branch" hello2 && + git tag BROKEN_HASH7 && + add_line_into_file "8(broken): third line on a broken branch" hello2 && + git tag BROKEN_HASH8 && + git rm missing/MISSING && + git commit -m "9(broken): Remove missing file" + git tag BROKEN_HASH9 && + rm .git/objects/39/f7e61a724187ab767d2e08442d9b6b9dab587d +' + +echo "" > expected.ok +cat > expected.missing-tree.default <error.txt && + test_cmp expected.missing-tree.default error.txt +' + +test_expect_success 'bisect fails if tree is broken on trial commit' ' + git bisect reset && + test_must_fail git bisect start BROKEN_HASH9 BROKEN_HASH4 2>error.txt && + git reset --hard broken && + git checkout broken && + test_cmp expected.missing-tree.default error.txt +' + +check_same() +{ + echo "Checking $1 is the same as $2" && + git rev-parse "$1" > expected.same && + git rev-parse "$2" > expected.actual && + test_cmp expected.same expected.actual +} + +test_expect_success 'bisect: --no-checkout - start commit bad' ' + git bisect reset && + git bisect start BROKEN_HASH7 BROKEN_HASH4 --no-checkout && + check_same BROKEN_HASH6 BISECT_HEAD && + git bisect reset +' + +test_expect_success 'bisect: --no-checkout - trial commit bad' ' + git bisect reset && + git bisect start broken BROKEN_HASH4 --no-checkout && + check_same BROKEN_HASH6 BISECT_HEAD && + git bisect reset +' + +test_expect_success 'bisect: --no-checkout - target before breakage' ' + git bisect reset && + git bisect start broken BROKEN_HASH4 --no-checkout && + check_same BROKEN_HASH6 BISECT_HEAD && + git bisect bad BISECT_HEAD && + check_same BROKEN_HASH5 BISECT_HEAD && + git bisect bad BISECT_HEAD && + check_same BROKEN_HASH5 bisect/bad && + git bisect reset +' + +test_expect_success 'bisect: --no-checkout - target in breakage' ' + git bisect reset && + git bisect start broken BROKEN_HASH4 --no-checkout && + check_same BROKEN_HASH6 BISECT_HEAD && + git bisect bad BISECT_HEAD && + check_same BROKEN_HASH5 BISECT_HEAD && + git bisect good BISECT_HEAD && + check_same BROKEN_HASH6 bisect/bad && + git bisect reset +' + +test_expect_success 'bisect: --no-checkout - target after breakage' ' + git bisect reset && + git bisect start broken BROKEN_HASH4 --no-checkout && + check_same BROKEN_HASH6 BISECT_HEAD && + git bisect good BISECT_HEAD && + check_same BROKEN_HASH8 BISECT_HEAD && + git bisect good BISECT_HEAD && + check_same BROKEN_HASH9 bisect/bad && + git bisect reset +' + +test_expect_success 'bisect: demonstrate identification of damage boundary' " + git bisect reset && + git checkout broken && + git bisect start broken master --no-checkout && + git bisect run sh -c ' + GOOD=\$(git for-each-ref \"--format=%(objectname)\" refs/bisect/good-*) && + git rev-list --objects BISECT_HEAD --not \$GOOD >tmp.\$\$ && + git pack-objects --stdout >/dev/null < tmp.\$\$ + rc=\$? + rm -f tmp.\$\$ + test \$rc = 0' && + check_same BROKEN_HASH6 bisect/bad && + git bisect reset +" + test_done