M14 工具箱:追查與自動化
- 用
git blame和git log -S/-G追查一段內容的來歷,並分清兩者; - 用
git bisect以二分法找出引入錯誤的 commit; - 設定 alias 和 config,並分清各個設定層級;
- 說出常用 hook 的用途,並用 hook 自動檢查 commit。
1blame 與 log -S:這裏是怎樣來的 blame and the pickaxe
git blame 檔案 逐行列出:最後改動這一行的 commit、作者、日期。名字雖然叫「怪責」,用途其實是找到那個 commit,讀它的說明,明白當時為甚麼這樣改。這也是 M2 要寫好 commit 說明的原因。
blame 有一個常見的陷阱:有人重新排版(改縮排、刪行尾空白、跑格式化工具),整個檔案的 blame 就都指向那個排版 commit。三個選項可以繞過它:
git blame -w:忽略只改了空白的行,追溯到更早的 commit;git blame -C:一段文字是從同一個 commit 改動過的另一個檔案搬過來的,就追到原來的檔案(重複寫-C -C會找得更廣);git blame --ignore-revs-file .git-blame-ignore-revs:把純排版 commit 的編號逐行寫進這個檔案,blame 就略過它們。設定git config blame.ignoreRevsFile .git-blame-ignore-revs之後每次自動生效;GitHub 網頁上的 blame 也會讀這個檔名。
blame 只看到每一行最後一次的改動。要找「某個字是何時加入或刪除的」,用 git log -S "字串"(俗稱 pickaxe):它列出令該字串出現次數改變的 commit。git log -G "正規表示式" 則列出改動的行(diff 裏加或刪的行)符合該模式的 commit。兩者的分別見對照。
2bisect:用二分法找出元兇 git bisect
你知道現在的版本有錯,也知道一個月前的版本沒有,中間有幾十個 commit。逐個檢查太慢;git bisect 每次把範圍砍一半:
git bisect start
git bisect bad # 現在的版本有錯
git bisect good v1.0 # 這個版本沒錯
# Git 切換到中間的 commit;你測試後告訴它結果:
git bisect good # 或 git bisect bad
# …重複,直到 Git 說出 "is the first bad commit"
git bisect reset # 回到原來的分支
1000 個 commit 也只需約 10 步(2¹⁰ = 1024)。Git 切換到的 commit 如果根本無法測試(例如編譯不了,和你要找的錯誤無關),用 git bisect skip 跳過它,Git 會改選附近另一個。
如果測試可以寫成腳本,git bisect run 測試命令 會全自動完成。Git 按腳本的結束狀態判斷:0=good;1–127(125 除外)=bad;125=skip(這個 commit 無法測試);其他值(例如 128 以上)令 bisect 中止。這也是 M2「一個 commit 一件事」的回報:找出的 commit 越小,越容易看出錯在哪裏。
git bisect skip;標錯了,可以用 git bisect log 存下記錄、刪掉錯的那行,再用 git bisect replay 重來。
3config 與 alias Configuration and aliases
| 層級 | 命令 | 檔案 | 影響 |
|---|---|---|---|
| 系統 | git config --system | $(prefix)/etc/gitconfig(Linux 通常是 /etc/gitconfig) | 這部電腦所有使用者 |
| 全域(個人) | git config --global | ~/.gitconfig | 你的所有倉庫 |
| 倉庫 | git config --local | .git/config | 只有這個倉庫;優先於上面兩層 |
| worktree | git config --worktree | .git/config.worktree | 只有目前這個 worktree(M8);優先於倉庫層。要先設定 extensions.worktreeConfig true |
同一個設定在幾層都有,越接近你正在工作的地方越優先。不知道某個值從哪裏來,用 --show-origin(顯示檔案)或 --show-scope(顯示層級)查。
常用設定:
git config --global user.name "你的名字" # M0
git config --global init.defaultBranch main
git config --global pull.rebase true # M9:pull 時預設 rebase
git config --global alias.lg "log --oneline --graph --all"
git config --global alias.st status
git lg # 等於 git log --oneline --graph --all
git config --list --show-origin # 每個設定來自哪個檔案
git config --show-scope --get pull.rebase # 這個值由哪一層決定
alias:把常用命令縮短
alias 是 config 裏 alias.名字 的設定,上面的 git lg 就是一例;它展開成另一條 Git 命令,後面還可以接參數。值以 ! 開頭時,alias 會交給 shell 執行,可以串起幾條命令或呼叫其他程式:
git config --global alias.root '!pwd' # 顯示倉庫最上層的路徑
git config --global alias.sync '!git fetch -p && git status -sb'
留意:! alias 一律在倉庫最上層執行,不論你當時在哪個子資料夾,所以裏面的相對路徑要從最上層算起。
4hooks:在關鍵時刻自動檢查 Git hooks
hook 是放在 .git/hooks/ 裏的腳本,Git 在特定時刻執行它。名字以 pre- 開頭的 hook(以及 commit-msg)在動作之前執行,腳本以非零狀態結束,就會中止那個動作;post- 開頭的在動作之後執行,動作已經完成,結束狀態不會令它撤銷,只能做通知、整理之類的善後。
檔名必須和 hook 名稱完全相同、沒有副檔名(pre-commit,不是 pre-commit.sh),而且要有執行權限(chmod +x .git/hooks/pre-commit)。沒有執行權限的 hook 會被略過,Git 只印一行提示;有副檔名的則完全不會被執行,連提示也沒有。.git/hooks/ 裏的 *.sample 檔就是這樣「停用」的範例。
| hook | 何時執行 | 非零狀態會中止動作? | 常見用途 |
|---|---|---|---|
pre-commit | commit 之前 | 會 | 檢查格式、TODO、密鑰是否被加入 |
commit-msg | 寫好說明之後、commit 建立之前 | 會 | 檢查說明格式(例如不准只寫「更新」) |
pre-push | push 之前 | 會 | 跑測試;防止推送到 main 或已合併的分支 |
post-commit | commit 建立之後 | 不會(commit 已建立) | 通知、記錄 |
post-merge | 合併之後 | 不會(合併已完成) | 自動安裝依賴、重建索引 |
.git/hooks/ 不會被提交或 clone,所以這些客戶端 hook 是個人的防線,不能代替伺服器上的保護規則和 CI(M11)。團隊要共用 hook,可以把腳本放在倉庫裏,再設定 git config core.hooksPath 腳本資料夾,或用 pre-commit 等工具管理(留意:設了 core.hooksPath,Git 就不再看 .git/hooks/)。緊急時 git commit --no-verify 可略過 pre-commit 和 commit-msg,git push --no-verify 可略過 pre-push,所以它們擋得住疏忽,擋不住刻意。另一類是伺服器端 hook:
pre-receive、update(收到 push、更新 ref 之前,可以拒絕)和 post-receive(更新之後,常用來觸發部署或通知),裝在伺服器的倉庫上,推送的人無法略過。GitHub、GitLab 等託管平台一般不讓你直接放這些腳本,改為提供保護規則、CI 和 webhook。
5對照與誤解 Easily confused
git blame | git log -S/-G | |
|---|---|---|
| 問的問題 | 現在這一行,最後是誰改的? | 這段內容是哪些 commit 加入、刪除或改動的? |
| 看到的歷史 | 每行只有最後一次改動 | 所有符合條件的 commit |
| 內容已被刪掉 | 看不到(那一行已不存在) | 找得到刪除它的 commit |
git log -S "字串" | git log -G "正規表示式" | |
|---|---|---|
| 比對甚麼 | 字串在檔案裏出現的次數,改動前後是否不同 | diff 裏加或刪的行是否符合模式 |
| 同一檔案裏把含該字串的行搬到別處 | 不算(次數沒變) | 通常算(該行在 diff 裏一刪一加) |
把 n=100 改成 n=120,搜 n= | 不算(仍然出現一次) | 算 |
| 適合 | 某個字是何時加入或刪除的 | 某類寫法是何時被改動過的 |
| 本機 hook | 伺服器規則/CI(M11) | |
|---|---|---|
| 在哪裏執行 | 每個人自己的電腦 | 伺服器,所有 push 和 PR 都經過 |
| 會隨 clone 帶走嗎 | 不會;要每人自行安裝或設定 core.hooksPath | 設定一次,對所有人有效 |
| 能否繞過 | 可以:--no-verify、刪掉腳本 | 只有在繞過名單上的人可以 |
| 強項 | 回饋最快,還未 commit 就提醒 | 真正的保證 |
- 「blame 指着誰,就是誰寫錯。」blame 只說最後一次碰這一行的 commit;可能只是改縮排或搬位置,要用
-w、-C或忽略清單再往前追。 - 「hook 結束狀態非零,動作就會取消。」只有動作之前的 hook(
pre-開頭和commit-msg)才會;post-commit、post-merge執行時動作已經完成。 - 「把 hook 腳本 commit 進倉庫,隊友就會自動執行。」
.git/hooks/不會被提交;放在倉庫裏的腳本,要每人設定core.hooksPath或用工具安裝才生效。 - 「bisect 會發現我標錯。」不會;它相信每一個 good/bad,標錯就給出錯的答案。
6速查 Cheat sheet
| 命令 | 作用 |
|---|---|
git blame -L 10,20 檔案 | 只看第 10–20 行 |
git blame -w -C 檔案 | 略過只改空白的改動;追蹤從其他檔案搬來的行 |
git config blame.ignoreRevsFile .git-blame-ignore-revs | blame 一律略過檔案裏列出的排版 commit |
git log -S "字串" --oneline | 字串出現次數改變的 commit |
git log -G "正規表示式" -p | 改動的行符合模式的 commit,連 diff 顯示 |
git log --grep "關鍵字" | 說明裏有關鍵字的 commit(M13) |
git bisect start → bad → good <版本> | 開始二分搜尋 |
git bisect skip | 這個 commit 無法測試,跳過 |
git bisect run ./test.sh | 自動 bisect:腳本回傳 0=good;1–127(125 除外)=bad;125=skip;其他值中止 bisect |
git bisect reset | 結束 bisect,回到原來的分支 |
git config --global alias.lg "…" | 建立 alias;值以 ! 開頭則交給 shell,在倉庫最上層執行 |
git config --list --show-scope --show-origin | 每個設定來自哪一層、哪個檔案 |
git config core.hooksPath .githooks | 使用倉庫裏共用的 hook 資料夾 |
chmod +x .git/hooks/pre-commit | 讓 hook 可以執行(沒有執行權限會被略過) |
git commit --no-verify/git push --no-verify | 略過 pre-commit、commit-msg/pre-push |
7小測 Quiz
本單元重點 Summary
blame找每一行最後的改動,log -S/-G找某段內容何時出現或消失;目的都是讀到當時的說明。bisect用二分法找出第一個壞 commit;commit 越小,越易看出錯誤。- config 分系統、全域、倉庫三層(另可加 worktree 層),越接近工作的地方越優先;alias 把常用命令縮短。
- hook 在本機自動檢查;只有動作之前的 hook 能中止動作。它是個人防線,團隊的保證仍靠保護規則和 CI。
延伸閱讀
- Pro Git 7.10 Debugging with Git(blame、bisect)
- Pro Git 8.1 Git Configuration、8.3 Git Hooks
- git bisect 官方文件
- pre-commit:管理共用 hook 的工具