记录一次看似普通、却让我真正理解 Git 分支历史的经历。

在这里插入图片描述

一、问题现场:一次“很正常”的 push

​ 那天我只是给代码加了一点 debug 日志,流程非常熟练:

git add ml/train_cnn.py
git commit -m "debug: add logs"
git push

​ 结果,Git 给了我一个并不友好的回复:

! [rejected] kylin/3.3.1-demo -> kylin/3.3.1-demo (fetch first)
error: failed to push some refs to 'http://xxx/algorithm/DeepLearning.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally.

​ 我心里第一反应是,我只是加了点日志, 没改接口、没动依赖, 为什么连 push 都不让我了?

​ 于是我照提示执行了:

git pull

​ 结果事情变得更“抽象”了。

二、Git 开始说人话,但我一时没听懂

git pull 的输出里,最关键的是这一句:

You have divergent branches and need to specify how to reconcile them.
fatal: Need to specify how to reconcile divergent branches.

​ 再一看状态:

Your branch and 'origin/kylin/3.3.1-demo' have diverged,
and have 1 and 20 different commits each.

​ 这句话信息量其实很大,但当时我并没有完全意识到。

在这里插入图片描述

三、什么叫 “branches have diverged”?

​ 我们先把 Git 的话翻译成人话。

​ 这句:

have 1 and 20 different commits each
  • 意思是:

    • 远端分支:比我本地多 20 个提交
    • 本地分支:比远端多 1 个提交
  • 也就是说,历史已经不是一条线了,而是这样:

    A --- B --- C --- D --- ... --- T   (origin)
     \
      E                               (local)
    
    • E:我刚提交的 debug: add logs
    • T:远端最新提交
  • Git 不知道该怎么把这两条历史“合并成一条”

  • 所以它停下来,问我一句话:“你希望历史最后长什么样?”

四、Git 给我的三个选项,分别意味着什么?

​ Git 的提示里,其实已经给了答案:

git config pull.rebase false  # merge
git config pull.rebase true   # rebase
git config pull.ff only       # fast-forward only

​ 这三种方式,本质上回答的是同一个问题:当本地和远端的提交历史已经分叉时, 你希望 Git 如何“整理这段历史”?

​ 下面我们一个一个来看。

4.1 merge:最安全,但最“吵”

git pull --no-rebase

​ 这是 Git 最保守、也最符合直觉 的做法。

(1) 流程
  • 它到底做了什么?

    当你执行 merge 时,Git 不会去“重写”任何已有提交,而是:

    1. 保留远端的提交历史
    2. 保留你本地的提交历史
    3. 额外创建一个 merge commit,把两条分叉的历史连接起来
  • 历史结构会变成这样:

    A --- B --- C --- ... --- T --- M
     \                       /
      E ---------------------
    
    • T:远端最新提交
    • E:你本地的新提交
    • M:Git 自动生成的 merge commit
(2) 优缺点
  • 优点
    • 绝对安全:不会丢提交
    • ✅ 不会修改任何已有 commit 的 hash
    • ✅ 非常适合对 Git 机制不熟的团队
  • merge 的代价
    • ❌ 会产生额外的 merge commit
    • ❌ 提交历史不再是“一条线”
    • git loggit bisectgit blame 可读性下降

尤其是在你只改了一点点代码的时候,一个 “Merge branch …” 提交,显得有些“用力过猛”。

4.2 rebase:最干净,但需要你明白在干什么(⭐ 推荐)

git pull --rebase

​ 相比 merge,rebase 的思路完全不同。

在这里插入图片描述

(1) rebase的核心思想

​ 不要保留“分叉”这件事本身,只保留“最终发生过哪些修改”。

  • Git 会做一件很巧妙的事:

    1. 把你本地的提交 暂时移除
    2. 先把分支移动到远端最新提交
    3. 再把你的提交 按顺序重新应用一遍
  • 最终历史会变成:

  • A --- B --- C --- ... --- T --- E
    

    从结果上看,就像是你是在远端最新代码的基础上,直接提交了 E

(2) 优缺点
  • 优点
    • ✅ 提交历史完全线性
    • ✅ 没有多余的 merge commit
    • ✅ 非常适合:
      • 小改动
      • debug
      • 文档修正
      • 算法实验代码
  • 缺点
    • ⚠️ rebase 会重写提交历史
    • ⚠️ 如果发生冲突,需要你亲自解决
    • ⚠️ 不要对已经被多人依赖的公共提交 rebase
  • 一句话总结:rebase 很优雅,但它假设你知道自己在做什么。

4.3 fast-forward only:理想但不现实

git pull --ff-only

​ fast-forward 是 Git 最“理想化”的模式。

  • 它的要求非常苛刻:

    • 本地没有任何独立提交
    • 本地分支只是落后于远端
  • 也就是说,历史必须是这样:

    A --- B --- C --- D   (origin)
                  ^
                local
    
    

    Git 才能简单地把指针“向前挪一格”。

    一旦你本地已经有提交(哪怕只有一个),ff-only 就会直接拒绝操作。

​ 要求本地没有任何独立提交,在真实项目中几乎用不上。

​ 因此在真实的多人协作项目中:fast-forward more 是一种**“理论上很好,实践中很少遇到”的模式**

五、我为什么选择 rebase?

​ 理解了三种策略之后,选择其实就变得非常自然了。

  • 结合我当时的实际情况:

    • ✔ 只改了 一个文件
    • ✔ 只是 debug 日志
    • ✔ 不想污染主干历史
    • ✔ 不希望多一个 merge commit
  • 在这种场景下:

    保留“分叉发生过”这件事,本身没有任何价值。

    我真正关心的只有一件事:最终代码 + 一条干净、可读的提交历史

  • 因此,rebase 是最合理、也最克制的选择。

六、最终的实战操作(非常简单)

6.1 rebase 到远端最新分支

git pull --rebase origin kylin/3.3.1-demo

​ 如果一切顺利,这一步会直接完成。

6.2 如果出现冲突,Git 会明确告诉你

​ 如果有冲突,Git 会提示你:

git status

​ Git 会列出冲突文件,并标记冲突位置。

​ 解决冲突后:

git add 冲突文件
git rebase --continue

​ Git 会继续“回放”剩余的提交。

6.3 如果你发现自己走错了路

​ 如果你发现自己改错了,也可以随时撤退:

git rebase --abort

​ 这条命令会把仓库恢复到 rebase 之前的状态,不会留下任何“半成品历史”。

6.4 push

git push origin kylin/3.3.1-demo

​ 此时,本地历史已经是远端的“自然延续”,push 会顺利通过。

七、事后的一点反思

​ 这次经历让我意识到一件事:Git 并不是在为难你,它只是在一个关键节点上,要求你对“历史形态”做出选择。

  • merge:强调安全

  • rebase:强调整洁

  • ff-only:强调理想状态

    理解这一点之后,Git 的很多“奇怪报错”都会变得合理。

八、一个我后来做的改进(推荐)

​ 为了不再每次被 Git 问一遍,我后来直接设置了默认策略:

git config --global pull.rebase true

​ 从此,git pull = git pull --rebase,历史默认保持线性,再也不慌 divergent branches

九、总结一句话

​ 当 Git 提示 branches have diverged 时,它真正的问题不是:**“你要不要 pull?”**而是: “你希望你的代码历史,看起来像什么?”

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐