git-hook

我在想一个问题,如果别人提交了什么,我能否及时知道?也就是git有更新的话,能否及时通知给各用户?方法是有的,git-hook是目前了解到的一种方法。

参考博客

创建git私有仓库
自定义Git-Git钩子

基本配置

我打算在阿里云服务器上搭一个git仓库来做测试,阿里云服务器上跑的ubuntu版本是:
Linux iZbp1d8sfcdxbwjyaf1xcuZ 4.4.0-174-generic #204-Ubuntu SMP Wed Jan 29 06:41:01 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

  1. 安装git:sudo apt-get install git
  2. 创建git用户,git用户用来通过ssh连接git服务:sudo adduser git,此时设置的git用户密码,也是连接git仓库的密码
  3. (可选)创建证书登录:收集所有需要登录的用户公钥,然后导入到/home/git/.ssh/authorized_keys文件即可,这也就是班上某些同学可以不用输入密码就可以连上老师的git的原因
  4. 初始化git仓库,假设仓库的位置想放在/srv/sample.git,在/srv目录下输入命令:sudo git init --bare sample.git,这样就创建了一个裸仓库,裸仓库没有working dir,纯粹是为了测试和共享
  5. 将git仓库的拥有者改为git:sudo chown -R git:git sample.git
  6. 出于安全考虑,git用户不应该登录shell,可以编辑/etc/passwd,找到类似一行:git:x:1001:1001:,,,:/home/git:/bin/bash,将其改为git:x:1001:1001:,,,:/home/git:/usr/bin/git-shell,这样,git用户可以正常通过ssh使用git,但无法登录shell
  7. 在本地通过ssh克隆该仓库:git clone git@serverAddress:/srv/sample.git
  8. 测试,新建一个readme.txt,然后git add .git commit -am "test"git push发现成功了

更多关于git的操作可以去看廖雪峰的博客,很详细!很有用!廖雪峰的博客

git hook

git钩子,即在某些特定动作(pull、push等)发生时触发自定义的脚本,git的钩子分为客户端钩子和服务器端钩子。
钩子都被存储在Git目录下的hooks子目录中。在我们创建的项目中客户端钩子位于.git/hooks,服务器端钩子位于hooks下,我们打开发现里面都是.sample为后缀的文件,这些文件都是示例脚本,如果想启用它们,将后缀去掉,然后chmod a+x 对应文件即可,注意钩子是根据文件名来区分执行的,所以某些操作必须对应相应的文件名。
我们随便打开几个,发现里面是shell脚本,官网中说了,任何正确命名的可执行脚本都可以正常使用,包括Ruby或Python或其他语言。

客户端钩子

客户端钩子分为提交工作流钩子、电子邮件工作流钩子和其他钩子,这里面很多钩子我不是很理解。注意:克隆某个版本库时,客户端钩子并不随同复制。如果需要靠这些脚本来强制维持某种策略时,建议在服务器端实现这一功能。

提交工作流钩子

  1. pre-commit:输入git commit后,但此时还没有commit时的钩子,可以用于检查要commit的快照,如测试运行、检查代码和代码风格等,如果以非零值退出了,git将放弃此次提交;
  2. prepare-commit-msg:不是很理解,但这个还是在commit之前的;
  3. commit-msg:不是很理解,但可以用于核对提交信息是否遵循指定的模板,这个还是在commit之前的;
  4. post-commit:在commit完成之后运行的,用于通知之类的事情

电子邮件工作流钩子

据官网所说,它们都是由git am命令调用的,好吧,我没用过,主要说一下有:applypatch-msg和pre-applypatch,直接跳过了。

其他钩子

  1. pre-rebase:运行于变基前的钩子,但是我没用过变基,只知道其在于整理所有的操作放在同一条运行线(我也不知道该咋叫了)上;
  2. post-rewrite:听说很大程度上和post-checkout和post-merge差不多,那也先跳过了;
  3. post-checkout:这个钩子执行于git checkout成功运行后,我们知道git checkout一般用来切换分支,所以我们可以用这个钩子来调整工作目录。其中包括放入大的二进制文件、自动生成文档或进行其他类似这样的操作。
  4. post-merge:在git merge成功运行后,该钩子会被调用,我们知道git merge一般用于合并分支,我们可以用该钩子恢复Git无法跟踪的工作区数据,比如权限数据。这个钩子也可以用来验证某些在Git控制之外的文件是否存在,这样你就能在工作区改变时,把这些文件复制进来。没怎么看懂hh。merge确实会导致工作区发生变化。
  5. pre-push:该钩子会在git push运行期间,更新了远程引用但尚未传送对象时被调用,可以在推送开始之前,用它验证对引用的更新操作(一个非零的退出码将终止推送过程)。
  6. pre-auto-gc:Git的一些日常操作在运行时,偶尔会调用git gc --auto进行垃圾回收,没用过hh,该钩子会在垃圾回收开始之前被调用,可以用它来提醒你现在要回收垃圾了,或者依情形判断是否要中断回收。

服务器端钩子

可以用于仓库管理员的一些操作,例如我下面的实践中的例子。

pre-receive

处理来自客户端的推送操作时,最先被调用的脚本是pre-receive。也就是首先对客户端推送的消息进行一些操作,例如检查等,可以拦截推送内容。

update

update脚本和pre-receive脚本十分类似,不同之处在于它会为每一个准备更新的分支各运行一次。如推送者同时向多个分支推送内容,pre-receive只运行一次,相比之下update则会为每一个被推送的分支各运行一次。不是很懂这个操作。

post-receive

客户端推送结束后的操作,可以用来更新其他系统服务或者通知用户,该脚本无法终止推送进程,不过客户端在它结束运行之前将保持连接状态,所以如果你想做其他操作需谨慎使用它,因为它将耗费你很长的一段时间。

git-hook实例

在学完关于git hook的基础知识后,下面开始实战,实战目的是要写出一个当有文件push到仓库后,仓库端发邮件给所有参与的开发者,提醒其仓库更新了。
要用到服务端的post-receive这个钩子,打算用这个钩子来调我们所写的发邮件的python脚本(post-receive.py)。
首先在sample.git/hooks里面存放着所有服务端的hook,所以我们要在里面新建一个post-receive和post-receive.py

post-receive

就是一个shell脚本,然后调用python

1
2
#! /bin/sh
python3 /home/lgx/Desktop/GitTestRepo/sample.git/hooks/post-receive.py

post-receive.py(发邮件)

这里我遇到的问题主要有以下几个,目前都解决了:

  1. 要使用SMTP发送邮件,首先对于各大邮箱运营商,都要去开启SMTP服务,然后获得授权码,填在代码中的authorizationCode(对于发送者邮箱而言),QQ邮箱没有尝试,但是163邮箱总是显示535错误(authentication failed);而126是可以的
  2. 126是可以的,注意构造From和To的时候不要使用Header,Subject可以,否则会出现554错误;
  3. 想在服务端获得最近一次git push的信息,然后作为邮件内容发送给开发者,注意可以用git show命令,但是我们的python程序怎么获取到该命令执行后的内容呢?可以提供两种方法,但是其中一种方法直接./post-receive执行没啥问题,但是实际用的时候就出问题了。
    第一种:使用GitPython,pip install gitpython,然后import git,然后就可以像代码中被注释起来的getTheGitLog()方法那样使用,这种方法挺好的,但是就是不知道为啥我在实测时,git push后报remote: ImportError: No module named 'git'这个错误,但是在服务端本地执行./post-receive就没问题,然后各种尝试都不行,像sys.path.append,目前不知道为啥;
    第二种:使用subprocess的check_output,这是自带的库,所以没啥问题,没出现找不到的问题,这个是创建了一个子进程,用来执行git show,然后父进程等待子进程完成,并返回子进程标准输出的结果,这个就挺好的吧。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# -*- coding: utf-8 -*-
import smtplib
from email.mime.text import MIMEText
from email.header import Header
import time
# import git
from subprocess import check_output
def getTheGitLog2():
log=check_output(['git','show']).decode()
return log
# def getTheGitLog():
# repo=git.Repo('***')# 这里填git仓库的位置
# command=repo.git
# return str(command.show())
def send_mail(theMessage):
# 填写服务器、发件人和收件人列表
host = 'smtp.126.com'# 目前163的邮箱不知道为啥不行,填对应的SMTP服务器地址
sender = '***'# 填发送方邮箱,这里指远程git仓库端的邮箱,当然也可以指定某个邮箱是专门发仓库改动通知的
receivers = ['***','***']# 这里填收件人邮箱列表
authorizationCode="***"# 发送方的邮箱授权码
# 构造邮件内容
# 获取git最新的提交记录
echo="Hi,the git repository has been updated:"
message = echo+'\n'+theMessage
theEmail = MIMEText(message, 'plain', 'utf-8')
theEmail['From'] = sender # 这里不要用Header编码,否则报554错误
theEmail['To'] = str(receivers)# 这里也不要用Header编码,否则报554错误
curTime = time.strftime("%Y-%m-%d %H:%M:%S",time.localtime())# 获取更新时间
subject = 'Git仓库更新:'+curTime
theEmail['Subject'] = Header(subject, 'utf-8')
try:
smtpObj = smtplib.SMTP_SSL(host,465)# ssl的端口是465,普通的是25,使用smtplib.SMTP方法
smtpObj.login(sender,authorizationCode)
smtpObj.sendmail(sender,receivers,theEmail.as_string())
smtpObj.quit()# 最好用quit,不用close
print("Reminder email has been sent!")
except smtplib.SMTPException as e:
print(e)
if __name__ == '__main__':
theMessage=getTheGitLog2()
#theMessage=getTheGitLog()
send_mail(theMessage)

此时我们可以在本地git里新建一个txt文件,执行add、commit、push操作后,发现:remote: Reminder email has been sent!,证明钩子成功调用python脚本,邮件发送后效果如下:
1
可以看到效果还是不错的嘛!可以看到钩子的功能是非常强大的,以后多加探索。

另外我把这个小项目push到GitHub上了,附github项目地址:git-push-automatic-remind