背景
項目組多人協作進行項目開發時,經常遇到如下情況:如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-msg
及git-hooks.conf
文件生成,以及對應的腳本邏輯和配置是否符合預期,並實際提交項目代碼,分別模擬commit message
和git config email
場景,測試結果是否與預期一致。
結語
本文主要通過demo形式演示基於Gradle Plugin
插件形式實現Git Hooks
檢測機制,以達到項目組通用及易維護的實際實現方案,實際主工程使用時,只需要將此獨立獨立Git
工程中的buildSrc
模塊,直接發布到marven
,主工程在buildscript
的dependencies
中配置上對應的插件classpath
即可。其他跟上述示例中的app
應用模塊一樣,直接應用插件並對應配置即可使用。
通過Gradle Plugin
,讓我們實現了原本不屬於項目版本管理范疇的邏輯整合和同步,從而可以實現整個項目組通用性的規范和易維護及擴展性的方案,不失為一種有效策略。
作者:HappyCorn
鏈接:https://juejin.im/post/5cce5df26fb9a031ee3c2355
來源:掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。