ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

13 — 暂存区深入:你挑出来准备交的作业

13 — 暂存区深入:你挑出来准备交的作业

写在前面:这一章要解决什么

如果你已经会了git add+git commit的两步走,可能心里还有一个没消化的疙瘩:

为什么非要多一个「暂存区」?我不能直接把书桌上的东西全交吗?

这一章就是专门回答这个问题的——而且会比你想象中挖得更深。

学完后,你应该能:

  1. 用自己的话说明:暂存区不是「过渡文件夹」,而是一份「快照提案」
  2. 会用git ls-files --stage看 index 里面到底记了什么
  3. 能解释git addgit commit各自从 index 读了什么、写了什么
  4. 知道为什么「部分暂存」(git add -p)是暂存区存在的最强理由
  5. 理解.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 commitGit 照着篮子里的清单,生成 tree + commit 对象提交就是「按清单拍照」
git add a.txt(a.txt 又改了)篮子里a.txt对应的哈希更新了篮子里的映射指向的是 add 那一刻的内容

关键理解:暂存区存的是「路径 → 内容哈希」的映射表,不是文件本身的副本。真正的文件内容已经由 Git 算好哈希、存进对象库(.git/objects/)了,暂存区只是记了一条「引用」。

2.4 暂存区的正式名字:index

三个名字说的是同一个东西:

名字出现场景
暂存区(staging area)git status输出、入门教程
indexGit 源码、底层文档、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.mdconfig.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--version
git version 2.43.0

5. 跟着做:打开暂存区看看里面到底有什么

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--stage
100644 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.txtgitstatus
On branch main Changes not staged for commit: (use "git add <file>..." to update what will be committed) modified: a.txt

再看 index:

gitls-files--stage
100644 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--stage
100644 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(-)

白话翻译提交发生的事:

  1. Git 读取 index 的全部内容(所有「路径→blob」映射)
  2. 根据这份映射,生成一棵tree 对象(目录结构快照)
  3. 再把 tree 对象包进commit 对象(加上作者、时间、说明、父提交)
  4. 移动分支指针到新的 commit

commit 不看工作区,只看 index。如果你忘了add,index 里还是旧哈希,那提交的就是旧内容。

5.6 再验证:提交后 index 和最新 commit 保持一致

提交完再查看git ls-files --stage,index 没变——它和刚才提交时一模一样。这就是「干净状态」:index 指向的内容 = 最新 commit 里的内容。

5.7 删掉文件:index 也会更新

gitrmb.txtgitls-files--stage
100644 e087c4a2... 0 a.txt

git rm做了两件事:① 从工作区删文件 ② 从 index 移除记录。注意:如果你只在工作区删了文件(没用git rm),index 里b.txt的记录还在——Git 会告诉你「工作区少了东西,但 index 还记着它」。

5.8 撤销暂存:git restore --staged

printf'temp\n'>c.txtgitaddc.txtgitls-files--stage
100644 e087c4a2... 0 a.txt 100644 5e1f3b7a... 0 c.txt

c.txt是临时文件,不想提交。从 index 移除:

gitrestore--stagedc.txtgitls-files--stage
100644 e087c4a2... 0 a.txt

白话翻译:restore --stagedc.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 --stagedindex 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.txta.txt从 index 消失不再提议这个文件进下次快照

7.2 index 的三种「对照差异」命令

你想看什么命令比的是哪两棵「树」
书桌比篮子多了什么改git diff工作区 vs index
篮子比档案柜多了什么git diff --stagedindex 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.md

Git 会逐块显示改动,问你每块要不要加进 index。输入y暂存这一块,输入n跳过。选完之后,index 里只有你挑选的那些行。-p就是「一块一块看,你说加才加」。

9.3 紧急修 bug:手上工作还没做完

你正在写新功能,写到一半,助教让你先修个 bug:

  1. 先把当前改动用git stash暂存到一边
  2. 修 bug,git add+git commit
  3. 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

  1. 新建仓库,创建x.txty.txt,add 并提交。
  2. git ls-files --stage查看 index,记下两个哈希。
  3. 修改x.txt,再次git add x.txt
  4. 再用git ls-files --stage查看 index,确认只有x.txt的哈希变了。

通过标准:你能指出哪一行变了,并用白话解释为什么会变。

实验乙:index 不自动跟踪工作区

  1. 在实验甲的基础上,修改x.txt不 add
  2. 运行git ls-files --stage,看x.txt对应的哈希有没有变。
  3. 运行git status,读懂输出。
  4. 运行git diff,确认「书桌比篮子多了什么」。
  5. 运行git diff --staged,确认「篮子比档案柜没多什么」。

通过标准:你能用「add 只记录那一刻,index 不会自动跟着工作区变」解释结果。

实验丙:部分暂存

  1. 创建mixed.txt,内容为三行「原始内容」,add 并提交。
  2. 修改文件为:第一行修了错别字、第二行不变、第三行新增功能代码。
  3. git add -p mixed.txt,只挑选第一行的修改进 index,第三行不选。
  4. git diff --staged确认 index 里只有第一行的改动。
  5. git diff确认工作区还有第三行的改动。

通过标准:你能说清「同一个文件可以分两批提交」。

实验丁:从 index 撤销暂存

  1. 新建temp.txtgit add temp.txt
  2. git ls-files --stage确认 index 里有它。
  3. git restore --staged temp.txt
  4. git ls-files --stage确认 index 里没它了。
  5. 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退出不再选。新手先用yn就够了。

问 6:暂存区会不会占很多磁盘空间?

不会。index 文件通常很小(只是映射表)。真正的文件内容在.git/objects/里,而且 Git 会压缩和复用——相同内容只存一份。

问 7:git add之后又改文件,之前 add 的内容去哪了?

还在.git/objects/里(作为 blob 对象),只是 index 不再指向它了。那个旧的 blob 以后会被 Git 的垃圾回收机制清理。

问 8:.git/index文件能手动编辑吗?

不能。它是二进制格式,手动改会损坏。想修改 index 的内容,通过 Git 命令(addrmrestore --staged等)来操作。


13. 总结

13.1 一页速记

记住什么
暂存区是什么一份「快照提案」:路径 → 内容哈希的映射表
正式名字index(.git/index)、也叫 staging area、cache
git add做了什么把文件当前内容的哈希写进 index
git commit做了什么读取 index 全部内容,生成 tree + commit 对象
怎么偷看 indexgit 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 diffgit diff --staged分别比的是哪两棵树
  • 会用git restore --staged从 index 撤销暂存
  • 知道.git/index是二进制文件,不能手动编辑
  • 提交前会看git diff --staged,而不是闭眼提交
返回列表