使用git hooks避免release分支merge到feature分支

简介

最近同事总是进行不规范的合并操作并且推送到了代码托管平台,导致重复开发,极大增加工作量,想寻找一下有没有非人工的避免方法。应该是可以通过一个客户端hooks解决的。

git hooks是git在hooks下的一系列固定命名的脚本,用于在某个特点阶段执行。

目标规则

  • 只允许master和同源 feature 分支合并到feature分支。
  • 只允许master和 feature 分支合并到release分支。

事故场景

  • 更新开发代码时pull拉取了另一个feature分支并合并提交。
  • 将release分支merge合并到了feature分支并提交。

因此我们大概需要编写pre-merge-commit这个hook,对合并分支的名称进行检测。

Git合并类型

03-04 Fast forward merge.svg

快进合并(Fast-forward Merges)是 Git 在合并两个没有分叉的分支时的默认行为,将 Some Feature 分支合并进 Main 分支,Git 只需要将 Main 分支的指向移动到最后一个 commit 节点上。

  • 快进合并(Fast-forward Merges)和显示合并(Explicit Merges)都会触发 post-merge 钩子。
  • 合并冲突时会触发 prepare-commit-msgcommit-msg 钩子。然而,commit-msg 钩子无法获取到合并进来的分支名称,因此只能使用 prepare-commit-msg 钩子。

hook脚本

  • git rev-parse --abbrev-ref HEAD可得到当前分支名称

post-merge 钩子

本地pull远程分支完成时触发 post-merge 钩子,由git merge调用。接受一个参数:a status flag specifying whether or not the merge being done was a squash merge。

  1. 如果当前分支是受保护的分支,并且合并进来的分支是禁止合并的分支,则撤销合并。
  2. post-merge 钩子被触发时,分支已经完成合并,并且 reflog 已经更新,因此我们可以通过 $GIT_REFLOG_ACTION 获取到合并进来的分支的信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/bin/env sh

echo 'this is post-merge hook...'
# 当前分支的名称
CURRENT_BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD)

# 受保护的分支
PROTECTED_BRANCH_NAME="feature-"

# 禁止合并的分支
FORBIDDEN_BRANCH_NAME="feature-"

# 如果当前分支为feature分支
if [[ "$CURRENT_BRANCH_NAME" == "$PROTECTED_BRANCH_NAME"* ]]; then
# 合并分支不为同源feature分支&&不为master
if [[ "$GIT_REFLOG_ACTION" != "$CURRENT_BRANCH_NAME" ]] && [["$GIT_REFLOG_ACTION" != *master*]]; then
echo "检测到非法合并: ${GIT_REFLOG_ACTION//merge / } ==into==> $CURRENT_BRANCH_NAME"
echo "撤销合并中..."
$(git reset --merge HEAD@{1})
echo "已撤销合并 done"
exit 1
fi
fi

post-merge经过实验后似乎在上述两种情况下皆不被调用,或者说还没到调用阶段。

pre-merge-commit 钩子

这个钩子在合并完成,获得提交消息之前由git merge调用,可以使用--no-verify选项绕过它。它不接受任何参数,并在合并成功执行之后和获取建议的提交日志消息以进行提交之前调用。从这个脚本中退出非零状态会导致Git合并命令在创建提交之前中止。

如果启用了pre-merge-commithook,则默认的预合并提交挂钩将运行pre-commithook。

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
#!/usr/bin/env sh

echo 'this is pre-merge-commit hook...'

# 检查合并的来源分支是否为release
CURRENT_BRANCH_NAME=$(git rev-parse --abbrev-ref --symbolic-full-name @{u})
echo 'CURRENT_BRANCH_NAME: $CURRENT_BRANCH_NAME...'

# 受保护的分支
PROTECTED_BRANCH_NAME="feature-"

# 禁止合并的分支
FORBIDDEN_BRANCH_NAME="feature-"

# 如果当前分支为feature分支
if [[ "$CURRENT_BRANCH_NAME" == "$PROTECTED_BRANCH_NAME"* ]]; then
# 合并分支不为同源feature分支&&不为master
if [[ "$GIT_REFLOG_ACTION" != "$CURRENT_BRANCH_NAME" ]] && [["$GIT_REFLOG_ACTION" != *master*]]; then
echo "检测到非法合并: ${GIT_REFLOG_ACTION//merge / } ==into==> $CURRENT_BRANCH_NAME"
echo "撤销合并中..."
$(git reset --merge HEAD@{1})
echo "已撤销合并 done"
exit 1
fi
fi

prepare-commit-msg(合并前提交)钩子

当处于merge状态时,git会在.git目录中生成一个MERGE_HEAD文件,不是merge状态则没有,所以可以判断这个文件是否存在来解决此问题。

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
#!/usr/bin/env sh

echo 'this is prepare-commit-msg hook...'
if [ -e $MERGE_HEAD ]; then
echo 'skip pre-commit hook...'
exit 0
fi
# 当前分支的名称
CURRENT_BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD)
echo CURRENT_BRANCH_NAME: "$CURRENT_BRANCH_NAME"
# 受保护的分支
PROTECTED_BRANCH_NAME="feature-"

# 禁止合并的分支
FORBIDDEN_BRANCH_NAME="feature-"

if [[ -e .git/MERGE_HEAD ]]; then
MERGE_HEAD=`cat .git/MERGE_HEAD`
echo MERGE_HEAD: "$MERGE_HEAD"
# 合并进来的分支名称
MERGE_BRANCH_NAME=$(git name-rev $MERGE_HEAD)
# 如果当前分支为受保护的分支
if [[ "$CURRENT_BRANCH_NAME" == "$PROTECTED_BRANCH_NAME"* ]]; then
## 合并进来的分支不为同源feature分支&&不为master
if [[ "$MERGE_BRANCH_NAME" != *"$CURRENT_BRANCH_NAME"* ]] && [[ "$MERGE_BRANCH_NAME" != *master* ]]; then
echo "检测到非法合并: ${MERGE_BRANCH_NAME//$MERGE_HEAD / } ==into==> $CURRENT_BRANCH_NAME"
echo "撤销合并中..."
$(git reset --keep HEAD~1)
echo "已撤销合并 done"
exit 1
fi
fi
fi

echo 'prepare-commit-msg hook done...'

上面这个prepare-commit-msg脚本经实验确实是正常工作的,不过会将正常5s左右的合并拖慢至30s,可根据实际需要使用。

参考博客

  1. [Git Hooks] 阻止某个分支合并到另一个分支

  2. 7.8 Git 工具 - 高级合并-

  3. githooks - Hooks used by Git

  4. Environment Variables

  5. 一文带你彻底学会 Git Hooks 配置

  6. Git高级操作: Git钩子

  7. Nov 20, 2021 - Prevent merge from a specific branch using Git Hooks,这个人编写了一个pre-merge-commit hook

  8. How to get origin merge branch name with git hook pre-merge-commit,使用环境变量GIT_REFLOG_ACTION获得要合并的分支