卷 II · 贴纸CH 08深度 8/24

origin/main 是谁的贴纸

同一个名字,三个不同的东西:你的 main、你本地的 origin/main、服务器上真实的 main它们是三张独立的贴纸,随时可能指向三个不同的提交。把这三张纸分清楚,「分布式」这个词才真正落地。

远程跟踪分支fetch vs pull快进upstream

它也在 refs 目录下

$ cat .git/refs/remotes/origin/main
1bada9988e4c142f884f13990c310e024c36f1ea

又是 41 个字节。和本地分支一模一样的东西,只是放在 refs/remotes/ 而不是 refs/heads/

注意这个事实带来的推论:它是一个纯本地的文件。它躺在你的硬盘上,Git 读它的时候不联网,服务器也完全不知道它的存在。

◆ 一句话破除误解

origin/main 不是「服务器上的 main 分支」。

它是「我上一次跟服务器通话时,它的 main 指向哪儿」的一张便条

便条是会过期的。服务器上的 main 已经往前跑了十个提交,你的便条还停在原地——而 Git 完全不知道,也不会主动去问

它只在三个时刻更新:fetchpull(它内含 fetch)、push(推成功了顺手更新)。

三张贴纸,动起来看

▶ 动手 · 三方贴纸

先点「别人推了一个上去」。盯着 origin/main——它纹丝不动。这就是关键。

然后点 git fetch,它才跟上。

再试试:本地提交几个,然后在还没 fetch 的情况下 push,看会发生什么。

fetch 和 pull 的真正区别

这两个命令的关系,一张表说完:

下载对象更新 origin/main动你的 main动工作区
git fetch
git pull

pull = fetch + merge就这么回事。它先把对象和便条更新了,然后立刻把 origin/main 合并进你的 main

◆ 为什么 fetch 永远是安全的

看上面那张表的后两列: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 pushgit pullgit status 里那句「ahead 2, behind 1」才知道该跟谁比。

新建的本地分支默认没有 upstream,所以第一次推要:

$ git push -u origin feat     # -u 就是 --set-upstream,顺手建立关联

忘了加 -u 的话,以后每次都得写全参数——这就是那个「为什么有的分支能直接 push,有的不行」的答案。

⌗ 掀开 .git 看一眼

远程配置和那条 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/mainrefs/remotes/ 下的一张 41 字节本地便条,记着「上次通话时服务器的 main 在哪」——它不会自己更新,git loggit statusgit branch -r 全都不联网。fetch 只更新便条和对象,所以永远安全;pull = fetch + merge,会动你的分支。

下一章:卷 II 的最后一张贴纸,也是最让人安心的一张——reflog。它记着 HEAD 走过的每一步,这意味着那些「看不见了」的提交,其实一直都还有名字可以叫。我们会先故意弄丢几个提交,再当场找回来。