#!/bin/sh setglobal USAGE = ''[help|start|bad|good|new|old|terms|skip|next|reset|visualize|replay|log|run]'' setglobal LONG_USAGE = ''git bisect help print this long help message. git bisect start [--term-{old,good}= --term-{new,bad}=] [--no-checkout] [ [...]] [--] [...] reset bisect state and start bisection. git bisect (bad|new) [] mark a known-bad revision/ a revision after change in a given property. git bisect (good|old) [...] mark ... known-good revisions/ revisions before change in a given property. git bisect terms [--term-good | --term-bad] show the terms used for old and new commits (default: bad, good) git bisect skip [(|)...] mark ... untestable revisions. git bisect next find next bisection to test and check it out. git bisect reset [] finish bisection search and go back to commit. git bisect visualize show bisect status in gitk. git bisect replay replay bisection log. git bisect log show bisect log. git bisect run ... use ... to automatically bisect. Please use "git help bisect" to get the full man page.'' setglobal OPTIONS_SPEC = '' source git-sh-setup setglobal _x40 = ''[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]'' setglobal _x40 = ""$_x40$_x40$_x40$_x40$_x40$_x40$_x40$_x40"" setglobal TERM_BAD = 'bad' setglobal TERM_GOOD = 'good' proc bisect_head { if test -f "$GIT_DIR/BISECT_HEAD" { echo BISECT_HEAD } else { echo HEAD } } proc bisect_autostart { test -s "$GIT_DIR/BISECT_START" || do { gettextln "You need to start by \"git bisect start\"" > !2 if test -t 0 { # TRANSLATORS: Make sure to include [Y] and [n] in your # translation. The program will only accept English input # at this point. gettext "Do you want me to do it for you [Y/n]? " > !2 read yesno match $yesno { with [Nn]* exit } bisect_start } else { exit 1 } } } proc bisect_start { # # Check for one bad and then some good revisions. # setglobal has_double_dash = '0'for arg in @Argv { match $arg { with -- setglobal has_double_dash = '1'; break } } setglobal orig_args = $[git rev-parse --sq-quote @Argv] setglobal bad_seen = '0' setglobal eval = '''' setglobal must_write_terms = '0' setglobal revs = '''' if test "z$[git rev-parse --is-bare-repository]" != zfalse { setglobal mode = '--no-checkout' } else { setglobal mode = '''' } while test $Argc -gt 0 { setglobal arg = $1 match $arg { with -- shift break with --no-checkout setglobal mode = '--no-checkout' shift with --term-good|--term-old shift setglobal must_write_terms = '1' setglobal TERM_GOOD = $1 shift with --term-good=*|--term-old=* setglobal must_write_terms = '1' setglobal TERM_GOOD = $(1#*=) shift with --term-bad|--term-new shift setglobal must_write_terms = '1' setglobal TERM_BAD = $1 shift with --term-bad=*|--term-new=* setglobal must_write_terms = '1' setglobal TERM_BAD = $(1#*=) shift with --* die $[eval_gettext "unrecognised option: '\$arg'] with * setglobal rev = $[git rev-parse -q --verify "$arg^{commit}] || do { test $has_double_dash -eq 1 && die $[eval_gettext "'\$arg' does not appear to be a valid revision] break } setglobal revs = ""$revs $rev"" shift } } for rev in [$revs] { # The user ran "git bisect start # ", hence did not explicitly specify # the terms, but we are already starting to # set references named with the default terms, # and won't be able to change afterwards. setglobal must_write_terms = '1' match $bad_seen { with 0 setglobal state = $TERM_BAD ; setglobal bad_seen = '1' with * setglobal state = $TERM_GOOD } setglobal eval = ""$eval bisect_write '$state' '$rev' 'nolog' &&"" } # # Verify HEAD. # setglobal head = $[env GIT_DIR=$GIT_DIR git symbolic-ref -q HEAD] || setglobal head = $[env GIT_DIR=$GIT_DIR git rev-parse --verify HEAD] || die $[gettext "Bad HEAD - I need a HEAD] # # Check if we are bisecting. # setglobal start_head = '''' if test -s "$GIT_DIR/BISECT_START" { # Reset to the rev from where we started. setglobal start_head = $[cat "$GIT_DIR/BISECT_START] if test "z$mode" != "z--no-checkout" { git checkout $start_head -- || die $[eval_gettext "Checking out '\$start_head' failed. Try 'git bisect reset '.] } } else { # Get rev from where we start. match $head { with refs/heads/*|$_x40 # This error message should only be triggered by # cogito usage, and cogito users should understand # it relates to cg-seek. test -s "$GIT_DIR/head-name" && die $[gettext "won't bisect on cg-seek'ed tree] setglobal start_head = $(head#refs/heads/) with * die $[gettext "Bad HEAD - strange symbolic ref] } } # # Get rid of any old bisect state. # bisect_clean_state || exit # # Change state. # In case of mistaken revs or checkout error, or signals received, # "bisect_auto_next" below may exit or misbehave. # We have to trap this to be able to clean up using # "bisect_clean_state". # trap 'bisect_clean_state' 0 trap 'exit 255' 1 2 3 15 # # Write new start state. # echo $start_head >"$GIT_DIR/BISECT_START" && do { test "z$mode" != "z--no-checkout" || git update-ref --no-deref BISECT_HEAD $start_head } && git rev-parse --sq-quote @Argv >"$GIT_DIR/BISECT_NAMES" && eval "$eval true" && if test $must_write_terms -eq 1 { write_terms $TERM_BAD $TERM_GOOD } && echo "git bisect start$orig_args" >>"$GIT_DIR/BISECT_LOG" || exit # # Check if we can proceed to the next bisect state. # bisect_auto_next trap '-' 0 } proc bisect_write { setglobal state = $1 setglobal rev = $2 setglobal nolog = $3 match $state { with "$TERM_BAD" setglobal tag = $state with "$TERM_GOOD"|skip setglobal tag = ""$state"-"$rev"" with * die $[eval_gettext "Bad bisect_write argument: \$state] } git update-ref "refs/bisect/$tag" $rev || exit echo "# $state: $[git show-branch $rev]" >>"$GIT_DIR/BISECT_LOG" test -n $nolog || echo "git bisect $state $rev" >>"$GIT_DIR/BISECT_LOG" } proc is_expected_rev { test -f "$GIT_DIR/BISECT_EXPECTED_REV" && test $1 = $[cat "$GIT_DIR/BISECT_EXPECTED_REV] } proc check_expected_revs { for _rev in [@Argv] { if ! is_expected_rev $_rev { rm -f "$GIT_DIR/BISECT_ANCESTORS_OK" rm -f "$GIT_DIR/BISECT_EXPECTED_REV" return } } } proc bisect_skip { setglobal all = '''' for arg in [@Argv] { match $arg { with *..* setglobal revs = $[git rev-list $arg] || die $[eval_gettext "Bad rev input: \$arg] with * setglobal revs = $[git rev-parse --sq-quote $arg] } setglobal all = ""$all $revs"" } eval bisect_state 'skip' $all } proc bisect_state { bisect_autostart setglobal state = $1 check_and_set_terms $state match "$Argc,$state" { with 0,* die "Please call 'bisect_state' with at least one argument." with 1,"$TERM_BAD"|1,"$TERM_GOOD"|1,skip setglobal bisected_head = $[bisect_head] setglobal rev = $[git rev-parse --verify $bisected_head] || die $[eval_gettext "Bad rev input: \$bisected_head] bisect_write $state $rev check_expected_revs $rev with 2,"$TERM_BAD"|*,"$TERM_GOOD"|*,skip shift setglobal hash_list = '''' for rev in [@Argv] { setglobal sha = $[git rev-parse --verify "$rev^{commit}] || die $[eval_gettext "Bad rev input: \$rev] setglobal hash_list = ""$hash_list $sha"" } for rev in [$hash_list] { bisect_write $state $rev } check_expected_revs $hash_list with *,"$TERM_BAD" die $[eval_gettext "'git bisect \$TERM_BAD' can take only one argument.] with * usage } bisect_auto_next } proc bisect_next_check { setglobal missing_good = '', missing_bad = '' git show-ref -q --verify refs/bisect/$TERM_BAD || setglobal missing_bad = 't' test -n $[git for-each-ref "refs/bisect/$TERM_GOOD-*] || setglobal missing_good = 't' match "$missing_good,$missing_bad,$1" { with ,,* : have both $TERM_GOOD and $TERM_BAD - ok with *, # do not have both but not asked to fail - just report. false with t,,"$TERM_GOOD" # have bad (or new) but not good (or old). we could bisect although # this is less optimum. eval_gettextln "Warning: bisecting only with a \$TERM_BAD commit." > !2 if test -t 0 { # TRANSLATORS: Make sure to include [Y] and [n] in your # translation. The program will only accept English input # at this point. gettext "Are you sure [Y/n]? " > !2 read yesno match $yesno { with [Nn]* exit 1 } } : bisect without $TERM_GOOD... with * setglobal bad_syn = $[bisect_voc bad] setglobal good_syn = $[bisect_voc good] if test -s "$GIT_DIR/BISECT_START" { eval_gettextln "You need to give me at least one \$bad_syn and one \$good_syn revision. (You can use \"git bisect \$bad_syn\" and \"git bisect \$good_syn\" for that.)" > !2 } else { eval_gettextln "You need to start by \"git bisect start\". You then need to give me at least one \$good_syn and one \$bad_syn revision. (You can use \"git bisect \$bad_syn\" and \"git bisect \$good_syn\" for that.)" > !2 } exit 1 } } proc bisect_auto_next { bisect_next_check && bisect_next || : } proc bisect_next { match "$Argc" { with 0 with * usage } bisect_autostart bisect_next_check $TERM_GOOD # Perform all bisection computation, display and checkout git bisect--helper --next-all $[test -f "$GIT_DIR/BISECT_HEAD" && echo --no-checkout] setglobal res = $Status # Check if we should exit because bisection is finished if test $res -eq 10 { setglobal bad_rev = $[git show-ref --hash --verify refs/bisect/$TERM_BAD] setglobal bad_commit = $[git show-branch $bad_rev] echo "# first $TERM_BAD commit: $bad_commit" >>"$GIT_DIR/BISECT_LOG" exit 0 } elif test $res -eq 2 { echo "# only skipped commits left to test" >>"$GIT_DIR/BISECT_LOG" setglobal good_revs = $[git for-each-ref --format="%(objectname)" "refs/bisect/$TERM_GOOD-*] for skipped in [$[git rev-list refs/bisect/$TERM_BAD --not $good_revs]] { setglobal skipped_commit = $[git show-branch $skipped] echo "# possible first $TERM_BAD commit: $skipped_commit" >>"$GIT_DIR/BISECT_LOG" } exit $res } # Check for an error in the bisection process test $res -ne 0 && exit $res return 0 } proc bisect_visualize { bisect_next_check fail if test $Argc = 0 { if test -n "$(DISPLAY+set)$(SESSIONNAME+set)$(MSYSTEM+set)$(SECURITYSESSIONID+set)" && type gitk >/dev/null !2 > !1 { set gitk } else { set git log } } else { match $1 { with git*|tig with -* set git log @Argv with * set git @Argv } } eval '"$@"' --bisect -- $[cat "$GIT_DIR/BISECT_NAMES] } proc bisect_reset { test -s "$GIT_DIR/BISECT_START" || do { gettextln "We are not bisecting." return } match "$Argc" { with 0 setglobal branch = $[cat "$GIT_DIR/BISECT_START] with 1 git rev-parse --quiet --verify "$1^{commit}" >/dev/null || do { setglobal invalid = $1 die $[eval_gettext "'\$invalid' is not a valid commit] } setglobal branch = $1 with * usage } if ! test -f "$GIT_DIR/BISECT_HEAD" && ! git checkout $branch -- { die $[eval_gettext "Could not check out original HEAD '\$branch'. Try 'git bisect reset '.] } bisect_clean_state } proc bisect_clean_state { # There may be some refs packed during bisection. git for-each-ref --format='%(refname) %(objectname)' refs/bisect/'*' | while read ref hash { git update-ref -d $ref $hash || exit } rm -f "$GIT_DIR/BISECT_EXPECTED_REV" && rm -f "$GIT_DIR/BISECT_ANCESTORS_OK" && rm -f "$GIT_DIR/BISECT_LOG" && rm -f "$GIT_DIR/BISECT_NAMES" && rm -f "$GIT_DIR/BISECT_RUN" && rm -f "$GIT_DIR/BISECT_TERMS" && # 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" } proc bisect_replay { setglobal file = $1 test "$Argc" -eq 1 || die $[gettext "No logfile given] test -r $file || die $[eval_gettext "cannot read \$file for replaying] bisect_reset while read git bisect command rev { test "$git $bisect" = "git bisect" || test $git = "git-bisect" || continue if test $git = "git-bisect" { setglobal rev = $command setglobal command = $bisect } get_terms check_and_set_terms $command match $command { with start setglobal cmd = ""bisect_start $rev"" eval $cmd with "$TERM_GOOD"|"$TERM_BAD"|skip bisect_write $command $rev with terms bisect_terms $rev with * die $[gettext "?? what are you talking about?] } } <"$file" bisect_auto_next } proc bisect_run { bisect_next_check fail while true { setglobal command = @Argv eval_gettextln "running \$command" @Argv setglobal res = $Status # Check for really bad run error. if test $res -lt 0 -o $res -ge 128 { eval_gettextln "bisect run failed: exit code \$res from '\$command' is < 0 or >= 128" > !2 exit $res } # Find current state depending on run success or failure. # A special exit code of 125 means cannot test. if test $res -eq 125 { setglobal state = ''skip'' } elif test $res -gt 0 { setglobal state = $TERM_BAD } else { setglobal state = $TERM_GOOD } # We have to use a subshell because "bisect_state" can exit. shell { bisect_state $state >"$GIT_DIR/BISECT_RUN" } setglobal res = $Status cat "$GIT_DIR/BISECT_RUN" if sane_grep "first $TERM_BAD commit could be any of" "$GIT_DIR/BISECT_RUN" \ >/dev/null { gettextln "bisect run cannot continue any more" > !2 exit $res } if test $res -ne 0 { eval_gettextln "bisect run failed: 'bisect_state \$state' exited with error code \$res" > !2 exit $res } if sane_grep "is the first $TERM_BAD commit" "$GIT_DIR/BISECT_RUN" >/dev/null { gettextln "bisect run success" exit 0; } } } proc bisect_log { test -s "$GIT_DIR/BISECT_LOG" || die $[gettext "We are not bisecting.] cat "$GIT_DIR/BISECT_LOG" } proc get_terms { if test -s "$GIT_DIR/BISECT_TERMS" { do { read TERM_BAD read TERM_GOOD } <"$GIT_DIR/BISECT_TERMS" } } proc write_terms { setglobal TERM_BAD = $1 setglobal TERM_GOOD = $2 if test $TERM_BAD = $TERM_GOOD { die $[gettext "please use two different terms] } check_term_format $TERM_BAD bad check_term_format $TERM_GOOD good printf '%s\n%s\n' $TERM_BAD $TERM_GOOD >"$GIT_DIR/BISECT_TERMS" } proc check_term_format { setglobal term = $1 git check-ref-format refs/bisect/"$term" || die $[eval_gettext "'\$term' is not a valid term] match $term { with help|start|terms|skip|next|reset|visualize|replay|log|run die $[eval_gettext "can't use the builtin command '\$term' as a term] with bad|new if test $2 != bad { # In theory, nothing prevents swapping # completely good and bad, but this situation # could be confusing and hasn't been tested # enough. Forbid it for now. die $[eval_gettext "can't change the meaning of term '\$term'] } with good|old if test $2 != good { die $[eval_gettext "can't change the meaning of term '\$term'] } } } proc check_and_set_terms { setglobal cmd = $1 match $cmd { with skip|start|terms with * if test -s "$GIT_DIR/BISECT_TERMS" && test $cmd != $TERM_BAD && test $cmd != $TERM_GOOD { die $[eval_gettext "Invalid command: you're currently in a \$TERM_BAD/\$TERM_GOOD bisect.] } match $cmd { with bad|good if ! test -s "$GIT_DIR/BISECT_TERMS" { write_terms bad good } with new|old if ! test -s "$GIT_DIR/BISECT_TERMS" { write_terms new old } } } } proc bisect_voc { match $1 { with bad echo "bad|new" with good echo "good|old" } } proc bisect_terms { get_terms if ! test -s "$GIT_DIR/BISECT_TERMS" { die $[gettext "no terms defined] } match "$Argc" { with 0 gettextln "Your current terms are $TERM_GOOD for the old state and $TERM_BAD for the new state." with 1 setglobal arg = $1 match $arg { with --term-good|--term-old printf '%s\n' $TERM_GOOD with --term-bad|--term-new printf '%s\n' $TERM_BAD with * die $[eval_gettext "invalid argument \$arg for 'git bisect terms'. Supported options are: --term-good|--term-old and --term-bad|--term-new.] } with * usage } } match "$Argc" { with 0 usage with * setglobal cmd = $1 get_terms shift match $cmd { with help git bisect -h with start bisect_start @Argv with bad|good|new|old|"$TERM_BAD"|"$TERM_GOOD" bisect_state $cmd @Argv with skip bisect_skip @Argv with next # Not sure we want "next" at the UI level anymore. bisect_next @Argv with visualize|view bisect_visualize @Argv with reset bisect_reset @Argv with replay bisect_replay @Argv with log bisect_log with run bisect_run @Argv with terms bisect_terms @Argv with * usage } }