Git Internals

传输协议、协商与 pack 交换

理解 fetch / clone / push 时的协商过程,能帮助你看懂 Git 不是在“整仓同步”,而是在交换对象与引用状态。

适合谁看
  • 想建立稳定 Git 心智模型的学习者
  • 经常遇到历史、引用、恢复问题的开发者
前置知识
  • 会看基础命令输出
  • 知道提交、分支、HEAD 这些名词
常见风险
  • 只背底层术语却不连接到实际命令
  • 把对象、引用、工作区混成一层理解

很多人第一次使用 Git 时,会把 clonefetchpush 想成:

  • “把仓库整个复制过来”
  • “把我本地的东西整个发过去”

但 Git 真正的传输逻辑远比这更精细。
它关心的是:

  • 双方各自已有哪部分对象
  • 哪些引用需要更新
  • 哪些对象需要被打包传输

这也是为什么 Git 在大型仓库里依然能保持很强的效率。

Git 传输到底在交换什么

从底层看,Git 主要在交换两类信息:

  1. 引用状态:例如分支和标签指向什么提交
  2. 对象数据:为让这些引用成立,双方还缺哪些对象

也就是说,fetch 不是在“复制整个仓库目录”,而是在做一次对象图同步协商。

为什么协商过程很关键

因为大多数时候,本地和远端并不是完全陌生的两个仓库。
它们往往已经共享大量历史。

所以 Git 需要先判断:

  • 你已经有什么
  • 我还缺什么
  • 哪些对象不必重复发送

这个“先判断再传”的过程,就是它高效的根源之一。

pack 在这里扮演什么角色

对象理论上可以一个个传,但这样效率太差。
所以 Git 通常会把需要发送的一组对象打成 pack,再进行传输。

这意味着:

  • 传输效率更高
  • 重复内容能被更好压缩
  • fetch / clone / push 的体感成本会低很多

所以理解 pack,不只是存储优化问题,也是在理解网络同步为什么能这么做。

一次 fetch / push 的底层心智模型双方先比较 refs 和已知对象,再只交换真正缺失的 pack 数据,而不是盲目复制整仓。
本地
refs/heads/mainorigin/mainobjects
refs 协商pack 交换
远端
refs/heads/mainrefs/tags/v2.0pack objects

clone 和 fetch 的底层差异

从体验看:

  • clone 像第一次拉全仓
  • fetch 像同步增量

但它们在底层都离不开同样的核心问题:

  • 远端有哪些 refs
  • 本地已有多少对象
  • 需要再补哪些 pack

不同的是,第一次 clone 时,本地通常“几乎什么都没有”,所以协商空间更小;而 fetch 通常更依赖双方已有历史的重叠。

push 为什么也是协商

很多人误以为 push 是“我把我这边的提交直接扔给服务器”。
其实 push 也涉及:

  • 远端当前 refs 状态
  • 是否允许更新这些 refs
  • 远端是否缺少相关对象
  • 更新是否会违反 fast-forward 或保护规则

所以 push 既是对象传输,也是引用更新请求。

Git 传输不是整仓复制,而是“引用状态 + 对象差量”的协商

一旦你用这个视角看 fetch 和 push,就更容易理解为什么网络传输、分支保护和 packfiles 会连成一体。

为什么这能帮助你理解实际问题

这个视角能帮你更好理解:

  • 为什么 fetch 很快
  • 为什么 push 会被拒绝
  • 为什么大仓库更依赖 pack 优化
  • 为什么某些网络或协议问题会直接表现成 clone/fetch 异常

常见误区

clone 就是下载一个项目目录

不是。它在下载对象数据库和引用图,而不是简单文件夹压缩包。

fetch 一定会把远端所有东西都拉下来

不一定。核心仍然是 refs 与对象协商。

push 只是上传我的提交

也不完整。它还在请求远端更新引用,并接受远端规则校验。

一句最值得记住的话

Git 的网络传输本质上是对象和引用的协商交换,而不是整仓盲目复制。