一、git hook
和其它版本控制系統一樣,Git 能在特定的重要動作發生時觸發自定義腳本。有兩組這樣的鈎子:用戶端鈎子和伺服器鈎子。用戶端鈎子由諸如送出和合并這樣的操作所調用,而伺服器端鈎子作用于諸如接收被推送的送出這樣的聯網操作。
鈎子都被存儲在 Git 目錄下的 hooks 子目錄中。 也即絕大部分項目中的 .git/hooks,預設存在的都是示例,其名字都是以 .sample 結尾,如果你想啟用它們,得先移除這個字尾。把一個正确命名且可執行的檔案放入 Git 目錄下的 hooks 子目錄中,即可激活該鈎子腳本。這樣一來,它就能被 Git 調用。
關于各種詳細的 hook 類型可以參考官方文檔 《自定義 Git - Git 鈎子》。
了解了這些 hook 鈎子,你就可以真的為所欲為了,你可以用來檢查消息、檢查代碼,可以用來觸發任意流程,譬如自動規範檢查等等,隻能說想象空間巨大無比。
二、commit msg 格式自動檢查
雖然有很多現成的 hook 可用,但是這裡還是給出一個簡單的例子示範下,這裡實作一個送出 message 格式的簡單檢查,要求送出消息單行且分兩部分,且有一定的字數限制。
2.1 編寫 commit-msg 腳本
#!/usr/bin/env python
# coding=utf-8
#
# commit msg check
import sys
import re
import io
if hasattr(sys.stdout, 'buffer'):
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
TIPS_INFO = '''
不符合commit規範,送出失敗(目前狀态等于沒做剛剛的commit操作)!
commit規範:
類型 詳細消息
規範樣例:
git commit -m "xxxxx xxxxxxxxxxxxx"
!!!!送出失敗!!!!
'''
def check_commit_line1_format(msg):
regOther = r'\S{5,} (.){10,100}'
matchObj = re.match(regOther, msg)
return matchObj
if __name__=="__main__":
with open(sys.argv[1], 'r') as f:
for line in f:
if (check_commit_line1_format(line)):
sys.exit(0)
else:
print(TIPS_INFO)
sys.exit(1)
既然編寫好了 commit hook 腳本,那新問題就來了,本地 hook 在項目的 .git 目錄下,.git 目錄又不受版本控制,是以在團隊推廣時,你不可能讓大家主動去放這個檔案,一方面可能會放錯,另一方面可能有些人壓根就不放,愛理不理,故而需要将這件事做成自動的,是以選擇在編譯項目時為拷貝切入點(因為你拽下來的項目一定會編譯的)。對于 android 項目來說,我們可以使用 gradle 編寫一個小任務來做這件事,具體如下:
/**
* git-hook-copy.gradle 檔案
*
* 本地項目 git hook 自動拷貝腳本
* 用法:
* apply from: 'git-hook-copy.gradle'
*/
/**
* 緊急開關
*/
def forbid = false
project.afterEvaluate {
if (forbid) {
preBuild.dependsOn 'resetGitHookConfig'
} else {
preBuild.dependsOn 'prepareGitHookConfig'
}
}
task prepareGitHookConfig(type: Copy) {
from getConfigFile()
into getGitHookDir()
}
task resetGitHookConfig {
doFirst {
File file = getGitHookFile('commit-msg')
if (file != null) {
file.delete()
}
}
}
def getGitHookFile(fileName) {
def dirPath = getGitHookDir()
if (dirPath != null && dirPath.length() > 0) {
def file = new File(dirPath, fileName)
if (file.exists()) {
return file
}
}
return null
}
def getConfigFile() {
File configFile = new File(project.rootDir, "git-hook/commit-msg")
if (configFile.exists()) {
return configFile.absolutePath
}
return null
}
def getGitHookDir() {
File gitHookDir = new File(project.rootDir, ".git/hooks/")
if (!gitHookDir.exists()) {
println("Your project can't find .git directory in the ${project.rootDir.absolutePath}," +
" please ensure it have been tracked by git VCS!")
return null
}
return gitHookDir.absolutePath
}
上面腳本有兩個任務,一個 reset,一個 config。reset 會将 .git hook 目錄下的規則删掉,等于沒有規則;config 是把項目根目錄下 git-hook 目錄下的 commit-msg hook 腳本複制到 .git hook 目錄下,這裡不用判斷是否已經存在檔案,直接覆寫即可,因為 gradle task 天生支援 UPDATE 機制,而且我們需要在修改 commit-msg 檔案後自動覆寫,是以不建議判斷 .git hook 下是否已經存在。
至此,我們可以進行一波操作了,譬如在指令行送出代碼,你會看到如下提示:

修複 log 格式後再進行 commit 即可。
三、總結
上面簡單介紹和實戰了一個小的 git hook 操作,感興趣你可以無限想象,和你的 checkstyle 什麼的,各種檢查什麼的結合起來都可以,反正師傅領進門,修行靠自己,需求靠團隊。
參考連結:
- git 的 hook 操作強大到難以置信,效率為王!