Git 全景課 / 第四部分 應用 · M14

M14 工具箱:追查與自動化

歷史記得好,就能回答三個問題:這一行是誰、何時、為甚麼改的?某個字是何時出現或消失的?錯誤是從哪個 commit 開始的?本單元用 blame、log -S、bisect 回答它們,再用 alias、config 和 hooks 把好習慣自動化。
約 60 分鐘互動 ×3 · 小測 4 題先修:M2、M6
學完本單元,你能夠:
  1. 用 git blame 和 git log -S/-G 追查一段內容的來歷,並分清兩者;
  2. 用 git bisect 以二分法找出引入錯誤的 commit;
  3. 設定 alias 和 config,並分清各個設定層級;
  4. 說出常用 hook 的用途,並用 hook 自動檢查 commit。

1blame 與 log -S:這裏是怎樣來的 blame and the pickaxe

git blame 檔案 逐行列出:最後改動這一行的 commit、作者、日期。名字雖然叫「怪責」,用途其實是找到那個 commit,讀它的說明,明白當時為甚麼這樣改。這也是 M2 要寫好 commit 說明的原因。

blame 有一個常見的陷阱:有人重新排版(改縮排、刪行尾空白、跑格式化工具),整個檔案的 blame 就都指向那個排版 commit。三個選項可以繞過它:

blame 只看到每一行最後一次的改動。要找「某個字是何時加入或刪除的」,用 git log -S "字串"(俗稱 pickaxe):它列出令該字串出現次數改變的 commit。git log -G "正規表示式" 則列出改動的行(diff 裏加或刪的行)符合該模式的 commit。兩者的分別見對照。

互動 1 blame 與 log -S

左欄是 git blame --date=short report.md 的結果(^ 開頭的是第一個 commit),點任何一行看那個 commit 的說明。下面輸入一個字串,模擬 git log -S。

(點一行,這裏會顯示 git show 的說明)

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 相信你的每一個回答 標錯一次 good/bad,Git 不會察覺,只會在錯的一半繼續找,最後指出一個錯的 commit。拿不準就用 git bisect skip;標錯了,可以用 git bisect log 存下記錄、刪掉錯的那行,再用 git bisect replay 重來。

互動 2 bisect 找錯誤

16 個 commit,最舊的(c1)沒錯,最新的(c16)有錯。藍框是 Git 切換到的 commit:按「測試」看結果,再告訴 Git 是 good 還是 bad。選擇哪個 commit、輸出的數字都與真實 Git 2.54 相同。為了練習,這裏標錯會被攔下;真實的 Git 不會攔,只會給出錯的答案。


3config 與 alias Configuration and aliases

層級命令檔案影響
系統git config --system$(prefix)/etc/gitconfig(Linux 通常是 /etc/gitconfig)這部電腦所有使用者
全域(個人)git config --global~/.gitconfig你的所有倉庫
倉庫git config --local.git/config只有這個倉庫;優先於上面兩層
worktreegit 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-commitcommit 之前會檢查格式、TODO、密鑰是否被加入
commit-msg寫好說明之後、commit 建立之前會檢查說明格式(例如不准只寫「更新」)
pre-pushpush 之前會跑測試;防止推送到 main 或已合併的分支
post-commitcommit 建立之後不會(commit 已建立)通知、記錄
post-merge合併之後不會(合併已完成)自動安裝依賴、重建索引
上面的 hook 只在本機 .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。

互動 3 hook 模擬

開啟 hook,修改檔案內容和說明,按 git commit 看看結果。


5對照與誤解 Easily confused

git blamegit 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-revsblame 一律略過檔案裏列出的排版 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

1. 你想知道報告裏「樣本數 120」這幾個字是何時被加進來的,即使那一行後來又被改過。最合適的是:
2. 在 500 個 commit 之間用 bisect 找錯誤,大約需要測試幾次?
3. 為甚麼 hook 不能代替伺服器上的保護規則?
4. 你在全域設定了 pull.rebase true,某個倉庫的 .git/config 設了 pull.rebase false。在那個倉庫裏:

本單元重點 Summary

延伸閱讀