likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 42

序言

在项目开发过程中,通常会涉及到不同的分支,例如开发分支测试分支主干分支等。

一般情况下,我们不能错误地将测试分支合并到其他分支中。然而,有时候由于一些意外情况,我们可能会不小心将测试分支合并到其他分支中,并且在不知情的情况下还添加了新的代码。直到上线之后才被发现,造成的后果可大可小,并且回滚操作也很麻烦。

那么,有没有办法在合并阶段阻止合并非法分支呢?答案是可以的,使用 Git Hooks 可以实现这一点。

什么是 Git Hooks?

Git Hooks 是在执行 Git 命令的过程中会触发的自定义脚本。常用的钩子(hooks)包括:pre-commitcommit-msgpre-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 还可以配合 commitlintlint-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-commitshell 脚本。脚本内容如下:

#!/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)

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

快进合并(Fast-forward Merges)是最简单的一种合并类型,如上图中将 Some Feature 分支合并进 Main 分支,Git 只需要将 Main 分支的指向移动到最后一个 commit 节点上。

快进合并(Fast-forward Merges)是 Git 在合并两个没有分叉的分支时的默认行为。如果你不希望采用这种方式,而是希望明确记录每次合并的情况,可以使用 git merge --no-ff 命令来进行显示合并(Explicit Merges)。

显示合并(Explicit Merges)

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

显示合并(Explicit Merges)会创建一个新的合并提交,将两个分支的更改明确地合并在一起。通过这种方式,可以清楚地看到合并的历史记录,并方便进行代码审查和追踪。其算法可以简单地描述为:递归地寻找路径最短的唯一共同祖先节点,然后以该祖先节点为 base 节点进行递归的三路合并。

如上图所示,Git 会找到 Some Feature 和 Main 的共同祖先节点作为 base 节点,然后进行递归合并,最终创建一个新的合并提交。

合并冲突

除此之外,Git 合并时可能会出现冲突。在这种情况下,需要手动解决冲突,然后重新执行 addcommit 操作。

编写钩子

接着,我们阅读 git hooks 文档得知:

  • 快进合并(Fast-forward Merges)和显示合并(Explicit Merges)都会触发 post-merge 钩子。

  • 合并冲突时会触发 prepare-merge-commitcommit-msg 钩子。然而,commit-msg 钩子无法获取到合并进来的分支名称,因此只能使用 prepare-merge-commit 钩子。

ok,让我们正式开始编写钩子。

post-merge 钩子

基本思路如下:

  1. 获取当前分支的名称
git rev-parse --abbrev-ref HEAD
  1. 获取合并进来的分支名称

post-merge 钩子被触发时,分支已经完成合并,并且 reflog 已经更新,因此我们可以通过 $GIT_REFLOG_ACTION 获取到合并进来的分支的信息。

  1. 如果当前分支是受保护的分支,并且合并进来的分支是禁止合并的分支,则撤销合并。

脚本内容如下:

#!/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 钩子

基本思路如下:

  1. 获取当前分支的名称
git rev-parse --abbrev-ref HEAD
  1. 获取合并进来的分支名称

在合并冲突阶段,.git/MERGE_HEAD 文件中会保留合并进来的分支的 hash,可以通过读取该文件获取对应的内容,然后使用 git name-rev [hash] 命令获取对应的分支名称。

  1. 如果当前分支是受保护的分支,并且合并进来的分支是禁止合并的分支,则撤销合并

脚本内容如下:

#!/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
评论
请登录