Skip to content

撤销合并的完整方案

合并错误的分支是开发中常见的意外。根据合并是否已推送、是否有他人拉取,处理方案各有不同。

未 push:reset --hard

如果合并还没有推送到远程,使用 git reset --hard 是最简单的方案:

bash
# 查看合并前的提交(合并提交的第一父提交)
git log --oneline
# abc123 (HEAD -> main) Merge branch 'wrong-feature'  ← 要撤销的合并
# def456 feat: 上一个正常提交

# 回退到合并前的状态(完全丢弃合并提交)
git reset --hard HEAD~1
# 或者
git reset --hard def456

# 验证
git log --oneline
# def456 (HEAD -> main) feat: 上一个正常提交

如果 ORIG_HEAD 存在(通常合并后会设置):

bash
# Git 在合并前会保存 HEAD 到 ORIG_HEAD
git reset --hard ORIG_HEAD

警告--hard 会丢弃合并提交中的所有未提交改动。确认后再执行。

已 push:revert -m 1

如果合并已经推送到远程分支(尤其是共享分支),不能使用 reset(会需要 force push 破坏他人工作),应使用 revert

bash
# 找到合并提交的 SHA-1
git log --oneline --merges
# abc123 Merge branch 'wrong-feature'

# revert 合并提交,保留主线(第一父提交)
git revert -m 1 abc123

# 推送(不需要 force push)
git push

-m 1 的含义:

  • -m 1:保留第一父提交(合并时所在的 main 分支)
  • -m 2:保留第二父提交(被合并的 feature 分支)

通常情况下,撤销合并时使用 -m 1

选择性撤销合并中的部分内容

如果不想完全撤销合并,只想撤销合并中某些具体的改动:

bash
# 方案1:revert 合并中的某些具体提交(而非整个合并提交)
git revert abc123  # revert 具体的功能提交

# 方案2:cherry-pick 保留需要的提交,revert 不需要的
# 先 revert 整个合并
git revert -m 1 <merge-commit>
# 再 cherry-pick 需要保留的部分
git cherry-pick <good-commit-from-feature>

revert 合并后再次合并的问题与解决

这是一个重要的陷阱,前一节已提到:

问题链:
1. feature 合并到 main → 合并提交 M
2. revert M → revert 提交 R(main 恢复)
3. feature 修复后想再次合并 → 直接 merge feature

问题:步骤3中,Git 认为 feature 上 M 之前的提交已经被引入过,
只会引入 M 之后的新提交,导致之前的功能代码丢失!

解决方案一:revert the revert

bash
# 在 feature 准备好后,先 revert 掉之前的 revert
git switch main
git revert R    # R 是之前 revert 合并的提交
# 这会重新引入 feature 的原始改动
# 然后再 merge feature 的新改动
git merge feature

解决方案二:在 feature 分支上变基

bash
# 在 feature 分支上,以 revert 后的 main 为基础重建提交
git switch feature
git rebase main    # 变基到 revert 后的 main
# feature 上的所有提交都变成新的提交(新的 SHA-1)
# 再次合并时,Git 会正确识别所有改动
git switch main
git merge feature

推荐方案二,因为更简洁,不会在主线留下额外的 revert 提交。

使用 squash 避免历史污染

当合并的分支有大量混乱的提交时,使用 squash 合并可以保持主线历史整洁:

bash
# Squash 合并(将 feature 的所有提交压缩为一个改动,不创建合并提交)
git merge --squash feature/login
git commit -m "feat: 添加用户登录功能"

Squash 的好处:

  • 主线历史简洁清晰
  • 功能分支的混乱提交不污染主线
  • 撤销时只需 revert 这一个 squash 提交

Squash 后 revert 更简单:

bash
# Squash 提交是普通提交,直接 revert 即可
git revert <squash-commit-sha>

重建分支历史的方案

极端情况下,如果主线历史已经非常混乱,可以考虑重建:

bash
# 方案:创建孤立提交,重新开始
# (适用于项目早期或所有人都同意的情况)
git switch --orphan clean-main
git add .
git commit -m "feat: 从某个稳定状态重新开始"

更常见的是:使用 git filter-repoBFG Repo-Cleaner 清理历史(见性能优化章节)。

完整决策流程

发现合并了错误的分支

    是否已推送到共享分支?
    /              \
   否               是
   ↓                ↓
reset --hard     revert -m 1 <merge-sha>
HEAD~1               ↓
                  git push

实际操作示例

完整示例:线上撤销错误合并

bash
# 1. 发现线上问题,找到错误合并提交
git log --oneline main | head -5
# abc123 (HEAD -> main, origin/main) Merge branch 'feature/broken'
# def456 feat: 上一个稳定版本

# 2. 立即 revert 合并提交
git revert -m 1 abc123
# 编辑器打开,写入 revert 原因
# revert: 撤销 feature/broken 的合并,该功能存在严重性能问题

# 3. 推送到远程
git push

# 4. 在 feature/broken 分支上修复问题
git switch feature/broken
# ... 修复 ...
git commit -m "fix: 修复性能问题"

# 5. 在 feature 分支上 rebase(重建提交)
git rebase main

# 6. 再次合并
git switch main
git merge feature/broken
git push

总结

场景方案命令
合并后立即发现,未 pushresetgit reset --hard ORIG_HEAD
已 push 到共享分支revertgit revert -m 1 <sha>
保持历史清洁squash 后 revertgit merge --squash
再次合并时避免问题feature 变基git rebase main(在 feature 上)

撤销合并是 Git 中较复杂的操作之一,理解各种方案的原理和适用条件,才能在紧急情况下做出正确选择。