13 — 暂存区深入:你挑出来准备交的作业
写在前面:这一章要解决什么
如果你已经会了git add+git commit的两步走,可能心里还有一个没消化的疙瘩:
为什么非要多一个「暂存区」?我不能直接把书桌上的东西全交吗?
这一章就是专门回答这个问题的——而且会比你想象中挖得更深。
学完后,你应该能:
- 用自己的话说明:暂存区不是「过渡文件夹」,而是一份「快照提案」
- 会用
git ls-files --stage看 index 里面到底记了什么 - 能解释
git add和git commit各自从 index 读了什么、写了什么 - 知道为什么「部分暂存」(
git add -p)是暂存区存在的最强理由 - 理解
.git/index是二进制文件,用文本编辑器打不开、也不该手动改
读者设定:大一同学,已经跟着第 02 章走过三区模型的「书桌/作业篮/档案柜」比喻,会基本的 add + commit,但不理解为什么要有暂存区。
1. 定位:为什么要讲暂存区深入
1.1 一句话先记住
暂存区不是「过渡用的临时文件夹」——它是你向 Git 提出的下一份快照长什么样的方案。
白话翻译:你先往篮子里挑东西,Git 就按篮子里的样子拍一张快照。篮子里有什么,下一笔提交就记什么;篮子里没有的,就算书桌上改翻天,提交也不会带上。
1.2 不理解暂存区会怎样(痛点场景表)
| 你遇到的问题 | 根因 | 后果 |
|---|---|---|
git commit后发现少了文件 | 以为 commit 会自动带上书桌所有修改 | 丢三落四,提交不完整 |
| 交了不该交的东西(密码、调试代码) | 习惯git add .全加 | 敏感信息进历史,很难彻底删 |
| 一次提交里混了修 bug 和加新功能 | 不知道可以只挑一部分暂存 | 以后回退时拆不开 |
git add之后又改了文件,提交的不是最新版 | 不理解 add 只记录那一瞬间 | 以为 Git 坏了 |
看到git diff输出为空就慌 | 不理解「没进篮子」和「已经在篮子里」的差异 | 浪费时间排查「假 bug」 |
核心痛点总结:不理解暂存区,你就无法精确控制「这一笔提交里到底记了什么」。
1.3 和你已经会的事对比
| 你已经会的(第 02 章) | 本章要往深走 |
|---|---|
| 暂存区 =「待交作业篮」 | 篮子里到底记了什么?怎么偷看? |
git add= 往篮子里放 | add 的瞬间,Git 在篮子里写了什么? |
git commit= 从篮子往档案柜记 | commit 是怎么读篮子的? |
提交前先git status | 除了 status,还有更硬核的「X 光」 |
git add .全加 | 部分暂存:一个文件只挑几行加进去 |
认知锚点:第 02 章让你「知道有篮子」;本章让你「打开篮子看看里面到底长什么样」。
2. 本质:暂存区到底是什么(白话 + 比喻 + 图解)
2.1 先看总图
图:三区模型详图。暂存区(index)在工作区和仓库之间,是「当前提议的快照」。
2.2 比喻升级:从「篮子」到「快照提案」
旧比喻:篮子 = 你把作业放进去,等一会儿一块交。
新比喻:篮子 = 你在填一张「快照申请单」。单子上列着:下次拍照时,每个文件该拍哪个版本。Git 按这张单子拍出来的照片,就是下一笔提交。
为什么要升级?因为「篮子」容易让人以为是「临时堆东西的地方」,实际上暂存区更精确:
- 它不是「堆东西」——它记的是每个路径对应哪个文件内容(blob 对象的哈希)
- 它不是「临时的」——提交完它不会自动清空,而是变成和最新提交一致的状态
- 它不是「只能全放或全不放」——你可以只放一个文件的某几行(部分暂存)
2.3 用书桌比喻拆开暂存区的每一层
| 你的动作 | 篮子里发生了什么 | 白话解释 |
|---|---|---|
git add a.txt | 篮子里记录:路径a.txt→ 当前内容算出的哈希 | 篮子不是「存了一份副本」,而是记了一条映射 |
再git add b.txt | 篮子里多了:路径b.txt→ 对应哈希 | 每加一个文件,篮子里的清单就多一行 |
git commit | Git 照着篮子里的清单,生成 tree + commit 对象 | 提交就是「按清单拍照」 |
git add a.txt(a.txt 又改了) | 篮子里a.txt对应的哈希更新了 | 篮子里的映射指向的是 add 那一刻的内容 |
关键理解:暂存区存的是「路径 → 内容哈希」的映射表,不是文件本身的副本。真正的文件内容已经由 Git 算好哈希、存进对象库(.git/objects/)了,暂存区只是记了一条「引用」。
2.4 暂存区的正式名字:index
三个名字说的是同一个东西:
| 名字 | 出现场景 |
|---|---|
| 暂存区(staging area) | git status输出、入门教程 |
| index | Git 源码、底层文档、git ls-files的参数 |
| 缓存(cache) | git diff --cached里的 cached |
底层文件叫.git/index,所以「index」这个名字最「原汁原味」。
2.5 为什么要有暂存区?——一个场景说服你
你正在写课程设计,书桌上同时开了三个文件:
report.md:写了一半新章节bugfix.py:刚修好一个运行错误config.json:临时加了调试用的密码(不想交)现在助教让你「先交修 bug 的部分,报告晚点再交」。
如果没有暂存区,你要么全部一起提交,要么手动把另外两个文件先复制出去、删掉、再提交——太痛苦了。
有了暂存区,你只需要:
gitaddbugfix.pygitcommit-m"fix: 修复运行错误"report.md和config.json都还在书桌上,但没进篮子,所以不会被这次提交带走。
这就是暂存区存在的核心理由:让你精确挑选「这一笔提交要记录什么」。
3. 建议学习顺序
先理解「暂存区 = 快照提案」 → 用 ls-files --stage 偷看 index → 动手:add 前后对比 index 变化 → 学部分暂存(add -p) → 理解 commit 怎么读 index → 常见踩坑场景 → 完成章末小实验不要一上来就背ls-files的各种参数。先搞懂「index 里记了什么」,再看命令只是换个方式查看而已。
4. 动手准备(建可丢弃目录)
请找一个可以随便删的练习目录。
mkdirlab-index-deepcdlab-index-deepgitinit-bmaingitconfig user.name"Ada Example"gitconfig user.email"ada@example.com"说明:
git init -b main:在本目录创建 Git 仓库,初始分支名叫main- 名字和邮箱只是写在提交记录上的「作者信息」(演示用虚构身份即可)
- 本系列命令样例在 Linux 上用Git 2.43.0验证过;你电脑版本接近即可
git--versiongit version 2.43.05. 跟着做:打开暂存区看看里面到底有什么
5.1 先做一次完整提交(制造一个「干净」状态)
printf'hello\n'>a.txtprintf'world\n'>b.txtgitadda.txt b.txtgitcommit-m"docs: 添加两个示例文件"输出类似:
[main (root-commit) a1b2c3d] docs: 添加两个示例文件 2 files changed, 2 insertions(+) create mode 100644 a.txt create mode 100644 b.txt白话翻译:在分支main上创建了第一笔提交(根提交),两个文件各插入一行。100644是文件模式,暂时不用深究。
5.2 用git ls-files --stage偷看 index
gitls-files--stage100644 fbbee8a7... 0 a.txt 100644 af17f6c1... 0 b.txt白话翻译:这是暂存区(index)的完整内容清单。每一行分四列:
| 列 | 内容 | 白话解释 |
|---|---|---|
| 第 1 列 | 100644 | 文件模式:普通文件、非可执行 |
| 第 2 列 | fbbee8a7... | 这个文件内容的 blob 对象哈希(前几位) |
| 第 3 列 | 0 | 暂存阶段编号:0 = 正常,1/2/3 = 合并冲突时用 |
| 第 4 列 | a.txt | 文件路径 |
你现在可以把 index 理解成一张表:
+----------+----------------+-----+--------+ | 模式 | blob 哈希 | 阶段 | 路径 | +----------+----------------+-----+--------+ | 100644 | fbbee8a7... | 0 | a.txt | | 100644 | af17f6c1... | 0 | b.txt | +----------+----------------+-----+--------+这就是「快照提案」的完整面貌:每条记录说的是「这个路径,用这个版本的文件内容」。
5.3 修改一个文件后,index 和工作区分道扬镳
printf'hello v2\n'>a.txtgitstatusOn branch main Changes not staged for commit: (use "git add <file>..." to update what will be committed) modified: a.txt再看 index:
gitls-files--stage100644 fbbee8a7... 0 a.txt 100644 af17f6c1... 0 b.txt白话翻译:工作区的a.txt已经变成hello v2,但 index 里a.txt对应的哈希还是fbbee8a7...(旧版)。这说明index 没有跟着工作区自动变——它只在git add时才更新。
5.4 用git add更新 index
gitadda.txtgitls-files--stage100644 e087c4a2... 0 a.txt 100644 af17f6c1... 0 b.txt白话翻译:a.txt的哈希从fbbee8a7...变成了e087c4a2...。index 更新了:现在指向a.txt的新版内容。这就是git add做的事:更新 index 里对应路径的哈希,指到当前工作区版本。
5.5git commit是怎么读 index 的
gitcommit-m"docs: 更新 a.txt 为 v2"[main b2c3d4e] docs: 更新 a.txt 为 v2 1 file changed, 1 insertion(+), 1 deletion(-)白话翻译提交发生的事:
- Git 读取 index 的全部内容(所有「路径→blob」映射)
- 根据这份映射,生成一棵tree 对象(目录结构快照)
- 再把 tree 对象包进commit 对象(加上作者、时间、说明、父提交)
- 移动分支指针到新的 commit
commit 不看工作区,只看 index。如果你忘了add,index 里还是旧哈希,那提交的就是旧内容。
5.6 再验证:提交后 index 和最新 commit 保持一致
提交完再查看git ls-files --stage,index 没变——它和刚才提交时一模一样。这就是「干净状态」:index 指向的内容 = 最新 commit 里的内容。
5.7 删掉文件:index 也会更新
gitrmb.txtgitls-files--stage100644 e087c4a2... 0 a.txtgit rm做了两件事:① 从工作区删文件 ② 从 index 移除记录。注意:如果你只在工作区删了文件(没用git rm),index 里b.txt的记录还在——Git 会告诉你「工作区少了东西,但 index 还记着它」。
5.8 撤销暂存:git restore --staged
printf'temp\n'>c.txtgitaddc.txtgitls-files--stage100644 e087c4a2... 0 a.txt 100644 5e1f3b7a... 0 c.txtc.txt是临时文件,不想提交。从 index 移除:
gitrestore--stagedc.txtgitls-files--stage100644 e087c4a2... 0 a.txt白话翻译:restore --staged把c.txt从 index 里移除了(取消暂存),但c.txt文件本身还在工作区——只是不再被「提议进下一次快照」了。这就是「从篮子里拿回来」,不是「从书桌上删掉」。
6. 命令分组(按场景分组,不按字母排序)
6.1 偷看 index(只读,放心用)
| 命令 | 干什么 | 白话 |
|---|---|---|
git ls-files --stage | 列出 index 里所有条目的完整信息 | 给篮子拍 X 光 |
git ls-files | 只列文件名 | 看篮子里有哪些文件 |
git status | 用人话告诉你哪块脏了 | 仪表盘 |
git diff | 工作区 vs index 的差异 | 书桌比篮子多了什么 |
git diff --staged | index vs 最新 commit 的差异 | 篮子比档案柜多了什么 |
6.2 往 index 里写(改变篮子内容)
| 命令 | 干什么 | 白话 |
|---|---|---|
git add 文件 | 把文件当前内容记录进 index | 往篮子里放(整份文件) |
git add -p 文件 | 交互式挑选文件的某些块放进 index | 往篮子里放(只挑几行) |
git add . | 当前目录下所有改动都加进 index | 全部扫进篮子(慎用) |
git rm 文件 | 删文件 + 从 index 移除 | 从书桌删 + 从篮子移除 |
git mv 旧 新 | 改名 + 更新 index | 书桌改名 + 篮子跟着改 |
6.3 从 index 里撤(取消暂存)
| 命令 | 干什么 | 白话 |
|---|---|---|
git restore --staged 文件 | 从 index 移除,保留工作区文件 | 从篮子里拿回来,书桌上还在 |
git rm --cached 文件 | 从 index 移除,保留工作区文件 | 同上(历史原因,两种写法) |
6.4 特殊场景
| 命令 | 干什么 | 白话 |
|---|---|---|
git commit -a -m "说明" | 对已跟踪文件自动 add + commit | 跳过手动 add(新文件不会自动加) |
git read-tree HEAD | 把 HEAD 对应的 tree 写进 index(底层命令) | 用档案柜里的快照重置篮子 |
7. 对照表(前后对比 / 选项对比)
7.1 index 在不同时刻的内容对照
| 时刻 | index 里a.txt指向 | 说明 |
|---|---|---|
| 刚 commit 完 | 和 HEAD 一致的哈希 | 干净状态 |
改了a.txt但没 add | 还是旧的哈希 | 工作区已变,index 没跟上 |
git add a.txt之后 | 新的哈希 | index 更新了 |
git restore --staged a.txt | 回到和 HEAD 一致 | 从篮子里撤回来了 |
git rm a.txt | a.txt从 index 消失 | 不再提议这个文件进下次快照 |
7.2 index 的三种「对照差异」命令
| 你想看什么 | 命令 | 比的是哪两棵「树」 |
|---|---|---|
| 书桌比篮子多了什么改 | git diff | 工作区 vs index |
| 篮子比档案柜多了什么 | git diff --staged | index vs HEAD |
| 书桌比档案柜多了什么 | git diff HEAD | 工作区 vs HEAD |
记忆口诀:diff看书桌、--staged看篮子、HEAD看整体。
7.3--cachedvs--staged对照
--staged和--cached完全一样——--staged是 Git 后来加的更直观的名字。新手建议用--staged,见名知义。
7.4 部分暂存 vs 整文件暂存对照
| 做法 | 什么时候用 | 命令 |
|---|---|---|
| 整文件暂存 | 改动只和一件事有关 | git add 文件 |
| 部分暂存 | 同一文件里混了多种改动 | git add -p 文件 |
| 全部暂存 | 想快速提交、确认没有多余文件 | git add .(先 status 确认!) |
8. 安全习惯(硬规矩 + 踩坑提醒)
8.1 硬规矩
| 规矩 | 原因 |
|---|---|
提交前先git status+git diff --staged | 确认篮子里是你想要的东西 |
不要无脑git add . | 容易把临时文件、密码文件加进去 |
理解add之后再改文件,提交的不是最新版 | index 记的是 add 那一刻的哈希 |
git restore --staged只撤销暂存,不删文件 | 从篮子里拿回来,书桌上还在 |
| 敏感文件永远不要 add 进 index | 进了历史极难彻底清除 |
8.2 踩坑提醒
| 坑 | 现象 | 怎么避 |
|---|---|---|
| 以为 commit 会自动 add | 提交后发现少文件 | 新文件必须手动 add |
| add 之后改文件不重新 add | 提交的是旧版 | commit 前再看一眼git diff |
git add .加了不该加的 | 临时文件进历史 | 先git status确认,再用.gitignore |
把git rm当成「只删 index」 | 工作区文件也没了 | 只删 index 用git rm --cached |
git add -p选错了块 | 暂存了不该暂存的行 | 可以git restore --staged撤销重来 |
8.3 推荐的暂存前检查流程
# 1. 看总览gitstatus# 2. 看书桌上的改动细节gitdiff# 3. 挑选要暂存的文件gitadd某些文件# 或 git add -p 做部分暂存# 4. 确认篮子内容gitdiff--staged# 5. 确认没多余的东西gitstatus# 6. 提交gitcommit-m"说明"9. 真实场景(作业提交、换电脑、紧急修 bug 等)
9.1 课程作业:只想交修 bug 的部分
你同时改了三个文件,但只有一个和 bug 修复相关:
gitaddbugfix.pygitdiff--stagedgitcommit-m"fix: 修复计算溢出错误"另外两个文件还在书桌上,不受影响。这就是暂存区帮你「挑作业」的能力。
9.2 同一文件里混了多种改动
你在report.md里既修了错别字,又写了半段新内容。现在只想先提交修错别字的:
gitadd-preport.mdGit 会逐块显示改动,问你每块要不要加进 index。输入y暂存这一块,输入n跳过。选完之后,index 里只有你挑选的那些行。-p就是「一块一块看,你说加才加」。
9.3 紧急修 bug:手上工作还没做完
你正在写新功能,写到一半,助教让你先修个 bug:
- 先把当前改动用
git stash暂存到一边 - 修 bug,
git add+git commit - 再
git stash pop把之前的改动恢复回来
这里暂存区的角色没变——你始终是通过add挑选「这次提交要记什么」。stash只是帮你临时腾出手。
9.4 多人协作前的准备
和同学合作前,每次提交前用git status+git diff --staged确认 index 里没有杂七杂八的东西。提交前审查diff --staged是协作的基本礼貌。
9.5 误加了敏感文件
gitrestore--stagedconfig.jsongitstatus如果还没 commit,restore --staged就够了。如果已经 commit 了,那就要用更重的方法(后面章节讲)。
10. 进阶补充(选读)
10.1.git/index是什么文件
暂存区在磁盘上对应.git/index,这是一个二进制文件。用文本编辑器打开会看到乱码。想查看就用git ls-files --stage。不要手动删或改.git/index——删了等于暂存区信息全丢。
10.2 index 的内部结构(了解即可)
index 文件大致包含:文件头(版本号、条目数)、条目列表(模式 + 哈希 + 阶段 + 路径)、扩展区(可选额外数据)、校验和(SHA-1)。更详细的格式说明见 index-format 文档。
10.3 index 和 tree 对象的关系
- index是「提案」:还没变成正式快照
- tree 对象是「已拍好的快照」:commit 时根据 index 生成
提交时,Git 把 index 里的所有条目组织成一棵 tree,存进.git/objects/。之后 index 的内容和这棵 tree 一致——但 index 本身不是 tree 对象,它是一个独立的数据结构。
10.4 合并冲突时 index 的特殊用法
正常情况下 index 里每个文件只有一个条目(阶段 = 0)。合并冲突时,同一文件会出现多个条目:
| 阶段 | 含义 |
|---|---|
| 1 | 共同祖先的版本 |
| 2 | 你这边(ours)的版本 |
| 3 | 对方(theirs)的版本 |
gitls-files--stage可能看到:
100644 abc1234 1 conflict.txt 100644 def5678 2 conflict.txt 100644 ghi9012 3 conflict.txt解决冲突后,index 会回到阶段 0 的正常状态。这个在合并章节会详细讲,这里先有个印象就行。
11. 小实验(动手练习 + 通过标准)
实验甲:用 X 光看 index
- 新建仓库,创建
x.txt和y.txt,add 并提交。 - 用
git ls-files --stage查看 index,记下两个哈希。 - 修改
x.txt,再次git add x.txt。 - 再用
git ls-files --stage查看 index,确认只有x.txt的哈希变了。
通过标准:你能指出哪一行变了,并用白话解释为什么会变。
实验乙:index 不自动跟踪工作区
- 在实验甲的基础上,修改
x.txt但不 add。 - 运行
git ls-files --stage,看x.txt对应的哈希有没有变。 - 运行
git status,读懂输出。 - 运行
git diff,确认「书桌比篮子多了什么」。 - 运行
git diff --staged,确认「篮子比档案柜没多什么」。
通过标准:你能用「add 只记录那一刻,index 不会自动跟着工作区变」解释结果。
实验丙:部分暂存
- 创建
mixed.txt,内容为三行「原始内容」,add 并提交。 - 修改文件为:第一行修了错别字、第二行不变、第三行新增功能代码。
- 用
git add -p mixed.txt,只挑选第一行的修改进 index,第三行不选。 - 用
git diff --staged确认 index 里只有第一行的改动。 - 用
git diff确认工作区还有第三行的改动。
通过标准:你能说清「同一个文件可以分两批提交」。
实验丁:从 index 撤销暂存
- 新建
temp.txt,git add temp.txt。 - 用
git ls-files --stage确认 index 里有它。 git restore --staged temp.txt。- 用
git ls-files --stage确认 index 里没它了。 - 用
ls确认文件本身还在工作区。
通过标准:你能区分「从 index 移除」和「从工作区删除」。
12. 常见问题 FAQ
问 1:暂存区到底有什么用?我不能直接 commit 所有改动吗?
可以,但你会失去精确控制力。暂存区的核心价值是让你挑选下一次提交要包含什么。当你一次改了多个文件、或者同一文件里混了不同类型的改动时,暂存区让你拆开提交,保持历史清晰。
问 2:git commit -a是不是绕过了暂存区?
不算绕过。-a只是对已跟踪文件自动执行了一步add,新文件仍然不会自动进提交。新手不推荐当默认用法。
问 3:index 里的哈希和工作区文件内容有什么关系?
Git 通过 SHA-1 算法把文件内容算出一个哈希值,然后用这个哈希当「钥匙」把内容存进.git/objects/(叫 blob 对象)。index 里记的正是这个哈希——通过它找到对应的文件内容。
问 4:为什么git diff有时输出是空的?
因为你可能已经把改动 add 进 index 了。git diff(无参数)比的是「工作区 vs index」,如果你刚 add 完、工作区没再改,那两者一样,diff 自然为空。想看「篮子比档案柜多了什么」,用git diff --staged。
问 5:git add -p的选项都是什么意思?
常用选项:y暂存这一块、n跳过、s拆成更小的块、q退出不再选。新手先用y和n就够了。
问 6:暂存区会不会占很多磁盘空间?
不会。index 文件通常很小(只是映射表)。真正的文件内容在.git/objects/里,而且 Git 会压缩和复用——相同内容只存一份。
问 7:git add之后又改文件,之前 add 的内容去哪了?
还在.git/objects/里(作为 blob 对象),只是 index 不再指向它了。那个旧的 blob 以后会被 Git 的垃圾回收机制清理。
问 8:.git/index文件能手动编辑吗?
不能。它是二进制格式,手动改会损坏。想修改 index 的内容,通过 Git 命令(add、rm、restore --staged等)来操作。
13. 总结
13.1 一页速记
| 点 | 记住什么 |
|---|---|
| 暂存区是什么 | 一份「快照提案」:路径 → 内容哈希的映射表 |
| 正式名字 | index(.git/index)、也叫 staging area、cache |
git add做了什么 | 把文件当前内容的哈希写进 index |
git commit做了什么 | 读取 index 全部内容,生成 tree + commit 对象 |
| 怎么偷看 index | git ls-files --stage |
| 怎么只加部分改动 | git add -p 文件 |
| 怎么从 index 撤销 | git restore --staged 文件 |
| 为什么要有暂存区 | 让你精确挑选「这次提交记什么」 |
.git/index是二进制 | 别手动改,用 Git 命令操作 |
13.2 本系列中的位置
01 认识 Git、装好工具 02 三区模型(书桌/篮子/档案柜) 03 日常:提交与查看历史 04 安全地撤销 05 分支与合并 06 远程协作 …… 12 提交历史深入 13 暂存区深入 ← 当前(打开篮子看内部) 14 分支深入 ……本章建在第 02 章的「三区模型」之上,但更关注 index 的内部机制,而不是停留在「篮子」的比喻层面。
13.3 思维升华
暂存区的本质不是「临时存放」,而是「精确挑选」。
没有 index,你只能在「全交」和「不交」之间二选一;
有了 index,你可以从一堆改动里挑出任意子集,组合成一笔逻辑清晰的提交。
学会部分暂存,你就从「Git 用户」迈向了「Git 掌控者」。
13.4 延伸阅读
- Pro Git 中文版 – 记录每次更新到仓库
- git ls-files 文档
- git add 文档
- index-format 文档
- gitglossary
命令输出样例验证环境:Git 2.43.0;演示作者信息为虚构:Ada Example <ada@example.com>。
13.5 检查清单
- 能用自己的话解释「暂存区 = 快照提案」,而不只是「临时中转站」
- 会用
git ls-files --stage查看 index 内容,读懂四列含义 - 能解释
git add更新了 index 的什么、git commit读了 index 的什么 - 知道
add之后改文件要重新add,能解释为什么 - 会用
git add -p进行部分暂存 - 知道
git diff和git diff --staged分别比的是哪两棵树 - 会用
git restore --staged从 index 撤销暂存 - 知道
.git/index是二进制文件,不能手动编辑 - 提交前会看
git diff --staged,而不是闭眼提交
