pre-commit
- Published time
前言#
大部分團隊都會用 CI 在程式碼推到遠端之後做檢查,但 CI 有一個先天的問題:它太慢了。從 push 到收到失敗通知,中間可能要等好幾分鐘,而失敗的原因往往只是少了一個分號,或是忘記跑 build。
比較好的做法是在更前面就攔下來。開發者可以在本地端用 pre-commit 這個 Git hook,在 commit 真正產生之前先跑一輪檢查,不合格就直接擋下來。壞掉的程式碼根本沒有機會被推上伺服器,不論對團隊還是個人開發效率都會有明顯的提升。
這篇會介紹如何用 Husky 設定 pre-commit,以及怎麼把它包成一個順手的 script。
Husky#
Husky 是一個 npm 套件,它讓使用者可以用直覺的方式註冊各式 Git hooks,把要執行的處理配置在各個 hook 中。
pre-commit 只是其中一個 hook,除此之外還有 commit-msg(檢查 commit 訊息格式)、post-commit、pre-push 等等,但這篇只會針對 pre-commit 做介紹。
Husky 存在的理由是:Git hooks 原本放在 .git/hooks/ 底下,而這個目錄不會被版本控制,所以沒辦法分享給團隊其他人。Husky 把 hooks 移到專案裡的 .husky/ 目錄,讓它可以跟著 repo 一起被 commit,團隊成員 npm install 之後就自動生效。
安裝 Husky#
Copied!npm install husky --save-dev
初始化 Husky#
進入專案資料夾後,啟用 Git hooks:
Copied!npx husky init
這一步會做三件事:
-
在根目錄下建立
.husky/目錄 -
給你一個
pre-commit的範例檔案 -
在
package.json加上preparescript:Copied!"scripts": { "prepare": "husky" }
prepare 是 npm 的生命週期腳本,會在 npm install 之後自動執行,這就是為什麼其他人 clone 專案後不需要額外設定,hooks 就會自己裝好。
設定檢查項目#
接著就可以在 .husky/pre-commit 裡寫下需要執行的 task,例如:
Copied!npm run build || exit 1
|| exit 1 是關鍵:只要 build 回傳非零的結束碼,這一行就會讓整個 hook 以失敗結束,Git 就會中止這次 commit。
這樣一來,每次 git commit 時就會先跑一次 build 來攔截錯誤。沒有報錯時的輸出如下:
要跑 lint 或測試也是同樣的寫法,一行一個:
Copied!npm run lint || exit 1 npm run build || exit 1
不過要注意的是,pre-commit 的執行時間會直接變成你每次 commit 的等待時間。如果專案的 build 要跑三分鐘,那放進 pre-commit 只會讓人想用 --no-verify 跳過它。只把「快」的檢查放在 pre-commit,慢的留給 CI,這個分工才會長久。
包成 script#
再進一步,可以把整個流程包成一個 script,通過 pre-commit 檢查後就自動推送:
Copied!#!/bin/zsh MESSAGE=$1 git add . git commit -m "$MESSAGE" || exit 1 git push origin main
同樣地,|| exit 1 讓 commit 失敗時整個 script 就停下來,不會繼續執行 push。
存成 git-push.zsh 之後記得給它執行權限:
Copied!chmod +x git-push.zsh
接著在終端機執行:
Copied!./git-push.zsh "commit message"
如果想從 Python 呼叫這個外部指令,可以這樣寫:
Copied!import subprocess msg = "input message" subprocess.run(["./git-push.zsh", msg], check=True)
check=True 會在 script 回傳非零結束碼時直接拋出例外,所以 pre-commit 擋下來的錯誤同樣會傳遞到 Python 這一層,不會被無聲地忽略掉。
結語#
pre-commit 的價值不在於它能做多複雜的檢查,而在於它把「發現錯誤」的時間點從 CI 的幾分鐘縮短到 commit 的幾秒鐘。設定成本只有幾行,但省下來的等待時間會隨著專案的生命週期不斷累積。
