日常使用 git 开发管理分支非常方便,但是偶尔也会出现一些问题,比如这次就遇到了回滚合并导致后续合并丢失部分内容这样的情况,本文介绍一下如何处理这种情况,分析事故原因。
Merge+Revert 方案在复杂合并和回滚时容易引入副作用,可能导致部分内容被意外撤销;而 Reset+Cherry-pick 方案通过重置和精确挑选提交,能更好地避免这些问题,分支内容更安全、可控。
推荐使用 Reset+Cherry-pick 方案。
方案对比 (简要)
| 阶段 | 方案一:Merge + Revert | 方案二:Reset + Cherry-pick |
|---|---|---|
| 阶段 1:初始状态 | a: b: 1 2 3c: 4 5 6 7 8 |
a: b: 1 2 3c: 4 5 6 7 8 |
| 阶段 2:b 合并 c 并继续开发 | b: 1 2 3 4 5 6 7 8 9 10 |
b: 1 2 3 4 5 6 7 8 9 10 |
| 阶段 3:b 回滚合并, b 继续开发 | b: 1 2 3 9 10 revert(merge c)差异:此时 revert(merge c) 实际撤销了 4,5,6,7,8 的更改,但 9,10 依然保留。 |
b: 1 2 3 9 10差异:左侧通过 revert 撤销合并,右侧直接 reset 到合并前并 cherry-pick 新提交,避免了 revert 带来的副作用。 |
| 阶段 4:a 合并 c 的 4,5,6 | a: 4 5 6 |
a: 4 5 6 |
| 阶段 5:a 合并 b | a: 4 5 6 1 2 3 revert(merge c) 9 10差异:revert(merge c) 也被合并进 a,导致 a 上 4,5,6 的更改被撤销。 |
a: 4 5 6 1 2 3 9 10差异:没有 revert(merge c) 的副作用,a 上 4,5,6 保持不变。 |
| 最终结果 | a: 4561 2 3 revert(merge c) 9 10a 分支的 4,5,6 被 revert(merge c) 撤销,内容丢失。 |
a: 4 5 6 1 2 3 9 10a 分支完整保留了 4,5,6,没有被撤销,内容安全。 |
上述操作的图形化演示
| 方案一:Merge + Revert | 方案二:Reset + Cherry-pick |
|---|---|
|
阶段1:初始状态
a:
b: 123 c: 45678 |
阶段1:初始状态
a:
b: 123 c: 45678 |
|
阶段2:b 合并 c 并继续开发
b: 12345678910
|
阶段2:b 合并 c 并继续开发
b: 12345678910
|
|
阶段3:b 回滚合并, b 继续开发
b: 12
3
4
5
6
7
8
910revert(merge c)
差异:
此时 revert(merge c) 实际撤销了 4,5,6,7,8 的更改,但 9,10 依然保留。 |
阶段3:b 重置并 cherry-pick
b: 123910
差异:
左侧通过 revert 撤销合并,右侧直接 reset 到合并前并 cherry-pick 新提交,避免了 revert 带来的副作用。 |
|
阶段4:a 合并 c 的 4,5,6
a:
456
|
阶段4:a 合并 c 的 4,5,6
a:
456
|
|
阶段5:a 合并 b
a:
456123
4
5
6
7
8revert(merge c)910
差异:
revert(merge c) 也被合并进 a,导致 a 上 4,5,6 的更改被撤销。 |
阶段5:a 合并 b
a:
456123910
差异:
没有 revert(merge c) 的副作用,a 上 4,5,6 保持不变。 |
|
最终结果
a:
456123revert(merge c)910
a 分支的 4,5,6 被 revert(merge c) 撤销,内容丢失。
|
最终结果
a:
456123910
a 分支完整保留了 4,5,6,没有被撤销,内容安全。
|
Merge + Revert 详细步骤
阶段 1: a, b, c 分支初始状态
分支 a
分支 b
1
2
3
分支 c
4
5
6
7
8
(初始状态,a 为空,b 有 1, 2, 3,c 有 4, 5, 6, 7, 8)
阶段 2: b 合并 c
分支 b
1
2
3
4
5
6
7
8
分支 c (不变)
4
5
6
7
8
分支 a (不变)
(b 合并 c,来自 c 的提交颜色为橙色)
阶段 3: b 回滚合并, b 继续开发
分支 b
1
2
3
4
5
6
7
8
9
10
revert(merge c)
分支 c (不变)
4
5
6
7
8
分支 a (不变)
(b 回滚合并 c 后继续开发)
阶段 4: a 合并 c 的 4, 5, 6
分支 a
4
5
6
分支 b (不变)
1
2
3
4
5
6
7
8
revert(merge c)
9
10
分支 c (不变)
4
5
6
7
8
(a 合并 c 的部分提交,颜色为橙色)
阶段 5: a 合并 b
分支 a (合并 b 后)
4
5
6
1
2
3
4
5
6
7
8
revert(merge c)
9
10
分支 b (不变)
1
2
3
4
5
6
7
8
revert(merge c)
9
10
分支 c (不变)
4
5
6
7
8
(a 合并 b,来自 b 的提交颜色为绿色)
阶段 6: a 分支最终状态 (体现 revert 效果)
分支 a (最终状态)
4
5
6
1
2
3
revert(merge c)
9
10
分支 b
1
2
3
revert(merge c)
9
10
分支 c
4
5
6
7
8
注意:被 revert 的来自 c 的提交在 a 分支上显示为删除线。
Reset + Cherry-pick 详细步骤
阶段 1: 初始状态
分支 a
分支 b
1
2
3
分支 c
4
5
6
7
8
(初始状态)
阶段 2: b 合并 c,并继续开发
分支 b
1
2
3
4
5
6
7
8
9
10
分支 c (不变)
4
5
6
7
8
分支 a (不变)
(b 合并了 c,之后又有提交 9 和 10)
阶段 3: b 重置和 Cherry-pick (带备份)
分支 b (reset 和 Cherry-pick)
- 备份 b 分支:
git checkout -b temp_b(当前在 b 分支时)临时分支 temp_b (备份)1 2 3 4 5 6 7 8 9 10 - 切换回 b:
git checkout b分支 b (当前)1 2 3 4 5 6 7 8 9 10 - 重置 b 到合并前:
git reset --hard <b_before_merge_c_hash>(假设 `` 是提交 3) 分支 b (reset 后)1 2 3 - Cherry-pick 提交 9 和 10:
git cherry-pick <hash_of_9>
git cherry-pick <hash_of_10>
或者批量操作git cherry-pick <merge_commit_hash>^..<latest_b_hash>分支 b (cherry-pick 后)1 2 3 9 10
分支 c (不变)
4
5
6
7
8
分支 a (不变)
(b 重置并 cherry-pick 完成,temp_b 作为备份)
阶段 4: a 合并 c 的 4, 5, 6
分支 a
4
5
6
分支 b (状态如阶段 3 结束)
1
2
3
9
10
分支 c (不变)
4
5
6
7
8
(a 合并了 c 的部分提交)
阶段 5: a 合并 b
分支 a (合并 b 后)
4
5
6
1
2
3
9
10
分支 b (不变)
1
2
3
9
10
分支 c (不变)
4
5
6
7
8
(a 合并了 b)
阶段 6: a 分支最终状态
分支 a (最终状态)
4
5
6
1
2
3
9
10
分支 b
1
2
3
9
10
分支 c
4
5
6
7
8
(最终,a 包含了来自 c 的 4, 5, 6 和来自 b 的 1, 2, 3, 9, 10)
有想法?欢迎通过邮件讨论。