【Git】从入门到精通(四)—— Git三大区域:工作区、暂存区与版本库

作者:全栈陈序员 系列:《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 +0800

    a.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 的核心篇,务必吃透:

  1. 三大区域 :
    • 工作区:你编辑的文件;
    • 暂存区 (Index):git add 后进入,"购物车";
    • 版本库 (Repository):git commit 后进入,永久历史。
  2. 文件状态:Untracked → Staged → Committed → Modified → Staged → ... 循环。
  3. 流转命令 :add(工作区→暂存区)、commit(暂存区→版本库)、reset/restore(反向)。
  4. diff 三件套 :git diff(工作区vs暂存区)、git diff --staged(暂存区vs版本库)、git diff HEAD(工作区vs版本库)。
  5. 暂存区的意义:部分提交、提交前审查、逻辑分离------让你掌控提交粒度。
  6. 物理实现 :暂存区是 .git/index,版本库是 .git/objects,分支是 refs/heads/ 下一个存哈希的文件。
  7. 常见误区 :暂存区≠版本库,工作区≠暂存区,commit 不提交未 add 的改动,commit -a 不能替代 add。

理解了三大区域,下一篇我们就能深入 git add 的各种用法------它正是"工作区→暂存区"那一步的关键命令,用法远比你想的丰富。

下一篇:《Git从入门到精通(五)------ git add:将文件加入暂存区》


九、练习题

巩固一下,答案可自己用 Git 验证:

  1. 创建一个仓库,新建文件 x.txt,执行 git add x.txt 后,文件处于什么状态?在哪些区域存在?
  2. 接上题,再修改 x.txt(不 add),此时 git status 会显示什么?工作区、暂存区、版本库里 x.txt 的内容分别是什么?
  3. git diff、git diff --staged、git diff HEAD 分别比较的是哪两个区域?
  4. 为什么 git commit(不带 -a)有时候提示"nothing to commit",即使你明明改了文件?
  5. 如何只暂存一个文件里的部分修改?(提示:下篇揭晓)
  6. .git/index 文件什么时候存在、什么时候不存在?
  7. 分支在 .git 里物理上是什么?为什么说 Git 分支"廉价"?

如果本文对你有帮助,欢迎点赞、收藏、关注。我是全栈陈序员,我们下篇见。

相关推荐
Elasticsearch1 小时前
AgentHack Shanghai|基于阿里云AI搜索 Elasticsearch x Qoder 构建 AI Agent 应用
elasticsearch
Sayai1 小时前
Elasticsearch ingest pipeline 被禁用之后:性能代价、安全治理与数据入库时间的四种替代方案
大数据·运维·安全·elasticsearch·搜索引擎·性能优化
YIN_尹1 小时前
【git原理与使用】git基本操作及底层原理(一)
java·git·elasticsearch
AI_work1 小时前
43 号令下招投标全流程留痕分析:电子卷宗 15 年保存与投标人文档合规体系重构
大数据·人工智能·ai·重构
源图客1 小时前
Apache Flink 2.3.0(最新版本)
大数据·flink·apache
AI 编程助手GPT2 小时前
GPT 改完配置别直接覆盖:用 git diff 审查两份文件
git·gpt
隔振降噪研究员2 小时前
裁断机一响,楼板为什么会跟着抖?
大数据·linux·数据库·人工智能
zhongben_keji2 小时前
本地生活 APP 开发:城市 × 品类 × 履约形态的三维网格与三套账
java·大数据·生活
2301_776045232 小时前
山寨币与比特币之间的关系
大数据·人工智能·区块链