使用git钩子防止合并分支

 

git是一款实用的版本管理工具,我们通过git init初始化一个git仓库,git会在当前目录为我们生成一个.git/目录,用来管理我们的版本文件信息。
在这个目录中有一个二级目录.git/hooks/,它里面存放了一些git执行的钩子脚本,在git运行的不同时期,执行不同的钩子。我们可以通过编写一些钩子脚本控制它的工作流程,比如在代码提交时进行邮件通知、代码格式检验等。

本文介绍的是一种通过编写钩子防止分支合并的案例,场景是在开发中有一些远超前于当前分支的分支(比如beta分支),如不慎将其合入开发分支(feature分支),然后还提交了,会给项目带来不必要的风险。

考虑日常工作情况,先使用git add, git commit提交代码,然后git merge合并分支。git merge进行的是一种三方合并,git分析了当前版本(以下简称F版本)、待合并版本(以下简称M版本)和它们的第一个共同祖先(也即分裂出它们两者的那个版本)这三者的差异,然后进行判断。如果M包含F的所有修改,则默认采用fast forward合并模式,直接把F所在分支的指针向前移动到M所在分支,合并结束,且不会执行任何钩子。

另一种情况是F和M形成了分裂(M不能包含F,因为修改了不同文件或者修改了相同的文件且造成冲突),在解决了所有可能发生的冲突后,git合并F和M的修改,创建一个全新版本(图2)。值得注意的是,如果在merge操作时带上-no-ff参数,则会强制按照这种方式合产生新版本。

第一种情况:

第二种情况:

在merge过程中会依次触发prepare-commit-msgcommit-msg钩子;如果有冲突,则在此之前,解决冲突并commit后触发pre-commit钩子,具体流程见下图。

git merge xxx三方合并是否快进合并结束是否冲突解决冲突后commit执行钩子pre-commit执行钩子prepare-commit-msg编辑提交合并信息执行钩子commit-msg合并结束yesnoyesno

下面来看钩子的编写,在.git/hooks文件夹下存放着一些git自带的钩子范例,且都以.sample为文件后缀。如果想让它们发挥作用,直接去掉这个后缀即可。钩子使用bash语法,在脚本中exit一个非零值即可中断git的行为。

比如说,把prepare-commit-msg.sample文件更名为prepare-commit-msg,并重写代码:

#!/bin/sh
echo "hook: prepare-commit-msg"
exit 1

那么通过正常git流程无法提交代码了,因为不管是commit操作还是merge操作(非快进),prepare-commit-msg钩子都要执行,而它exit 1表明是非正常终止,git就直接放弃了后续操作。

再来说下解决前文问题的思路:
我们在prepare-commit-msg钩子中实现控制,预设一个黑名单,对于所有git merge xxx操作,读取xxx对应的分支信息,与黑名单进行匹配,如果匹配命中,则终止操作。
需要思考的子问题:

  1. 钩子应该能自动检测当前操作是merge还是commit (它们都会触发commit-msg)
  2. 需要一个检测机制,精确匹配当前merge操作的分支是否在黑名单中

问题一是容易处理的,钩子执行时在全局已经给了我们几个参数,可以通过$x语法获取到,比如说对于prepare-commit-msg钩子:

#!/bin/sh
echo "hook: prepare-commit-msg"
# 钩子的路径
# .git/hooks/prepare-commit-msg
echo $0 
# 提交信息文件路径(这是一个临时文件)
# merge操作 .git/MERGE_MSG ; commit操作 .git/COMMIT_EDITMSG
echo $1
# 操作类型
# merge操作 merge ; commit操作 message
echo $2
exit 1

$1参数是提交信息文件路径,merge操作的文件路径为".git/MERGE_MSG",我们自然可以判断如果这个文件存在,则是merge操作。当然也可以直接用$2操作类型判断,但commit-msg钩子中没有$2参数。

再来看问题二,怎样才能精确匹配分支?首先要找到待合并的分支是啥,在merge操作过程中,git会在.git目录生成MERGE_HEAD, MERGE_MODE, MERGE_MSG三个文件,分别存放的是待合并分支的sha-1值,合并模式和合并信息,读取MERGE_HEAD文件即可获取分支信息,nice。

接下来,要获取黑名单中分支的sha-1值,它们保存在.git/refs/heads这个目录里,我们遍历这个目录读取对应分支名的文件,即可拿到sha-1值

接下来就是匹配操作,就不赘述了。代码如下:

#!/bin/sh

BLACKLIST=("beta" "gamma")
forbid_list=()

if [[ -e .git/MERGE_HEAD ]]; then
  heads=`ls .git/refs/heads`

  for bl in "${BLACKLIST[@]}"; do
    if [[ -n `echo ${heads} | grep -oP "(^| )${bl} "` ]]; then
      forbid_list+=(`cat .git/refs/heads/${bl}`)
    fi
  done

  merge_head=`cat .git/MERGE_HEAD`
  for br in "${forbid_list[@]}"; do
    if [[ ${merge_head} == ${br} ]]; then
      echo -e "\033[41;37m 合并了黑名单中的分支 \033[0m\n\r"
      echo -e "\033[41;37m 请使用 git merge --abort 命令终止合并 \033[0m"
      exit 1
    fi
  done
fi

最后一点也是最重要的,前面提到过fastward合并模式无法触发任何钩子,所以必须使用强制产生新版本的--no-ff模式,钩子才能发挥作用。推荐做法是使用git别名: git config --global alias.mg 'merge --no-ff', 以后合并操作使用git mg xxx, 保证钩子执行的同时还能少按几次键。

posted @ 2019-12-07 14:02  SteelArm  阅读(927)  评论(0)    收藏  举报