git-bisect.shon commit bisect: make skipped array functions more generic (aaaff9e)
   1#!/bin/sh
   2
   3USAGE='[help|start|bad|good|skip|next|reset|visualize|replay|log|run]'
   4LONG_USAGE='git bisect help
   5        print this long help message.
   6git bisect start [<bad> [<good>...]] [--] [<pathspec>...]
   7        reset bisect state and start bisection.
   8git bisect bad [<rev>]
   9        mark <rev> a known-bad revision.
  10git bisect good [<rev>...]
  11        mark <rev>... known-good revisions.
  12git bisect skip [(<rev>|<range>)...]
  13        mark <rev>... untestable revisions.
  14git bisect next
  15        find next bisection to test and check it out.
  16git bisect reset [<branch>]
  17        finish bisection search and go back to branch.
  18git bisect visualize
  19        show bisect status in gitk.
  20git bisect replay <logfile>
  21        replay bisection log.
  22git bisect log
  23        show bisect log.
  24git bisect run <cmd>...
  25        use <cmd>... to automatically bisect.
  26
  27Please use "git help bisect" to get the full man page.'
  28
  29OPTIONS_SPEC=
  30. git-sh-setup
  31require_work_tree
  32
  33_x40='[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]'
  34_x40="$_x40$_x40$_x40$_x40$_x40$_x40$_x40$_x40"
  35
  36bisect_autostart() {
  37        test -s "$GIT_DIR/BISECT_START" || {
  38                echo >&2 'You need to start by "git bisect start"'
  39                if test -t 0
  40                then
  41                        echo >&2 -n 'Do you want me to do it for you [Y/n]? '
  42                        read yesno
  43                        case "$yesno" in
  44                        [Nn]*)
  45                                exit ;;
  46                        esac
  47                        bisect_start
  48                else
  49                        exit 1
  50                fi
  51        }
  52}
  53
  54bisect_start() {
  55        #
  56        # Verify HEAD.
  57        #
  58        head=$(GIT_DIR="$GIT_DIR" git symbolic-ref -q HEAD) ||
  59        head=$(GIT_DIR="$GIT_DIR" git rev-parse --verify HEAD) ||
  60        die "Bad HEAD - I need a HEAD"
  61
  62        #
  63        # Check if we are bisecting.
  64        #
  65        start_head=''
  66        if test -s "$GIT_DIR/BISECT_START"
  67        then
  68                # Reset to the rev from where we started.
  69                start_head=$(cat "$GIT_DIR/BISECT_START")
  70                git checkout "$start_head" -- || exit
  71        else
  72                # Get rev from where we start.
  73                case "$head" in
  74                refs/heads/*|$_x40)
  75                        # This error message should only be triggered by
  76                        # cogito usage, and cogito users should understand
  77                        # it relates to cg-seek.
  78                        [ -s "$GIT_DIR/head-name" ] &&
  79                                die "won't bisect on seeked tree"
  80                        start_head="${head#refs/heads/}"
  81                        ;;
  82                *)
  83                        die "Bad HEAD - strange symbolic ref"
  84                        ;;
  85                esac
  86        fi
  87
  88        #
  89        # Get rid of any old bisect state.
  90        #
  91        bisect_clean_state || exit
  92
  93        #
  94        # Check for one bad and then some good revisions.
  95        #
  96        has_double_dash=0
  97        for arg; do
  98            case "$arg" in --) has_double_dash=1; break ;; esac
  99        done
 100        orig_args=$(git rev-parse --sq-quote "$@")
 101        bad_seen=0
 102        eval=''
 103        while [ $# -gt 0 ]; do
 104            arg="$1"
 105            case "$arg" in
 106            --)
 107                shift
 108                break
 109                ;;
 110            *)
 111                rev=$(git rev-parse -q --verify "$arg^{commit}") || {
 112                    test $has_double_dash -eq 1 &&
 113                        die "'$arg' does not appear to be a valid revision"
 114                    break
 115                }
 116                case $bad_seen in
 117                0) state='bad' ; bad_seen=1 ;;
 118                *) state='good' ;;
 119                esac
 120                eval="$eval bisect_write '$state' '$rev' 'nolog'; "
 121                shift
 122                ;;
 123            esac
 124        done
 125
 126        #
 127        # Change state.
 128        # In case of mistaken revs or checkout error, or signals received,
 129        # "bisect_auto_next" below may exit or misbehave.
 130        # We have to trap this to be able to clean up using
 131        # "bisect_clean_state".
 132        #
 133        trap 'bisect_clean_state' 0
 134        trap 'exit 255' 1 2 3 15
 135
 136        #
 137        # Write new start state.
 138        #
 139        echo "$start_head" >"$GIT_DIR/BISECT_START" &&
 140        git rev-parse --sq-quote "$@" >"$GIT_DIR/BISECT_NAMES" &&
 141        eval "$eval" &&
 142        echo "git bisect start$orig_args" >>"$GIT_DIR/BISECT_LOG" || exit
 143        #
 144        # Check if we can proceed to the next bisect state.
 145        #
 146        bisect_auto_next
 147
 148        trap '-' 0
 149}
 150
 151bisect_write() {
 152        state="$1"
 153        rev="$2"
 154        nolog="$3"
 155        case "$state" in
 156                bad)            tag="$state" ;;
 157                good|skip)      tag="$state"-"$rev" ;;
 158                *)              die "Bad bisect_write argument: $state" ;;
 159        esac
 160        git update-ref "refs/bisect/$tag" "$rev" || exit
 161        echo "# $state: $(git show-branch $rev)" >>"$GIT_DIR/BISECT_LOG"
 162        test -n "$nolog" || echo "git bisect $state $rev" >>"$GIT_DIR/BISECT_LOG"
 163}
 164
 165is_expected_rev() {
 166        test -f "$GIT_DIR/BISECT_EXPECTED_REV" &&
 167        test "$1" = $(cat "$GIT_DIR/BISECT_EXPECTED_REV")
 168}
 169
 170mark_expected_rev() {
 171        echo "$1" > "$GIT_DIR/BISECT_EXPECTED_REV"
 172}
 173
 174check_expected_revs() {
 175        for _rev in "$@"; do
 176                if ! is_expected_rev "$_rev"; then
 177                        rm -f "$GIT_DIR/BISECT_ANCESTORS_OK"
 178                        rm -f "$GIT_DIR/BISECT_EXPECTED_REV"
 179                        return
 180                fi
 181        done
 182}
 183
 184bisect_skip() {
 185        all=''
 186        for arg in "$@"
 187        do
 188            case "$arg" in
 189            *..*)
 190                revs=$(git rev-list "$arg") || die "Bad rev input: $arg" ;;
 191            *)
 192                revs=$(git rev-parse --sq-quote "$arg") ;;
 193            esac
 194            all="$all $revs"
 195        done
 196        eval bisect_state 'skip' $all
 197}
 198
 199bisect_state() {
 200        bisect_autostart
 201        state=$1
 202        case "$#,$state" in
 203        0,*)
 204                die "Please call 'bisect_state' with at least one argument." ;;
 205        1,bad|1,good|1,skip)
 206                rev=$(git rev-parse --verify HEAD) ||
 207                        die "Bad rev input: HEAD"
 208                bisect_write "$state" "$rev"
 209                check_expected_revs "$rev" ;;
 210        2,bad|*,good|*,skip)
 211                shift
 212                eval=''
 213                for rev in "$@"
 214                do
 215                        sha=$(git rev-parse --verify "$rev^{commit}") ||
 216                                die "Bad rev input: $rev"
 217                        eval="$eval bisect_write '$state' '$sha'; "
 218                done
 219                eval "$eval"
 220                check_expected_revs "$@" ;;
 221        *,bad)
 222                die "'git bisect bad' can take only one argument." ;;
 223        *)
 224                usage ;;
 225        esac
 226        bisect_auto_next
 227}
 228
 229bisect_next_check() {
 230        missing_good= missing_bad=
 231        git show-ref -q --verify refs/bisect/bad || missing_bad=t
 232        test -n "$(git for-each-ref "refs/bisect/good-*")" || missing_good=t
 233
 234        case "$missing_good,$missing_bad,$1" in
 235        ,,*)
 236                : have both good and bad - ok
 237                ;;
 238        *,)
 239                # do not have both but not asked to fail - just report.
 240                false
 241                ;;
 242        t,,good)
 243                # have bad but not good.  we could bisect although
 244                # this is less optimum.
 245                echo >&2 'Warning: bisecting only with a bad commit.'
 246                if test -t 0
 247                then
 248                        printf >&2 'Are you sure [Y/n]? '
 249                        read yesno
 250                        case "$yesno" in [Nn]*) exit 1 ;; esac
 251                fi
 252                : bisect without good...
 253                ;;
 254        *)
 255                THEN=''
 256                test -s "$GIT_DIR/BISECT_START" || {
 257                        echo >&2 'You need to start by "git bisect start".'
 258                        THEN='then '
 259                }
 260                echo >&2 'You '$THEN'need to give me at least one good' \
 261                        'and one bad revisions.'
 262                echo >&2 '(You can use "git bisect bad" and' \
 263                        '"git bisect good" for that.)'
 264                exit 1 ;;
 265        esac
 266}
 267
 268bisect_auto_next() {
 269        bisect_next_check && bisect_next || :
 270}
 271
 272bisect_checkout() {
 273        _rev="$1"
 274        _msg="$2"
 275        echo "Bisecting: $_msg"
 276        mark_expected_rev "$_rev"
 277        git checkout -q "$_rev" -- || exit
 278        git show-branch "$_rev"
 279}
 280
 281is_among() {
 282        _rev="$1"
 283        _list="$2"
 284        case "$_list" in *$_rev*) return 0 ;; esac
 285        return 1
 286}
 287
 288handle_bad_merge_base() {
 289        _badmb="$1"
 290        _good="$2"
 291        if is_expected_rev "$_badmb"; then
 292                cat >&2 <<EOF
 293The merge base $_badmb is bad.
 294This means the bug has been fixed between $_badmb and [$_good].
 295EOF
 296                exit 3
 297        else
 298                cat >&2 <<EOF
 299Some good revs are not ancestor of the bad rev.
 300git bisect cannot work properly in this case.
 301Maybe you mistake good and bad revs?
 302EOF
 303                exit 1
 304        fi
 305}
 306
 307handle_skipped_merge_base() {
 308        _mb="$1"
 309        _bad="$2"
 310        _good="$3"
 311        cat >&2 <<EOF
 312Warning: the merge base between $_bad and [$_good] must be skipped.
 313So we cannot be sure the first bad commit is between $_mb and $_bad.
 314We continue anyway.
 315EOF
 316}
 317
 318#
 319# "check_merge_bases" checks that merge bases are not "bad".
 320#
 321# - If one is "good", that's good, we have nothing to do.
 322# - If one is "bad", it means the user assumed something wrong
 323# and we must exit.
 324# - If one is "skipped", we can't know but we should warn.
 325# - If we don't know, we should check it out and ask the user to test.
 326#
 327# In the last case we will return 1, and otherwise 0.
 328#
 329check_merge_bases() {
 330        _bad="$1"
 331        _good="$2"
 332        _skip="$3"
 333        for _mb in $(git merge-base --all $_bad $_good)
 334        do
 335                if is_among "$_mb" "$_good"; then
 336                        continue
 337                elif test "$_mb" = "$_bad"; then
 338                        handle_bad_merge_base "$_bad" "$_good"
 339                elif is_among "$_mb" "$_skip"; then
 340                        handle_skipped_merge_base "$_mb" "$_bad" "$_good"
 341                else
 342                        bisect_checkout "$_mb" "a merge base must be tested"
 343                        return 1
 344                fi
 345        done
 346        return 0
 347}
 348
 349#
 350# "check_good_are_ancestors_of_bad" checks that all "good" revs are
 351# ancestor of the "bad" rev.
 352#
 353# If that's not the case, we need to check the merge bases.
 354# If a merge base must be tested by the user we return 1 and
 355# otherwise 0.
 356#
 357check_good_are_ancestors_of_bad() {
 358        test -f "$GIT_DIR/BISECT_ANCESTORS_OK" &&
 359                return
 360
 361        _bad="$1"
 362        _good=$(echo $2 | sed -e 's/\^//g')
 363        _skip="$3"
 364
 365        # Bisecting with no good rev is ok
 366        test -z "$_good" && return
 367
 368        _side=$(git rev-list $_good ^$_bad)
 369        if test -n "$_side"; then
 370                # Return if a checkout was done
 371                check_merge_bases "$_bad" "$_good" "$_skip" || return
 372        fi
 373
 374        : > "$GIT_DIR/BISECT_ANCESTORS_OK"
 375
 376        return 0
 377}
 378
 379bisect_next() {
 380        case "$#" in 0) ;; *) usage ;; esac
 381        bisect_autostart
 382        bisect_next_check good
 383
 384        # Get bad, good and skipped revs
 385        bad=$(git rev-parse --verify refs/bisect/bad) &&
 386        good=$(git for-each-ref --format='^%(objectname)' \
 387                "refs/bisect/good-*" | tr '\012' ' ') &&
 388        skip=$(git for-each-ref --format='%(objectname)' \
 389                "refs/bisect/skip-*" | tr '\012' ' ') || exit
 390
 391        # Maybe some merge bases must be tested first
 392        check_good_are_ancestors_of_bad "$bad" "$good" "$skip"
 393        # Return now if a checkout has already been done
 394        test "$?" -eq "1" && return
 395
 396        # Perform bisection computation, display and checkout
 397        git bisect--helper --next-exit
 398        res=$?
 399
 400        # Check if we should exit because bisection is finished
 401        test $res -eq 10 && exit 0
 402
 403        # Check for an error in the bisection process
 404        test $res -ne 0 && exit $res
 405
 406        return 0
 407}
 408
 409bisect_visualize() {
 410        bisect_next_check fail
 411
 412        if test $# = 0
 413        then
 414                case "${DISPLAY+set}${SESSIONNAME+set}${MSYSTEM+set}${SECURITYSESSIONID+set}" in
 415                '')     set git log ;;
 416                set*)   set gitk ;;
 417                esac
 418        else
 419                case "$1" in
 420                git*|tig) ;;
 421                -*)     set git log "$@" ;;
 422                *)      set git "$@" ;;
 423                esac
 424        fi
 425
 426        not=$(git for-each-ref --format='%(refname)' "refs/bisect/good-*")
 427        eval '"$@"' refs/bisect/bad --not $not -- $(cat "$GIT_DIR/BISECT_NAMES")
 428}
 429
 430bisect_reset() {
 431        test -s "$GIT_DIR/BISECT_START" || {
 432                echo "We are not bisecting."
 433                return
 434        }
 435        case "$#" in
 436        0) branch=$(cat "$GIT_DIR/BISECT_START") ;;
 437        1) git show-ref --verify --quiet -- "refs/heads/$1" ||
 438               die "$1 does not seem to be a valid branch"
 439           branch="$1" ;;
 440        *)
 441            usage ;;
 442        esac
 443        git checkout "$branch" -- && bisect_clean_state
 444}
 445
 446bisect_clean_state() {
 447        # There may be some refs packed during bisection.
 448        git for-each-ref --format='%(refname) %(objectname)' refs/bisect/\* |
 449        while read ref hash
 450        do
 451                git update-ref -d $ref $hash || exit
 452        done
 453        rm -f "$GIT_DIR/BISECT_EXPECTED_REV" &&
 454        rm -f "$GIT_DIR/BISECT_ANCESTORS_OK" &&
 455        rm -f "$GIT_DIR/BISECT_LOG" &&
 456        rm -f "$GIT_DIR/BISECT_NAMES" &&
 457        rm -f "$GIT_DIR/BISECT_RUN" &&
 458        # Cleanup head-name if it got left by an old version of git-bisect
 459        rm -f "$GIT_DIR/head-name" &&
 460
 461        rm -f "$GIT_DIR/BISECT_START"
 462}
 463
 464bisect_replay () {
 465        test -r "$1" || die "cannot read $1 for replaying"
 466        bisect_reset
 467        while read git bisect command rev
 468        do
 469                test "$git $bisect" = "git bisect" -o "$git" = "git-bisect" || continue
 470                if test "$git" = "git-bisect"; then
 471                        rev="$command"
 472                        command="$bisect"
 473                fi
 474                case "$command" in
 475                start)
 476                        cmd="bisect_start $rev"
 477                        eval "$cmd" ;;
 478                good|bad|skip)
 479                        bisect_write "$command" "$rev" ;;
 480                *)
 481                        die "?? what are you talking about?" ;;
 482                esac
 483        done <"$1"
 484        bisect_auto_next
 485}
 486
 487bisect_run () {
 488    bisect_next_check fail
 489
 490    while true
 491    do
 492      echo "running $@"
 493      "$@"
 494      res=$?
 495
 496      # Check for really bad run error.
 497      if [ $res -lt 0 -o $res -ge 128 ]; then
 498          echo >&2 "bisect run failed:"
 499          echo >&2 "exit code $res from '$@' is < 0 or >= 128"
 500          exit $res
 501      fi
 502
 503      # Find current state depending on run success or failure.
 504      # A special exit code of 125 means cannot test.
 505      if [ $res -eq 125 ]; then
 506          state='skip'
 507      elif [ $res -gt 0 ]; then
 508          state='bad'
 509      else
 510          state='good'
 511      fi
 512
 513      # We have to use a subshell because "bisect_state" can exit.
 514      ( bisect_state $state > "$GIT_DIR/BISECT_RUN" )
 515      res=$?
 516
 517      cat "$GIT_DIR/BISECT_RUN"
 518
 519      if grep "first bad commit could be any of" "$GIT_DIR/BISECT_RUN" \
 520                > /dev/null; then
 521          echo >&2 "bisect run cannot continue any more"
 522          exit $res
 523      fi
 524
 525      if [ $res -ne 0 ]; then
 526          echo >&2 "bisect run failed:"
 527          echo >&2 "'bisect_state $state' exited with error code $res"
 528          exit $res
 529      fi
 530
 531      if grep "is first bad commit" "$GIT_DIR/BISECT_RUN" > /dev/null; then
 532          echo "bisect run success"
 533          exit 0;
 534      fi
 535
 536    done
 537}
 538
 539
 540case "$#" in
 5410)
 542    usage ;;
 543*)
 544    cmd="$1"
 545    shift
 546    case "$cmd" in
 547    help)
 548        git bisect -h ;;
 549    start)
 550        bisect_start "$@" ;;
 551    bad|good)
 552        bisect_state "$cmd" "$@" ;;
 553    skip)
 554        bisect_skip "$@" ;;
 555    next)
 556        # Not sure we want "next" at the UI level anymore.
 557        bisect_next "$@" ;;
 558    visualize|view)
 559        bisect_visualize "$@" ;;
 560    reset)
 561        bisect_reset "$@" ;;
 562    replay)
 563        bisect_replay "$@" ;;
 564    log)
 565        cat "$GIT_DIR/BISECT_LOG" ;;
 566    run)
 567        bisect_run "$@" ;;
 568    *)
 569        usage ;;
 570    esac
 571esac