1.3 三区流转:工作区、暂存区与仓库


1.3 三区流转:工作区、暂存区与仓库

本节摘要:工作区是磁盘文件,暂存区是 index 文件里的一棵待提交 tree,仓库是对象库。本节用 git status 的原始输出对照三区状态,讲清"已暂存/未暂存/未追踪"三种判定的来源,并解释为什么 Linus 要坚持"准备提交"与"实际提交"两步走。

学习目标

  1. 能把三区各自对应到 .git 内外的具体文件
  2. 能根据 git status 输出反推每个文件在两两比对中的差异
  3. 能解释暂存区作为"一棵 tree"的含意与部分暂存技巧
  4. 能说出三种 hard 撤销(checkout/reset/restore)分别动哪两个区

一、三区到底是什么

先把地理确立下来。工作区是你眼睛看得见的项目目录,.git 之外的一切。暂存区是 .git/index 这一个二进制文件,里面登记着"路径 → blob 哈希 → 模式位"的列表——上一节说过,tree 记录目录结构,而 index 本质上就是下一棵 tree 的草稿。仓库(本地版本库)狭义上指 .git/objects 对象库加上提交链。三区关系一句话:工作区是现场,index 是证物袋,对象库是证物室。

git status 的判定逻辑完全建立在两两比对上:HEAD 指向的 tree 与 index 比,得出"已暂存的变更";index 与工作区比,得出"未暂存的变更";工作区里 index 没登记的路径,就是"未追踪文件"。做个三层各不相同的实验:

printf "base\n" > f.txt git add f.txt && git commit -m base printf "staged\n" > f.txt && git add f.txt # index 有 staged 版本 printf "workdir\n" > f.txt # 工作区又改了 printf "new\n" > g.txt # 未追踪 git status --short # MM f.txt <- 第一列 HEADvsindex 已暂存,第二列 indexvs工作区 未暂存 # ?? g.txt

MM 两个字母精确描述了同一个文件在三个区的三种状态。新手常在这里迷失:"我明明 add 过了,为什么 status 还有红字?"因为 add 之后你又改了工作区,第二列的比对立即失效。理解了两次比对,这类困惑自动消解。

二、三区流转图

每条边都可以单独验证。特别值得取证的是 git commit 的瞬间:Git 把 index 当前内容固化成 tree、包上元数据生成 commit、把当前分支引用挪到新提交——三步全是"新建对象加挪指针",没有一步是"修改"。这也解释了 git commit --amend 为什么能"改"上次提交:它新建一个以旧提交为底的提交,把分支指针挪过去,旧提交原地不动。

为什么非要两步走? 暂存区是 Linus 的坚持:一次提交应当是一个逻辑完整的变更单元。有了 index,你可以把一次大改动拆成多次提交——改了五个文件修两个 bug,先 add 其中三个提交,再 add 剩下两个提交。没有 index 的系统(比如 SVN 的直接提交)做不到这种精细切分。

# 部分暂存:交互式挑 hunk git add -p f.txt # 它会把 diff 切成块逐个询问:y 暂存 n 跳过 s 拆更细 e 手工编辑

add -p 的存在本身就是 index 设计价值的最好证明:提交内容可以精确到"半个文件的几个代码块"。另一个进阶技巧是 git add -u(只更新已追踪文件,不引入新文件)与 git add -A(全部),团队里约定好哪种默认行为能少踩不少坑。

三、回填方向与撤销的预备知识

流向仓库是"提交",反向是"回填"。三种常见回填要分清动哪个区:

命令 index 工作区 典型用途
git restore --staged f 回填为 HEAD 不动 add 错了,取消暂存
git restore f 不动 回填为 index 丢弃工作区改动
git reset --hard HEAD 回填为 HEAD 回填为 HEAD 全部不要了(危险)

注意中间那行:restore f(旧写法 checkout -- f)是把工作区覆盖成 index 的内容,不是覆盖成上次提交。如果 add 之后又改乱了,restore 回到的是 add 时的样子。这个细节坑过非常多人,根源就是没分清"回填源"。

⚠️ 常见坑:reset --hard 会把工作区未提交改动直接抹掉且无法从 reflog 恢复(reflog 只记引用移动,不记工作区)。动手前先 git stash 或至少 git status 扫一眼有没有未暂存的宝贝。

四、给状态判定再补两块拼图

.gitignore 的作用点在工作区一侧。 它只影响"未追踪"的判定,让 status 不再显示某些路径,不影响已追踪文件。所以先提交了 app.log 再补 .gitignore 是没用的,得先 git rm --cached app.log 让它脱离 index。

index 不比对内容,比对哈希。 status 的"是否修改"是拿工作区文件算哈希(考虑换行转换后)与 index 登记 哈希比较。这也是为什么 touch 一个文件不改内容,Git 现代版本不会误报修改——哈希没变就当没变,只有时间戳引发重新检查,检查完仍是静默的。

💡 关键直觉:把 index 当成"购物车"而不是"备份区"。它装的是你下一次提交将要包含的东西,随时可以增删调整;真正进了对象库的东西(提交过的)反而是不可变、绝对安全的。新手怕 Git 会弄丢代码,实际上危险只存在于"工作区 → index"之间——凡是还没 commit 的内容,都悬。

本节要点回顾

  • 三区对应实体:工作区=磁盘、暂存区=.git/index 文件、仓库=对象库加提交链。
  • status 是两次比对:HEAD 的 tree 对 index、index 对工作区,MM 双状态由此而来。
  • index 是 tree 草稿:commit 把草稿盖章成正式快照,支持 add -p 级别的精细切分。
  • 回填三方向:restore --staged 退袋、restore 弃改、reset --hard 全弃。
  • 危险边界:未提交的内容不受对象模型保护,reflog 救不了没提交过的工作区。

第 1 章到此结案。你已经能解剖对象、走通哈希链、画出三区流转——接下来进入第 2 章,去看那些 41 字节的指针如何撑起分支的全部魔法。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U