/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}
场景:在本地 Gerrit 工程的 xxx1 分支执行 git pull origin xxx1 后,提交历史中多出一个 Merge branch 'xxx1' of … into xxx1 的提交,看起来像一次“空提交”。本文从 Git 原理和 Gerrit 评审流程两个角度,解释它为什么会出现、为什么在 Gerrit 中通常要尽量避免,以及如何修复和预防。
核心要点
- git pull 本质上是 git fetch + git merge 的组合。
- 当本地分支和远端分支发生分叉时,默认 merge 策略会生成一个 merge commit,提交信息通常以 Merge branch … 开头。
- 这个提交通常不携带文件变更,但它并不是 git commit –allow-empty 意义上的空提交,而是用于连接两条历史线的合并提交。
- Gerrit 默认通常不允许将 merge commit 推送到 refs/for/<branch> 进行评审,并且可能干扰 Change-Id 的追踪。
- 在 Gerrit 的个人开发分支上,更推荐使用 git pull –rebase origin xxx1,或配置 git config pull.rebase true。
- 如果已经产生 merge commit 且尚未推送,可以撤销后改用 rebase;推送前应检查历史是否线性、Change-Id 是否保留。
1. 现象:为什么 pull 后多了一个 Merge branch 提交?
执行:
git pull origin xxx1
等价于先抓取远端更新,再尝试合并到当前分支:
git fetch origin xxx1
git merge FETCH_HEAD
如果此时满足以下条件:
- 本地 xxx1 有远端没有的提交,例如你之前已经执行过 commit;
- 远端 xxx1 也有新的提交;
那么两条历史线就发生了分叉(diverged)。Git 默认的 merge 策略会创建一个新的 merge commit,把两条历史连接起来。提交信息通常类似:
Merge branch 'xxx1' of ssh://gerrit.example.com/project into xxx1
这个提交本身可能没有文件内容变更,所以看起来像“空提交”。但严格来说,它是 merge commit,有两个父提交,并不是 –allow-empty 创建的空提交。
2. git pull 的核心原理
git pull 的关键不在“拉取”,而在“整合”。整合方式决定了最终的提交历史形态。
| 本地没有独有提交,远端有新提交 | Fast-forward | 本地指针直接前移,不产生额外提交 |
| 本地有独有提交,远端也有新提交 | Recursive merge | 生成 merge commit,历史分叉后汇合 |
| 使用 –rebase | Fetch + Rebase | 本地提交重放到远端最新提交之后,历史保持线性 |
| 使用 –ff-only | 仅允许快进 | 不能快进时直接报错,不自动合并 |
2.1 Fast-forward
如果本地 xxx1 是远端 origin/xxx1 的祖先,Git 可以直接快进:
A — B — C (origin/xxx1)
↑
本地 xxx1
执行 pull 后,本地分支直接指向 C,不会产生 merge commit。
2.2 分叉后的 Merge
如果本地有提交 L1,远端有提交 R1:
L1 (本地 xxx1)
/
A — B
\\
R1 (origin/xxx1)
默认 git pull 会生成 merge commit M:
L1
/ \\
A — B M (HEAD)
\\ /
R1
M 就是 Merge branch … 提交。它可能没有引入新的文件差异,但改变了提交历史结构。
2.3 Rebase 的整合方式
git pull –rebase 会先把本地提交“摘下来”,再把它们依次重放到远端最新提交之后:
A — B — R1 — L1'
这样历史是一条直线,不会产生 merge commit。注意 L1 变成了 L1',提交 SHA 会改变,但提交信息底部的 Change-Id 应保持不变。
3. 为什么 Gerrit 工作流对 Merge commit 敏感?
Gerrit 不是简单的 Git 远端,它围绕“每个提交对应一个 Change”的评审模型工作。自动 merge commit 会带来两个主要问题。
3.1 Gerrit 通常拒收推送到 refs/for 的 merge commit
正常推送评审使用:
git push origin HEAD:refs/for/xxx1
Gerrit 默认配置下通常会拒绝包含 merge commit 的推送,提示类似:
! [remote rejected] HEAD -> refs/for/xxx1
(you are not allowed to upload merges)
只有项目在 project.config 中对 refs/for/refs/* 显式授予 pushMerge 权限时,merge commit 才可能被接受。但在常规 Gerrit 评审流程中,保持线性历史、避免自动 merge commit 是更稳妥的做法。
3.2 可能干扰 Change-Id 追踪
Gerrit 通过提交信息底部的:
Change-Id: Ixxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
来追踪同一个评审的多个 patch set。rebase 或 amend 后,提交 SHA 可以变化,但 Change-Id 必须保留。如果 merge commit 夹在历史中,后续操作不当可能导致:
- merge commit 缺少 Change-Id;
- 多个提交携带相同 Change-Id;
- Gerrit 找不到原有 Change,或拒绝推送。
因此,在 Gerrit 分支上执行 pull 后,推送前应确认历史中没有多余 merge commit,并且 Change-Id 仍位于提交信息底部。
4. 常用操作与修复方式
4.1 先查看是否分叉
推荐先 fetch,再观察:
git fetch origin
git log –graph –oneline –decorate HEAD..origin/xxx1
如果本地有独有提交,可以查看:
git log –graph –oneline –decorate origin/xxx1..HEAD
确认分叉情况后,再决定使用 merge 还是 rebase。
4.2 推荐:使用 rebase 拉取
git pull –rebase origin xxx1
这会把远端更新作为基底,把本地尚未推送的提交重放到其后,避免产生 merge commit。
如果希望长期默认使用 rebase:
git config –global pull.rebase true
之后直接执行 git pull 就会走 rebase 路径。也可以只对当前仓库配置:
git config pull.rebase true
4.3 希望显式感知分叉:–ff-only
git pull –ff-only origin xxx1
该命令只在可以快进时更新;如果已经分叉,会直接报错,不会自动产生 merge commit。适合希望手动处理分叉的场景。
4.4 已经产生了 Merge commit,但还没推送
如果刚刚执行完 git pull 产生了 merge commit,且工作区干净、尚未推送,可以撤销这次合并后改用 rebase:
git reset –hard ORIG_HEAD
git pull –rebase origin xxx1
ORIG_HEAD 通常指向 merge 前的 HEAD。reset –hard 会丢弃工作区未提交修改,执行前务必确认:
git status
如果不想使用 reset –hard,也可以先查看历史,再基于 origin/xxx1 执行 rebase。核心目标是:丢弃自动生成的 merge commit,把自己的本地提交重放到远端最新提交之后。
4.5 rebase 冲突处理
如果 git pull –rebase 过程中出现冲突:
git status
# 编辑冲突文件
git add <冲突文件>
git rebase –continue
想放弃本次 rebase:
git rebase –abort
4.6 推送评审前检查
推送 Gerrit 评审通常使用:
git push origin HEAD:refs/for/xxx1
推送前建议检查:
# 历史中是否还有 merge commit
git log –oneline –merges origin/xxx1..HEAD
# 查看最新提交信息,确认 Change-Id 仍在底部
git log -1
# 确认工作区干净
git status
如果 –merges 没有输出,说明你的分支相对远端是线性的,更适合 Gerrit 评审。
5. 注意事项清单
不要把 Merge branch … 当成普通空提交直接推送。
它是 merge commit,Gerrit 默认通常不接受,且可能破坏评审历史。
Gerrit 个人开发分支优先使用 git pull –rebase。
可以配置 pull.rebase=true 作为默认行为。
rebase 会改写本地提交 SHA,但 Change-Id 必须保留。
推送后 Gerrit 会将其识别为同一 Change 的新 patch set。
不要随意 rebase 多人共享的公共历史。
rebase 适合自己尚未合入的本地提交;如果分支多人共享且提交已被他人依赖,应先与团队约定。
执行 reset –hard 前必须确认工作区干净。
它会丢弃未提交修改。更安全的做法是先 git status、git stash 或提交。
推送评审使用 refs/for/<branch>,不要误推 refs/heads/<branch>。
后者是直接更新分支,通常需要额外权限,也会绕过评审流程。
如果项目确实允许 merge commit,也要确认 pushMerge 权限和团队规范。
默认拒绝不代表所有项目都拒绝,但 Gerrit 评审流通常更偏好线性历史。
6. 总结
git pull origin xxx1 后出现 Merge branch … 提交,并不是 Git 故障,而是默认 merge 策略在本地与远端历史分叉时的正常产物。它通常不携带文件变更,但本质上是一个 merge commit。
在 Gerrit 工作流中,这类提交默认可能被 refs/for/xxx1 拒收,也可能给 Change-Id 追踪带来麻烦。更推荐的做法是:
git pull –rebase origin xxx1
或长期配置:
git config –global pull.rebase true
如果已经产生 merge commit 且尚未推送,可以撤销后改用 rebase;推送前检查历史是否线性、Change-Id 是否保留。把 pull 的整合方式从默认 merge 调整为 rebase,是 Gerrit 工程中保持提交历史干净、评审流程顺畅的关键习惯。
网硕互联帮助中心





评论前必须登录!
注册