作者:Julia Evans。原文发布于 2024 年 2 月 1 日:Dealing with diverged git branches。本文为经授权的中文翻译整理,命令风险和现行 Git 文档核对结果以编辑补注标出。
本地 main 与远端同名分支分叉,是不少人使用 Git 时最容易卡住的情况。难处有两层:首先,像 non-fast-forward 这样的错误并不直接告诉新手发生了什么;其次,即使确认已经分叉,也没有一个适合所有仓库的修复命令。
要选择处理方式,必须先问:两边的提交是否都要保留?是否有人已经基于它们继续工作?你要改变的是本地历史,还是远端历史?原文围绕这几个问题解释了不同路径,而不是把某个命令当作万能答案。

四种关系,只有一种叫分叉
本地和远端分支指向同一个提交时,两边完全同步。本地指向远端历史中的较早提交时,是本地落后;反过来,远端指向本地历史中的较早提交时,是本地领先。这两种线性关系通常可以用快进方式前移较旧的一边。
分叉则不同:两边有共同祖先,但此后各自出现了另一边没有的提交。例如本地是 A—B—C—D—E,远端是 A—B—C—F。此时单纯把指针向前移动,不能同时保留 D、E 和 F 的历史关系。
先刷新远端跟踪信息,再读提示
最直接的识别办法是先获取远端状态,再查看当前分支:
git fetch
git status
在分叉时,可能看到如下消息:
Your branch and 'origin/main' have diverged,
and have 1 and 2 different commits each, respectively.
这表示本地当前分支独有 1 个提交,origin/main 独有 2 个提交。这里的 origin/main 是本地保存的远端跟踪引用,只有在相应的获取操作之后,它才能反映那次获取时的远端状态。
执行 git push 时出现 ! [rejected] main -> main (non-fast-forward),也值得检查。它可能意味着分叉,也可能只是本地单纯落后,不能仅凭这一行就决定强推。
执行 git pull 时,Git 可能提示 You have divergent branches and need to specify how to reconcile them。提示中列出的三个配置分别表示合并、变基和仅允许快进:
git config pull.rebase false
git config pull.rebase true
git config pull.ff only
这些是互斥的策略示例,不是一组应依次执行的“修复步骤”。第一种会尝试合并;第二种会尝试变基;第三种在真正分叉时拒绝继续。已有配置也会影响 pull 的表现,所以没有看到同一句报错,并不表示分支一定未分叉。
默认不带 --global 的设置作用于当前仓库。原文作者倾向于每次明确选择策略,因为她有时想保留两边,有时想舍弃一边,并不希望一个全局默认值替自己作出判断。
保留双方改动:变基或合并
Julia 在希望保留两边改动时,通常使用:
git pull --rebase
它先获取远端信息,再把本地相应提交重新应用到上游历史之上。对于尚未共享的本地提交,这可以让历史保持线性。但重新应用通常会生成新的提交标识;如果别人已经依赖被改写的提交,就需要按团队工作流协调,而不能只把历史“变整齐”当作理由。
另一个选择是:
git pull --no-rebase
它尝试合并本地与远端历史。在原文所讨论的分叉场景中,成功合并通常会创建合并提交,并可能打开编辑器让你确认提交说明。作者说明自己习惯变基,但承认其他工作流完全有理由采用合并。
编辑补注:两条路径都可能遇到冲突。应逐个理解并解决冲突,再按 Git 提示继续;不确定时可以用对应的 git rebase --abort 或 git merge --abort 放弃该操作。开始前应先保存未提交内容并保留可回退的提交引用;分支备份只保住提交,不会自动备份未提交文件。
确实要替换远端历史:理解强推的代价
原文中,作者有时在只有自己提交的私有仓库里,先推送提交,随后用 git commit --amend 修改最近一次提交,再使用 git push --force 替换远端历史。它的意思不是“帮我解冲突”,而是允许远端分支不经快进就被重新指向。
风险:远端独有提交可能不再能通过该分支访问;如果其他协作者已经继续提交,强推可能覆盖他们的工作。原文作者通常为多人仓库启用分支保护,禁止强推。本文保留这一路径的语义说明,不把强推作为遇到推送失败后的默认动作。
git push --force-with-lease 多了一项条件:远端引用必须仍等于本地预期的值,推送才会通过。它能减少覆盖未预期远端更新的风险,但也有边界。使用隐式预期值时,编辑器的后台自动 fetch 会更新远端跟踪引用,可能让保护失去“我已经看过这份历史”的实际含义。
现行 Git push 文档还说明,可以显式指定 --force-with-lease=<refname>:<expect>,把预期远端值固定为已经核实的提交;这仍然是强制更新,不能代替对被覆盖历史的审查。不要把未经核实的提交值当作租约依据。
原文提到的 --force-if-includes 可以与未显式指定预期值的 --force-with-lease 配合,检查远端跟踪引用的尖端是否已经纳入本地改写依据。Git 文档明确:如果单独使用它,或与显式 <refname>:<expect> 形式一起使用,它不额外生效。它也不保证你已经理解每个提交的业务含义。
保留远端、重新安置本地工作
如果本地提交本来就不该出现在 main,可以先给它们留下另一个分支,再让本地 main 与远端对齐。原文用 new-branch 保存误放在主分支上的提交:
git checkout main
git checkout -b new-branch
git checkout main
git fetch
git status
到这里,新分支保留了原有提交。接下来的 git reset --hard origin/main 是破坏性步骤:它移动当前分支,同时重置索引和工作区,可能永久丢失未提交改动。它不是紧接上面代码就应该无条件执行的命令。应先确认所在分支、远端名称、备份,以及 git status 中的每项改动。
原文还列出两种替代方式:切到其他分支后运行 git branch -f main origin/main,或使用 git fetch origin main:main --force。它们同样会强制移动本地分支,不能因为没有出现 reset --hard 就当作安全的通用替代。
编辑补注:Git reset 文档说明,--hard 除了丢弃被跟踪文件的改动,还会删除妨碍写入被跟踪文件的未跟踪文件或目录。不能把“未跟踪”误当作绝不会受影响。保存提交、保存未提交内容和检查未跟踪文件是不同的事情。
读懂提示,比背一个修复命令更可靠
分叉本身不是损坏,只是两边沿不同路径继续前进。获取并比较历史之后,如果双方工作都需要保留,就在合并与变基之间按团队约定选择;如果明确舍弃一侧,先确认影响范围和回退点,再考虑会改写历史的操作。原文想澄清的,正是这些命令各自承诺保留什么、又可能丢掉什么。
审核说明:所有示例仅做静态语义与风险审查。本次未创建实验仓库、未执行强推、硬重置或其他 Git 示例,也没有验证某个真实仓库是否可以恢复。没有发现额外问题不代表不存在其他风险。












暂无评论内容