Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

【bigo】githooks与分支管理的结合实践 #63

Open
sluggishpj opened this issue Aug 25, 2021 · 0 comments
Open

【bigo】githooks与分支管理的结合实践 #63

sluggishpj opened this issue Aug 25, 2021 · 0 comments

Comments

@sluggishpj
Copy link

引言

项目开发时,有开发分支,测试分支,主干分支等。一般不能把测试分支合并到其他分支里,然而可能一不小心(手抖)合并了,甚至在不知情的情况下还加了新的东西,后面上线时才发现(或者没发现,直接把测试分支的代码带到了线上),后果可大可小,回滚时也麻烦。

那能不能在合并阶段直接禁止合并非法分支呢?答案是可以的。只要解决了下面问题即可。

  • 是否在合并中?
  • 当前分支名叫啥?
  • 要合并进来的分支名又叫啥?
  • 当前分支 和 要合并进来的分支 2 者是否满足条件【比如 要合并进来的分支 不能是 测试分支】

前置知识

git hooks

git hooks,简单来说就是在执行 git 命令的过程中会触发的钩子函数(脚本程序)。只要知道特定 git 命令会触发什么 hooks,就可以做对应处理,比如可以用来检查提交信息是否符合规范(如 commitlint),以及本文即将要讨论的阻止合并某些分支。

git 合并

命令: git merge <branch>

合并可能有 3 种情况

  • fast-forward merge: 合并时,当前分支和要合并进来的分支,分支历史没有分叉【简单理解就是 要合并的分支是基于当前分支前进的,且当前分支自新分支新建后 再也没发生过变更】。可以通过 git merge --no-ff <branch>变为第 2 种合并情况

fast-forward merge | atlassian

  • no fast-forward merge: 新增 1 个历史节点,其直接父节点指向为要合并的 2 个分支

no fast-forward merge | atlassian

  • merge conflict: 合并冲突了,此时需要解决冲突,然后重新 add & commit

图片来自:https://www.atlassian.com/git/tutorials/using-branches/git-merge

前置说明
这里的项目是前端项目,使用 husky 管理 git hooks

问题解答

是否在合并中

通过查阅 git hooks 可知,merge 阶段可能会触发以下钩子【之所以说可能是因为 merge 有多种情况,每种情况触发的钩子不太一致】:

  • pre-merge-commit
  • prepare-commit-msg
  • commit-msg
  • post-merge
合并情况\触发钩子 pre-merge-commit prepare-commit-msg commit-msg post-merge
fast-forward merge
no fast-forward merge
merge conflict 解决完冲突后 add & commit

merge conflict 会有中间态 (当前分支 | MERGING),从初始态到中间态,不会触发 merge 相关的钩子。当解决完冲突后,开始 add&commit 时,才会触发对应钩子

根据合并情况,使用到的钩子如下:

  • fast-forward mergeno fast-forward merge: 使用 post-merge 钩子进行逻辑处理
  • merge conflict: 使用 prepare-commit-msg进行逻辑处理【因为 commit-msg 钩子无法获取到合并进来的分支名,故只能使用 prepare-commit-msg

获取当前分支名

git rev-parse --abbrev-ref HEAD

REF: https://stackoverflow.com/questions/6245570/how-to-get-the-current-branch-name-in-git

获取合并进来的分支名

post-merge

在此钩子处理 no fast-forward mergefast-forward merge 合并。

post-merge 钩子触发时,分支已经合并了,并且 reflog 也更新了,所以可以通过 git reflog 获取到合并进来的分支信息

前 2 种合并情况,git reflog -1 返回的日志格式如下

  • no fast-forward merge: e7cb874 HEAD@{0}: merge feat/no-fast-forward: Merge made by the 'recursive' strategy.
  • fast-forward merge: 724446f HEAD@{0}: merge feat/fast-forward: Fast-forward

可以通过正则匹配提取对应的分支名,代码如下

const { execSync } = require('child_process');

function getMergeBranch() {
  // 从 reflog 提取合并进来的分支名
  function getBranchNameFromReflog(reflogMessage) {
    const reg = /@\{\d+\}: merge (.*):/;
    return reg.exec(reflogMessage)[1];
  }

  const reflogMessage = execSync('git reflog -1', { encoding: 'utf8' });
  const mergedBranchName = getBranchNameFromReflog(reflogMessage);
  return mergedBranchName;
}

prepare-commit-msg

在此钩子处理合并冲突的情况。

因冲突未解决,reflog 也不会更新,因此无法通过 reflog 获取到合并进来的分支。

不过在合并冲突阶段,.git/MERGE_HEAD 中会保留合并进来分支的 hash。
prepare-commit-msg 触发时,可以通过读取该文件获取对应的内容,再通过 git name-rev [hash] 命令获取对应的分支名

const { execSync } = require('child_process');
const path = require('path');
const fs = require('fs');

// 从 .git/MERGE_HEAD (sha) 提取合并进来的分支名
function getMergeBranch() {
  try {
    const mergeHeadPath = path.resolve(process.cwd(), '.git/MERGE_HEAD');
    const mergeHeadSha = fs.readFileSync(mergeHeadPath, { encoding: 'utf8' });
    const mergeBranchInfo = execSync(`git name-rev ${mergeHeadSha}`);
    return / (.*?)\n/.exec(mergeBranchInfo)[1];
  } catch (err) {
    return '';
  }
}

合并分支是否符合要求

这个根据各自场景处理就行了。比如在合并错误分支后,进行提示,让操作者自行决定是否回滚等。

const { execSync } = require('child_process');
const readline = require('readline');

function showConfirm(currentBranch, mergeBranch, inConflict) {
  log(`检测到非法合并: ${mergeBranch} ==into==> ${currentBranch}`);

  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
  });

  rl.question(`是否撤销本次合并?(y/n) `, (answer) => {
    if (answer === 'y') {
      log('撤销合并中...');
      if (inConflict) {
        log(`exec: git merge --abort`);
        execSync('git merge --abort');
        log('已撤销合并 done');
        rl.close();
        process.exit(-1);
      } else {
        log(`exec: git reset --merge HEAD@{1}`);
        execSync('git reset --merge HEAD@{1}');
        log('已撤销合并 done');
        rl.close();
        process.exit(0);
      }
    } else {
      rl.close();
      process.exit(0);
    }
  });
};

其他问题

在第一个问题【是否在合并中】,最终采用了 post-mergeprepare-commit-msg 2 个钩子来做对应的拦截处理,但这 2 个钩子在no fast-forward merge的情况下都会被触发到,也就是会重复执行拦截处理。
此时需要进行判断,当且仅当在 merge conflict 才去执行 prepare-commit-msg 钩子中的拦截逻辑。保证每种合并情况只会触发一次拦截逻辑。

思路是检测 .git/MERGE_MSG 文件是否存在,以及其中的内容是否是冲突信息。

const { execSync } = require('child_process');
const path = require('path');
const fs = require('fs');

function isMergingConflict() {
  // 是否合并中
  const mergeMsgPath = path.resolve(process.cwd(), '.git/MERGE_MSG');
  const isMerging = fs.existsSync(mergeMsgPath);
  if (!isMerging) {
    return false;
  }

  try {
    const mergeMsg = fs.readFileSync(mergeMsgPath, { encoding: 'utf8' });
    return /\n# Conflicts:\n/.test(mergeMsg); // 如果是冲突则能匹配上
  } catch (err) {}
  return false;
}

总结

git-hooks-prevent-merge-summary

参考

https://git-scm.com/docs/githooks
https://www.atlassian.com/git/tutorials/using-branches/git-merge

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant