pre-commit

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#

npm install husky --save-dev

初始化 Husky#

進入專案資料夾後,啟用 Git hooks:

npx husky init

這一步會做三件事:

  • 在根目錄下建立 .husky/ 目錄

  • 給你一個 pre-commit 的範例檔案

  • 在 package.json 加上 prepare script:

    "scripts": { "prepare": "husky" }

prepare 是 npm 的生命週期腳本,會在 npm install 之後自動執行,這就是為什麼其他人 clone 專案後不需要額外設定,hooks 就會自己裝好。

設定檢查項目#

接著就可以在 .husky/pre-commit 裡寫下需要執行的 task,例如:

npm run build || exit 1

|| exit 1 是關鍵:只要 build 回傳非零的結束碼,這一行就會讓整個 hook 以失敗結束,Git 就會中止這次 commit。

這樣一來,每次 git commit 時就會先跑一次 build 來攔截錯誤。沒有報錯時的輸出如下:

pre-commit hook 執行成功後在終端機顯示的通過訊息

要跑 lint 或測試也是同樣的寫法,一行一個:

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 檢查後就自動推送:

#!/bin/zsh MESSAGE=$1 git add . git commit -m "$MESSAGE" || exit 1 git push origin main

同樣地,|| exit 1 讓 commit 失敗時整個 script 就停下來,不會繼續執行 push。

存成 git-push.zsh 之後記得給它執行權限:

chmod +x git-push.zsh

接著在終端機執行:

./git-push.zsh "commit message"

如果想從 Python 呼叫這個外部指令,可以這樣寫:

import subprocess msg = "input message" subprocess.run(["./git-push.zsh", msg], check=True)

check=True 會在 script 回傳非零結束碼時直接拋出例外,所以 pre-commit 擋下來的錯誤同樣會傳遞到 Python 這一層,不會被無聲地忽略掉。

結語#

pre-commit 的價值不在於它能做多複雜的檢查,而在於它把「發現錯誤」的時間點從 CI 的幾分鐘縮短到 commit 的幾秒鐘。設定成本只有幾行,但省下來的等待時間會隨著專案的生命週期不斷累積。