[Git Hooks] 阻止某个分支合并到另一个分支
序言
在项目开发过程中,通常会涉及到不同的分支,例如开发分支、测试分支和主干分支等。
一般情况下,我们不能错误地将测试分支合并到其他分支中。然而,有时候由于一些意外情况,我们可能会不小心将测试分支合并到其他分支中,并且在不知情的情况下还添加了新的代码。直到上线之后才被发现,造成的后果可大可小,并且回滚操作也很麻烦。
那么,有没有办法在合并阶段阻止合并非法分支呢?答案是可以的,使用 Git Hooks 可以实现这一点。
什么是 Git Hooks?
Git Hooks 是在执行 Git 命令的过程中会触发的自定义脚本。常用的钩子(hooks)包括:pre-commit
、commit-msg
、pre-push
等。举个例子,我们可以在 pre-commit
触发时进行代码格式验证,在 commit-msg
触发时对 commit
消息和提交用户进行验证,在 pre-push
触发时进行单元测试、e2e 测试等操作。
在执行这些脚本时,如果以非零的值退出程序,将会中断 Git 的提交/推送流程。因此,在 hooks 脚本中,当验证消息/代码不通过时,我们可以使用非零值来退出程序,从而中断 Git 的流程。
exit 1
然而,.git/hooks
无法通过 git push
推送到远程仓库,也无法通过 git pull
拉取回来。因此,在项目团队成员之间分享与同步 Git Hooks 就成了一个问题。为了解决这个问题,我们可以使用 Husky。
什么是 Husky?
Husky 是方便使用 Git Hooks 的工具,它集成了 Git Hooks 的功能,使其更加便捷。除此之外,Husky 还可以配合 commitlint
、lint-staged
等工具,用于检查提交信息的格式以及对代码进行格式化等操作。
安装 Husky
yarn add husky --dev
# or
npm install -D husky
初始化 Husky
npx husky init
npx husky init
命令实际上是向 package.json
文件的 scripts
中添加 prepare
脚本
{
"scripts": {
"prepare": "husky install"
}
}
prepare
脚本会在 npm install
后自动执行。也就是说,当我们安装完项目依赖后,husky install
命令会被执行,它会在项目根目录下创建一个名为 .husky
的目录,并将其指定为 Git Hooks 的目录。
测试 Husky
Husky 提供了一个命令来添加 Git 钩子:husky add <file> [cmd]
。
假设我们要添加 pre-commit
钩子:
npx husky add .husky/pre-commit "npm run test"
运行完该命令后,会在 .husky
目录下新增一个名为 pre-commit
的 shell
脚本。脚本内容如下:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm run test
当我们执行 git commit
时,会先执行 pre-commit
脚本。
到这里,我们的测试就结束了。那么,我们应该编写哪些钩子来阻止合并呢?在此之前,让我们先了解一下 Git 合并类型。
Git 合并类型
Git 有多种合并类型,其中常见的有:快进合并(Fast-forward Merges)和 显示合并(Explicit Merges)。接下来会分别介绍这些合并类型。
快进合并(Fast-forward Merges)
快进合并(Fast-forward Merges)是最简单的一种合并类型,如上图中将 Some Feature 分支合并进 Main 分支,Git 只需要将 Main 分支的指向移动到最后一个 commit 节点上。
快进合并(Fast-forward Merges)是 Git 在合并两个没有分叉的分支时的默认行为。如果你不希望采用这种方式,而是希望明确记录每次合并的情况,可以使用 git merge --no-ff
命令来进行显示合并(Explicit Merges)。
显示合并(Explicit Merges)
显示合并(Explicit Merges)会创建一个新的合并提交,将两个分支的更改明确地合并在一起。通过这种方式,可以清楚地看到合并的历史记录,并方便进行代码审查和追踪。其算法可以简单地描述为:递归地寻找路径最短的唯一共同祖先节点,然后以该祖先节点为 base 节点进行递归的三路合并。
如上图所示,Git 会找到 Some Feature 和 Main 的共同祖先节点作为 base 节点,然后进行递归合并,最终创建一个新的合并提交。
合并冲突
除此之外,Git 合并时可能会出现冲突。在这种情况下,需要手动解决冲突,然后重新执行 add 和 commit 操作。
编写钩子
接着,我们阅读 git hooks 文档得知:
-
快进合并(Fast-forward Merges)和显示合并(Explicit Merges)都会触发
post-merge
钩子。 -
合并冲突时会触发
prepare-merge-commit
和commit-msg
钩子。然而,commit-msg
钩子无法获取到合并进来的分支名称,因此只能使用prepare-merge-commit
钩子。
ok,让我们正式开始编写钩子。
post-merge 钩子
基本思路如下:
- 获取当前分支的名称
git rev-parse --abbrev-ref HEAD
- 获取合并进来的分支名称
当 post-merge
钩子被触发时,分支已经完成合并,并且 reflog 已经更新,因此我们可以通过 $GIT_REFLOG_ACTION
获取到合并进来的分支的信息。
- 如果当前分支是受保护的分支,并且合并进来的分支是禁止合并的分支,则撤销合并。
脚本内容如下:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# 当前分支的名称
CURRENT_BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD)
# 受保护的分支
PROTECTED_BRANCH_NAME="master"
# 禁止合并的分支
FORBIDDEN_BRANCH_NAME="test"
if [[ "$CURRENT_BRANCH_NAME" == *"$PROTECTED_BRANCH_NAME"* ]]; then
if [[ "$GIT_REFLOG_ACTION" == *"$FORBIDDEN_BRANCH_NAME"* ]]; 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 钩子
基本思路如下:
- 获取当前分支的名称
git rev-parse --abbrev-ref HEAD
- 获取合并进来的分支名称
在合并冲突阶段,.git/MERGE_HEAD
文件中会保留合并进来的分支的 hash,可以通过读取该文件获取对应的内容,然后使用 git name-rev [hash]
命令获取对应的分支名称。
- 如果当前分支是受保护的分支,并且合并进来的分支是禁止合并的分支,则撤销合并
脚本内容如下:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# 当前分支的名称
CURRENT_BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD)
# 受保护的分支
PROTECTED_BRANCH_NAME="master"
# 禁止合并的分支
FORBIDDEN_BRANCH_NAME="test"
if [[ -e .git/MERGE_HEAD ]]; then
MERGE_HEAD=`cat .git/MERGE_HEAD`
# 合并进来的分支名称
MERGE_BRANCH_NAME=$(git name-rev $MERGE_HEAD)
if [[ "$CURRENT_BRANCH_NAME" == *"$PROTECTED_BRANCH_NAME"* ]]; then
if [[ "$MERGE_BRANCH_NAME" == *"$FORBIDDEN_BRANCH_NAME"* ]]; then
echo "检测到非法合并: ${MERGE_BRANCH_NAME//$MERGE_HEAD / } ==into==> $CURRENT_BRANCH_NAME"
echo "撤销合并中..."
$(git reset --keep HEAD~1)
echo "已撤销合并 done"
exit 1
fi
fi
fi
转载自:https://juejin.cn/post/7250687457800470585