本文首发于:https://github.com/bigo-frontend/blog/ 欢迎关注、转载。
引言
项目开发时,有开发分支,测试分支,主干分支等。一般不能把测试分支合并到其他分支里,然而可能一不小心(手抖)合并了,甚至在不知情的情况下还加了新的东西,后面上线时才发现(或者没发现,直接把测试分支的代码带到了线上),后果可大可小,回滚时也麻烦。
那能不能在合并阶段直接禁止合并非法分支呢?答案是可以的。只要解决了下面问题即可。
- 是否在合并中?
- 当前分支名叫啥?
- 要合并进来的分支名又叫啥?
- 当前分支 和 要合并进来的分支 2 者是否满足条件【比如 要合并进来的分支 不能是 测试分支】
前置知识
git hooks
git hooks,简单来说就是在执行 git 命令的过程中会触发的钩子函数(脚本程序)。只要知道特定 git 命令会触发什么 hooks,就可以做对应处理,比如可以用来检查提交信息是否符合规范(如 commitlint),以及本文即将要讨论的阻止合并某些分支。
git 合并
命令: git merge <branch>
合并可能有 3 种情况
- fast-forward merge: 合并时,当前分支和要合并进来的分支,分支历史没有分叉【简单理解就是 要合并的分支是基于当前分支前进的,且当前分支自新分支新建后 再也没发生过变更】。可以通过
git merge --no-ff <branch>
变为第 2 种合并情况
- no fast-forward merge: 新增 1 个历史节点,其直接父节点指向为要合并的 2 个分支
- 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 merge
和no 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 merge
和 fast-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-merge
和 prepare-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;
}
总结
参考
https://git-scm.com/docs/githooks
https://www.atlassian.com/git/tutorials/using-branches/git-merge
欢迎大家留言讨论,祝工作顺利、生活愉快!
我是bigo前端,下期见。