鈎子介紹
自定義鈎子分為:項目鈎子和全局鈎子
自定義全局鈎子:
全局鈎子目錄結構:
(注意:excludes目錄結構是我們自定義的目錄,規則邏輯在update.d/update.py腳本里實現的,非gitlab官方提供功能)
/opt/gitlab/embedded/service/gitlab-shell/custom_hooks
├── excludes
│ └── excludes.txt
└── update.d
└── update.py
鈎子目錄:
自定全局鈎子目錄: /opt/gitlab/embedded/service/gitlab-shell/custom_hooks ,其中 update.d/update.py 是我們自定義的鈎子腳本, 腳本使用python語言。
如何擴展和修改全局鈎子指定目錄?
1.修改 /etc/gitlab/gitlab.rb
配置文件中的配置項:gitlab_shell['custom_hooks_dir'] = "/opt/gitlab/embedded/service/gitlab-shell/custom_hooks"
2. 執行 sudo gitlab-ctl reconfigure 命令來使配置生效。
Tips:不要嘗試直接修改 <gitlab-shell>/config.yml 的文件內容,文件中有說明:
This file is managed by gitlab-ctl. Manual changes will be erased! To change the contents below, edit /etc/gitlab/gitlab.rb and runsudo gitlab-ctl reconfigure.
3.在自定的 custom_hooks_dir 目錄(即我們的custom_hooks目錄)下可創建三個文件夾對應三類 server hook name :
-
- pre-receive.d
- update.d
- post-receive.d
- 在每個文件夾下可創建任意文件,在對應的 hook 時期,gitlab 就會主動調用
- 文件名以 ~ 結尾的文件會被忽略
- 如果想看這部分的實現細節可以看 <gitlab-shell>/lib/gitlab_custom_hook.rb 文件
如何對單個項目排除全局鈎子?
在 /opt/gitlab/embedded/service/gitlab-shell/custom_hooks/excludes/excludes.txt 文件中新加一行你要排除的git的地址即可:例如: https://git.test.abc/example.git 。 (該功能是自定義實現的,非官方提供)
自定義項目鈎子
項目鈎子的目錄為固定目錄:
<project>.git/custom_hooks/ 例如我們的項目自定義目錄為: /var/opt/gitlab/git-data/repositories/frontend/testWeb.git/custom_hooks/update ,update是我們自定義的鈎子腳本。
鈎子的執行順序
鈎子將按照以下的順序執行
<project>.git/hooks/
- symlink togitlab-shell/hooks
global dir<project>.git/hooks/<hook_name>
- executed bygit
itself, this isgitlab-shell/hooks/<hook_name>
<project>.git/custom_hooks/<hook_name>
- per project hook (this is already existing behavior)<project>.git/custom_hooks/<hook_name>.d/*
- per project hooks<project>.git/hooks/<hook_name>.d/*
OR<custom_hooks_dir>/<hook_name.d>/*
- global hooks: all executable files (minus editor backup files)
鈎子校驗規則
1、提交合並代碼到test分支前,需要先合並代碼到dev驗證通過后方能push到test
2、提交合並代碼到master分支前,需要先合並代碼到dev、test驗證通過后方能push到master
3、不同分支不能相互合並(如果需要合並,請合並后刪除一個不需要的分支方能提交)
4、合並代碼到dev、test、master,merge過程出錯了怎么辦?
可以直接在dev、test、master分支上修改代碼,但是需要在備注comment里填寫FIX_MERGE_ERROR 就可以直接提交(但請不要正常的代碼也使用這個命令!!!!!否則后果很嚴重)
git鈎子原理
在編寫鈎子的過程中發現鈎子原理如下:
1.gitlab默認安裝完畢后在server端會有兩個比較重要的目錄。
<project>.git 目錄例如:/var/opt/gitlab/git-data/repositories/test/automation.git (我們稱這個目錄為git的project) 目錄樹結構如下:
├── branches
├── config
├── description
├── HEAD
├── hooks -> /opt/gitlab/embedded/service/gitlab-shell/hooks
├── hooks.old.1500269284
│ ├── applypatch-msg.sample
│ ├── commit-msg.sample
│ ├── post-update.sample
│ ├── pre-applypatch.sample
│ ├── pre-commit.sample
│ ├── prepare-commit-msg.sample
│ ├── pre-push.sample
│ ├── pre-rebase.sample
│ ├── pre-receive.sample
│ └── update.sample
├── info
│ └── exclude
├── objects
│ ├── info
│ └── pack
└── refs
├── heads
└── tags
這個目錄結構和git本地項目.git 的目錄結構相似。目錄中保存了git操作相關的版本信息。在git本地修改的元數據信息提交后都會保存在該目錄中。
/opt/gitlab/embedded/service/gitlab-shell/hooks
改目錄是git默認鈎子的目錄。在git的低版本( GitLab 8.15.以下)中擴展全局鈎子可以該目錄的hooks目錄中直接修改或新加,但是可能影響到原有的鈎子,因此不建議使用,推薦方式見本章1.3 如何修改全局鈎子。
2.git 鈎子的腳本代碼中: 0 正常退出,用戶可以 push;非 0 異常退出,中斷提交(pre-receive 和 update)
3.通過本人研究發下,鈎子(腳本中調用的shell的命令)其實是在server端的 <project>.git git目錄 執行的。舉例:當執行automation項目的全局鈎子時鈎子內執行的shell命令會在/var/opt/gitlab/git-data/repositories/test/automation.git 目錄中執行,可以通過在鈎子中pwd來打印當前鈎子的執行目錄發現的。
鈎子的python代碼:

1 #!/usr/bin/env python 2 # coding=utf-8 3 ''' 4 該腳本在pre-receive或post-receive鈎子中被調用,也可以直接將該文件作為git的鈎子使用 5 若鈎子為shell腳本,則需要加入以下代碼調用該腳本: 6 while read line;do 7 echo $line | python $PATH/pre-receive.py 8 done 9 當用戶執行git push的時候會在遠程版本庫上觸發此腳本 10 該腳本的主要作用:獲取用戶提交至版本庫的文件列表,提交者及時間信息 11 ''' 12 13 import sys, subprocess 14 import re 15 import os 16 17 __author__ = "zhanghuiwen" 18 excludPath ="/opt/gitlab/embedded/service/gitlab-shell/custom_hooks/excludes/excludes.txt"; 19 baseGitUrl="http://172.26.0.80:8081" 20 21 22 class Trigger(object): 23 24 25 def __init__(self): 26 ''' 27 初始化文件列表信息,提交者信息,提交時間,當前操作的分支 28 ''' 29 self.pushAuthor = "" 30 self.pushTime = "" 31 self.fileList = [] 32 self.ref = "" 33 34 35 36 def __getGitInfo(self): 37 ''' 38 ''' 39 self.oldObject = sys.argv[2] 40 self.newObject = sys.argv[3] 41 self.ref = sys.argv[1] 42 43 # 跳過排除的項目 44 def _skipExcludeProjects_(self): 45 ''' 46 跳過掃描的項目 47 ''' 48 rev = subprocess.Popen("pwd", shell=True, stdout=subprocess.PIPE); 49 gitServerRepoPath = rev.stdout.readline(); # 路徑'/var/opt/gitlab/git-data/repositories/alpha/testhook.git' 50 paths = gitServerRepoPath.split("repositories"); 51 projectPath = paths[1]; # /alpha/testhook.git 52 rev.stdout.close(); 53 54 # 讀取配置中的文件 55 lines = open(excludPath, "r"); 56 for line in lines: 57 realLine = line.strip("\n"); 58 result = realLine.replace(baseGitUrl,"") 59 if projectPath.strip(" ").strip("\n") == result.strip(" ").strip("\n"): 60 lines.close() 61 print ("例外項目允許不經過dev和test直接提交") 62 exit(0) 63 else: 64 pass 65 lines.close() 66 # 繼續執行 67 68 def __getPushInfo(self): 69 ''' 70 git show命令獲取push作者,時間,以及文件列表 71 文件的路徑為相對於版本庫根目錄的一個相對路徑 72 ''' 73 rev = subprocess.Popen('git rev-list ' + self.oldObject + '..' + self.newObject, shell=True, 74 stdout=subprocess.PIPE) 75 pushList = rev.stdout.readlines() 76 pushList = [x.strip() for x in pushList] 77 # 循環獲取每次提交的文件列表 78 for pObject in pushList: 79 p = subprocess.Popen('git show ' + pObject, shell=True, stdout=subprocess.PIPE) 80 pipe = p.stdout.readlines() 81 pipe = [x.strip() for x in pipe] 82 self.pushAuthor = pipe[1].strip("Author:").strip() 83 self.pushTime = pipe[2].strip("Date:").strip() 84 85 self.fileList.extend(['/'.join(fileName.split("/")[1:]) for fileName in pipe if 86 fileName.startswith("+++") and not fileName.endswith("null")]) 87 88 uBranch = self.ref.split('/')[len(self.ref.split('/')) - 1] 89 print '提交分支: %s' % uBranch 90 print '提交變動from:%s to:%s' % (self.oldObject, self.newObject) 91 print '提交的commit:%s' % pushList 92 # if uBranch == 'dev': 93 # return 94 # 循環獲取每次提交的文件列表 95 for pObject in pushList: 96 # 判斷是否是merge commit,如果是merge commit則忽略 97 gitCatFileCmd = ('git cat-file -p %s') % (pObject) 98 p = subprocess.Popen(gitCatFileCmd, shell=True, stdout=subprocess.PIPE) 99 pipe = p.stdout.readlines() 100 pipe = [x.strip() for x in pipe] 101 i = 0 102 for branch in pipe: 103 if branch.startswith('parent '): 104 i += 1 105 if i >= 2: 106 continue 107 108 # 如果提交的帶上的msg是FIX_MERGE_ERROR則可以通行(避免合錯分支引起的問題) 109 msgLine = pipe[-1] 110 print msgLine 111 if msgLine == 'FIX_MERGE_ERROR': 112 continue 113 # if not re.match(r'^(\w+)-(\d+)', msgLine): 114 # print '\033[1;35m %s 提交的信息沒有帶上jira編號,請確認添加 \033[0m' % pObject 115 # exit(-1) 116 listCmd = ('git branch --contains %s') % (pObject) 117 p = subprocess.Popen(listCmd, shell=True, stdout=subprocess.PIPE) 118 pipe = p.stdout.readlines() 119 pipe = [x.strip() for x in pipe] 120 print 'commit:%s->所屬分支:%s' % (pObject, pipe) 121 # 如果是master分支push提交,必須先提交dev、test 122 if 'master' == uBranch: 123 if 'dev' not in pipe or 'test' not in pipe: 124 print '\033[1;35m 合並到master的分支必須先在dev、test上經過驗證合並才能提交,具體錯誤提交的hash:%s \033[0m' % pObject 125 exit(-1) 126 elif 'test' == uBranch: 127 if 'dev' not in pipe: 128 print '\033[1;35m 合並到test的分支必須先在dev上經過驗證合並才能提交,具體錯誤提交的hash:%s \033[0m' % pObject 129 exit(-1) 130 branchs = set() 131 isMaster = True 132 for branch in pipe: 133 branch = branch.replace('* ', '') 134 if 'master' == branch: 135 isMaster = False 136 break 137 if 'test' == branch or 'dev' == branch or 'dev-permission' == branch or 'test-permission' == branch: 138 continue 139 # elif uBranch != 'master' and uBranch != 'test' and uBranch != 'dev' and branch != uBranch: 140 # print '\033[1;35m 提交失敗!你合並提交的分支來自於多個分支,請確認,你的分支%s,其他分支%s \033[0m' % (uBranch, branch) 141 # exit(-1) 142 branchs.add(branch) 143 if len(branchs) >= 2 and isMaster: 144 print '\033[1;35m 提交失敗!你合並提交的分支來自於多個分支,請確認%s \033[0m' % pipe 145 exit(-1) 146 147 def getGitPushInfo(self): 148 ''' 149 返回文件列表信息,提交者信息,提交時間 150 ''' 151 self.__getGitInfo() 152 self._skipExcludeProjects_() 153 self.__getPushInfo() 154 print '=========================================' 155 print "Time:", self.pushTime 156 print "Author:", self.pushAuthor 157 print "Ref:", self.ref 158 print "Files:", self.fileList 159 print '=========================================' 160 161 162 if __name__ == "__main__": 163 t = Trigger() 164 t.getGitPushInfo()