# 暂存区到底是什么：从 git add 到 git commit，中间那一层为什么存在

工作区、暂存区、本地仓库三个区域各自做什么，add 和 commit 分别改变了什么

> Git 基础操作 · 暂存区 · 约 7 分钟 · 09 月 24 日

## 本篇要点

1. Git 把工作分成三个区域：工作区是你磁盘上正在编辑的文件，暂存区（技术名 index，位于 .git 目录内）记录下一次提交各文件取哪个版本，本地仓库存放已经生成的提交历史。
2. git add 把文件“此刻的内容”登记进暂存区，登记的是内容而不是文件名，所以 add 之后再修改文件，必须重新 add，否则进提交的仍是旧版本。
3. git commit 只读取暂存区，把它变成一个新的提交存进本地仓库，不读工作区，也不带走没 add 的改动，因此提交后 git status 常常还有内容。
4. git status -s 的两列分别对应暂存区相对上次提交的变化和工作区相对暂存区的变化；git diff 比较工作区与暂存区，git diff --staged 比较暂存区与上次提交。
5. 暂存区存在的必要性来自工作区的半成品性质：它让你先检查（git diff --staged）再提交，把一次连续修改拆成多个边界清楚的提交，还可以留下不打算提交的改动。
6. git commit -a 只是自动 add 已跟踪文件的改动，不含新文件；git add . 只是扩大登记范围，两者都没有改变三区域规则。
7. 暂存区存的是内容而不是文件名，这也是 Git 能在合并冲突时记录“内容尚未确定”、并让你用 add 表示冲突已解决的基础。

---

如果你已经会用 `git add`、`git commit`、`git push`，多半也遇到过两种让人发懵的情况：明明改了三处，提交完发现只有两处进了历史；或者 `add` 之后又顺手改了一行，结果进到提交里的还是改之前的版本。这些都不是 Git 出了错，而是它在按“三个区域”的规则办事。搞清这三个区域，`add` 和 `commit` 各自在做什么就都有了解释，后面分支、合并、处理冲突也全都建立在它上面。

## 你的每次操作，落在三个不同的地方

写代码的时候，你面前是磁盘上真实存在的那些文件，能打开、能运行。这就是**工作区**（working tree）。工作区不是 Git 专有的东西，编辑器和终端看到的都是它。

`git init` 或 `git clone` 之后，项目目录里会多出一个 `.git` 目录。你所有的提交历史、每个版本的文件内容都打包放在这里，这就是**本地仓库**（Git 目录）。从别人那里克隆一个项目时，被完整复制过去的就是它。

工作区和本地仓库之间还有一层：**暂存区**（staging area）。它对应 `.git` 目录里一个叫 `index` 的文件，你在文件管理器里看不到它，但它记录着一份很具体的东西——**下一次提交时，每个文件分别取哪个版本**。它的技术名字叫 index（索引），日常说法叫暂存区；两套名字并存，是因为前者来自 Git 很早期的实现，后来大家口中的“暂存”逐渐成了更通行的叫法 [1]。

![Git 中文件的状态流转：未跟踪、已修改、已暂存、未修改四个状态，以及 add、commit、编辑等操作如何在它们之间切换](https://git-scm.com/book/en/v2/images/lifecycle.png)

三个区域之间，文件的流动是这样的：

```mermaid
flowchart LR
  W["工作区：你正在编辑的文件"] -->|"git add"| S["暂存区（index）：下一次提交的内容清单"]
  S -->|"git commit"| R["本地仓库（.git）：提交历史"]
  S -->|"git restore --staged"| W
```

工作区里没 `add` 过的文件和改动，和暂存区、仓库都没有关系。Git 把“已经进入过一次提交，或者已经进过暂存区”的文件叫**已跟踪**，其余的叫**未跟踪**。这也是为什么新写的文件不会被自动纳入管理：你不明确说一声，Git 不会自己动手。

## git add：把“此刻的内容”登记进暂存区

很多人对 `add` 的理解是“把这个文件加到项目里”。更准确的说法是：`git add <file>` 把这个文件**当前这一份内容**登记到暂存区，意思是“下一次提交请包含这个版本”。对一个全新的未跟踪文件来说，第一次 `add` 让它变成已跟踪；对一个已跟踪文件来说，`add` 的作用是把它最新的改动更新进那份清单。

“登记的是内容”这一点，用一个小实验就能看清楚：

```bash
echo "hello" > readme.md
git status            # readme.md 显示为 ??（未跟踪）
git add readme.md     # 它现在进入了暂存区
echo "second line" >> readme.md
git status
```

最后一步的 `git status` 会告诉你一件初看很矛盾的事：`readme.md` 同时出现在“将要提交的更改”和“尚未暂存的更改”两个列表里。原因很简单——你 `add` 的时候文件只有一行 `hello`，暂存区登记的就是那一行；后来加的第二行只写在磁盘上，还没有登记过。这时候 `git commit`，进到历史里的是只有 `hello` 的版本，第二行会留在工作区，等着你下一次提交 [2]。

所以 `add` 记下的是你敲命令那一刻文件的样子。如果你在 `add` 之后又改了同一个文件，就必须再 `add` 一次，新改动才会覆盖暂存区里那份旧的登记。

用简短模式看状态，两列的含义会清楚得多：

```bash
$ git status -s
A   readme.md      # 左列有 A：暂存区里这是一个新文件
AM  notes.md       # A 说明它已进暂存区，M 说明工作区里又改了
 M  app.js         # 只有右列有 M：改了但没 add
```

左列说的是暂存区相对于上一次提交多了什么，右列说的是工作区相对于暂存区多了什么。一个文件左右两边都有字母，就说明它在暂存区和工作区里是两份不同的内容。

## git commit：把暂存区变成一次提交

`git commit` 做的事情只有一件：把暂存区当前记录的那份清单，写成一个新的提交存进本地仓库。它不读工作区，也不关心工作区里还剩多少没 `add` 的东西。

这解释了两件事。第一，提交之后 `git status` 往往还有内容，因为没 `add` 的改动从头到尾都待在磁盘上，和这次提交无关。第二，想确认自己到底要提交什么，别用 `git diff`，要用 `git diff --staged`：

- `git diff` 比较的是**工作区 vs 暂存区**，也就是“我改了但还没登记的”；
- `git diff --staged` 比较的是**暂存区 vs 上一次提交**，也就是“这次提交真正会带上的”。

提交前跑一次 `git diff --staged`，就是把暂存区用好最直接的方式：你还有机会在改动进入历史之前，发现里面混了不该进来的东西。反过来，如果 `add` 错了，`git restore --staged <file>` 会把该文件从暂存区撤出，工作区里的内容不会有任何变化。

## 为什么非要留这一层

既然最终都要进仓库，为什么不让 `commit` 直接对着工作区干活？因为工作区随时处在半成品状态。

一个典型场景：你在改一个 bug，顺手把日志格式也调了，还留了一行只在本地调试用的打印。这三样东西你都想留在磁盘上，但把它们混进同一个提交，历史就变浑了。有了暂存区，你可以只 `add` 与 bug 相关的那个文件，提交；再 `add` 格式调整，提交；调试打印一直不 `add`，就永远不会进历史。还能更细：只登记同一个文件里的部分改动，让不同的改动分别进入不同的提交。暂存区把“我下一步要提交什么”变成了一个具体的、可以逐条增删的集合，而不是由工作区当前的样子替你决定。

这带来两个后面会反复用到的能力：提交前能检查（`git diff --staged`），以及能把一次连续的工作拆成边界清楚的若干提交。后一件事在多人协作里格外重要——评审人看到的就是这些提交，拆得干净，别人才看得懂你每一步在做什么。

还有一层更底层的原因：暂存区里存的是**内容**，而不只是文件名。正是这个设计，让 Git 在合并两个分支、遇到内容冲突时能记下“这个文件还没定下来”，也让你在解决冲突后用 `add` 表示“这块我认可了”。这部分等讲到合并和冲突时会重新出现 [3]。

## 两个常见的误会

**`git commit -a` 并没有绕过这套规则。** 它只是替你省了一次手：提交前自动把**所有已跟踪文件**的最新改动 `add` 一遍。新写的、还没跟踪过的文件不在其中。它方便，但不等于“把工作区全部提交”。

**`git add .` 也不是“更省事的 commit”。** 它的意思是“把当前目录下该登记的内容都登记进来”，范围更大，反而更容易把不想要的东西带进清单。更稳的做法是精确 `add` 你要的那几个文件，提交前再用 `git diff --staged` 看一眼。

到这里，三区域模型就闭合了：工作区是你在改的地方，暂存区是下一次提交的草稿，本地仓库是已经落定的历史。`add` 在工作区和暂存区之间搬运，`commit` 在暂存区和本地仓库之间搬运。这条主线在接下来讲分支、合并和远程协作时会一次次重新出现。

## 术语表

- 工作区（working tree）：磁盘上真实存在、你能打开和运行的那些文件，编辑器和终端操作的就是它。
- 暂存区 / index：记录下一次提交时各文件分别取哪个版本的一份清单，实际存放在 .git 目录的 index 文件里，界面上看不到。
- 本地仓库（Git 目录）：.git 目录，存放所有提交历史与各版本的文件内容，克隆项目时被完整复制的就是它。
- 已跟踪 / 未跟踪：进入过一次提交或暂存区、Git 会持续留意的文件是已跟踪，其余是未跟踪，需要 add 才会被纳入管理。
- git diff --staged：比较暂存区与上一次提交，回答“这次提交真正会包含什么”。

## 来源

1. [Git 邮件列表：index 这个名字的由来，以及“暂存区”叫法的演变](https://public-inbox.org/git/20090127153837.GB1321@spearce.org/)
2. [Pro Git 第 2 章：Recording Changes to the Repository — 暂存、add 后再修改的行为、git status -s 与 git diff / git diff --staged](https://git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository)
3. [Linus Torvalds 关于 git index 为何是核心、以及它与合并冲突关系的说明](https://public-inbox.org/git/Pine.LNX.4.64.0602031752160.3969@g5.osdl.org/)
4. [Pro Git 第 1 章：What is Git? — 工作区、暂存区、Git 目录的定义与基本工作流](https://git-scm.com/book/en/v2/Getting-Started-What-is-Git%3F)

---

原文：https://pangzhengboyin.com/articles/git-staging-area-working-tree-commit-387503a0

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