通過Gradle Plugin實現Git Hooks檢測機制

HappyCorn發表於2019-05-07

背景

專案組多人協作進行專案開發時,經常遇到如下情況:如Git Commit資訊混亂,又如提交者資訊用了自己非公司的私人郵箱等等。因此,有必要在Git操作過程中的適當時間點上,進行必要的如統一規範、安全檢測等常規性的例行檢測。

面對此類需求,Git為我們提供了Git Hooks機制。在每個專案根目錄下,都存在一個隱藏的.git目錄,目錄中除了Git本身的專案程式碼版本控制以外,還帶有一個名為hooks的目錄,預設情況下,內建了常用的一些Git Hooks事件檢測模板,並以.sample結尾,其內部對應的是shell指令碼。實際使用時,需要將.sample結尾去掉,且對應的指令碼可以是其他型別,如大家用的比較多的python等。

顧名思義,Git Hooks稱之為Git 鉤子,意指在進行Git操作時,會對應觸發相應的鉤子,類似於寫程式碼時在特定時機用到的回撥。這樣,就可以在鉤子中進行一些邏輯判斷,如實現大家常見的Git Commit Message規範等,以及其他相對比較複雜的邏輯處理等。

多人協作的專案開發,即便已經實現了Git Hooks,但由於此目錄並非屬於Git版本管理,因此也不能直接達到專案組成員公共使用並直接維護的目的。

那麼,是否可以有一種機制,可以間接的將其納入到Git專案版本管理的範疇,從而可以全組通用,且能直接維護?

答案是可以的。

對於Android專案開發,通過利用自定義的Gradle Plugin外掛,可以達到這一目的。


實現

專案中應用自定義的Gradle Plugin,並在Gradle Plugin中處理好對應的Git Hooks檔案的邏輯。後續需要維護時,也只需要修改對應的Gradle Plugin即可。

下面主要通過例項展示具體的完整過程,以達到如下兩個目的:
1,統一規範Git Commit時的message格式,在不符合規範要求的情況下commit失敗;
2,統一規範Git Commit提交者的郵箱,只能使用公司的郵箱,具體通過檢測郵箱字尾實現。

具體過程如下:
1,新建對應的Git工程,包含預設的app示例應用模組。
2,新建模組,命名為buildSrc,此模組主要是真正的實現自定的外掛。此模組名稱不可修改(因為此獨立專案構建時,會將buildSrc命名的模組自動加入到構建過程,這樣,app模組中只需要直接apply plugin對應的外掛名稱即可)。
3,自定義外掛,實現主體邏輯。
buildSrc模組主要目錄結果如下:

buildSrc
|____libs
|____build.gradle
|____src
| |____main
| | |____resources
| | | |____META-INF
| | | | |____gradle-plugins
| | | | | |____Git-Hooks-Plugin.properties
| | | |____commit-msg
| | |____groovy
| | | |____com
| | | | |____corn
| | | | | |____githooks
| | | | | | |____GitHooksUtil.groovy
| | | | | | |____GitHooksExtension.groovy
| | | | | | |____GitHooksPlugin.groovy
複製程式碼

GitHooksPlugin實現:

package com.corn.githooks

import org.gradle.api.GradleException
import org.gradle.api.Plugin
import org.gradle.api.Project

class GitHooksPlugin implements Plugin<Project> {

        @Override
        void apply(Project project) {
            project.extensions.create(GitHooksExtension.NAME, GitHooksExtension, project)


            project.afterEvaluate {
                GitHooksExtension gitHooksExtension = project.extensions.getByName(GitHooksExtension.NAME)

                if (!GitHooksUtil.checkInstalledPython(project)) {
                    throw new GradleException("GitHook require python env, please install python first!", e)
                }

                File gitRootPathFile = GitHooksUtil.getGitHooksPath(project, gitHooksExtension)
                if (!gitRootPathFile.exists()) {
                    throw new GradleException("Can't found project git root file, please check your gitRootPath config value")
                }

                GitHooksUtil.saveHookFile(gitRootPathFile.absolutePath, "commit-msg")

                File saveConfigFile = new File(gitRootPathFile.absolutePath + File.separator + "git-hooks.conf")

                saveConfigFile.withWriter('utf-8') { writer ->
                    writer.writeLine '## 程式自動生成,請勿手動改動此檔案!!! ##'
                    writer.writeLine '[version]'
                    writer.writeLine "v = ${GitHooksExtension.VERSION}"
                    writer.writeLine '\n'
                    if (gitHooksExtension.commit != null) {
                        writer.writeLine '[commit-msg]'
                        writer.writeLine "cm_regex=${gitHooksExtension.commit.regex}"
                        writer.writeLine "cm_doc_url=${gitHooksExtension.commit.docUrl}"
                        writer.writeLine "cm_email_suffix=${gitHooksExtension.commit.emailSuffix}"
                    }
                }
            }
    }
}
複製程式碼

對應的GitHooksExtension擴充套件為:

package com.corn.githooks

import org.gradle.api.Project

class GitHooksExtension {

    public static final String NAME = "gitHooks"
    public static final String VERSION = "v1.0"

    private Project project

    String gitRootPath
    Commit commit

    GitHooksExtension(Project project) {
        this.project = project
    }

    def commit(Closure closure) {
        commit = new Commit()
        project.configure(commit, closure)
    }


    class Commit {
        // commit規範正則
        String regex = ''
        // commit規範文件url
        String docUrl = ''
        String emailSuffix = ''

        void regex(String regex) {
            this.regex = regex
        }

        void docUrl(String docUrl) {
            this.docUrl = docUrl
        }

        void emailSuffix(String emailSuffix){
            this.emailSuffix = emailSuffix
        }
    }
}
複製程式碼

GitHooksUtil工具類:

package com.corn.githooks

import org.gradle.api.GradleException
import org.gradle.api.Project
import org.gradle.process.ExecResult

import java.nio.file.Files

class GitHooksUtil {

    static File getGitHooksPath(Project project, GitHooksExtension config) {
        File configFile = new File(config.gitRootPath)
        if (configFile.exists()) {
            return new File(configFile.absolutePath + File.separator + ".git" + File.separator + "hooks")
        }
        else {
            return new File(project.rootProject.rootDir.absolutePath + File.separator + ".git" + File.separator + "hooks")
        }
    }

    static void saveHookFile(String gitRootPath, String fileName) {
        InputStream is = null
        FileOutputStream fos = null

        try {
            is = GitHooksUtil.class.getClassLoader().getResourceAsStream(fileName)
            File file = new File(gitRootPath + File.separator + fileName)
            file.setExecutable(true)

            fos = new FileOutputStream(file)
            Files.copy(is, fos)

            fos.flush()
        } catch (Exception e) {
            throw new GradleException("Save hook file failed, file: " + gitRootPath + " e:" + e, e)
        } finally {
            closeStream(is)
            closeStream(fos)
        }
    }

    static void closeStream(Closeable closeable) {
        if(closeable == null) {
            return
        }

        try {
            closeable.close()
        } catch (Exception e) {
            // ignore Exception
        }
    }


    static boolean checkInstalledPython(Project project) {
        ExecResult result
        try {
            result = project.exec {
                executable 'python'
                args '--version'
            }
        } catch (Exception e) {
            e.printStackTrace()
        }

        return result != null && result.exitValue == 0
    }
}
複製程式碼

resources目錄中,META-INF.gradle-plugins實現對Gradle Plugin的配置,檔案Git-Hooks-Plugin.properties檔名字首Git-Hooks-Plugin表示外掛名,對應的implementation-class指定外掛的實際實現類。

|____resources
| | | |____META-INF
| | | | |____gradle-plugins
| | | | | |____Git-Hooks-Plugin.properties

--------------------------------------------
implementation-class=com.corn.githooks.GitHooksPlugin

複製程式碼

commit-msg檔案直接放到resources目錄中,通過程式碼拷貝到指定的Git Hooks目錄下。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
import re
import os

if sys.version > '3':
    PY3 = True
    import configparser
else:
    PY3 = False
    import ConfigParser as configparser
    reload(sys)
    sys.setdefaultencoding('utf8')

argvs = sys.argv
# print(argvs)
commit_message_file = open(sys.argv[1])
commit_message = commit_message_file.read().strip()

CONFIG_FILE = '.git' + os.path.sep + 'hooks' + os.path.sep + 'git-hooks.conf'

config = configparser.ConfigParser()
config.read(CONFIG_FILE)

if not config.has_section('commit-msg'):
    print('未找到配置檔案: ' + CONFIG_FILE)
    sys.exit(1)

cm_regex = str(config.get('commit-msg', 'cm_regex')).strip()
cm_doc_url = str(config.get('commit-msg', 'cm_doc_url')).strip()
cm_email_suffix = str(config.get('commit-msg', 'cm_email_suffix')).strip()

ret = os.popen('git config user.email', 'r').read().strip()

if not ret.endswith(cm_email_suffix):
    print ('===============================  Commit Error ====================================')
    print ('==> Commit email格式出錯,請將git config中郵箱設定為標準郵箱格式,公司郵箱字尾為:' + cm_email_suffix)
    print ('==================================================================================\n')
    commit_message_file.close()
    sys.exit(1)

# 匹配規則, Commit 要以如下規則開始
if not re.match(cm_regex, commit_message):
    print ('===============================  Commit Error ====================================')
    print ('==> Commit 資訊寫的不規範 請仔細參考 Commit 的編寫規範重寫!!!')
    print ('==> 匹配規則: ' + cm_regex)
    if cm_doc_url:
        print ('==> Commit 規範文件: ' + cm_doc_url)
    print ('==================================================================================\n')
    commit_message_file.close()
    sys.exit(1)
commit_message_file.close()
複製程式碼

至此,buildSrc模組外掛部分已經完成。

4,app應用模組中應用外掛,並測試效果。 app應用模組的build.gralde檔案應用外掛,並進行相應配置。

app模組build.gralde相應配置:
----------------------------------------
apply plugin: 'com.android.application'

....
....

apply plugin: 'Git-Hooks-Plugin'

gitHooks {

    gitRootPath rootProject.rootDir.absolutePath

    commit {
        // git commit 強制規範
        regex "^(新增:|特性:|:合併:|Lint:|Sonar:|優化:|Test:|合版:|發版:|Fix:|依賴庫:|解決衝突:)"
        // 對應提交規範具體說明文件
        docUrl "http://xxxx"

        // git commit 必須使用公司郵箱
        emailSuffix "@corn.com"
    }

}

....
....

複製程式碼

應用外掛後,來到專案工程的.git/hooks/目錄,檢視是否有對應的commit-msggit-hooks.conf檔案生成,以及對應的指令碼邏輯和配置是否符合預期,並實際提交專案程式碼,分別模擬commit messagegit config email場景,測試結果是否與預期一致。


結語

本文主要通過demo形式演示基於Gradle Plugin外掛形式實現Git Hooks檢測機制,以達到專案組通用及易維護的實際實現方案,實際主工程使用時,只需要將此獨立獨立Git工程中的buildSrc模組,直接釋出到marven,主工程在buildscriptdependencies中配置上對應的外掛classpath即可。其他跟上述示例中的app應用模組一樣,直接應用外掛並對應配置即可使用。

通過Gradle Plugin,讓我們實現了原本不屬於專案版本管理範疇的邏輯整合和同步,從而可以實現整個專案組通用性的規範和易維護及擴充套件性的方案,不失為一種有效策略。


作者:HappyCorn
連結:https://juejin.im/post/5cce5df26fb9a031ee3c2355
來源:掘金
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。

相關文章