#!/bin/sh
-USAGE='[start|bad|good|skip|next|reset|visualize|replay|log|run]'
-LONG_USAGE='git bisect start [<bad> [<good>...]] [--] [<pathspec>...]
+USAGE='[help|start|bad|good|skip|next|reset|visualize|replay|log|run]'
+LONG_USAGE='git bisect help
+ print this long help message.
+git bisect start [<bad> [<good>...]] [--] [<pathspec>...]
reset bisect state and start bisection.
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.
git bisect log
show bisect log.
git bisect run <cmd>...
- use <cmd>... to automatically bisect.'
+ use <cmd>... to automatically bisect.
+
+Please use "git help bisect" to get the full man page.'
OPTIONS_SPEC=
. git-sh-setup
}
bisect_autostart() {
- test -f "$GIT_DIR/BISECT_NAMES" || {
+ test -s "$GIT_DIR/BISECT_START" || {
echo >&2 'You need to start by "git bisect start"'
if test -t 0
then
bisect_start() {
#
- # Verify HEAD. If we were bisecting before this, reset to the
- # top-of-line master first!
+ # Verify HEAD.
#
head=$(GIT_DIR="$GIT_DIR" git symbolic-ref -q HEAD) ||
head=$(GIT_DIR="$GIT_DIR" git rev-parse --verify HEAD) ||
die "Bad HEAD - I need a HEAD"
+
#
- # Check that we either already have BISECT_START, or that the
- # branches bisect, new-bisect don't exist, to not override them.
+ # Check if we are bisecting.
#
- test -s "$GIT_DIR/BISECT_START" ||
- if git show-ref --verify -q refs/heads/bisect ||
- git show-ref --verify -q refs/heads/new-bisect; then
- die 'The branches "bisect" and "new-bisect" must not exist.'
- fi
start_head=''
- case "$head" in
- refs/heads/bisect)
- branch=`cat "$GIT_DIR/BISECT_START"`
- git checkout $branch || exit
- ;;
- refs/heads/*|$_x40)
- # This error message should only be triggered by cogito usage,
- # and cogito users should understand it relates to cg-seek.
- [ -s "$GIT_DIR/head-name" ] && die "won't bisect on seeked tree"
- start_head="${head#refs/heads/}"
- ;;
- *)
- die "Bad HEAD - strange symbolic ref"
- ;;
- esac
+ if test -s "$GIT_DIR/BISECT_START"
+ then
+ # Reset to the rev from where we started.
+ start_head=$(cat "$GIT_DIR/BISECT_START")
+ git checkout "$start_head" || exit
+ else
+ # Get rev from where we start.
+ case "$head" in
+ refs/heads/*|$_x40)
+ # This error message should only be triggered by
+ # cogito usage, and cogito users should understand
+ # it relates to cg-seek.
+ [ -s "$GIT_DIR/head-name" ] &&
+ die "won't bisect on seeked tree"
+ start_head="${head#refs/heads/}"
+ ;;
+ *)
+ die "Bad HEAD - strange symbolic ref"
+ ;;
+ esac
+ fi
#
- # Get rid of any old bisect state
+ # Get rid of any old bisect state.
#
- bisect_clean_state
+ bisect_clean_state || exit
#
# Check for one bad and then some good revisions.
break
;;
*)
- rev=$(git rev-parse --verify "$arg^{commit}" 2>/dev/null) || {
+ rev=$(git rev-parse -q --verify "$arg^{commit}") || {
test $has_double_dash -eq 1 &&
die "'$arg' does not appear to be a valid revision"
break
esac
done
- sq "$@" >"$GIT_DIR/BISECT_NAMES"
- test -n "$start_head" && echo "$start_head" >"$GIT_DIR/BISECT_START"
- eval "$eval"
- echo "git-bisect start$orig_args" >>"$GIT_DIR/BISECT_LOG"
+ #
+ # 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" &&
+ sq "$@" >"$GIT_DIR/BISECT_NAMES" &&
+ eval "$eval" &&
+ 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
}
bisect_write() {
good|skip) tag="$state"-"$rev" ;;
*) die "Bad bisect_write argument: $state" ;;
esac
- git update-ref "refs/bisect/$tag" "$rev"
+ git update-ref "refs/bisect/$tag" "$rev" || exit
echo "# $state: $(git show-branch $rev)" >>"$GIT_DIR/BISECT_LOG"
- test -z "$nolog" && echo "git-bisect $state $rev" >>"$GIT_DIR/BISECT_LOG"
+ 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() {
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." ;;
*)
if test -t 0
then
printf >&2 'Are you sure [Y/n]? '
- case "$(read yesno)" in [Nn]*) exit 1 ;; esac
+ read yesno
+ case "$yesno" in [Nn]*) exit 1 ;; esac
fi
: bisect without good...
;;
*)
THEN=''
- test -f "$GIT_DIR/BISECT_NAMES" || {
+ test -s "$GIT_DIR/BISECT_START" || {
echo >&2 'You need to start by "git bisect start".'
THEN='then '
}
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" | {
+ while read line
+ do
+ echo "$line &&"
+ done
+ echo ':'
+ }
return
fi
# Let's parse the output of:
# "git rev-list --bisect-vars --bisect-all ..."
- eval_rev_list "$_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 "$_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 branch -D new-bisect 2> /dev/null
- git checkout -q -b new-bisect "$bisect_rev" || exit
- git branch -M new-bisect bisect
- git show-branch "$bisect_rev"
+ bisect_checkout "$bisect_rev" "$bisect_nr revisions left to test after this (roughly $bisect_steps steps)"
}
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
}
bisect_reset() {
- test -f "$GIT_DIR/BISECT_NAMES" || {
+ test -s "$GIT_DIR/BISECT_START" || {
echo "We are not bisecting."
return
}
case "$#" in
- 0) if [ -s "$GIT_DIR/BISECT_START" ]; then
- branch=`cat "$GIT_DIR/BISECT_START"`
- else
- branch=master
- fi ;;
+ 0) branch=$(cat "$GIT_DIR/BISECT_START") ;;
1) git show-ref --verify --quiet -- "refs/heads/$1" ||
die "$1 does not seem to be a valid branch"
branch="$1" ;;
*)
usage ;;
esac
- if git checkout "$branch"; then
- # Cleanup head-name if it got left by an old version of git-bisect
- rm -f "$GIT_DIR/head-name"
- rm -f "$GIT_DIR/BISECT_START"
- bisect_clean_state
- fi
+ git checkout "$branch" && bisect_clean_state
}
bisect_clean_state() {
# There may be some refs packed during bisection.
- git for-each-ref --format='%(refname) %(objectname)' refs/bisect/\* refs/heads/bisect |
+ git for-each-ref --format='%(refname) %(objectname)' refs/bisect/\* |
while read ref hash
do
- git update-ref -d $ref $hash
+ git update-ref -d $ref $hash || exit
done
- rm -f "$GIT_DIR/BISECT_LOG"
- rm -f "$GIT_DIR/BISECT_NAMES"
- rm -f "$GIT_DIR/BISECT_RUN"
+ 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" &&
+ # Cleanup head-name if it got left by an old version of git-bisect
+ rm -f "$GIT_DIR/head-name" &&
+
+ rm -f "$GIT_DIR/BISECT_START"
}
bisect_replay () {
test -r "$1" || die "cannot read $1 for replaying"
bisect_reset
- while read bisect command rev
+ while read git bisect command rev
do
- test "$bisect" = "git-bisect" || continue
+ test "$git $bisect" = "git bisect" -o "$git" = "git-bisect" || continue
+ if test "$git" = "git-bisect"; then
+ rev="$command"
+ command="$bisect"
+ fi
case "$command" in
start)
cmd="bisect_start $rev"
cmd="$1"
shift
case "$cmd" in
+ help)
+ 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 "$@" ;;