经过前面的学习, 我们已经把 Git 的“本地模型”理得非常清楚了(工作区 → 暂存区 → 仓库 → HEAD/branch/ref),接下来进入最后一块大拼图 —— 远程仓库与分布式交互。
为什么需要远程仓库?
- 本地仓库已经能完整保存历史,但如果多人协作,每个人各搞一套仓库,没办法互通。
- 于是我们需要一个 公共仓库(remote) ,它其实就是另一个 Git 仓库,大家通过它来交换提交。
👉 Git 的“分布式”本质就是:每个人都完整保存一份仓库,通过远程进行同步。
远程仓库(Remote)的本质
- 远程仓库就是另一个
.git/objects+.git/refs的集合。 - 在本地我们用一个“别名”来指代它,比如常见的
origin。 - 远程仓库的分支会记录在本地的 远程引用(remote-tracking branch) 中,比如:
origin/main → 表示远程的 main 分支
四个动作与案例演示
a. 场景设定
- 远程仓库名:
origin - 分支名:
main - 本地仓库和远程仓库都有一个
main分支。 - HEAD 总是指向某个分支(正常状态),而分支是提交链上的指针。
初始状态:
远程仓库(origin):
origin/main: C1 -- C2
本地仓库:
main: C1 -- C2
HEAD -> main
此时,本地和远程完全一致。
b. fetch(取回远程对象,不合并)
假设远程有人新提交了 C3:
远程仓库(origin):
origin/main: C1 -- C2 -- C3
本地仓库(fetch 后):
origin/main: C1 -- C2 -- C3 (远程分支指针更新)
main: C1 -- C2 (本地分支没动)
HEAD -> main
👉 fetch 的作用就是:更新本地保存的远程指针(origin/main),但不改动你的 main。
所以这时候你本地和远程是 “分歧状态” 。
关于fetch
命令
git fetch origin→ 拉取远程仓库origin的所有分支信息。git fetch origin main→ 只拉取远程的main分支。
两者本质一样,区别只是范围。
fetch 前
远程仓库:
origin/main: C1 -- C2 -- C3
本地仓库:
origin/main: C1 -- C2 (本地保存的远程指针落后)
main: C1 -- C2
HEAD -> main
fetch 后
远程仓库:
origin/main: C1 -- C2 -- C3
本地仓库:
origin/main: C1 -- C2 -- C3 (更新成功)
main: C1 -- C2
HEAD -> main
👉 结论:
- fetch 的作用就是:更新本地保存的远程指针(
origin/main)。 - 本质上就是“让本地对远程的快照更新到最新”,但不会改动本地分支(
main)。
c. merge(合并远程改动到本地)
如果你执行:
git merge origin/main
本地会把远程的 C3 合并进来:
远程仓库:
origin/main: C1 -- C2 -- C3
本地仓库:
origin/main: C1 -- C2 -- C3
main: C1 -- C2 -- C3 (合并后前进到 C3)
HEAD -> main
👉 merge 的效果:把远程分支的提交合并到本地分支。如果本地没有新提交,结果就是 fast-forward(快进)。
关于 merge
情况 A:本地没有新提交(fast-forward)
我们刚说过,结果就是本地 main 指针直接快进到远程的最新提交。
情况 B:本地有新提交
比如:
远程仓库:
origin/main: C1 -- C2 -- C3
本地仓库(fetch 后):
origin/main: C1 -- C2 -- C3
main: C1 -- C2 -- C4
HEAD -> main
现在 main 和 origin/main 分叉了。
执行:
git merge origin/main
Git 会创建一个新的 合并提交(merge commit),把两个历史合并起来:
远程仓库:
origin/main: C1 -- C2 -- C3
本地仓库(merge 后):
origin/main: C1 -- C2 -- C3
main: C1 -- C2 -- C4
\
C5 (merge commit)
/
origin/main ---------- C3
HEAD -> main
其中:
- C5 是一个新提交,包含了 C3 和 C4 的更改。
- 这样,历史变成了分支汇合的结构(DAG 里的一个节点有两个父亲)。
总结 merge 的两种结果
- 快进(fast-forward) :当本地分支正好落后于远程分支时,本地指针直接移动。
- 合并提交(merge commit) :当本地和远程都有新提交时,Git 新建一个提交把它们合并起来。
d. rebase(变基远程改动到本地)
rebase 前
远程仓库:
origin/main: C1 -- C2 -- C3
本地仓库:
origin/main: C1 -- C2 -- C3
main: C1 -- C2 -- C4
HEAD -> main
origin/main有:C1, C2, C3main有:C1, C2, C4- 分歧点在 C2。
👉 C3 是远程独有提交,C4 是本地独有提交。
执行
git rebase origin/main
含义是:
- 把
main分支上那些 不在 origin/main 里的提交(这里就是 C4)“摘下来”。 - 把
main指针移到origin/main的最新位置(C3)。 - 再把刚才摘下来的提交 依次重放(即应用 patch)。
rebase 后
远程仓库:
origin/main: C1 -- C2 -- C3
本地仓库:
origin/main: C1 -- C2 -- C3
main: C1 -- C2 -- C3 -- C4'
HEAD -> main
- 注意 C4 被重放了一遍,所以生成了 C4' (新的哈希)。
- C3 永远在前,因为我们指定的基底是
origin/main,所以必须以远程最新提交为起点。
e. push(推送本地分支指针到远程)
假设经过 rebase,你的本地 main 已经比远程领先:
远程仓库:
origin/main: C1 -- C2 -- C3
本地仓库:
origin/main: C1 -- C2 -- C3
main: C1 -- C2 -- C3 -- C4'
HEAD -> main
执行:
git push origin main
结果:
远程仓库:
origin/main: C1 -- C2 -- C3 -- C4'
本地仓库:
origin/main: C1 -- C2 -- C3 -- C4'
main: C1 -- C2 -- C3 -- C4'
HEAD -> main
👉 push 的本质:把远程分支指针更新到和本地一致。
如果远程和本地有分歧,push 会失败(除非强推 --force)。
总结
Git 的远程交互,本质就是:
fetch→ 更新本地保存的远程指针merge/rebase→ 把远程改动整合到本地分支push→ 更新远程分支指针,使其和本地一致
所以一句话:
👉 Git 的远程交互,本质就是 本地分支指针 和 远程分支指针 的同步与合并。
pull与前面四个动作的关系
前置思考
提到Git的远程交互,我们最容易想到的就是pull和push,可为什么我们前面讲Git的远程交互,讲了fetch、merge/rebase、push,却唯独没提到pull?
先把四个动作区分开
-
fetch:
- 只下载远程的对象(commit/tree/blob)并更新本地的远程分支指针(
origin/main等) 。 - 它不会改动你的当前分支。
- 本地和远程“差异”会并存。
- 只下载远程的对象(commit/tree/blob)并更新本地的远程分支指针(
-
merge:
- 把远程分支的改动合并进你当前分支。
- 如果没有冲突,并且你本地没有新提交,就会 快进(fast-forward) 。
- 如果双方都有提交,就可能产生一个新的合并提交。
-
rebase:
- 另一种整合改动的方式。
-
push:
- 把你的本地分支指针更新到远程。
- 如果远程落后于你,没问题;
- 如果远程领先你,你就得先 fetch + merge/rebase 才能 push。
那 pull 到底是什么?
git pull = git fetch + git merge 的简写
默认情况下,git pull 就是:
git fetch origin main
git merge origin/main
所以 pull = fetch + 整合改动(merge/rebase) 。
👉 这就是为什么我在前面只强调 fetch / merge / rebase / push,因为 pull 并不是“新东西”,它只是快捷方式。
pull 让人困惑的地方
pull 有两个重要点:
-
pull 的“整合方式”可以配置:
- 默认是 merge,所以历史可能产生分叉:
本地: c1 -- c2
\
origin/main: c3
pull 后: c1 -- c2 ---- merge_commit
/
c3
但你也可以配置 git config pull.rebase true,让 pull 自动执行 rebase:
本地: c1 -- c2
origin/main: c1 -- c2 -- c3
pull 后: c1 -- c2 -- c3 -- c2'
所以 pull 的结果不唯一。
-
pull 一步到位,但失去了“可控性” :
- 如果你先
fetch,你可以清楚地看到远程分支和本地分支的差异,再决定 merge 还是 rebase。 - 如果直接
pull,你就没得选,它直接帮你合并或变基了。
- 如果你先
我们应该怎么用?
- 学习阶段 / 精细操作时:建议用
fetch + merge/rebase,更直观更安全。 - 日常协作 / 熟练后:
pull可以省事,但最好明确设置好默认行为:
git config --global pull.rebase false # 使用 merge(默认)
git config --global pull.rebase true # 使用 rebase
总结
fetch是 更新远程分支指针,不动本地分支。merge/rebase是 把远程改动整合进本地。pull就是 fetch + merge/rebase 的组合命令。- 我没直接提
pull,是因为理解底层(fetch/merge/rebase/push)之后,pull 就自然不神秘了。