搜索 K
主题
git bisect 是 Git 内置的二分查找工具,用于在提交历史中快速定位引入 bug 的那个提交。它通过不断将搜索范围缩小一半,在 O(log n) 的时间复杂度内找到问题提交——1000 个提交只需约 10 次测试。
git bisect 就像猜数字游戏——Git 每次选一个中间的提交让你测试,根据结果缩小范围,直到找到「第一个坏的提交」。
git bisect start [<bad> [<good>...]]
git bisect good [<rev>]
git bisect bad [<rev>]
git bisect reset [<commit>]
git bisect skip [<rev>...]
git bisect run <cmd> [<arg>...]
git bisect log
git bisect replay <logfile>| 提交数量 | 最多测试次数 | 计算方式 |
|---|---|---|
| 10 | 4 | ⌈log₂(10)⌉ |
| 100 | 7 | ⌈log₂(100)⌉ |
| 1,000 | 10 | ⌈log₂(1000)⌉ |
| 10,000 | 14 | ⌈log₂(10000)⌉ |
| 100,000 | 17 | ⌈log₂(100000)⌉ |
线性查找 1000 个提交最坏需要 1000 次测试。bisect 只需 10 次,因为每次测试都将搜索范围缩小一半。
# 1. 开始 bisect
git bisect start
# 2. 标记当前版本为 bad(有 bug)
git bisect bad
# 3. 标记一个已知没问题的版本为 good
git bisect good v2.0.0
# 或使用 SHA
git bisect good abc1234
# Git 输出类似:
# Bisecting: 512 revisions left to test after this (roughly 9 steps)
# [def5678...] Some commit message
# 4. 测试当前检出的代码
npm test
# 或手动测试
# 5. 根据结果标记
git bisect good # 这个版本没有 bug
# 或
git bisect bad # 这个版本有 bug
# 6. 重复步骤 4-5,直到 Git 找到问题提交
# abc1234 is the first bad commit
# commit abc1234
# Author: 张三 <zhangsan@example.com>
# Date: Mon Mar 3 10:00:00 2026 +0800
# feat: 新的支付流程
# 7. 结束 bisect,回到原始分支
git bisect reset# 一行命令启动 bisect 并标记 bad/good
git bisect start HEAD v2.0.0
# 等价于:
# git bisect start
# git bisect bad HEAD
# git bisect good v2.0.0
# 也可以指定多个 good
git bisect start HEAD v2.0.0 v1.9.0old 和 new 替代 good 和 bad 当查找的不是 bug,而是某个行为变化(无所谓好坏)时,使用 old / new 更语义化:
# 查找「什么时候性能下降了」
git bisect start --term-old=fast --term-new=slow
# 或使用默认的 old/new
git bisect start
git bisect old v2.0.0 # 旧行为(快)
git bisect new HEAD # 新行为(慢)某些提交可能无法编译或测试(比如 WIP 提交),可以跳过:
# 当前检出的提交无法编译
git bisect skip
# 跳过指定提交
git bisect skip abc1234
# 跳过一个范围
git bisect skip abc1234..def5678如果跳过的提交正好在「第一个坏提交」附近,Git 可能无法精确定位问题提交,会返回一个可能的范围而非确切的提交。
git bisect run) 手动 bisect 需要反复测试和标记,git bisect run 可以全自动完成。
# 使用测试脚本自动 bisect
git bisect start HEAD v2.0.0
git bisect run npm test
# Git 自动执行:
# 1. 检出中间提交
# 2. 运行 npm test
# 3. 根据退出码判断 good/bad(0=good, 非0=bad)
# 4. 重复直到找到问题提交| 退出码 | 含义 |
|---|---|
| 0 | good(当前版本没问题) |
| 1-124 | bad(当前版本有问题) |
| 125 | skip(当前版本无法测试) |
| 126 | 脚本不可执行(bisect 报错) |
| 127 | 命令未找到(bisect 报错) |
| 128+ | bisect 报错并中止 |
# 创建 bisect 测试脚本
cat > /tmp/bisect-test.sh << 'EOF'
#!/bin/bash
# 1. 尝试构建项目
npm run build 2>/dev/null
if [ $? -ne 0 ]; then
exit 125 # 无法构建,跳过
fi
# 2. 运行特定的测试
npm test -- --testPathPattern="payment" 2>/dev/null
exit $? # 测试结果作为 bisect 判断依据
EOF
chmod +x /tmp/bisect-test.sh
# 运行自动 bisect
git bisect start HEAD v2.0.0
git bisect run /tmp/bisect-test.shgrep 检测特定代码变更 # 查找「什么时候引入了某个函数调用」
git bisect start HEAD v2.0.0
git bisect run sh -c 'grep -q "dangerousFunction" src/main.ts && exit 1 || exit 0'
# 存在 dangerousFunction → bad (exit 1)
# 不存在 → good (exit 0)# 只运行与问题相关的单个测试
git bisect start HEAD v2.0.0
git bisect run npx jest --testPathPattern="auth.test" --bail
# Python 项目
git bisect run python -m pytest tests/test_payment.py -x
# Go 项目
git bisect run go test ./pkg/auth/... -run TestLoginTimeout应用的 API 响应时间从 100ms 变成了 500ms,但不知道是哪次提交导致的:
# 创建性能测试脚本
cat > /tmp/perf-test.sh << 'EOF'
#!/bin/bash
# 构建项目
npm run build 2>/dev/null || exit 125
# 启动服务
npm start &
SERVER_PID=$!
sleep 3
# 测试响应时间
RESPONSE_TIME=$(curl -s -o /dev/null -w "%{time_total}" http://localhost:3000/api/users)
# 关闭服务
kill $SERVER_PID 2>/dev/null
# 判断:响应时间 > 0.3s 视为 bad
if (( $(echo "$RESPONSE_TIME > 0.3" | bc -l) )); then
exit 1 # bad
else
exit 0 # good
fi
EOF
chmod +x /tmp/perf-test.sh
git bisect start HEAD v2.0.0
git bisect run /tmp/perf-test.sh按钮颜色突然变了,自动化测试不覆盖这种视觉问题:
# 手动 bisect(UI 问题通常需要手动查看)
git bisect start HEAD v2.0.0
# 每次检出后启动开发服务器
npm run dev
# 手动检查按钮颜色
# 颜色正确
git bisect good
# 颜色错误
git bisect badgit log --all --oneline -- src/components/Button.css 缩小可疑范围项目突然无法编译,但 CI 上最近 50 个提交都没有单独导致编译失败:
# 构建失败通常是多个修改累积的结果
git bisect start HEAD v2.0.0
git bisect run sh -c 'npm run build 2>/dev/null'
# 如果某些提交的 package.json 变了,需要重新安装依赖
git bisect start HEAD v2.0.0
git bisect run sh -c 'npm ci && npm run build'某个测试时好时坏,用 bisect 找到引入不稳定性的提交:
# 多次运行测试,判断是否 flaky
cat > /tmp/flaky-test.sh << 'EOF'
#!/bin/bash
npm run build 2>/dev/null || exit 125
# 运行 10 次测试,如果有任何一次失败就标记为 bad
for i in {1..10}; do
npm test -- --testPathPattern="flaky-test" 2>/dev/null
if [ $? -ne 0 ]; then
exit 1 # bad:测试不稳定
fi
done
exit 0 # good:10 次都通过
EOF
chmod +x /tmp/flaky-test.sh
git bisect start HEAD v2.0.0
git bisect run /tmp/flaky-test.shgit revert 修复 bisect 找到问题提交后,立即 revert:
# 1. bisect 找到问题提交
git bisect start HEAD v2.0.0
git bisect run npm test
# abc1234 is the first bad commit
# 2. 结束 bisect
git bisect reset
# 3. 查看问题提交的内容
git show abc1234
# 4. revert 问题提交
git revert --no-edit abc1234
git push origin main详见 Git revert
monorepo 中提交很多,可以通过路径过滤缩小 bisect 范围:
# 只关注特定目录的修改
git bisect start HEAD v2.0.0 -- packages/payment/
# 这会让 Git 只考虑修改了 packages/payment/ 的提交
# 大幅减少需要测试的提交数量# 保存当前 bisect 过程
git bisect log > bisect-log.txt
# 在同一或另一个仓库中回放
git bisect replay bisect-log.txtbisect-log.txt 内容示例:
git bisect start
# good: [abc1234...] v2.0.0
git bisect good abc1234
# bad: [def5678...] HEAD
git bisect bad def5678
# good: [111222...] Some commit
git bisect good 111222
# bad: [333444...] Another commit
git bisect bad 333444# 查看当前 bisect 状态
git bisect visualize
# 在 gitk 中显示剩余的搜索范围
# 如果没有 gitk,使用 log
git bisect visualize --oneline
# 或
git log --oneline --decorate refs/bisect/bad --not refs/bisect/good-*git stash # bisect 过程中需要临时修改(如修改配置文件)
git stash push -m "bisect 临时配置"
# ... 测试 ...
git stash pop
# 或在测试脚本中处理
cat > /tmp/bisect-with-config.sh << 'EOF'
#!/bin/bash
# 临时修改配置
cp /tmp/test-config.json ./config.json
# 构建和测试
npm run build && npm test
RESULT=$?
# 恢复原始配置
git checkout -- config.json
exit $RESULT
EOFgit blame 深入分析 bisect 找到问题提交后,用 blame 进一步分析具体哪行代码有问题:
# bisect 定位到 abc1234
git show abc1234 --stat
# 修改了 3 个文件
# 用 blame 查看具体修改
git blame -L 100,120 src/payment.ts
# 看每一行是哪个提交引入的# 查看 good 和 bad 之间有多少提交
git log --oneline v2.0.0..HEAD | wc -l
# 256 个提交 → 约 8 步
# 或让 bisect start 时告诉你
git bisect start HEAD v2.0.0
# Bisecting: 128 revisions left to test after this (roughly 7 steps)在 bisect 前先用其他手段缩小范围:
# 如果知道 bug 与某个文件相关
git log --oneline -- src/buggy-file.ts
# 找到最近修改这个文件的提交作为 bad 的候选
# 如果知道 bug 与某个关键字相关
git log --oneline -S "problematicFunction"
# 如果知道大致时间范围
git log --oneline --since="2 weeks ago" --until="1 week ago"# 跨越较大版本范围时,依赖可能变化
cat > /tmp/bisect-with-deps.sh << 'EOF'
#!/bin/bash
# 检测 package.json 是否变化,按需重装依赖
if ! git diff --quiet HEAD~1 -- package.json package-lock.json 2>/dev/null; then
npm ci 2>/dev/null || exit 125
fi
npm test
EOF# 在 ~/.gitconfig 中添加
[alias]
# 快速开始 bisect
bs = bisect start
# 标记好/坏
bg = bisect good
bb = bisect bad
# 跳过
bsk = bisect skip
# 重置
br = bisect reset
# 自动 bisect(运行测试)
brun = bisect run
# 查看日志
blog = bisect log
# 使用
git bs HEAD v2.0.0 # 开始
git bg # 标记好
git bb # 标记坏
git br # 结束普通 bisect 需要反复检出不同版本,可以用 worktree 并行测试:
# 创建多个 worktree 进行并行测试
git worktree add ../bisect-test-1 <sha-1>
git worktree add ../bisect-test-2 <sha-2>
git worktree add ../bisect-test-3 <sha-3>
# 在不同终端中同时测试
# 根据结果手动标记 good/bad详见 Git worktree
应用运行一段时间后内存不断增长,需要找到引入泄漏的提交:
cat > /tmp/memory-test.sh << 'EOF'
#!/bin/bash
npm run build 2>/dev/null || exit 125
# 启动应用
node dist/server.js &
PID=$!
sleep 2
# 记录初始内存
MEM_START=$(ps -o rss= -p $PID)
# 发送 1000 个请求模拟负载
for i in $(seq 1 1000); do
curl -s http://localhost:3000/api/data > /dev/null
done
sleep 2
# 记录最终内存
MEM_END=$(ps -o rss= -p $PID)
kill $PID 2>/dev/null
# 计算内存增长比例
GROWTH=$(echo "scale=2; ($MEM_END - $MEM_START) / $MEM_START * 100" | bc)
echo "内存增长: ${GROWTH}%"
# 增长超过 50% 判定为泄漏
if (( $(echo "$GROWTH > 50" | bc -l) )); then
exit 1 # bad
else
exit 0 # good
fi
EOF
chmod +x /tmp/memory-test.sh
git bisect start HEAD v2.0.0
git bisect run /tmp/memory-test.sh某个 npm/pip 依赖更新后出现了问题,但不确定是哪次更新:
# 只关注依赖文件的变更
git bisect start HEAD v2.0.0 -- package.json package-lock.json
# 或使用脚本检测特定依赖版本
cat > /tmp/dep-test.sh << 'EOF'
#!/bin/bash
npm ci 2>/dev/null || exit 125
# 检查特定依赖的行为
node -e "
const pkg = require('./node_modules/problematic-lib/package.json');
console.log('版本:', pkg.version);
" 2>/dev/null || exit 125
npm test -- --testPathPattern="integration" 2>/dev/null
EOF
git bisect start HEAD v2.0.0
git bisect run bash /tmp/dep-test.sh页面布局在某次提交后错位,使用 Puppeteer 自动截图对比:
cat > /tmp/visual-test.sh << 'EOF'
#!/bin/bash
npm run build 2>/dev/null || exit 125
npm start &
PID=$!
sleep 3
# 使用 Puppeteer 截图并与基准图对比
node -e "
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('http://localhost:3000');
await page.screenshot({ path: '/tmp/current.png' });
await browser.close();
})();
" 2>/dev/null
kill $PID 2>/dev/null
# 使用 ImageMagick 对比截图差异
DIFF=$(compare -metric RMSE /tmp/baseline.png /tmp/current.png /tmp/diff.png 2>&1 | head -1 | cut -d'(' -f2 | cut -d')' -f1)
# 差异超过阈值 → bad
if (( $(echo "$DIFF > 0.05" | bc -l) )); then
exit 1
else
exit 0
fi
EOF
git bisect start HEAD v2.0.0
git bisect run bash /tmp/visual-test.sh项目依赖复杂的环境(数据库、Redis 等),使用 Docker 隔离测试环境:
cat > /tmp/docker-test.sh << 'EOF'
#!/bin/bash
# 构建 Docker 镜像
docker build -t bisect-test . 2>/dev/null || exit 125
# 使用 docker-compose 启动完整环境
docker-compose up -d 2>/dev/null || exit 125
sleep 5
# 在容器中运行测试
docker-compose exec -T app npm test
RESULT=$?
# 清理
docker-compose down 2>/dev/null
exit $RESULT
EOF
git bisect start HEAD v2.0.0
git bisect run bash /tmp/docker-test.shdocker-compose down -v 清除数据卷--no-cache 构建以避免缓存导致的误判团队中有人发现了 bug,但没时间做完整的 bisect。可以分工协作:
# 1. 发起人:确定范围并记录
git bisect start HEAD v2.0.0
git bisect bad
git bisect good v2.0.0
# 测试几个中间点...
git bisect good
git bisect bad
# 2. 保存进度
git bisect log > ~/bisect-progress.txt
git bisect reset
# 3. 分享给同事
# "帮我继续 bisect,进度在 bisect-progress.txt 中"
# 4. 同事继续
git bisect replay ~/bisect-progress.txt
# 从上次的进度继续标记 good/bad
git bisect good
# ...
# 5. 找到结果后分享
git bisect log > ~/bisect-complete.txt
git bisect resetgit log --diff-filter 精准定位 # bisect 找到问题提交后,进一步分析修改类型
git show abc1234 --diff-filter=A --name-only # 新增的文件
git show abc1234 --diff-filter=M --name-only # 修改的文件
git show abc1234 --diff-filter=D --name-only # 删除的文件
# 查看哪个函数被修改了(需要语言支持)
git show abc1234 -p --function-contextgit log --bisect 查看搜索路径 # 在 bisect 过程中查看 Git 的搜索路径
git log --bisect --oneline
# 显示 bisect 算法考虑的提交范围#!/bin/bash
# bisect-template.sh — 通用 bisect 测试脚本模板
# 使用方式:git bisect run ./bisect-template.sh
set -e
# ============ 配置区 ============
BUILD_CMD="npm run build"
TEST_CMD="npm test -- --testPathPattern='payment'"
SETUP_CMD="npm ci"
# ================================
# 1. 环境准备(失败则跳过)
if [ -n "$SETUP_CMD" ]; then
eval "$SETUP_CMD" 2>/dev/null || exit 125
fi
# 2. 构建(失败则跳过)
if [ -n "$BUILD_CMD" ]; then
eval "$BUILD_CMD" 2>/dev/null || exit 125
fi
# 3. 测试(结果作为 bisect 判断依据)
eval "$TEST_CMD" 2>/dev/null
exit $?将常用的 bisect 测试脚本保存在项目的 scripts/ 目录中(加入 .gitignore),下次遇到类似问题可以直接复用:
scripts/
bisect-api-test.sh
bisect-performance.sh
bisect-memory-leak.shgit bisect resetbisect 完成后如果不 reset,你会停留在 detached HEAD 状态,新的提交不属于任何分支:
# bisect 找到了问题提交
# abc1234 is the first bad commit
# ❌ 忘记 reset,直接开始修改
vim src/fix.ts
git commit -m "fix: ..." # 提交在 detached HEAD 上!
# ✅ 先 reset 回到原始分支
git bisect reset
# 然后再开始修复不小心将 good 标记为 bad 或反过来,会导致结果错误:
# 如果发现标记错了
git bisect log # 查看标记历史
git bisect log > /tmp/log.txt # 保存
git bisect reset # 重置
# 编辑 log 文件,修正错误的标记
vim /tmp/log.txt
# 回放修正后的过程
git bisect replay /tmp/log.txtbisect run 的脚本如果本身有 bug(比如有时返回错误的退出码),会导致 bisect 定位到错误的提交。
建议:先在已知的 good 和 bad 版本上手动运行脚本,确认退出码正确。
如果 good 和 bad 之间有大规模代码重构(文件重命名、目录结构变更),测试脚本可能在某些提交上无法运行。使用 exit 125 跳过这些提交。
不限于 bug。任何在两个版本之间发生变化的行为都可以用 bisect 定位:
使用 --term-old / --term-new 自定义术语更语义化。
可以。bisect 基于提交图工作,不局限于单个分支。你可以标记不同分支上的提交为 good/bad。
# 查看剩余搜索范围
git bisect visualize --oneline
# 查看已标记的记录
git bisect logbisect 开始时会检出不同的提交,工作区会被覆盖。建议:
git bisect start 前确认工作区干净bisect run 的脚本需要什么权限?脚本需要可执行权限:
chmod +x test-script.sh
git bisect run ./test-script.sh
# 或直接用 sh 运行
git bisect run sh -c 'npm test'# 在 Git Bash 中使用(推荐)
git bisect run bash -c 'npm test'
# 在 PowerShell 中
git bisect run cmd /c "npm test"| 目标 | 命令 |
|---|---|
| 开始 bisect | git bisect start |
| 快速开始 | git bisect start <bad> <good> |
| 标记当前版本为坏 | git bisect bad |
| 标记当前版本为好 | git bisect good |
| 标记指定版本 | git bisect good/bad <sha> |
| 跳过当前版本 | git bisect skip |
| 自动 bisect | git bisect run <script> |
| 查看进度 | git bisect visualize --oneline |
| 查看标记历史 | git bisect log |
| 保存日志 | git bisect log > log.txt |
| 回放日志 | git bisect replay log.txt |
| 结束 bisect | git bisect reset |
| 回到指定提交 | git bisect reset <commit> |
| 自定义术语 | git bisect start --term-old=X --term-new=Y |
| 限定文件路径 | git bisect start <bad> <good> -- <path> |
Git bisect 使用加权二分搜索而非简单的中点切分。它考虑提交图的拓扑结构(合并提交有多个父提交),选择能最有效地将搜索空间减半的提交:
# 搜索范围和步数的关系
# N 个提交 → 约 log2(N) 步
# 8 个提交 → 约 3 步
# 64 个提交 → 约 6 步
# 512 个提交 → 约 9 步
# 4096 个提交 → 约 12 步
# 即使有 4000 个提交,最多也只需要 12 次测试!skip 对搜索效率的影响 # 每次 skip 都会降低搜索效率
# 如果连续 skip,bisect 可能无法精确定位
# bisect 可能报告一个范围而非单个提交
# "There are only 'skip'ped commits left to test.
# The first bad commit could be any of:
# abc1234
# def5678
# ghi9012"
# 解决方案:手动测试这些候选提交
for sha in abc1234 def5678 ghi9012; do
git checkout $sha
npm test && echo "$sha: GOOD" || echo "$sha: BAD"
done# ❌ 不好的实践:中间提交无法编译
# commit 1: 删除旧 API
# commit 2: 添加新 API(依赖 commit 1 的删除)
# → commit 1 单独时无法编译
# ✅ 好的实践:每个提交都能独立编译和测试
# commit 1: 添加新 API(同时保留旧 API)
# commit 2: 迁移调用方到新 API
# commit 3: 删除旧 API
# → 每个提交都能独立编译
# 使用 rebase -i 整理提交时也要注意保持这个原则
git rebase -i main
# 重排提交时确保每个中间状态都是可编译的这不仅对 bisect 有用,也是良好的 Git 实践。如果项目 CI 对每个提交都运行测试(而非只测 PR),能自动保证这个原则。
# GitHub Actions 中对每个提交运行测试
on:
push:
branches: [main]
# 每个推送到 main 的提交都会被测试# 1. 在 bug 报告模板中加入 bisect 信息
# Bug Report Template:
# - [ ] 已确认最近的正常版本(good): ___
# - [ ] 首次出现问题的版本(bad): ___
# - [ ] 可自动化的测试命令: ___
# 2. 在修复 PR 中记录 bisect 结果
# "使用 git bisect 定位到 abc1234 引入了此问题"
# "bisect 测试命令: npm test -- --testPathPattern='payment'"
# 3. 修复后添加回归测试
# 确保同样的 bug 不会再次出现# GitHub Actions:当测试失败时自动 bisect
name: Auto Bisect on Failure
on:
workflow_dispatch:
inputs:
bad_ref:
description: '首次失败的提交/标签'
required: true
good_ref:
description: '最后正常的提交/标签'
required: true
test_command:
description: '测试命令'
default: 'npm test'
jobs:
bisect:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Run bisect
run: |
git bisect start ${{ inputs.bad_ref }} ${{ inputs.good_ref }}
git bisect run sh -c '${{ inputs.test_command }}'
echo "问题提交:"
git bisect log | tail -5
git bisect reset# bisect 找到问题提交后,查看哪些测试覆盖了修改的代码
git show abc1234 --name-only
# src/payment/checkout.ts
# 找到覆盖这个文件的测试
grep -rl "checkout" __tests__/
# __tests__/payment/checkout.test.ts
# 只运行相关测试来验证
npm test -- --testPathPattern="checkout"git blame — bisect 后进一步分析具体代码行git log — 查看提交历史,辅助缩小 bisect 范围