天天看點

git 的 hook 操作

一、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 下是否已經存在。

至此,我們可以進行一波操作了,譬如在指令行送出代碼,你會看到如下提示:

git 的 hook 操作

修複 log 格式後再進行 commit 即可。

三、總結

上面簡單介紹和實戰了一個小的 git hook 操作,感興趣你可以無限想象,和你的 checkstyle 什麼的,各種檢查什麼的結合起來都可以,反正師傅領進門,修行靠自己,需求靠團隊。

參考連結:

  • git 的 hook 操作強大到難以置信,效率為王!