Git Hooks可以在執行各個動作的前後自動去觸發某個腳本來做前置處理或是後置處理,這個功能很常被用來在commit前自動檢查程式碼的排版或是寫法上的問題,確保commit過的程式碼排版和風格是一致的。Git Hooks有分為在客戶端(client-side)觸發的Hook,以及在伺服器端(server-side)觸發的Hook,這篇文章只會介紹在客戶端觸發的Hook。



當我們執行git init指令替專案加入Git版本控制後,專案目錄底下會多出一個.git目錄,這個目錄用來存放Git設定與版本控制所需的檔案,它並不能被add進Git倉庫中。在.git/hooks目錄中可以放置這個專案要使用的Git Hooks腳本,這些腳本會有特定的檔名,用來決定它是在Git執行哪個動作時會被觸發。

客戶端的Hook觸發時機和檔案名稱的關係如下表(非全部):

觸發時機 檔案名稱(按觸發的先後順序排列)
git am執行之前 applypatch-msgpre-applypatch
git am執行之後 post-applypatch
git commit執行之前 pre-commitprepare-commit-msg(commit訊息的編輯器打開前)、commit-msg
git commit執行之後 post-commit
git merge執行之前 pre-merge-commit
git rebase執行之前 pre-rebase
git push執行之前 pre-push

每個腳本在被觸發執行時,各自會有不同的引數傳入。在.git/hooks目錄中,會有以.sample結尾的Shell腳本檔案,會說明該Git Hooks腳本應會被傳入什麼樣的引數,以及它該根據什麼樣的狀況回傳什麼exit status,您也可以查閱官方文件獲得更詳細的說明。雖然.sample檔案是sh腳本,但要用Bash腳本,甚至是任意執行檔也都是可以的。要被用作Git Hooks腳本的檔案必須具有可執行權限才可以被觸發執行。

撰寫Shell腳本的時候可以用ShellCheck來檢查Shell腳本的問題。

Git Hooks的基本應用

commit前自動檢查專案

可以加入pre-commit腳本來實現commit前自動檢查專案。如果在加入腳本後,臨時在commit時不需要檢查的話,可以在使用git commit指令時加上--no-verify參數來略過腳本。

Node.js專案
#!/bin/bash

npm run lint

通常Node.js專案會在package.jsonscripts欄位下設定lint腳本,來呼叫lint工具去檢查JavaScript等程式碼的語法。所以在pre-commit腳本中去執行npm run lint

Cargo專案
#!/bin/bash

set -e

cargo +nightly fmt -- --check
cargo clippy --all-targets -- -D warnings

set -e指令可以讓Bash在執行一系列指令的過程中,如果其中一行指令出現問題,就立刻回傳它的exit status,而不再執行下去。

保存與共享Git Hooks

由於Git Hooks腳本預設存放在無法被git add.git目錄下,因此如果要能使它被push到遠端的Git倉庫上,就需要換個位置放,而不是放在.git/hooks

筆者自己是習慣在專案目錄底下再建立一個.githooks目錄,並把Git Hooks腳本都放在這個目錄下。

當然,我們還是要讓Git知道Git Hooks腳本放在哪才行,可以執行如下的指令:

git config core.hooksPath .githooks

用如上的指令將Git的core.hooksPath欄位設定為Git Hooks腳本所在的目錄即可。注意這個設定也只會在目前的這個本地端專案有效,並不會被push到遠端的Git倉庫上。

我們可以在CONTRIBUTING.mdREADME.md中撰寫說明,讓剛clone這個Git專案的開發者主動去執行這個指令。也可以把這個指令寫在程式專案自動化腳本的某個位置。像是Node.js專案,可以在package.jsonscripts欄位下設定prepare腳本,來呼叫這個指令。prepare腳本會在npm ci或是npm i的時候被執行。當我們clone一個Node.js專案後,通常首先要做的事情就是npm ci或是npm i,所以利用prepare腳本來設定Git Hooks的目錄。

不過別的環境裡可能會沒有git指令工具,此時git config指令會執行失敗,而導致後續原本要跑的流程中止了。將指令改成以下這樣,再加進程式專案裡應該會比較好一點:

git config core.hooksPath .githooks || exit 0