快轉到主要內容
  1. 教學文章/

Git bisect 實戰:快速定位壞 commit 與除錯流程完全攻略

·9 分鐘· loading · loading · ·
Git Bisect Debugging Regression Version-Control
每日拍拍
作者
每日拍拍
科學家 X 科技宅宅
目錄
版本控制: Git - 本文屬於一個選集。
§ 10: 本文

每個專案都會遇到這種狀況:昨天還好好的,今天測試突然紅一片。 你打開 git log,發現中間有二十幾個 commit。 每個 commit message 看起來都很無辜,但每個又都有一點可疑。 「refactor config loader」很可疑。 「small cleanup」也很可疑。 「fix typo」最可疑,因為它通常不只修 typo。 這時候如果靠猜,拍拍君保證你會開始懷疑人生。 更好的方法是讓 Git 幫你玩二分搜尋。 這就是 git bisect。 它可以在一段 commit 歷史裡,自動縮小範圍,找出第一個把程式弄壞的 commit。 不是靠玄學,不是靠直覺,是很樸素、很可靠的二分搜尋。 今天這篇就來完整拆解 git bisect:從手動判定,到用測試腳本自動找壞 commit。 學完之後,你看到 regression 不會再只想重開機或喝咖啡。 你會很冷靜地說:「交給 bisect 吧。」

一、什麼是 regression?
#

Regression 指的是:原本正常的功能,因為某次改動又壞掉了。 例如:

  • 昨天登入 API 還會回傳 token,今天變成 500。
  • 上週 CLI 可以解析 --verbose,現在突然不認得。
  • 前一版測試全綠,這版多了三個紅燈。
  • 某個效能最佳化之後,實際上慢了十倍。 Regression 最麻煩的地方不是「壞了」。 而是你通常不知道是哪個 commit 造成的。 如果最近只有一個 commit,那很好辦。 如果最近有五十個 commit,而且還是多人協作,那就精彩了。 人工一個個 checkout 測試當然可以,但那是線性搜尋。 五十個 commit 最壞情況要試五十次。 git bisect 用的是二分搜尋。 五十個 commit 大約只要六次左右就能找出答案。 這差距非常大。

二、bisect 的核心想法
#

bisect 的意思就是「二分」。 你先告訴 Git 兩件事:

  1. 哪個 commit 是壞的。
  2. 哪個 commit 是好的。 例如:目前 main 是壞的,而兩週前的 tag v1.2.0 是好的。 Git 接著會 checkout 到中間某個 commit。 你測一下程式。 如果這個 commit 是好的,就告訴 Git:
git bisect good

如果是壞的,就告訴 Git:

git bisect bad

Git 會根據你的回答,把搜尋範圍砍半。 重複幾次後,它就會指出第一個壞 commit。 整個流程很像:

  • Git:「這個 commit 壞嗎?」
  • 你:「壞。」
  • Git:「那更早一點。這個呢?」
  • 你:「好。」
  • Git:「懂了,問題在中間。再切一半。」 很樸素,也很有效。

三、建立一個最小範例
#

我們先用一個迷你 Python 專案示範。

mkdir bisect-demo
cd bisect-demo
git init

建立 price.py

def apply_discount(price: int, percent: int) -> int:
    return price - price * percent // 100

建立測試 test_price.py

from price import apply_discount


def test_apply_discount():
    assert apply_discount(100, 20) == 80
    assert apply_discount(199, 10) == 180

提交第一版:

git add price.py test_price.py
git commit -m "add discount calculator"

現在假設後面陸續有幾個 commit。 其中某次重構不小心把公式改成:

def apply_discount(price: int, percent: int) -> int:
    return price * percent // 100

這其實回傳的是「折扣金額」,不是「折扣後價格」。 但後面又加了 README、CLI、文件更新。 等你發現測試壞掉時,歷史已經多了好幾個 commit。 這時候就讓 bisect 上場。

四、手動使用 git bisect
#

第一步:啟動 bisect。

git bisect start

告訴 Git:目前版本是壞的。

git bisect bad

接著找一個你確定是好的版本。 可以用 git log --oneline 找 commit hash,然後:

git bisect good <good-commit-hash>

Git 會自動 checkout 到中間某個 commit。 你現在要測試它是好是壞。

python -m pytest

如果測試通過:

git bisect good

如果測試失敗:

git bisect bad

Git 會再 checkout 到下一個候選 commit。 你繼續測。 最後 Git 會輸出類似:

abc1234 is the first bad commit
commit abc1234
Author: ...
Date: ...

    refactor discount formula

這就是第一個壞 commit。 拍拍君覺得這一刻很爽,因為你不是猜到兇手,是把兇手逼出來。

五、結束 bisect 狀態
#

找到壞 commit 之後,不要忘記結束 bisect。

git bisect reset

這會讓工作目錄回到你啟動 bisect 前所在的位置。 如果你忘記 reset,可能會困在某個舊 commit 上。 然後你會疑惑:「咦,我的 branch 怎麼不見了?」 其實只是 detached HEAD。 所以每次 bisect 完,請養成習慣:

git bisect reset

很重要,真的很重要。

六、用 git bisect run 自動化
#

手動 bisect 已經很好用。 但如果判斷好壞的方法可以寫成指令,那就更棒。 例如測試通過代表 good,測試失敗代表 bad:

python -m pytest

完整流程如下:

git bisect start
git bisect bad HEAD
git bisect good <good-commit-hash>
git bisect run python -m pytest

Git 會自動 checkout 不同 commit。 每次 checkout 後執行 python -m pytest。 如果 exit code 是 0,Git 視為 good。 如果 exit code 是 1127,Git 視為 bad。 最後自動找出第一個壞 commit。 這超級適合:

  • 測試 suite 可以穩定重現問題。
  • lint 或 type check 某天開始壞掉。
  • CLI 某個指令開始回傳錯誤。
  • build 從某次 commit 後開始失敗。 例如 Python 專案:
git bisect run uv run pytest tests/test_price.py

Rust 專案也可以:

git bisect run cargo test

只要指令的 exit code 能代表好壞,就能自動跑。

七、寫一個專門的 bisect script
#

真實專案常常不只是跑測試而已。 可能需要安裝依賴、產生 fixture、跑特定測試,或檢查輸出文字。 這時候可以寫一個 script。 例如 scripts/bisect-check.sh

#!/usr/bin/env bash
set -euo pipefail

python -m pytest tests/test_price.py

加上執行權限:

chmod +x scripts/bisect-check.sh

然後:

git bisect run ./scripts/bisect-check.sh

如果要檢查 CLI 輸出,也可以這樣:

#!/usr/bin/env bash
set -euo pipefail

output=$(python cli.py)
test "$output" = "80"

test 成功會回傳 0,失敗會回傳非零。 Git 就知道這個 commit 是 good 還是 bad。 這種 script 很適合留下來。 下次同類型 regression 再發生,你可以直接重用。

八、skip:遇到不能測的 commit 怎麼辦?
#

Bisect 過程中,有時候 Git 會 checkout 到一個尷尬 commit。 例如那個 commit 剛好測試檔還沒加入,或舊版依賴設定壞掉。 這時候不要硬說 good 或 bad。 可以用:

git bisect skip

Git 會跳過這個 commit,改測其他候選。 如果你用 bisect run,script 也可以用 exit code 125 表示 skip。

#!/usr/bin/env bash
set -euo pipefail

if [ ! -f pyproject.toml ]; then
  exit 125
fi

uv run pytest tests/test_price.py

125 對 Git bisect 來說是:「這個 commit 無法測,請跳過。」 這在專案歷史改過測試工具、build system、檔案結構時很實用。

九、bisect 的 good commit 要怎麼選?
#

git bisect 需要你提供一個 good commit。 這個 good commit 不一定要很久以前。 只要你確定它是好的就行。 常見選法有幾種:

git bisect good v1.4.2

或:

git bisect good <last-green-commit>

也可以用昨天還正常的 commit:

git bisect good main@{yesterday}

選 good commit 的原則是:不要選「你希望它是好的」。 要選「你確定它是好的」。 如果 good 起點其實已經壞了,bisect 結果就會失真。 這跟拿錯量尺一樣,後面算得再精準,也只是精準地錯。

十、只針對某個路徑 bisect
#

有時候你知道問題只可能在某個資料夾。 git bisect start 可以加路徑限制。

git bisect start -- src/cli tests/cli

然後照常標記 good / bad:

git bisect bad HEAD
git bisect good v1.4.2

Git 只會考慮影響這些路徑的 commit。 這可以減少搜尋範圍。 但也要小心。 如果你路徑限制太窄,真正的壞 commit 可能被排除。 例如 bug 表面上在 CLI,實際上是 config parser 改壞。 除非你很確定問題範圍,否則拍拍君建議先不要加路徑限制。 先讓 bisect 找完整歷史。 找不到或範圍太大時,再考慮縮小。

十一、用 bisect 找效能退化
#

git bisect 不只能找測試壞掉,也能找效能 regression。 重點是把「好或壞」寫成可重複的判斷。 例如 benchmark 輸出毫秒數:

python bench.py

可以寫 script 判斷是否超過門檻:

#!/usr/bin/env bash
set -euo pipefail

time_ms=$(python bench.py)

python - <<PY
import sys
value = float("$time_ms")
threshold = 250.0
sys.exit(0 if value <= threshold else 1)
PY

然後:

git bisect run ./scripts/bisect-perf.sh

不過效能測試有一個大坑:它可能不穩定。 CPU 負載、快取、背景程序都會影響結果。 所以效能 bisect 最好重複跑多次取中位數、固定輸入資料、門檻不要設得太貼。 二分搜尋很聰明,但它不會通靈。 你的判斷函式要穩。

十二、常見陷阱
#

第一個陷阱是 flaky test。 如果同一個 commit 有時候通過、有時候失敗,bisect 會被誤導。 先確認問題可重現,必要時在 script 裡重跑幾次。

#!/usr/bin/env bash
set -euo pipefail

for i in 1 2 3; do
  python -m pytest tests/test_price.py
done

第二個陷阱是環境沒有重建。 Bisect 會 checkout 不同 commit,不同 commit 可能需要不同依賴。 如果你一直沿用同一個環境,有時候會得到假結果。 在乾淨 clone 裡做 bisect 通常比較安全。 或至少先把手邊工作 commit / stash 起來。 第三個陷阱是忘記目前在 detached HEAD。 Bisect 過程中 Git 會 checkout 到特定 commit。 你可以讀檔、跑測試、做臨時實驗,但不要順手開始正式開發。 結束時請回到:

git bisect reset

這個指令值得再講一次,因為真的很多人會忘。

十三、找出壞 commit 之後要做什麼?
#

git bisect 找到壞 commit,不代表修好了。 它只是幫你定位。 接下來可以做幾件事。 第一,讀 diff。

git show <bad-commit>

第二,確認原因。 不要只看到 commit message 就下結論。 有時候真正問題不是那行程式,而是測試資料或隱含假設改了。 第三,寫一個 regression test。 如果 bug 沒有測試守住,它可能下次又回來。 第四,修 bug。 第五,在 commit message 裡提到 bisect 結果。

Fix discount calculation regression

git bisect identified abc1234 as the first bad commit.
The refactor returned the discount amount instead of final price.
Add regression coverage for non-round prices.

未來的你會很感動。 未來的你很容易生氣,要善待他。

十四、團隊協作中的 bisect 流程
#

在團隊裡,bisect 也很有價值。 但要注意溝通方式。 不要一找到壞 commit 就衝去說:「就是你寫壞的!」 比較好的說法是:

我用 git bisect 找到 regression 從 abc1234 開始。看起來可能跟 discount refactor 有關,我正在補 regression test。 重點是事實,不是指責。 程式會壞很正常。 Commit 被 bisect 找到,不代表作者粗心。 它只代表那次改動引入了一個未被測試捕捉的行為變化。 成熟的團隊會把這件事當成改進測試與流程的機會。 不是抓戰犯。 拍拍君喜歡這種團隊,因為大家比較不會爆炸。

十五、bisect 與 CI 的搭配
#

如果你的 CI 保存每個 commit 的結果,bisect 會更好用。 你可以先從 CI 找到最後一個綠燈 commit,以及第一個紅燈 commit。

git bisect start
git bisect bad <first-red-commit>
git bisect good <last-green-commit>
git bisect run ./scripts/ci-check.sh

這比從很久以前的 release 開始找快很多。 也比較不會被無關歷史干擾。 如果測試可以縮小到單一案例,就更快:

git bisect run uv run pytest tests/test_config.py::test_load_nested_config

越精準的檢查,bisect 越快。 但不要精準到漏掉 bug。 這中間要拿捏。

十六、指令速查
#

啟動:

git bisect start

標記目前 commit 壞掉:

git bisect bad

標記某個 commit 是好的:

git bisect good <commit>

跳過不能判定的 commit:

git bisect skip

自動執行測試:

git bisect run <command>

結束並回到原本位置:

git bisect reset

看目前 bisect log:

git bisect log

把過程存下來給同事:

git bisect log > bisect-log.txt

重播一次 bisect log:

git bisect replay bisect-log.txt

十七、一個實戰模板
#

拍拍君常用模板長這樣。 先確認工作目錄乾淨:

git status

啟動 bisect:

git bisect start

設定壞點與好點:

git bisect bad HEAD
git bisect good v1.4.2

自動跑特定測試:

git bisect run uv run pytest tests/test_bug.py::test_regression_case

找到結果後看 diff:

git show <first-bad-commit>

結束:

git bisect reset

接著補 regression test、修 bug、開 PR。 然後喝水。 不要忘記喝水。

十八、什麼時候不適合用 bisect?
#

git bisect 很強,但不是萬能。 以下情況可能不太適合:

  • 問題無法穩定重現。
  • 好壞標準很模糊。
  • 每次測試成本非常高。
  • 歷史中有大量 commit 無法 build。
  • bug 不是單一 commit 引入,而是多個變更互動造成。 遇到這些情況,bisect 仍然可能有幫助。 但你要更小心解讀結果。 有些 bug 是 A commit 和 B commit 同時存在才會爆。 Bisect 找到的「第一個壞 commit」只是讓壞狀態首次出現的點。 不一定代表修復只需要 revert 那個 commit。 工具負責縮小範圍,人負責判斷語意。

結語
#

git bisect 是 Git 裡被低估的神器。 它不常出現在每天的 workflow 裡。 但當 regression 真的發生時,它可以幫你省下大量時間。 今天記住幾個重點就好:

  • 先找一個確定好的 commit。
  • 再標記目前壞掉的 commit。
  • good / bad 回答 Git 的問題。
  • 能自動化就用 git bisect run
  • 遇到不能判定就 skip
  • 結束一定要 git bisect reset。 Debug 最怕的是亂猜。 git bisect 讓你把猜測變成搜尋,讓混亂變成範圍。 讓「不知道哪裡壞」變成「就是這個 commit 開始壞」。 下次專案突然壞掉,先不要急著翻桌。 讓 Git 幫你切一半,再切一半,再切一半。 拍拍君相信你會很快抓到兇手。

延伸閱讀
#

版本控制: Git - 本文屬於一個選集。
§ 10: 本文

相關文章

Git cherry-pick 實戰:精準搬運 commit、修補 hotfix 與分支同步
·11 分鐘· loading · loading
Git Cherry-Pick Hotfix Commit Version-Control
Git stash 實戰:暫存工作現場、切換任務與 patch 管理完全攻略
·11 分鐘· loading · loading
Git Git-Stash Patch Workflow Version-Control
Git rebase 完全攻略:整理 commit 歷史、互動式 rebase 與衝突處理
·11 分鐘· loading · loading
Git Rebase Interactive-Rebase Commit-History Version-Control
Git Branch 策略完全攻略:feature branch、main、hotfix 與協作流程
·11 分鐘· loading · loading
Git Branch Workflow Feature-Branch Hotfix
GitHub Actions 進階:Matrix Build + 自動發佈 PyPI
·5 分鐘· loading · loading
Github-Actions Cicd Pypi Matrix-Build Python
Github Actions: 自動化你的工作流
·5 分鐘· loading · loading
版本控制 Github Git