本节摘要:Git 本地操作的全部秘密藏在一个三区模型里:工作区是你真正编辑文件的地方,暂存区(又称索引)是"待提交清单",本地仓库是保存全部历史的档案库。本节逐个拆解三区各自存什么、文件状态如何流转,并解释"为什么 Git 非要插一个暂存区在中间"这个新手最困惑的问题。
阅读完本节,你应当能够:
一个新手用 Git 时最常有的困惑:我都 git add 了,也 git commit 了,为什么有时还要再 add 一次?我明明"保存"了,怎么状态还不是干净的?
这些困惑的根源,是大家默认 Git 只有两个区域——"我看到的文件"和"Git 存的历史"。实际上 Git 中间还隔了一层"暂存区"。理解了第三层,前面所有困惑自动消失。
先给一个直觉类比:把仓库想成一个餐厅后厨。工作区是案板——你在这切菜、备料,随时改;暂存区是点菜单——你把哪几道菜要下锅登记上去,还没做;本地仓库是已上桌的成品——固定下来了,客人(历史)随时能查。你可以在案板上切十种菜,但只把其中三种写到点菜单上;后厨(commit)只按点菜单做菜,案板上没登记的料,永远不会被做成菜。
这个类比虽然朴素,但抓住了三区的核心分工:工作区管"你手上有什么",暂存区管"你决定提交什么",仓库管"历史留下了什么"。三者可以不一致,正是 Git 灵活性的来源。
顺带说明:这个类比里的"后厨(commit)"只在你有意喊单时才动——Git 不会自动提交,一切都等你手动触发。这也是 Git 与一些"自动保存版本"类工具的区别:它把"何时记录"的决定权完全交给你,代价是你得记得主动记录,收益是历史完全由你的意图塑造,没有系统自作主张。
工作区就是你在文件管理器里看到的项目目录——包含 .git 目录的那一层。你在这里新建文件、修改文件、删除文件。工作区里发生的事情,Git 都看在眼里,但在你明确告诉它之前,它不会记录任何东西。
工作区的文件分两类:未跟踪(Untracked,Git 从没见过)和已跟踪(Tracked,已被纳入版本控制)。已跟踪文件又分三种状态:
| 状态 | 含义 | 例 |
|---|---|---|
| Unmodified | 内容与最近一次提交一致 | 什么都没动 |
| Modified | 工作区改了,还没 add | 改了一半 |
| Staged | 已 add,等 commit | 登记到点菜单了 |
注意一个细节:Staged 的文件在工作区可能同时是 Modified——你先 add 了它,后来又改了它。此时暂存区存的是"add 那一刻的版本",工作区是"最新版本",两者不同,status 会同时显示"已暂存修改"和"未暂存修改"两行。这恰恰是暂存区存在意义的活例子。
暂存区是一个中间区域,物理上是一个索引文件(位于 .git/index),记录了"下一次提交将包含哪些文件、这些文件当时的快照"。你可以把它理解成一份待提交清单。
它带来了三样关键能力:
正是这三样能力,让"提交"从"无脑保存"升级为"有意图地记录变化"。这也是为什么 Git 不愿意把 add 和 commit 合并成一个命令——合并了,选择性就没了。
本地仓库位于项目根目录的 .git 隐藏目录里,保存着全部历史。它的内部结构在 2.3 节初始化时展开,这里先记住它的组成:
本地仓库回答"这个项目从出生到现在每一步长什么样"。它才是 Git 真正存储历史的地方,工作区会变、暂存区会清空,只有仓库里的历史是永久的(除非你主动改写,那是第 5 章的事)。

三个区之间的搬运由几个命令完成:
正向两条:add 把工作区改动搬进暂存区,commit 把暂存区快照固化进仓库;反向三条:checkout/restore 从仓库或暂存区把内容搬回工作区。这就是三区之间全部的通路。
文件的状态流转可以画成一张状态机:
三区模型在实践中有几个直接推论,值得记牢:
推论一:commit 只认暂存区。 你在工作区改得再欢,不 add 就 commit,Git 会告诉你"没有要提交的改动"。这是新手第一周最常见的困惑,原因就是三区没建立。
推论二:add 是快照不是移动。 git add 不把文件"搬"进暂存区,而是记录文件当前内容的快照,并更新索引里的引用。所以 add 之后你再改文件,暂存区里仍是 add 那一刻的版本——这是 Git 的特色,也解释了"为什么 add 完还要 add"。
推论三:commit 会清空"暂存区与仓库的差异",但不会动工作区。 提交完成后,暂存区与仓库内容一致,工作区的改动(如果有)继续保留,需要再 add 才能纳入下次提交。
把抽象落到具体命令上。假设你刚初始化了一个仓库,开始写一个项目:
git init # 建立仓库,三区齐备,全是空的 echo v1 > app.py # 在工作区创建文件 git status # 看到 app.py 是 Untracked git add app.py # 文件进入暂存区,状态变 Staged git commit -m "添加应用入口" # 快照固化进仓库,状态变 Unmodified echo v2 >> app.py # 工作区又改了 git status # app.py 变 Modified(未暂存) git add app.py # 再次 add,暂存区更新为新版本 git commit -m "增加功能" # 又一次固化
跑完这一串,你会亲眼看到文件状态在四种状态之间流转。特别留意第二次修改时 git status 的输出——它同时告诉你"文件在暂存区有版本、在工作区又有新改动",这正是三区不一致的标准画面。多跑几遍直到你能预判每一步 status 的输出,三区模型就算真吃透了。
有个值得说明的例外:如果你的工作习惯是"一次只做一件事、改完立刻提交",那么暂存区的选择性你用不上,会嫌 add 多此一举。这是正常的——很多人的 Git 用得"很糙"也很健康。但只要你开始遇到"这次改动里混着两件不相干的事"或"改到一半老板让你先提交一部分",暂存区就是唯一解药。所以建议:平时可以糙,但要练会"分步提交"这个手艺,关键时刻能救命。
| 常见动作 | 影响哪个区 | 对历史的影响 |
|---|---|---|
| 修改文件 | 工作区 | 无 |
| git add | 暂存区 | 无 |
| git commit | 仓库 | 新增一个提交 |
| git restore | 工作区 | 无 |
| git restore --staged | 暂存区 | 无 |
⚠️ 常见坑:改完文件直接 commit,漏了 add,导致提交里没包含最新改动。提交前养成先
git status看一眼的习惯,能救你无数次。
💡 关键直觉:把暂存区当成"给提交画押的清单"。画押之前,你随时可以改主意——撤下某个文件、换掉某个版本。一旦画押(commit),这单就永久进档案了。
"Git 为什么要多此一举设个暂存区?" 因为真实工作天然是"杂乱"的——一个下午你可能同时改了 bug、加了注释、调整了格式,这些不该混在一个提交里。暂存区让你先把它们拆开、归类,再分别提交。没有暂存区的系统(如 SVN),提交就是"全量打包",历史里全是"一次改了一堆东西"的糊涂账。
"暂存区会占很多空间吗?" 不会。暂存区只是一个索引文件,记录的是"哪些文件的哪个快照要进下次提交"的引用关系,不是文件副本本身。真正的文件内容在对象数据库里按需存储,未变文件复用,几乎不占额外空间。
"git checkout 和 git restore 都能把仓库内容搬回工作区,有什么区别?" checkout 是老命令,功能杂(切分支、恢复文件都管);restore 是 Git 2.23 起为"恢复文件"单独设计的新命令,意图更明确。现代实践推荐:切分支用 switch,恢复文件用 restore,后面章节会展开。
"三区模型只适用于本地吗?" 三区是本地模型,但远程协作也有对应物——远程仓库相当于"别人家的仓库",push/pull 是本地与远程之间的搬运,第 4 章细讲。先把三区吃透,远程只是多了一个"外部仓库"角色。
三区模型不是孤岛,它几乎是整本教程所有概念的接口,提前把接口认出来,后面会顺畅得多:
这些接口现在不必深究,但请记住一句话:凡是 Git 的高级操作让你晕,就回到三区模型问一句"它动的是哪个区"——九成的困惑都能当场解开。比如"为什么切分支时我改的文件跟着消失又出现?"答案是:切换分支 = 把工作区更新成目标分支的内容,你未提交的改动当然会被"覆盖"(Git 会阻止危险覆盖,但状态本身确实变了)。再比如"为什么 amend 之后哈希变了?"——因为 amend 动了仓库区的提交对象,对象一变,身份证号(哈希)自然就变。三区模型就是这张全局地图的坐标原点。
下一节动手初始化仓库——
git init把普通目录变成 Git 仓库,三区中的"本地仓库"正式诞生。