三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Chromium/Gerrit 开发实战:合并后代码被还原?一份完整的故障排查与回退指南

Chromium/Gerrit 开发实战:合并后代码被还原?一份完整的故障排查与回退指南

问题的核心场景

在 Chromium 这样的超大型开源项目开发中,我们经常需要将特性分支(A)的代码合并到主线分支(B)。理想情况下,一切顺利。但现实中,冲突解决是最大的风险点。

你遇到的典型情况:

1. 将 A 分支合并到 B 分支
2. 解决了一堆冲突
3. 编译通过了(表面上万事大吉)
4. Push 到 Gerrit
5. 后来发现大量代码被还原了——在解决冲突时,错误地选择了上游(或下游)的版本,导致自己的业务逻辑被覆盖

此时你需要的不是零散的命令,而是一套完整的诊断-决策-执行流程。

第一步:诊断合并方式——这是所有操作的前提

Git 的合并有本质不同的两种路径:merge 和 rebase。它们的回退方式天差地别。你必须先搞清楚自己用的是哪种。

执行命令:

```bash
git log --oneline --graph -20
```

场景判断:Merge 的特征

如果你看到这样的拓扑结构:

```
* a1b2c3 Merge branch 'A' into B
|\
| * xxxx (A分支的提交)
| * yyyy
* | zzzz (B分支原有的提交)
```

结论:这是 merge。 它创建了一个明确的合并提交(merge commit),有两个父节点。

场景判断:Rebase 的特征

如果你看到的是一条直线:

```
* eeee (rebase后的A'提交)
* dddd
* cccc
* bbbb (B分支原有的提交)
```

结论:这是 rebase。 A 分支的提交被“重放”在 B 分支的最新提交之上,历史呈线性,没有合并提交。

第二步:根据诊断结果选择回退策略

情况一:Git Merge 的回退(最安全可控)

场景假设:

```
B1---B2--------M (B分支)
\ /
A1---A2
```

M 就是那个有问题的合并提交。

方案 A:尚未 Push(理想情况)

如果你在 git push 之前就发现了问题,直接用硬重置:

```bash
git reset --hard HEAD~1
```

这会撤销合并提交 M,B 分支回到 B2 的状态,就像什么都没发生过。

方案 B:已经 Push 到远端(团队开发的标准做法)

绝对不要用 git reset + git push --force,除非这个分支只有你一个人在用。在团队协作中,这会给同事带来灾难。

正确的做法是反向提交(revert the merge):

```bash
git revert -m 1 <merge_commit_sha>
# 例如:git revert -m 1 a1b2c3
```

这里 -m 1 是关键参数:它告诉 Git,我们要以主线分支(B 分支)为保留对象,撤销由合并操作引入的所有变更。这个命令会生成一个新的 commit,其内容刚好与合并操作相反。

优点:

· 不改写已发布的历史
· 对 Gerrit 和其他协作者完全安全
· 是可追踪的、明确的“撤销”操作

情况二:Git Rebase 的回退(更隐蔽,更常见于 Gerrit)

Rebase 后没有 merge commit,所以 git revert -m 无效。

场景假设:

```
原始状态:
B1---B2 (B)
\
A1---A2

rebase 后:
B1---B2---A1'---A2' (B)
```

关键救命工具:git reflog

reflog 记录了你在本地仓库中的所有 HEAD 移动历史,包括那些在 git log 中不可见的、被抛弃的提交。它是你后悔药的生产地。

执行:

```bash
git reflog -10
```

你会看到类似输出:

```
a1b2c3d HEAD@{0}: rebase (finish): returning to refs/heads/B
e4f5g6h HEAD@{1}: rebase (pick): A2
i7j8k9l HEAD@{2}: rebase (pick): A1
m0n1o2p HEAD@{3}: rebase (start): checkout m0n1o2p
q3r4s5t HEAD@{4}: commit: B2
```

记下 rebase 之前的 commit(这里是 HEAD@{4} 或直接记下其 SHA q3r4s5t)。

方案 A:该分支只有你一个人(可以强力重写历史)

```bash
# 直接硬重置到 rebase 前的状态
git reset --hard HEAD@{4}
# 或 git reset --hard q3r4s5t

# 强制推送
git push --force-with-lease
```

--force-with-lease 比 --force 安全,它会检查远端分支是否在你拉取之后有别人推送了新提交,从而防止你意外覆盖他人的工作。

方案 B:已经有人基于你 rebase 后的分支开发(更安全)

此时不能 reset。你需要用“反向打补丁”或者“逐个撤销 rebase 来的提交”的方式。

方法一:反向 diff 生成补丁

```bash
# old_sha 是 rebase 前的提交
git diff HEAD <old_sha> > rollback.patch
git apply rollback.patch
git commit -m "Rollback wrong rebase conflict resolution"
```

这个方法会把工作区恢复到 rebase 前的状态,并提交为一个新的 commit。

方法二:逐个 revert
如果 rebase 产生的提交不多(A1'、A2'),可以直接 revert 它们:

```bash
git revert <A2'_sha>
git revert <A1'_sha>
```

注意 revert 的顺序是倒序的。

更精细的方案:不全盘回退,只恢复被误覆盖的文件

你已经解决了大量冲突、通过了编译,可能其中只有一部分冲突是解错的。全盘回退代价太大。

推荐流程:

1. 创建备份分支,以防万一
```bash
git branch backup_before_rollback
```
2. 定位被“还原”的文件
找到合并前的那个 commit(通过你之前记下的或 reflog 找到的 old_sha)。
```bash
# 查看哪些文件被改动了,以及改动规模
git diff <old_sha> HEAD --stat
```
3. 精确恢复
如果确认是某几个文件被错误覆盖了,就直接从旧版本中检出它们:
```bash
# 从合并前的提交中,恢复指定文件到当前工作区并暂存
git checkout <old_sha> -- path/to/your/file1.cc path/to/your/file2.h
git commit -m "Restore incorrectly overwritten files during merge"
```

核心教训:冲突解决的元认知

“代码被还原”的根源,99% 是在解决冲突时的误操作:

```
<<<<<<< HEAD
Zero Browser 新逻辑 (你的修改)
=======
Chromium 原生逻辑 (上游的修改)
>>>>>>> A
```

你错误地选择了 theirs 或者手动删除了自己的逻辑。代码结构没破坏,编译自然通过,但业务功能丢失了。

完整操作清单:面对大型仓库,我会这样做

1. 瞬间保护现场:git branch backup_$(date +%Y%m%d_%H%M%S)
2. 诊断:执行 git log --oneline --graph -15 和 git reflog -10,判断是 merge 还是 rebase,定位“合并前”的提交点。
3. 评估影响范围:git diff <old_sha> HEAD --stat,判断是 10 个文件还是 500 个文件被毁。
4. 决策与执行:
· 少量文件被误覆盖:精准 checkout 恢复(推荐首选)。
· 整个合并/变基严重失败:根据 merge/rebase 类型,选择 git revert -m 1 或 reset/反向补丁方案。
5. 推送并沟通:将回退操作 push 到 Gerrit,并在变更说明中清晰解释发生了什么以及你采取了什么恢复措施。

最后,永远记得:在开始任何可能改变历史或复杂的合并操作前,先记下当前的 HEAD SHA 或执行一次 git branch。这个习惯会在关键时刻救你一命。

← 返回列表