origin/main 是谁的贴纸
同一个名字,三个不同的东西:你的 main、你本地的 origin/main、服务器上真实的 main。它们是三张独立的贴纸,随时可能指向三个不同的提交。把这三张纸分清楚,「分布式」这个词才真正落地。
它也在 refs 目录下
$ cat .git/refs/remotes/origin/main 1bada9988e4c142f884f13990c310e024c36f1ea
又是 41 个字节。和本地分支一模一样的东西,只是放在 refs/remotes/ 而不是 refs/heads/。
注意这个事实带来的推论:它是一个纯本地的文件。它躺在你的硬盘上,Git 读它的时候不联网,服务器也完全不知道它的存在。
origin/main 不是「服务器上的 main 分支」。
它是「我上一次跟服务器通话时,它的 main 指向哪儿」的一张便条。
便条是会过期的。服务器上的 main 已经往前跑了十个提交,你的便条还停在原地——而 Git 完全不知道,也不会主动去问。
它只在三个时刻更新:fetch、pull(它内含 fetch)、push(推成功了顺手更新)。
三张贴纸,动起来看
先点「别人推了一个上去」。盯着 origin/main——它纹丝不动。这就是关键。
然后点 git fetch,它才跟上。
再试试:本地提交几个,然后在还没 fetch 的情况下 push,看会发生什么。
fetch 和 pull 的真正区别
这两个命令的关系,一张表说完:
| 下载对象 | 更新 origin/main | 动你的 main | 动工作区 | |
|---|---|---|---|---|
git fetch | ✓ | ✓ | ✗ | ✗ |
git pull | ✓ | ✓ | ✓ | ✓ |
pull = fetch + merge。就这么回事。它先把对象和便条更新了,然后立刻把 origin/main 合并进你的 main。
看上面那张表的后两列:fetch 不碰你的分支,也不碰你的工作区。
它做的全部事情是:把服务器上有而你没有的对象下载到对象库(只增不改,第 5 章),然后更新几张 refs/remotes/ 下的便条。
没有任何一个你的东西会被改动。所以 git fetch 是 Git 里最安全的命令之一——不管什么情况,先 fetch 一下看看,永远不会有坏处。
这也是很多人推荐的习惯:用 git fetch + 看一眼 + 再决定怎么合,而不是无脑 git pull。因为 pull 会在你还没看清楚状况的时候就自动合并,而合并可能产生冲突、或者产生一个你并不想要的合并提交。
fetch 之后想看看服务器那边多了什么:
# 我落后了哪些提交 $ git log HEAD..origin/main # 我领先了哪些提交 $ git log origin/main..HEAD # 两边各自独有的,一起看(左右分开显示) $ git log --left-right --oneline HEAD...origin/main # 只要个数字 $ git rev-list --left-right --count HEAD...origin/main
注意 .. 和 ... 的区别:两个点是「A 有 B 没有」,三个点是「两边各自独有的」(对称差)。这个区别在第 16 章讲 merge-base 时会更清楚——因为三个点的定义正是「从 merge-base 分开之后两边各走的路」。
那个 non-fast-forward
你 push 时最常撞的墙:
$ git push ! [rejected] main -> main (non-fast-forward) error: failed to push some refs to 'origin' hint: Updates were rejected because the tip of your current branch is behind hint: its remote counterpart.
「快进(fast-forward)」这个词第 16 章会给精确定义,但现在可以先记一个够用的版本:
把贴纸从 A 挪到 B 是「快进」,当且仅当 A 是 B 的祖先。
也就是说:B 已经包含了 A 的全部历史,挪过去不会让任何提交失去引用。
反过来,如果 A 不是 B 的祖先,那么把贴纸从 A 挪到 B,A 那边独有的提交就没人指了——在服务器上,那意味着别人推上去的东西凭空消失。
所以服务器默认只接受快进的推送。这不是在为难你,是在保护那些你可能根本不知道存在的提交。
撞上它,先搞清楚是哪种情况:
| 情况 | 怎么发生的 | 怎么办 |
|---|---|---|
| 你落后了 | 别人推了新东西,你没拉 | git pull(或 fetch + merge/rebase),然后再推 |
| 你改写了历史 | 你 amend / rebase / reset 过已推送的提交 | 想清楚:确实该强推吗?要推就用 --force-with-lease |
| 别人改写了历史 | 同事强推过,你本地还是老的 | 先去问清楚,别自己乱推。你一强推可能就把他的覆盖了 |
--force 和 --force-with-lease 差得远git push --force:「我不管服务器上现在是什么,用我的覆盖。」
问题在于——你以为服务器上是你 fetch 时看到的那个状态,但同事可能在你 fetch 之后又推了几个提交。你一个 --force,把他的活儿全变成孤儿了(他能救回来,第 20 章,但那是一段尴尬的对话)。
git push --force-with-lease:「如果服务器上还是我上次见到的那个样子,就用我的覆盖;否则拒绝。」
它拿你本地的 origin/main 便条当作凭据去比对。便条过期了 → 说明有人在你之后推过 → 拒绝。
结论:把 --force 从肌肉记忆里删掉,永远用 --force-with-lease。
但要小心一个陷阱:如果你在强推前跑了 git fetch,便条就被更新成最新的了,--force-with-lease 的保护也就失效了——它会以为「服务器就是我以为的样子」。所以要检查就在 fetch 之后、强推之前,用眼睛看一遍。Git 2.30 之后可以用 --force-if-includes 来加固这个场景。
upstream:那个「省下来的参数」
为什么你能直接 git push 而不用写 git push origin main?因为分支有个配置叫 upstream(上游):
$ git branch -vv * main 1bada99 [origin/main] second feat a3f21c9 [origin/feat: ahead 2] 新功能
方括号里就是 upstream。它存在 .git/config:
[branch "main"] remote = origin merge = refs/heads/main
有了它,git push、git pull、git status 里那句「ahead 2, behind 1」才知道该跟谁比。
新建的本地分支默认没有 upstream,所以第一次推要:
$ git push -u origin feat # -u 就是 --set-upstream,顺手建立关联
忘了加 -u 的话,以后每次都得写全参数——这就是那个「为什么有的分支能直接 push,有的不行」的答案。
远程配置和那条 refspec:
$ cat .git/config
[remote "origin"] url = git@github.com:you/repo.git fetch = +refs/heads/*:refs/remotes/origin/*
那行 fetch = 就是整章的机制所在,读法是:
+refs/heads/* : refs/remotes/origin/* ↑ ↑ 服务器上的 存到我本地的哪里 (源) (目的) 开头的 + 表示「允许非快进更新」——因为便条本来就该无条件反映服务器现状
这叫 refspec。它明明白白地写着:把服务器上 refs/heads/ 下的所有东西,抄到我本地的 refs/remotes/origin/ 下。
「抄」这个动作就是 fetch 的全部。你甚至可以手工改这行来只跟踪部分分支:
# 只跟踪 main,别的分支一概不抄(大仓库能省不少) $ git config remote.origin.fetch '+refs/heads/main:refs/remotes/origin/main'
再看看删掉的远程分支为什么不会自动消失:
$ git fetch --prune # 清掉那些服务器上已经没有的便条 $ git config fetch.prune true # 或者让它每次都自动清
因为 fetch 的默认行为是「抄新的」而不是「同步」——服务器上没有的分支,它不会主动帮你删便条。这就是为什么 git branch -r 里常年躺着一堆早就删掉的分支。
说了半天,Git 的「分布式」到底是什么意思?现在可以给一个精确的回答了。
不是「每个人都有完整副本」——那只是结果。真正的技术根基是第 2 章那句话:同一份内容,在任何机器上、离线状态下,算出的名字必然相同。
所以:
- 我在飞机上提交十次,你在家里提交十次,我们的对象库可以直接合并,不会撞车,不需要任何协调。
origin在技术上没有任何特权。它只是一个你起了名字的 URL。你完全可以有五个远程,或者把同事的机器当远程直接拉。- 「中央仓库」是一个纯粹的社会约定,不是 Git 的概念。GitHub 之所以是中心,是因为大家都同意它是。
对比 SVN 那种中心化版本号(1、2、3……):那个号必须由服务器分配,因为只有服务器知道下一个是几。所以离线提交在模型上就不可能——不是没实现,是做不到。
「同事说他推上去了,我 git log origin/main 怎么还是旧的?」
因为你看的是便条,不是服务器。git log 是纯本地操作,它一个网络包都不会发。
git fetch 一下,便条更新了,你就看到了。
这个误解特别顽固,因为 origin/main 这个名字实在太像「远程的东西」了。把它读成「origin 的快照·main」会好很多。
顺带三个相关的:
git branch -r也不联网,列的全是本地便条。git status那句「Your branch is behind 'origin/main' by 3 commits」也不联网——它拿本地便条比的。所以它说「up to date」不代表真的最新,只代表「跟你上次 fetch 时一样」。- 想看真·服务器状态而不改本地任何东西:
git ls-remote origin。这个是真联网的。
这一章的一句话
origin/main 是 refs/remotes/ 下的一张 41 字节本地便条,记着「上次通话时服务器的 main 在哪」——它不会自己更新,git log/git status/git branch -r 全都不联网。fetch 只更新便条和对象,所以永远安全;pull = fetch + merge,会动你的分支。
下一章:卷 II 的最后一张贴纸,也是最让人安心的一张——reflog。它记着 HEAD 走过的每一步,这意味着那些「看不见了」的提交,其实一直都还有名字可以叫。我们会先故意弄丢几个提交,再当场找回来。