引言
在基于Gerrit + CI的研发流程中,提交标题的唯一性检查是保证提交历史整洁的重要手段。然而在实际开发中,我们偶尔会遇到这样一种棘手的情况:两个不同作者的提交,拥有完全相同的提交标题,且各自独立通过了Verify CI并成功合入目标分支 。当这种情况发生时,后续所有新增的提交都会因为提交标题重复而触发CI的-2失败,整个研发流程被彻底阻塞。
作为一名DevOps或SRE工程师,你很可能被叫来救火。本文将系统性地梳理这类问题的成因,并给出完整的修复方案和操作步骤。
一、问题场景还原
1.1 问题是如何产生的
在一个多人协作的项目中,假设有两个开发者A和B,在几乎相同的时间段内,各自基于目标分支(如master)开发了不同的功能。由于沟通不及时或其他原因,两人提交的Commit Message标题完全一致 ------例如都是feat: add node agent。
两个提交各自通过代码评审和Verify CI后,先后被合入master分支。此时,master分支的历史中存在两条提交记录:
commit abc1234 (作者A)
feat: add node agent
commit def5678 (作者B)
feat: add node agent
提交标题相同,但提交哈希、作者、代码变更内容均不相同。然而,master分支上的提交历史已经出现了重复标题。
1.2 CI重复标题检查机制
许多团队的CI流水线中会配置提交信息规范检查,其中包括提交标题去重检查。常见的实现方式是在CI脚本中提取指定范围内的所有提交标题,通过sort | uniq -d检测重复项,一旦发现重复即终止流程并返回-2:
bash
git log --pretty=format:%s $range > ./subjects
duplicates="$(cat ./subjects | sort | uniq -d)"
if [ "$duplicates" != "" ]; then
echo "发现重复提交: $duplicates"
exit 1
fi
当master分支中已经存在重复标题时,后续任何新的提交(无论标题是什么),只要CI脚本扫描到历史中存在重复标题,就会直接失败。这意味着整个目标分支的CI门禁被永久阻塞,任何新代码都无法合入。
1.3 问题根源
这个问题的本质是:提交标题的唯一性约束本应在提交时检查,但两个重复标题的提交是在不同时间、各自独立通过CI的。当第二个重复标题的提交被合入时,CI只检查了当时的提交历史(那时还没有重复),并未预见到未来会出现重复。当两个重复提交同时存在于分支中时,CI的全局检查就会触发失败。
类似的场景在Gerrit中也会因重复的Change-Id导致推送被拒绝。虽然问题的具体表现略有不同,但根本原因都是唯一性约束被破坏后导致后续操作被阻塞。
二、修复方案全景图
修复的核心思路是:在保留所有有效代码变更的前提下,消除重复的提交标题。这需要DevOps工程师介入,手动重建分支历史。
整体修复流程如下:
备份当前分支 → 找到最后一个OK的历史提交 → 基于该提交创建新分支 →
逐个cherry-pick需要保留的提交 → 处理冲突 → 推送新分支 → 通知团队切换
三、详细操作步骤
3.1 第一步:锁定目标分支,防止新的合入
在开始修复之前,必须确保没有新的提交被合入目标分支,否则修复工作将变得极其复杂。
bash
# 联系团队,暂停向目标分支的推送
# 在Gerrit中可以考虑临时锁定分支,或通过项目权限设置禁止推送
3.2 第二步:备份当前分支
在进行任何破坏性操作之前,先为当前分支创建一个备份标签或备份分支:
bash
# 切换到目标分支(假设为 master)
git checkout master
# 拉取最新代码
git pull origin master
# 创建备份标签(推荐,便于回溯)
git tag backup/master-$(date +%Y%m%d-%H%M%S)
# 或创建备份分支
git branch backup/master-before-fix
3.3 第三步:查找历史中最后一个"健康"的提交
所谓"健康"的提交,是指在该提交时刻,目标分支的提交历史中不存在重复的提交标题 。通常这是两个重复提交中较早合入的那一个的前一个提交。
bash
# 查看提交历史,定位问题提交
git log --oneline --graph -20
# 或使用更详细的方式查看提交标题
git log --pretty=format:"%h %an %s" -20
假设提交历史如下:
abcd123 (最新) 作者B feat: add node agent ← 第二个重复提交
efgh456 作者A feat: add node agent ← 第一个重复提交
ijkl789 作者C fix: memory leak ← 最后一个健康的提交
那么ijkl789就是我们需要的健康基点。
3.4 第四步:基于健康提交创建新分支
bash
# 基于健康提交创建新分支
git checkout -b master-fix ijkl789
这里git checkout -b <新分支名> <起始点>表示从指定的提交创建一个新分支并切换过去。
此时,master-fix分支的代码状态与ijkl789时刻完全一致,不包含任何重复提交。
3.5 第五步:使用cherry-pick逐个恢复需要保留的提交
现在需要将健康提交之后的所有有效提交逐个应用到新分支上。Cherry-pick的作用正是从一个分支中挑选特定的提交并应用到当前分支。
bash
# 列出健康提交之后的所有提交
git log --oneline ijkl789..master
# 逐个cherry-pick,注意跳过会导致重复的提交
# 假设需要保留的提交有:mnop012、qrst345、abcd123(第二个重复提交)
# 但我们需要修改第二个重复提交的标题
git cherry-pick mnop012
git cherry-pick qrst345
关键操作 :当cherry-pick到第二个重复提交(abcd123)时,我们需要修改其提交标题以消除重复。
bash
# 使用 -e 选项在cherry-pick时编辑提交信息
git cherry-pick -e abcd123
此时会弹出编辑器,将提交标题从feat: add node agent修改为feat: add node agent (fix author B)或其他不重复的标题。
注意 :由于abcd123已经被cherry-pick过来,它会生成一个新的提交哈希,与原始提交不同。
3.6 第六步:处理可能的冲突
如果在cherry-pick过程中遇到冲突,Git会暂停操作:
bash
# 查看冲突状态
git status
# 手动解决冲突后
git add .
git cherry-pick --continue
如果冲突无法解决或发现某个提交不应该被保留,可以使用以下命令放弃当前操作:
bash
git cherry-pick --abort
3.7 第七步:验证新分支
在新分支上执行完整的验证:
bash
# 确认提交历史中没有重复标题
git log --pretty=format:"%s" | sort | uniq -d
# 确认代码编译通过
make build # 或项目对应的编译命令
# 确认所有需要的提交都已恢复
git log --oneline
3.8 第八步:推送新分支并替换原分支
bash
# 推送新分支到远程
git push origin master-fix
# 在Gerrit中,需要强制推送以替换原有的master分支
# 注意:此操作需要管理员权限
git push -f origin master-fix:master
3.9 第九步:通知团队并解封分支
修复完成后:
- 通知团队成员分支已修复,可以继续开发
- 解除分支锁定
- 建议团队成员重新基于最新的
master分支拉取开发分支
四、完整的操作命令汇总
以下是整个修复过程的命令速查:
bash
# 1. 备份
git checkout master && git pull
git tag backup/master-before-fix
# 2. 查找健康提交
git log --oneline --graph -20
# 3. 创建新分支
git checkout -b master-fix <健康提交哈希>
# 4. 逐个cherry-pick需要保留的提交
git cherry-pick <提交1>
git cherry-pick <提交2>
git cherry-pick -e <重复提交> # 编辑标题以消除重复
git cherry-pick <提交N>
# 5. 处理冲突(如有)
git status
# 解决冲突后
git add .
git cherry-pick --continue
# 6. 验证
git log --pretty=format:"%s" | sort | uniq -d
# 7. 推送
git push -f origin master-fix:master
# 8. 清理(确认一切正常后)
git branch -d master-fix
五、预防措施
为了避免此类问题再次发生,建议采取以下措施:
-
CI检查时机优化 :将提交标题去重检查的范围限定为当前提交及之前N个提交,而非全量历史,避免因历史遗留问题阻塞新提交
-
提交时实时检查:利用Git hooks(pre-commit或pre-push)在本地提交时就检查提交标题是否与远程分支已有提交重复
-
代码评审环节把关:在Gerrit的代码评审中,要求评审者检查提交标题是否与已有提交重复
-
规范提交标题格式:强制要求提交标题包含功能模块或Issue编号,从源头降低标题重复的概率
-
建立监控告警:定期扫描目标分支的提交历史,发现重复标题时及时告警,避免问题积累
六、总结
提交标题重复导致CI阻塞是一个典型的历史遗留问题引发的连锁反应。虽然问题本身不复杂,但修复过程需要DevOps工程师具备扎实的Git操作能力,尤其是对cherry-pick、checkout -b等命令的熟练运用。
git cherry-pick的核心价值在于能够从复杂的提交历史中精确地挑选需要的提交,而不会引入不需要的变更。在分支修复场景中,它正是我们"精确重建"分支历史的关键工具。
作为DevOps工程师,面对此类问题时应保持冷静:先备份、再定位、后重建、终验证。只要遵循这一原则,大多数分支历史问题都能得到安全、可靠的解决。