# 远程仓库到底连的是什么：origin、origin/main，以及 fetch 和 pull 的区别

origin 只是一条配置，origin/main 只是本地书签；fetch 只下载并挪书签，pull 则在此之后立刻按默认策略改动你的当前分支

> Git 远程仓库 · Git 基础操作 · HEAD 与引用 · 约 6 分钟 · 09 月 24 日

## 本篇要点

1. 远程仓库在你的仓库里只是一条配置：remote 名字、URL，加上一条 `源:目的地` 形式的 refspec，`git clone` 自动把这条配置和名字 `origin` 一起建好。
2. `origin/main` 一类的远程跟踪分支是本地引用，存放在 `.git/refs/remotes/origin/` 下，由 `+refs/heads/*:refs/remotes/origin/*` 这条 refspec 映射生成。
3. 远程跟踪分支记录的是“上次通信时远程的状态”，它是一个可能过期的书签，只有 fetch、pull 这类通信动作才会更新它，你不能直接改、也不能在它上面提交。
4. `git fetch` 只做两件事：下载本地缺的提交对象，把远程跟踪分支挪到新位置；它不改本地分支、不改工作区、不改索引。
5. `git push` 是把本地提交送到远程并要求远程分支指针前移，这个前移必须是快进，否则会被拒绝。
6. `git pull` 等于先 fetch，再按当前分支的 upstream 把远程分支整合进来；第二步走 merge、rebase 还是只允许快进，由 `--ff-only`、`--rebase`、`--no-rebase` 或对应的 `pull.ff`、`pull.rebase` 配置决定。
7. upstream 是 `branch.<名字>.remote` 和 `branch.<名字>.merge` 两条配置，`clone` 自动配好，`git branch -vv` 能看到它以及 ahead/behind。
8. 从 Git 2.27 起，未配置调和方式时 Git 会要求你表态，后来这条提示只在历史真的分叉时出现，并从警告变成中止；现行手册把 `--ff-only` 列为未指定调和方式时的默认，所以“pull 会自动生成合并提交”这个印象已经过时。
9. 本地分支、本地书签、服务器分支是三个独立位置，同步循环是 push、fetch、merge 或 rebase 三条边的不同组合；`git status` 的 ahead/behind 就是在比较本地分支和书签。

---

前面三篇都在同一个仓库里打转：暂存区、分支、合并。现在场景变了——同一份代码，服务器上有一份，你机器上一份，同事机器上还有一份。你敲的 `git push` 和 `git pull` 到底在和谁说话、改变了哪里的什么？这一篇把这个问题讲清楚。

## origin 只是一条记录

先破一个常见的错觉：`origin` 不是什么特殊的东西，更不是“服务器的名字”。它是你的仓库里一条普通配置，写在工作目录下 `.git/config` 的一个小节里：

```ini
[remote "origin"]
	url = https://github.com/you/project.git
	fetch = +refs/heads/*:refs/remotes/origin/*
```

`git remote -v` 能直接看到这段内容。`git clone` 做的事就是：把代码下载下来，同时自动加上这一段配置，并且给这个 remote 起个名字叫 `origin`——名字是自动取的，你可以改（`git remote rename`），也可以一个仓库配好几个 remote，起别的名字 [5]。

所以一句 `git push origin main`，前半截的 `origin` 只是让 Git 去查上面这段配置、拿到 URL 而已。名字换了，行为一样。

## 你手上并没有远程的分支，只有一份本地书签

再看上面那行 `fetch`。它的形式是 `源:目的地`：

- 冒号左边 `refs/heads/*` 指的是**远程那侧**的分支命名空间；
- 冒号右边 `refs/remotes/origin/*` 指的是**你本地**的存放位置；
- 开头的 `+` 表示即使更新不是快进也照更不误 [4]。

换句话说，每次 fetch，Git 都在做一件事：把远程 `refs/heads/` 下的每个分支，映射成你本地 `refs/remotes/origin/` 下的同名引用。

还记得上一篇的结论吗——分支说到底就是 `.git/refs/heads/` 里的一个文件，里面写着一个提交哈希。`origin/main` 用的是同一套机制，只是放在 `.git/refs/remotes/origin/` 下。它和本地分支有两点关键区别：

1. 你不直接改它，Git 只在你与远程通信时替你更新；
2. 它记的**不是“远程现在是什么”，而是“我上次和远程通信时远程是什么”**。

这就是“远程跟踪分支”（remote-tracking branch）。Pro Git 里管它叫书签，很贴切 [1]。它可能已经过期：同事十分钟前推了新提交，只要你没 fetch，你的 `origin/main` 就还指着旧位置，你也完全看不见他的提交。

用 `git branch -r` 能看到它们，典型输出是：

```text
  origin/HEAD -> origin/main
  origin/main
```

那个 `origin/HEAD` 是个符号引用，标记远程的默认分支是哪个。`origin/main` 本身是个不能在上面提交的引用，要基于它干活，用 `git switch -c 新分支 origin/main`；如果远程存在和本地分支同名的分支，`git switch 那个名字` 时 Git 也会自动帮你建好。

## fetch 只做两件事，都不碰你的代码

`git fetch origin` 的行为可以拆成两步 [2]：

1. 问远程：你每个分支现在指向哪个哈希？把本地缺的提交对象下载进来；
2. 把 `refs/remotes/origin/*` 这些书签挪到新位置。

就这些。它**不碰** `refs/heads/main`，不碰工作区文件，也不碰暂存区（索引）。所以 fetch 完之后，你打开编辑器会发现一个字节都没变——只有书签动了。你能“看见”别人的提交，是因为书签指过去了，`git log origin/main` 这条路走得通了。这正好接上前两篇的那条主线：`git log` 只显示从某个起点能到达的提交，而能不能到达，取决于有没有引用指过去。

## push 是反方向，而且有前提

`git push origin main` 展开写是 `refs/heads/main:refs/heads/main`：把我这些提交送上去，然后要求远程的 `main` 指针挪到它。这个“挪指针”必须能快进，否则会被拒绝并提示 non-fast-forward——因为硬挪会丢掉远程上别人已有的提交 [4]。

push 成功之后，你本地的 `origin/main` 通常也会跟着更新，因为远程刚接受了什么，Git 心里有数。

## pull = fetch + 第二步

这是本篇最需要分清的一处。

`git pull` 不带参数时，先执行一次 fetch（带上同样的参数，只是去掉合并相关的选项），然后**在当前分支上**决定要整合哪个远程分支——默认就是当前分支的 upstream。接着才动手 [3]。

这里的 upstream（上游分支）就是配置里的两条：

```ini
[branch "main"]
	remote = origin
	merge = refs/heads/main
```

`clone` 时自动写好，意思是“我的 main 对应 origin 的 main”。`git branch -vv` 能看到 `[origin/main]` 以及 ahead/behind 的数字 [1]。

第二步具体干什么，由选项或配置决定：`--ff-only`、`--rebase`、`--no-rebase`（也就是 merge）、`--squash` 四条路 [3]。如果走 merge，那就是上一篇讲的那整套：能快进就挪指针，不能快进就三路合并，两头改同一块就冲突、停在半路等你去 `git add` 和 `git commit`。

所以两者的区别可以一句话概括：

- **fetch 是只读的侦察**，看完你自己决定怎么办；
- **pull 是侦察加立刻动手**，而且是按默认策略直接改你的当前分支。

新手对远程的很多困惑，都来自这一条命令把两件性质完全不同的事捆在了一起。

## “pull 会自己帮我合并好”这个印象已经过时

老版本的默认行为等价于“能快进就快进，不能快进就生成合并提交”。从 Git 2.27（2020 年）起，Git 开始要求你表明态度——没配过 `pull.rebase` 或 `pull.ff` 时，它会打印一段提示：

```text
  git config pull.rebase false  # merge (the default strategy)
  git config pull.rebase true   # rebase
  git config pull.ff only       # fast-forward only
```

之后几个版本里这段提示调整过：改成只在历史真的分叉时才出现，并且从“警告”升级为直接中止：

```text
fatal: Need to specify how to reconcile divergent branches.
```

现在的官方手册把 `--ff-only` 列为“没有指定调和方式时的默认”：能快进就快进，不能快进就停下来报错 [3][6]。也就是说，默认不再替你生成合并提交了。

所谓“分叉”（diverged），就是你的 `main` 和 `origin/main` 各自都有了对方没有的提交。这时候 Git 要你明确选：

- `git merge origin/main`：生成一个合并提交，就是上一篇的 non-fast-forward 情形；
- `git rebase origin/main`：把你的本地提交搬到远程提交之后，历史变成一条直线，但本地提交会被重写成新哈希（rebase 留到以后细讲）；
- `git reset --hard origin/main`：直接丢弃本地提交，只在确定不要的时候用。

想省掉每次表态，也可以配一次 `git config --global pull.rebase false`，让它继续按 merge 走。

对刚上手的人，更稳的流程是把两步拆开：先 `git fetch`，用 `git status`、`git branch -vv` 或 `git log --oneline --graph origin/main` 看清差了多少，再自己决定 `git merge origin/main` 还是 `git rebase origin/main`。这样网络操作不会顺手改掉你的工作区。

## 三个位置之间的同步循环

说到底，远程协作里有三个各自独立的“main”：

- 你本地的分支：`.git/refs/heads/main`
- 你的书签：`.git/refs/remotes/origin/main`
- 服务器上的分支：origin 那侧的 `refs/heads/main`

```mermaid
flowchart LR
  L["本地分支 refs/heads/main"] -->|"push 上传提交"| S["服务器 refs/heads/main"]
  S -->|"fetch 下载提交并挪书签"| R["书签 refs/remotes/origin/main"]
  R -->|"merge 或 rebase 整合"| L
```

`git status` 里那句 “Your branch is ahead of 'origin/main' by 2 commits”，比的就是本地分支和书签；换成 behind，就是书签跑到前面去了。这也解释了一个常见现象：你在 fetch 之后又写了很久代码，期间别人推了新提交，这时你 push 会被拒绝——因为书签已经过期，Git 判断不了你是不是会覆盖别人的东西。办法还是先 `git fetch` 再整合。

## 一句话收束

`origin` 只是一条“名字加 URL 加一条映射规则”的配置；`origin/main` 是你本地的书签，随时可能过期，只能靠 fetch 更新；fetch 只下载和更新书签，pull 则在 fetch 之后立刻按默认策略改动你的当前分支——而这个默认策略现在要求你先表态。把 fetch 和 pull 分开看，远程同步就不再是黑箱。

## 术语表

- remote（远程配置）：`.git/config` 里的一段配置，把一个名字、一个 URL 和一条 refspec 绑在一起，`origin` 是 `git clone` 自动给的名字，没有特殊含义。
- refspec：形如 `+refs/heads/*:refs/remotes/origin/*` 的映射规则，冒号左边是远程侧的引用，右边是本地存放位置，开头的 `+` 表示允许非快进地更新。
- 远程跟踪分支：本地 `.git/refs/remotes/origin/` 下的引用，如 `origin/main`，代表“上次通信时远程的状态”，是只读书签而非真正的分支。
- upstream（上游分支）：本地分支通过 `branch.<名字>.remote` 和 `branch.<名字>.merge` 记住自己对应远程的哪条分支，不带参数的 `pull` 和 `push` 就靠它决定找谁。
- `git fetch`：只下载缺失的提交对象并更新远程跟踪分支，不动本地分支和工作区。
- `git pull`：先 fetch，再按配置在当前分支上执行 merge 或 rebase，等于把侦察和动手合成一步。
- 分叉（diverged）：本地分支和它的远程跟踪分支各自有了对方没有的提交，此时 Git 要求你明确选择 merge、rebase 还是只允许快进。

## 来源

1. [Pro Git：Remote Branches — 远程跟踪分支的定义、书签类比、跟踪分支与 git branch -vv 的 ahead/behind](https://git-scm.com/book/en/v2/Git-Branching-Remote-Branches)
2. [Git 官方文档：git-fetch — 默认 refspec、remote.<name>.fetch 的两种用法、fetch 更新远程跟踪分支](https://git-scm.com/docs/git-fetch)
3. [Git 官方文档：git-pull — pull 先 fetch 再整合、四种整合方式、--ff-only 作为未指定调和方式时的默认、upstream](https://git-scm.com/docs/git-pull)
4. [Pro Git：The Refspec — refspec 的 + 前缀、源与目的地写法，push 时非快进被拒绝](https://git-scm.com/book/en/v2/Git-Internals-The-Refspec)
5. [Pro Git：Working with Remotes — git remote、git remote show origin 与远程分支跟踪信息](https://git-scm.com/book/en/v2/Git-Basics-Working-with-Remotes)
6. [Stack Overflow：该 pull 提示自 Git 2.27 引入，以及 pull.rebase / pull.ff 三种配置各自的含义](https://stackoverflow.com/questions/62653114/how-can-i-deal-with-this-git-warning-pulling-without-specifying-how-to-reconci)

---

原文：https://pangzhengboyin.com/articles/git-remote-origin-fetch-vs-pull-9d6d92b7

> **庞征博引** · 想学的，慢慢都会
>
> 庞征博引是把想学的东西写成连载的 AI 学习工具。说出想学什么，它会先了解你的基础，再把主题写成一篇篇 5–10 分钟能读完的文章；边读边问，接下来学什么跟着你走。这篇就是这样写出来的。
>
> 开始你自己的连载 → https://pangzhengboyin.com
