Merge branch 'cc/maint-1.6.0-bisect-fix'
authorJunio C Hamano <gitster@pobox.com>
Fri, 27 Feb 2009 09:03:21 +0000 (01:03 -0800)
committerJunio C Hamano <gitster@pobox.com>
Fri, 27 Feb 2009 09:03:21 +0000 (01:03 -0800)
* cc/maint-1.6.0-bisect-fix:
bisect: fix quoting TRIED revs when "bad" commit is also "skip"ped

Conflicts:
git-bisect.sh

1  2 
git-bisect.sh
t/t6030-bisect-porcelain.sh
diff --combined git-bisect.sh
index 85db4ba40022e3a9e5790879d6d21fa59475b316,f9a5c0bdf4382c09a978ce42b013c507c0fb1e0c..a857db447ceaf293f6e4ddad3c9038e18b96c0ca
@@@ -9,7 -9,7 +9,7 @@@ git bisect bad [<rev>
          mark <rev> a known-bad revision.
  git bisect good [<rev>...]
          mark <rev>... known-good revisions.
 -git bisect skip [<rev>...]
 +git bisect skip [(<rev>|<range>)...]
          mark <rev>... untestable revisions.
  git bisect next
          find next bisection to test and check it out.
@@@ -172,40 -172,6 +172,40 @@@ bisect_write() 
        test -n "$nolog" || echo "git bisect $state $rev" >>"$GIT_DIR/BISECT_LOG"
  }
  
 +is_expected_rev() {
 +      test -f "$GIT_DIR/BISECT_EXPECTED_REV" &&
 +      test "$1" = $(cat "$GIT_DIR/BISECT_EXPECTED_REV")
 +}
 +
 +mark_expected_rev() {
 +      echo "$1" > "$GIT_DIR/BISECT_EXPECTED_REV"
 +}
 +
 +check_expected_revs() {
 +      for _rev in "$@"; do
 +              if ! is_expected_rev "$_rev"; then
 +                      rm -f "$GIT_DIR/BISECT_ANCESTORS_OK"
 +                      rm -f "$GIT_DIR/BISECT_EXPECTED_REV"
 +                      return
 +              fi
 +      done
 +}
 +
 +bisect_skip() {
 +        all=''
 +      for arg in "$@"
 +      do
 +          case "$arg" in
 +            *..*)
 +                revs=$(git rev-list "$arg") || die "Bad rev input: $arg" ;;
 +            *)
 +                revs=$(sq "$arg") ;;
 +          esac
 +            all="$all $revs"
 +        done
 +        eval bisect_state 'skip' $all
 +}
 +
  bisect_state() {
        bisect_autostart
        state=$1
        1,bad|1,good|1,skip)
                rev=$(git rev-parse --verify HEAD) ||
                        die "Bad rev input: HEAD"
 -              bisect_write "$state" "$rev" ;;
 +              bisect_write "$state" "$rev"
 +              check_expected_revs "$rev" ;;
        2,bad|*,good|*,skip)
                shift
                eval=''
                                die "Bad rev input: $rev"
                        eval="$eval bisect_write '$state' '$sha'; "
                done
 -              eval "$eval" ;;
 +              eval "$eval"
 +              check_expected_revs "$@" ;;
        *,bad)
                die "'git bisect bad' can take only one argument." ;;
        *)
@@@ -279,67 -243,88 +279,73 @@@ bisect_auto_next() 
        bisect_next_check && bisect_next || :
  }
  
 -eval_rev_list() {
 -      _eval="$1"
 -
 -      eval $_eval
 -      res=$?
 -
 -      if [ $res -ne 0 ]; then
 -              echo >&2 "'git rev-list --bisect-vars' failed:"
 -              echo >&2 "maybe you mistake good and bad revs?"
 -              exit $res
 -      fi
 -
 -      return $res
 -}
 -
  filter_skipped() {
        _eval="$1"
        _skip="$2"
  
        if [ -z "$_skip" ]; then
 -              eval_rev_list "$_eval"
 +              eval "$_eval"
                return
        fi
  
        # Let's parse the output of:
        # "git rev-list --bisect-vars --bisect-all ..."
-       eval "$_eval" | while read hash line
-       do
-               case "$VARS,$FOUND,$TRIED,$hash" in
-                       # We display some vars.
-                       1,*,*,*) echo "$hash $line" ;;
-                       # Split line.
-                       ,*,*,---*) ;;
-                       # We had nothing to search.
 -      eval_rev_list "$_eval" | {
++      eval "$_eval" | {
+               VARS= FOUND= TRIED=
+               while read hash line
+               do
+                       case "$VARS,$FOUND,$TRIED,$hash" in
+                       1,*,*,*)
+                               # "bisect_foo=bar" read from rev-list output.
+                               echo "$hash &&"
+                               ;;
+                       ,*,*,---*)
+                               # Separator
+                               ;;
                        ,,,bisect_rev*)
-                               echo "bisect_rev="
+                               # We had nothing to search.
+                               echo "bisect_rev= &&"
                                VARS=1
                                ;;
-                       # We did not find a good bisect rev.
-                       # This should happen only if the "bad"
-                       # commit is also a "skip" commit.
                        ,,*,bisect_rev*)
-                               echo "bisect_rev=$TRIED"
+                               # We did not find a good bisect rev.
+                               # This should happen only if the "bad"
+                               # commit is also a "skip" commit.
+                               echo "bisect_rev='$TRIED' &&"
                                VARS=1
                                ;;
-                       # We are searching.
                        ,,*,*)
+                               # We are searching.
                                TRIED="${TRIED:+$TRIED|}$hash"
                                case "$_skip" in
                                *$hash*) ;;
                                *)
-                                       echo "bisect_rev=$hash"
-                                       echo "bisect_tried=\"$TRIED\""
+                                       echo "bisect_rev=$hash &&"
+                                       echo "bisect_tried='$TRIED' &&"
                                        FOUND=1
                                        ;;
                                esac
                                ;;
-                       # We have already found a rev to be tested.
-                       ,1,*,bisect_rev*) VARS=1 ;;
-                       ,1,*,*) ;;
-                       # ???
-                       *) die "filter_skipped error " \
-                           "VARS: '$VARS' " \
-                           "FOUND: '$FOUND' " \
-                           "TRIED: '$TRIED' " \
-                           "hash: '$hash' " \
-                           "line: '$line'"
-                       ;;
-               esac
-       done
+                       ,1,*,bisect_rev*)
+                               # We have already found a rev to be tested.
+                               VARS=1
+                               ;;
+                       ,1,*,*)
+                               ;;
+                       *)
+                               # Unexpected input
+                               echo "die 'filter_skipped error'"
+                               die "filter_skipped error " \
+                                   "VARS: '$VARS' " \
+                                   "FOUND: '$FOUND' " \
+                                   "TRIED: '$TRIED' " \
+                                   "hash: '$hash' " \
+                                   "line: '$line'"
+                               ;;
+                       esac
+               done
+               echo ':'
+       }
  }
  
  exit_if_skipped_commits () {
        fi
  }
  
 +bisect_checkout() {
 +      _rev="$1"
 +      _msg="$2"
 +      echo "Bisecting: $_msg"
 +      mark_expected_rev "$_rev"
 +      git checkout -q "$_rev" || exit
 +      git show-branch "$_rev"
 +}
 +
 +is_among() {
 +      _rev="$1"
 +      _list="$2"
 +      case "$_list" in *$_rev*) return 0 ;; esac
 +      return 1
 +}
 +
 +handle_bad_merge_base() {
 +      _badmb="$1"
 +      _good="$2"
 +      if is_expected_rev "$_badmb"; then
 +              cat >&2 <<EOF
 +The merge base $_badmb is bad.
 +This means the bug has been fixed between $_badmb and [$_good].
 +EOF
 +              exit 3
 +      else
 +              cat >&2 <<EOF
 +Some good revs are not ancestor of the bad rev.
 +git bisect cannot work properly in this case.
 +Maybe you mistake good and bad revs?
 +EOF
 +              exit 1
 +      fi
 +}
 +
 +handle_skipped_merge_base() {
 +      _mb="$1"
 +      _bad="$2"
 +      _good="$3"
 +      cat >&2 <<EOF
 +Warning: the merge base between $_bad and [$_good] must be skipped.
 +So we cannot be sure the first bad commit is between $_mb and $_bad.
 +We continue anyway.
 +EOF
 +}
 +
 +#
 +# "check_merge_bases" checks that merge bases are not "bad".
 +#
 +# - If one is "good", that's good, we have nothing to do.
 +# - If one is "bad", it means the user assumed something wrong
 +# and we must exit.
 +# - If one is "skipped", we can't know but we should warn.
 +# - If we don't know, we should check it out and ask the user to test.
 +#
 +# In the last case we will return 1, and otherwise 0.
 +#
 +check_merge_bases() {
 +      _bad="$1"
 +      _good="$2"
 +      _skip="$3"
 +      for _mb in $(git merge-base --all $_bad $_good)
 +      do
 +              if is_among "$_mb" "$_good"; then
 +                      continue
 +              elif test "$_mb" = "$_bad"; then
 +                      handle_bad_merge_base "$_bad" "$_good"
 +              elif is_among "$_mb" "$_skip"; then
 +                      handle_skipped_merge_base "$_mb" "$_bad" "$_good"
 +              else
 +                      bisect_checkout "$_mb" "a merge base must be tested"
 +                      return 1
 +              fi
 +      done
 +      return 0
 +}
 +
 +#
 +# "check_good_are_ancestors_of_bad" checks that all "good" revs are
 +# ancestor of the "bad" rev.
 +#
 +# If that's not the case, we need to check the merge bases.
 +# If a merge base must be tested by the user we return 1 and
 +# otherwise 0.
 +#
 +check_good_are_ancestors_of_bad() {
 +      test -f "$GIT_DIR/BISECT_ANCESTORS_OK" &&
 +              return
 +
 +      _bad="$1"
 +      _good=$(echo $2 | sed -e 's/\^//g')
 +      _skip="$3"
 +
 +      # Bisecting with no good rev is ok
 +      test -z "$_good" && return
 +
 +      _side=$(git rev-list $_good ^$_bad)
 +      if test -n "$_side"; then
 +              # Return if a checkout was done
 +              check_merge_bases "$_bad" "$_good" "$_skip" || return
 +      fi
 +
 +      : > "$GIT_DIR/BISECT_ANCESTORS_OK"
 +
 +      return 0
 +}
 +
  bisect_next() {
        case "$#" in 0) ;; *) usage ;; esac
        bisect_autostart
        bisect_next_check good
  
 +      # Get bad, good and skipped revs
 +      bad=$(git rev-parse --verify refs/bisect/bad) &&
 +      good=$(git for-each-ref --format='^%(objectname)' \
 +              "refs/bisect/good-*" | tr '\012' ' ') &&
        skip=$(git for-each-ref --format='%(objectname)' \
                "refs/bisect/skip-*" | tr '\012' ' ') || exit
  
 +      # Maybe some merge bases must be tested first
 +      check_good_are_ancestors_of_bad "$bad" "$good" "$skip"
 +      # Return now if a checkout has already been done
 +      test "$?" -eq "1" && return
 +
 +      # Get bisection information
        BISECT_OPT=''
        test -n "$skip" && BISECT_OPT='--bisect-all'
 -
 -      bad=$(git rev-parse --verify refs/bisect/bad) &&
 -      good=$(git for-each-ref --format='^%(objectname)' \
 -              "refs/bisect/good-*" | tr '\012' ' ') &&
        eval="git rev-list --bisect-vars $BISECT_OPT $good $bad --" &&
        eval="$eval $(cat "$GIT_DIR/BISECT_NAMES")" &&
        eval=$(filter_skipped "$eval" "$skip") &&
        # commit is also a "skip" commit (see above).
        exit_if_skipped_commits "$bisect_rev"
  
 -      echo "Bisecting: $bisect_nr revisions left to test after this"
 -      git checkout -q "$bisect_rev" || exit
 -      git show-branch "$bisect_rev"
 +      bisect_checkout "$bisect_rev" "$bisect_nr revisions left to test after this"
  }
  
  bisect_visualize() {
  
        if test $# = 0
        then
 -              case "${DISPLAY+set}${MSYSTEM+set}${SECURITYSESSIONID+set}" in
 +              case "${DISPLAY+set}${SESSIONNAME+set}${MSYSTEM+set}${SECURITYSESSIONID+set}" in
                '')     set git log ;;
                set*)   set gitk ;;
                esac
@@@ -547,8 -421,6 +553,8 @@@ bisect_clean_state() 
        do
                git update-ref -d $ref $hash || exit
        done
 +      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" &&
@@@ -645,10 -517,8 +651,10 @@@ case "$#" i
          git bisect -h ;;
      start)
          bisect_start "$@" ;;
 -    bad|good|skip)
 +    bad|good)
          bisect_state "$cmd" "$@" ;;
 +    skip)
 +        bisect_skip "$@" ;;
      next)
          # Not sure we want "next" at the UI level anymore.
          bisect_next "$@" ;;
index dd7eac84ea191fb797075eca3706498edad68b32,0b81e65aa3897f40f4ef420bae7ed4f7bf4c5675..052a6c90f5a184ddc82f2db1a2907a1b1104166c
@@@ -216,6 -216,31 +216,31 @@@ test_expect_success 'bisect skip: canno
        else
                test $? -eq 2 &&
                grep "first bad commit could be any of" my_bisect_log.txt &&
+               ! grep $HASH1 my_bisect_log.txt &&
+               ! grep $HASH2 my_bisect_log.txt &&
+               grep $HASH3 my_bisect_log.txt &&
+               grep $HASH4 my_bisect_log.txt &&
+               git bisect reset
+       fi
+ '
+ # $HASH1 is good, $HASH4 is both skipped and bad, we skip $HASH3
+ # and $HASH2 is good,
+ # so we should not be able to tell the first bad commit
+ # among $HASH3 and $HASH4
+ test_expect_success 'bisect skip: with commit both bad and skipped' '
+       git bisect start &&
+       git bisect skip &&
+       git bisect bad &&
+       git bisect good $HASH1 &&
+       git bisect skip &&
+       if git bisect good > my_bisect_log.txt
+       then
+               echo Oops, should have failed.
+               false
+       else
+               test $? -eq 2 &&
+               grep "first bad commit could be any of" my_bisect_log.txt &&
                ! grep $HASH1 my_bisect_log.txt &&
                ! grep $HASH2 my_bisect_log.txt &&
                grep $HASH3 my_bisect_log.txt &&
@@@ -313,25 -338,8 +338,25 @@@ test_expect_success 'bisect run & skip
        grep "$HASH6 is first bad commit" my_bisect_log.txt
  '
  
 -test_expect_success 'bisect starting with a detached HEAD' '
 +test_expect_success 'bisect skip only one range' '
 +      git bisect reset &&
 +      git bisect start $HASH7 $HASH1 &&
 +      git bisect skip $HASH1..$HASH5 &&
 +      test "$HASH6" = "$(git rev-parse --verify HEAD)" &&
 +      test_must_fail git bisect bad > my_bisect_log.txt &&
 +      grep "first bad commit could be any of" my_bisect_log.txt
 +'
  
 +test_expect_success 'bisect skip many ranges' '
 +      git bisect start $HASH7 $HASH1 &&
 +      test "$HASH4" = "$(git rev-parse --verify HEAD)" &&
 +      git bisect skip $HASH2 $HASH2.. ..$HASH5 &&
 +      test "$HASH6" = "$(git rev-parse --verify HEAD)" &&
 +      test_must_fail git bisect bad > my_bisect_log.txt &&
 +      grep "first bad commit could be any of" my_bisect_log.txt
 +'
 +
 +test_expect_success 'bisect starting with a detached HEAD' '
        git bisect reset &&
        git checkout master^ &&
        HEAD=$(git rev-parse --verify HEAD) &&
@@@ -367,120 -375,6 +392,120 @@@ test_expect_success 'bisect does not cr
        git branch -D bisect
  '
  
 +# This creates a "side" branch to test "siblings" cases.
 +#
 +# H1-H2-H3-H4-H5-H6-H7  <--other
 +#            \
 +#             S5-S6-S7  <--side
 +#
 +test_expect_success 'side branch creation' '
 +      git bisect reset &&
 +      git checkout -b side $HASH4 &&
 +      add_line_into_file "5(side): first line on a side branch" hello2 &&
 +      SIDE_HASH5=$(git rev-parse --verify HEAD) &&
 +      add_line_into_file "6(side): second line on a side branch" hello2 &&
 +      SIDE_HASH6=$(git rev-parse --verify HEAD) &&
 +      add_line_into_file "7(side): third line on a side branch" hello2 &&
 +      SIDE_HASH7=$(git rev-parse --verify HEAD)
 +'
 +
 +test_expect_success 'good merge base when good and bad are siblings' '
 +      git bisect start "$HASH7" "$SIDE_HASH7" > my_bisect_log.txt &&
 +      grep "merge base must be tested" my_bisect_log.txt &&
 +      grep $HASH4 my_bisect_log.txt &&
 +      git bisect good > my_bisect_log.txt &&
 +      test_must_fail grep "merge base must be tested" my_bisect_log.txt &&
 +      grep $HASH6 my_bisect_log.txt &&
 +      git bisect reset
 +'
 +test_expect_success 'skipped merge base when good and bad are siblings' '
 +      git bisect start "$SIDE_HASH7" "$HASH7" > my_bisect_log.txt &&
 +      grep "merge base must be tested" my_bisect_log.txt &&
 +      grep $HASH4 my_bisect_log.txt &&
 +      git bisect skip > my_bisect_log.txt 2>&1 &&
 +      grep "Warning" my_bisect_log.txt &&
 +      grep $SIDE_HASH6 my_bisect_log.txt &&
 +      git bisect reset
 +'
 +
 +test_expect_success 'bad merge base when good and bad are siblings' '
 +      git bisect start "$HASH7" HEAD > my_bisect_log.txt &&
 +      grep "merge base must be tested" my_bisect_log.txt &&
 +      grep $HASH4 my_bisect_log.txt &&
 +      test_must_fail git bisect bad > my_bisect_log.txt 2>&1 &&
 +      grep "merge base $HASH4 is bad" my_bisect_log.txt &&
 +      grep "fixed between $HASH4 and \[$SIDE_HASH7\]" my_bisect_log.txt &&
 +      git bisect reset
 +'
 +
 +# This creates a few more commits (A and B) to test "siblings" cases
 +# when a good and a bad rev have many merge bases.
 +#
 +# We should have the following:
 +#
 +# H1-H2-H3-H4-H5-H6-H7
 +#            \  \     \
 +#             S5-A     \
 +#              \        \
 +#               S6-S7----B
 +#
 +# And there A and B have 2 merge bases (S5 and H5) that should be
 +# reported by "git merge-base --all A B".
 +#
 +test_expect_success 'many merge bases creation' '
 +      git checkout "$SIDE_HASH5" &&
 +      git merge -m "merge HASH5 and SIDE_HASH5" "$HASH5" &&
 +      A_HASH=$(git rev-parse --verify HEAD) &&
 +      git checkout side &&
 +      git merge -m "merge HASH7 and SIDE_HASH7" "$HASH7" &&
 +      B_HASH=$(git rev-parse --verify HEAD) &&
 +      git merge-base --all "$A_HASH" "$B_HASH" > merge_bases.txt &&
 +      test $(wc -l < merge_bases.txt) = "2" &&
 +      grep "$HASH5" merge_bases.txt &&
 +      grep "$SIDE_HASH5" merge_bases.txt
 +'
 +
 +test_expect_success 'good merge bases when good and bad are siblings' '
 +      git bisect start "$B_HASH" "$A_HASH" > my_bisect_log.txt &&
 +      grep "merge base must be tested" my_bisect_log.txt &&
 +      git bisect good > my_bisect_log2.txt &&
 +      grep "merge base must be tested" my_bisect_log2.txt &&
 +      {
 +              {
 +                      grep "$SIDE_HASH5" my_bisect_log.txt &&
 +                      grep "$HASH5" my_bisect_log2.txt
 +              } || {
 +                      grep "$SIDE_HASH5" my_bisect_log2.txt &&
 +                      grep "$HASH5" my_bisect_log.txt
 +              }
 +      } &&
 +      git bisect reset
 +'
 +
 +check_trace() {
 +      grep "$1" "$GIT_TRACE" | grep "\^$2" | grep "$3" >/dev/null
 +}
 +
 +test_expect_success 'optimized merge base checks' '
 +      GIT_TRACE="$(pwd)/trace.log" &&
 +      export GIT_TRACE &&
 +      git bisect start "$HASH7" "$SIDE_HASH7" > my_bisect_log.txt &&
 +      grep "merge base must be tested" my_bisect_log.txt &&
 +      grep "$HASH4" my_bisect_log.txt &&
 +      check_trace "rev-list" "$HASH7" "$SIDE_HASH7" &&
 +      git bisect good > my_bisect_log2.txt &&
 +      test -f ".git/BISECT_ANCESTORS_OK" &&
 +      test "$HASH6" = $(git rev-parse --verify HEAD) &&
 +      : > "$GIT_TRACE" &&
 +      git bisect bad > my_bisect_log3.txt &&
 +      test_must_fail check_trace "rev-list" "$HASH6" "$SIDE_HASH7" &&
 +      git bisect good "$A_HASH" > my_bisect_log4.txt &&
 +      grep "merge base must be tested" my_bisect_log4.txt &&
 +      test_must_fail test -f ".git/BISECT_ANCESTORS_OK" &&
 +      check_trace "rev-list" "$HASH6" "$A_HASH" &&
 +      unset GIT_TRACE
 +'
 +
  #
  #
  test_done