Git篇(8):远程仓库(Remote)与 push/pull/fetch

145 阅读7分钟

经过前面的学习, 我们已经把 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

现在 mainorigin/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, C3
  • main 有:C1, C2, C4
  • 分歧点在 C2

👉 C3 是远程独有提交C4 是本地独有提交


执行
git rebase origin/main

含义是:

  1. main 分支上那些 不在 origin/main 里的提交(这里就是 C4)“摘下来”。
  2. main 指针移到 origin/main 的最新位置(C3)。
  3. 再把刚才摘下来的提交 依次重放(即应用 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 等)
    • 它不会改动你的当前分支。
    • 本地和远程“差异”会并存。
  • 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 有两个重要点:

  1. 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 的结果不唯一。

  1. 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 就自然不神秘了。