|
| 1 | +#!/bin/bash |
| 2 | +# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. |
| 3 | +# |
| 4 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | +# you may not use this file except in compliance with the License. |
| 6 | +# You may obtain a copy of the License at |
| 7 | +# |
| 8 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | +# |
| 10 | +# Unless required by applicable law or agreed to in writing, software |
| 11 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | +# See the License for the specific language governing permissions and |
| 14 | +# limitations under the License. |
| 15 | + |
| 16 | +set -euo pipefail |
| 17 | + |
| 18 | +# When we bisect, we need to ensure that the venvs are refreshed b/c the commit could |
| 19 | +# habe changed the uv.lock or 3rdparty submoduels, so we need to force a rebuild to be safe |
| 20 | +export NRL_FORCE_REBUILD_VENVS=true |
| 21 | +print_usage() { |
| 22 | + cat <<EOF |
| 23 | +Usage: GOOD=<good_ref> BAD=<bad_ref> tools/bisect-script.sh [command ...] |
| 24 | +
|
| 25 | +Runs a git bisect session between GOOD and BAD to find the first bad commit. |
| 26 | +Sets NRL_FORCE_REBUILD_VENVS=true to ensure test environments are rebuilt to match commit's uv.lock. |
| 27 | +
|
| 28 | +Examples: |
| 29 | + GOOD=56a6225 BAD=32faafa tools/bisect-script.sh uv run --group dev pre-commit run --all-files |
| 30 | + GOOD=464ed38 BAD=c843f1b tools/bisect-script.sh uv run --group test pytest tests/unit/test_foobar.py |
| 31 | +
|
| 32 | + # Example ouptut: |
| 33 | + # 1. Will run until hits the first bad commit. |
| 34 | + # 2. Will show the bisect log (what was run) and visualize the bisect. |
| 35 | + # 3. Reset git bisect state to return you to the git state you were originally. |
| 36 | + # |
| 37 | + # 25e05a3d557dfe59a14df43048e16b6eea04436e is the first bad commit |
| 38 | + # commit 25e05a3d557dfe59a14df43048e16b6eea04436e |
| 39 | + # Author: Terry Kong <terryk@nvidia.com> |
| 40 | + # Date: Fri Sep 26 17:24:45 2025 +0000 |
| 41 | + # |
| 42 | + # 3==4 |
| 43 | + # |
| 44 | + # Signed-off-by: Terry Kong <terryk@nvidia.com> |
| 45 | + # |
| 46 | + # tests/unit/test_foobar.py | 2 +- |
| 47 | + # 1 file changed, 1 insertion(+), 1 deletion(-) |
| 48 | + # bisect found first bad commit |
| 49 | + # + RUN_STATUS=0 |
| 50 | + # + set +x |
| 51 | + # [bisect] --- bisect log --- |
| 52 | + # # bad: [c843f1b994cb7e331aa8bc41c3206a6e76e453ef] try echo |
| 53 | + # # good: [464ed38e68dcd23f0c1951784561dc8c78410ffe] add passing foobar |
| 54 | + # git bisect start 'c843f1b' '464ed38' |
| 55 | + # # good: [8b8b3961e9cdbc1b4a9b6a912f7d36d117952f62] try visualize |
| 56 | + # git bisect good 8b8b3961e9cdbc1b4a9b6a912f7d36d117952f62 |
| 57 | + # # bad: [25e05a3d557dfe59a14df43048e16b6eea04436e] 3==4 |
| 58 | + # git bisect bad 25e05a3d557dfe59a14df43048e16b6eea04436e |
| 59 | + # # good: [c82e0b69d52b8e1641226c022cb487afebe8ba99] 2==2 |
| 60 | + # git bisect good c82e0b69d52b8e1641226c022cb487afebe8ba99 |
| 61 | + # # first bad commit: [25e05a3d557dfe59a14df43048e16b6eea04436e] 3==4 |
| 62 | + # [bisect] --- bisect visualize (oneline) --- |
| 63 | + # 25e05a3d (HEAD) 3==4 |
| 64 | +
|
| 65 | +Exit codes inside the command determine good/bad: |
| 66 | + 0 -> good commit |
| 67 | + non-zero -> bad commit |
| 68 | + 125 -> skip this commit (per git-bisect convention) |
| 69 | +
|
| 70 | +Environment variables: |
| 71 | + GOOD Commit-ish known to be good (required) |
| 72 | + BAD Commit-ish suspected bad (required) |
| 73 | + (The script will automatically restore the repo state with 'git bisect reset' on exit.) |
| 74 | +
|
| 75 | +Notes: |
| 76 | + - The working tree will be reset by git bisect. Ensure you have no uncommitted changes. |
| 77 | + - If GOOD is an ancestor of BAD with 0 or 1 commits in between, git can |
| 78 | + conclude immediately; the script will show the result and exit without |
| 79 | + running your command. |
| 80 | +EOF |
| 81 | +} |
| 82 | + |
| 83 | +# Minimal color helpers: blue for info, red for errors (TTY-only; NO_COLOR disables) |
| 84 | +BLUE=""; RED=""; NC="" |
| 85 | +if [[ -z "${NO_COLOR:-}" ]] && { [[ -t 1 ]] || [[ -t 2 ]]; }; then |
| 86 | + BLUE=$'\033[34m' |
| 87 | + RED=$'\033[31m' |
| 88 | + NC=$'\033[0m' |
| 89 | +fi |
| 90 | + |
| 91 | +iecho() { printf "%b%s%b\n" "$BLUE" "$*" "$NC"; } |
| 92 | +fecho() { printf "%b%s%b\n" "$RED" "$*" "$NC" >&2; } |
| 93 | + |
| 94 | +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then |
| 95 | + print_usage |
| 96 | + exit 0 |
| 97 | +fi |
| 98 | + |
| 99 | +if [[ -z "${GOOD:-}" || -z "${BAD:-}" ]]; then |
| 100 | + fecho "ERROR: GOOD and BAD environment variables are required." |
| 101 | + echo >&2 |
| 102 | + print_usage >&2 |
| 103 | + exit 2 |
| 104 | +fi |
| 105 | + |
| 106 | +if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then |
| 107 | + fecho "ERROR: Not inside a git repository." |
| 108 | + exit 2 |
| 109 | +fi |
| 110 | + |
| 111 | +# Ensure there is a command to run |
| 112 | +if [[ $# -lt 1 ]]; then |
| 113 | + fecho "ERROR: Missing command to evaluate during bisect." |
| 114 | + echo >&2 |
| 115 | + print_usage >&2 |
| 116 | + exit 2 |
| 117 | +fi |
| 118 | + |
| 119 | +USER_CMD=("$@") |
| 120 | + |
| 121 | +# Require a clean working tree |
| 122 | +git update-index -q --refresh || true |
| 123 | +if ! git diff --quiet; then |
| 124 | + fecho "ERROR: Unstaged changes present. Commit or stash before bisect." |
| 125 | + exit 2 |
| 126 | +fi |
| 127 | +if ! git diff --cached --quiet; then |
| 128 | + fecho "ERROR: Staged changes present. Commit or stash before bisect." |
| 129 | + exit 2 |
| 130 | +fi |
| 131 | + |
| 132 | +# On interruption or script error, print helpful message |
| 133 | +on_interrupt_or_error() { |
| 134 | + local status=$? |
| 135 | + if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then |
| 136 | + if git bisect log >/dev/null 2>&1; then |
| 137 | + iecho "[bisect] Script interrupted or failed (exit ${status})." |
| 138 | + iecho "[bisect] Restoring original state with 'git bisect reset' on exit." |
| 139 | + fi |
| 140 | + fi |
| 141 | +} |
| 142 | +trap on_interrupt_or_error INT TERM ERR |
| 143 | + |
| 144 | +# Always reset bisect on exit to restore original state |
| 145 | +cleanup_reset() { |
| 146 | + if [[ -n "${BISECT_NO_RESET:-}" ]]; then |
| 147 | + # Respect user's request to not reset the bisect |
| 148 | + return |
| 149 | + fi |
| 150 | + if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then |
| 151 | + if git bisect log >/dev/null 2>&1; then |
| 152 | + git bisect reset >/dev/null 2>&1 || true |
| 153 | + fi |
| 154 | + fi |
| 155 | +} |
| 156 | +trap cleanup_reset EXIT |
| 157 | + |
| 158 | +# Check if we are already in a bisect session |
| 159 | +if git bisect log >/dev/null 2>&1; then |
| 160 | + fecho "[bisect] We are already in a bisect session. Please reset the bisect manually if you want to start a new one." |
| 161 | + exit 1 |
| 162 | +fi |
| 163 | + |
| 164 | +set -x |
| 165 | +git bisect start "$BAD" "$GOOD" |
| 166 | +set +x |
| 167 | + |
| 168 | +# Detect immediate conclusion (no midpoints to test) |
| 169 | +if git bisect log >/dev/null 2>&1; then |
| 170 | + if git bisect log | grep -q "first bad commit:"; then |
| 171 | + iecho "[bisect] Immediate conclusion from endpoints; no midpoints to test." |
| 172 | + iecho "[bisect] --- bisect log ---" |
| 173 | + git bisect log | cat |
| 174 | + exit 0 |
| 175 | + fi |
| 176 | +fi |
| 177 | + |
| 178 | +set -x |
| 179 | +set +e # Temporarily allow the command to fail to capture the exit status |
| 180 | +git bisect run "${USER_CMD[@]}" |
| 181 | +RUN_STATUS=$? |
| 182 | +set -e |
| 183 | +set +x |
| 184 | + |
| 185 | +# Show bisect details before cleanup |
| 186 | +if git bisect log >/dev/null 2>&1; then |
| 187 | + iecho "[bisect] --- bisect log ---" |
| 188 | + git bisect log | cat |
| 189 | +fi |
| 190 | + |
| 191 | +exit $RUN_STATUS |
| 192 | + |
| 193 | + |
0 commit comments