作者:全栈陈序员 系列:《Git版本控制从入门到精通》 适用人群:零基础到入门进阶 阅读时间:约 18 分钟
写在前面
上一篇我们跑通了"创建仓库 → add → commit → log"的闭环。但很多新手会有这样的困惑:
- 为什么
git add之后文件还在,还要再git commit?为什么不一步到位? git status里那些Changes to be committed、Changes not staged for commit、Untracked files到底在说什么?- 为什么有时候改了文件,
git commit却说"nothing to commit"?
这些困惑的根源,都指向同一个核心概念------Git 的三大区域。
理解三大区域,是理解 Git 的分水岭。 理解了它,Git 的所有命令在你眼里都会变得自然;不理解,Git 命令对你就是一堆要背的黑魔法。
本篇会把这个概念讲透,并用完整的终端演示让你亲眼看到文件在三个区域间的流转。
一、Git的三大区域概念
Git 管理文件时,文件会处于三个不同的"区域":
1.1 工作区(Working Directory)
就是你用 ls 能看到的、你直接编辑的那些文件。
$ ls
README.md src/ config.js
这些文件就在你的硬盘上,你用编辑器打开、修改、保存,操作的都是工作区。工作区是最直观的区域------你看到的代码就是它。
特点:
- 包含仓库里所有被追踪和未追踪的文件;
- 你日常编辑直接作用于此;
- 它的内容不一定和仓库已提交的版本一致------你改了没提交,工作区就是新的,仓库里还是旧的。
1.2 暂存区(Staging Area / Index)
git add 之后、git commit 之前,文件改动暂存的地方。
暂存区是 Git 独有的设计(SVN 没有这个概念)。它像一个**"购物车"**:你在工作区挑挑拣拣(修改各种文件),把想买的放进购物车(git add),最后一起结账(git commit)。
特点:
- 是一个二进制文件 ,位于
.git/index; - 记录的是"下一次 commit 要包含哪些文件的哪些版本";
- 可以部分暂存 :同一个文件改了10处,你可以只暂存其中3处(
git add -p,下篇讲); - 提交后暂存区清空(变成和最新提交一致的状态)。
1.3 版本库(Repository)
git commit 之后,改动永久写入的地方。
版本库就是 .git 目录里存储的所有提交历史。每一次 commit 都会在版本库里创建一个不可变的提交对象,永久记录"这一刻整个项目的快照"。
特点:
- 存储在
.git/objects里; - 每个提交有唯一的 SHA-1 哈希;
- 不可变(提交后无法修改内容,只能新建提交来覆盖或撤销);
- 是"历史"的最终归宿。
1.4 三者关系一图流
┌─────────────┐ git add ┌─────────────┐ git commit ┌─────────────┐
│ 工作区 │ ─────────→ │ 暂存区 │ ──────────→ │ 版本库 │
│ (Working │ │ (Staging │ │ (Repository) │
│ Directory) │ │ Area) │ │ .git/ │
└─────────────┘ └─────────────┘ └─────────────┘
你编辑的文件 下次要提交的 永久历史记录
反向也有命令:
git checkout/git restore:把版本库或暂存区的内容拉回工作区;git reset:把暂存区的内容退回工作区(取消暂存)。
二、文件的三种状态
对应三大区域,文件有三种状态:
| 状态 | 含义 | 所在区域 |
|---|---|---|
| Committed(已提交) | 文件已安全保存在版本库 | 版本库 |
| Staged(已暂存) | 文件修改已加入暂存区,下次将提交 | 暂存区 |
| Modified(已修改) | 文件被改了,但还没暂存 | 工作区 |
还有一种特殊状态:
| 状态 | 含义 |
|---|---|
| Untracked(未追踪) | Git 完全不知道这个文件(新建且没 add 过) |
2.1 状态转换图
git add
Untracked ──────────────────→ Staged
│ │
│ git add │ git commit
▼ ▼
Staged ─────────────────────→ Committed
│
│ 编辑文件
▼
Modified
│
│ git add
▼
Staged ──→ Committed ...
一个文件会在这些状态间反复横跳:修改 → 暂存 → 提交 → 再修改 → 再暂存 → 再提交......
三、完整生命周期演示
光讲概念太抽象,我们用一个完整的终端会话,一步步观察文件在三个区域间的流转。建议你打开终端跟着做。
3.1 准备仓库
$ mkdir regions-demo && cd regions-demo
$ git init -b main
Initialized empty Git repository in /Users/chensen/regions-demo/.git/
3.2 第一步:创建文件(进入工作区)
$ echo "hello" > a.txt
$ git status
On branch main
No commits yet
Untracked files:
(use "git add <file>..." to include in what will be committed)
a.txt
nothing added to commit but untracked files present (use "git add" to track)
状态 :a.txt 是 Untracked,在工作区,暂存区和版本库都没有。
3.3 第二步:git add(进入暂存区)
$ git add a.txt
$ git status
On branch main
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: a.txt
状态 :a.txt 从 Untracked 变成 Staged (Changes to be committed)。工作区有文件,暂存区记录了"要提交 a.txt",版本库还没有。
3.4 第三步:git commit(进入版本库)
$ git commit -m "新增a.txt"
[main (root-commit) 1111111] 新增a.txt
1 file changed, 1 insertion(+)
create mode 100644 a.txt
$ git status
On branch main
nothing to commit, working tree clean
状态 :a.txt 进入版本库,成为 Committed 。三个区域现在一致,working tree clean 表示工作区干净(没有未提交的改动)。
3.5 第四步:修改文件(回到工作区 Modified)
$ echo "world" >> a.txt
$ cat a.txt
hello
world
$ git status
On branch main
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: a.txt
状态 :a.txt 变成 Modified (Changes not staged for commit)。注意:
- 工作区 :内容是
hello\nworld(2行); - 暂存区 :还是上次 add 时的
hello(1行); - 版本库 :第一次 commit 时的
hello(1行)。
三个区域内容不一致 了!这就是为什么要 git add 再 git commit------你要告诉 Git "这次改动我确认了,请暂存"。
3.6 第五步:再次 add + commit
$ git add a.txt
$ git status
On branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: a.txt
$ git commit -m "a.txt追加world"
[main 2222222] a.txt追加world
1 file changed, 1 insertion(+)
$ git status
On branch main
nothing to commit, working tree clean
状态:再次 Committed,三个区域重新一致。
3.7 第六步:用 git diff 观察区域差异
git diff 家族能精确显示区域间的差异,是理解三大区域的最佳工具。
# 修改文件但不 add
$ echo "third line" >> a.txt
# git diff:工作区 vs 暂存区
$ git diff
diff --git a/a.txt b/a.txt
index 3b8e900..1f7c4d2 100644
--- a/a.txt
+++ b/a.txt
@@ -1,2 +1,3 @@
hello
world
+third line
git diff(无参数)显示工作区与暂存区 的差异:工作区比暂存区多了 third line。
# git diff --staged(或 --cached):暂存区 vs 版本库
$ git diff --staged
# (输出为空,因为暂存区和版本库当前一致)
# git diff HEAD:工作区 vs 版本库
$ git diff HEAD
diff --git a/a.txt b/a.txt
index 3b8e900..1f7c4d2 100644
--- a/a.txt
+++ b/a.txt
@@ -1,2 +1,3 @@
hello
world
+third line
git diff HEAD 显示工作区与版本库的差异,跳过暂存区直接对比。
diff 三件套记忆:
| 命令 | 比较对象 |
|---|---|
git diff |
工作区 vs 暂存区 |
git diff --staged |
暂存区 vs 版本库 |
git diff HEAD |
工作区 vs 版本库 |
四、为什么要有暂存区
很多新手问:SVN 改完直接 commit 就行,Git 为什么非要先 add 再 commit,多此一举?
这是 Git 的设计哲学,暂存区带来三个重要能力:
4.1 部分提交(最实用)
想象这个场景:你在修 Bug A,修到一半发现还有个 Bug B 顺手能修,于是又改了几个文件。现在你改了5个文件,但其中3个属于 Bug A,2个属于 Bug B。
没有暂存区(SVN):你要么5个一起提交(提交信息怎么写?"修了A和B"?不规范),要么先把B的改动暂存到别处、提交A、再恢复B的改动、提交B(极其麻烦)。
有暂存区(Git):
$ git add bugA_1.js bugA_2.js bugA_3.js
$ git commit -m "fix: 修复Bug A"
$ git add bugB_1.js bugB_2.js
$ git commit -m "fix: 修复Bug B"
甚至同一个文件里,你可以用 git add -p 只暂存某几个代码块 ,把一次修改拆成多个逻辑提交。这让你的提交历史干净、有逻辑、便于审查和回滚。
4.2 提交前的最后审查
git add 后、git commit 前,你可以用 git diff --staged 仔细检查"我到底要提交什么",避免误提交调试代码、密码、print 语句。这是很多团队 Code Review 前的自检步骤。
4.3 逻辑分离
暂存区让你把"开发过程"和"提交动作"解耦:你可以一整天都在 add 各种改动到暂存区,最后在下班前一次性 commit 成一个有意义的提交。或者把不同主题的改动分批 add + commit。
4.4 一句话总结
暂存区让你掌控提交的粒度和内容,而不是"改了什么就只能提交什么"。
五、深入 .git 目录验证三大区域
光说不够,我们打开 .git 看看三大区域在物理上长什么样。
5.1 .git/index ------ 暂存区的物理实现
$ git add a.txt
$ ls -la .git/index
-rw-r--r-- 1 chensen staff 104 Nov 2 10:30 .git/index
.git/index 是一个二进制文件,记录了暂存区的所有信息。用 git ls-files --stage 可以查看其内容:
$ git ls-files --stage
100644 1f7c4d2e3a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d 0 a.txt
输出含义:文件权限 100644、对象哈希 1f7c4d2...、暂存版本号 0、文件名 a.txt。
关键 :暂存区存的是"文件名 → 对象哈希"的映射,对象本身存在 .git/objects 里。
5.2 .git/objects ------ 版本库的对象存储
$ find .git/objects -type f
.git/objects/1f/7c4d2e3a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d
.git/objects/22/2222222222222222222222222222222222222222
.git/objects/33/3333333333333333333333333333333333333333
...
每个文件名是哈希的前2位+剩余38位。用 git cat-file 查看对象内容:
# 查看一个 blob 对象(文件内容)
$ git cat-file -p 1f7c4d2e3a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d
hello
world
third line
# 查看对象类型
$ git cat-file -t 1f7c4d2e3a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d
blob
Git 有四种对象类型:
-
blob:文件内容;
-
tree:目录结构(文件名 + blob 哈希);
-
commit:提交(指向一个 tree + 父提交 + 作者 + 信息);
-
tag:标签对象(带注释的标签)。
查看最新提交对象
$ git cat-file -p HEAD
tree 4444444444444444444444444444444444444444
parent 2222222222222222222222222222222222222222
author 全栈陈序员 chensen@example.com 1735516200 +0800
committer 全栈陈序员 chensen@example.com 1735516200 +0800a.txt追加world
这就是版本库的物理形态:一堆用哈希索引的对象,互相引用,形成一棵提交树。
5.3 .git/HEAD ------ 当前位置
$ cat .git/HEAD
ref: refs/heads/main
HEAD 是一个指针,指向"当前在哪个分支"。它指向 refs/heads/main。
5.4 .git/refs ------ 引用存储
$ cat .git/refs/heads/main
2222222222222222222222222222222222222222
refs/heads/main 存的是 main 分支最新提交的哈希。分支本质上就是一个文件,存着一个40字符的哈希------这就是 Git 分支"廉价"的根本原因。
HEAD → refs/heads/main → 2222222(提交对象)
│
▼
tree 444444
│
▼
blob a.txt内容
六、区域间的转换命令总结
| 从 | 到 | 命令 | 说明 |
|---|---|---|---|
| 工作区 | 暂存区 | git add <file> |
暂存文件改动 |
| 暂存区 | 版本库 | git commit -m "msg" |
提交暂存区内容 |
| 版本库 | 暂存区 | git reset --soft HEAD~1 |
撤销最近一次提交,保留暂存 |
| 暂存区 | 工作区 | git restore --staged <file> / git reset HEAD <file> |
取消暂存(改动保留在工作区) |
| 版本库 | 工作区 | git restore <file> / git checkout -- <file> |
丢弃工作区改动,恢复到已提交版本 |
| 版本库 | 工作区+暂存区 | git reset --hard HEAD |
彻底回到已提交状态(慎用,丢失工作区改动) |
记忆口诀:
add是"往上"走(工作区→暂存区);commit是"再往上"(暂存区→版本库);reset/restore是"往下"走(撤销)。
七、常见误区
误区1:暂存区 = 版本库
错 。暂存区是"准备提交的暂存地",版本库是"已提交的永久记录"。git add 后文件在暂存区,还没进版本库 ,必须 git commit 才真正入库。断电、误操作可能丢失暂存区内容,但已提交的版本库内容几乎不会丢。
误区2:工作区 = 暂存区
错 。你改了文件,工作区变了,但暂存区没变 (除非你 add)。这就是为什么 git commit(不带 -a)不会提交你刚改的文件------它只提交暂存区里的内容。
$ echo "new" >> a.txt # 工作区变了
$ git commit -m "test" # 暂存区没变,所以:
nothing added to commit but untracked files present # 没东西可提交
误区3:git commit -a 能替代 git add
git commit -a 会自动 add 已追踪文件的修改并提交,看似省事,但:
- 它不会 add 新文件(Untracked 的文件仍不会被提交);
- 它无法做部分提交(会把所有已追踪文件的修改一股脑提交);
- 它违背了暂存区"精细控制提交内容"的设计初衷。
建议 :新手别用 -a,养成 add → 检查 → commit 的好习惯。熟练后偶尔用 -a 提交小改动可以,但别依赖。
误区4:git add . 就是把所有改动提交了
错 。git add . 只是把改动放进暂存区 ,还没提交。还需要 git commit 才进版本库。两步!
误区5:修改文件后直接 commit 就能提交
错 。修改文件后,改动在工作区,不在暂存区 ,git commit 不会提交它(除非加 -a)。必须先 git add。
误区6:暂存区的内容会自动同步工作区
错 。你 git add 后,暂存区记录了那一刻的快照。之后你再修改工作区文件,暂存区不会自动更新 ,它还停留在上次 add 的状态。要让暂存区跟上,必须再次 git add。
$ echo "v1" > f.txt && git add f.txt # 暂存 "v1"
$ echo "v2" >> f.txt # 工作区变成 "v1\nv2"
$ git diff # 显示工作区比暂存区多了 v2
$ git diff --staged # 显示暂存区比版本库多了 v1
误区7:git commit 会提交工作区所有改动
错 。git commit(不带 -a)只提交暂存区的内容。工作区里没 add 的改动不会被提交。这是 Git 和 SVN 最大的行为差异之一,新手最容易踩坑。
八、用一张表彻底搞懂三大区域
最后用一张"文件生命周期"表把所有状态和区域对应关系固化下来:
| 操作后 | 工作区 | 暂存区 | 版本库 | git status 显示 |
|---|---|---|---|---|
新建文件 x,未 add |
有 x | 无 | 无 | Untracked: x |
git add x |
有 x | 有 x | 无 | new file: x |
git commit |
有 x | 有 x | 有 x | clean |
| 修改 x,未 add | 有 x(新) | 有 x(旧) | 有 x(旧) | modified: x (not staged) |
git add x |
有 x(新) | 有 x(新) | 有 x(旧) | modified: x (staged) |
git commit |
有 x(新) | 有 x(新) | 有 x(新) | clean |
| 删除 x(rm),未 add | 无 x | 有 x | 有 x | deleted: x (not staged) |
git add x(或 -u) |
无 x | 无 x | 有 x | deleted: x (staged) |
git commit |
无 x | 无 x | 无 x | clean |
把这张表存下来,遇到状态困惑时对照查,比死记命令靠谱得多。
一个生活化的类比
如果三大区域还是觉得抽象,用"做菜"来类比就很好懂:
- 工作区 就像你的案板:食材(文件)都在上面,你随时切、炒、调味,案板上的东西一直在变。
- 暂存区 就像你的备菜盘:你把处理好的食材一份份装盘,准备下锅。你可以挑挑拣拣,只把想做的菜装盘,不想做的留在案板。
- 版本库 就像你的菜谱书:一旦把一盘菜"定型"写进菜谱(commit),它就永久记录下来了,以后随时可以照着做(checkout 回历史版本)。
你不会把案板上所有东西一股脑倒进锅里(不 git add . 就 commit),而是挑好装盘再下锅(add 精选后 commit)。这就是暂存区的意义------让你掌控"这一锅要做什么"。
记住这个类比,三大区域的关系就再也不会搞混了。后续所有关于 add、commit、reset、restore 的命令,都可以套进这个类比里理解,比死记硬背命令参数有效得多。
八、小结
本篇是理解 Git 的核心篇,务必吃透:
- 三大区域 :
- 工作区:你编辑的文件;
- 暂存区 (Index):
git add后进入,"购物车"; - 版本库 (Repository):
git commit后进入,永久历史。
- 文件状态:Untracked → Staged → Committed → Modified → Staged → ... 循环。
- 流转命令 :
add(工作区→暂存区)、commit(暂存区→版本库)、reset/restore(反向)。 - diff 三件套 :
git diff(工作区vs暂存区)、git diff --staged(暂存区vs版本库)、git diff HEAD(工作区vs版本库)。 - 暂存区的意义:部分提交、提交前审查、逻辑分离------让你掌控提交粒度。
- 物理实现 :暂存区是
.git/index,版本库是.git/objects,分支是refs/heads/下一个存哈希的文件。 - 常见误区 :暂存区≠版本库,工作区≠暂存区,
commit不提交未 add 的改动,commit -a不能替代add。
理解了三大区域,下一篇我们就能深入 git add 的各种用法------它正是"工作区→暂存区"那一步的关键命令,用法远比你想的丰富。
下一篇:《Git从入门到精通(五)------ git add:将文件加入暂存区》
九、练习题
巩固一下,答案可自己用 Git 验证:
- 创建一个仓库,新建文件
x.txt,执行git add x.txt后,文件处于什么状态?在哪些区域存在? - 接上题,再修改
x.txt(不 add),此时git status会显示什么?工作区、暂存区、版本库里x.txt的内容分别是什么? git diff、git diff --staged、git diff HEAD分别比较的是哪两个区域?- 为什么
git commit(不带-a)有时候提示"nothing to commit",即使你明明改了文件? - 如何只暂存一个文件里的部分修改?(提示:下篇揭晓)
.git/index文件什么时候存在、什么时候不存在?- 分支在
.git里物理上是什么?为什么说 Git 分支"廉价"?
如果本文对你有帮助,欢迎点赞、收藏、关注。我是全栈陈序员,我们下篇见。