先记住这个答案
工作区是你直接编辑的文件目录,存放未跟踪或已修改但未暂存的实际内容;暂存区(index)记录下一次提交将纳入的文件快照,相当于候选提交清单;仓库(.git)保存所有已提交的不可变快照及元数据。文件流转路径是:工作区修改后,用 git add 把当前文件内容写入暂存区;用 git commit 将暂存区内容固化成一个新的提交存入仓库;git checkout 或 git restore 可从仓库或暂存区把内容写回工作区。
- 工作区是实际文件目录,暂存区是下次提交的快照
- git add 写入暂存区,git commit 固化到仓库
- 未跟踪文件在 add 前不被 Git 记录
三棵树的存储机制与流转命令
工作区是磁盘上的文件树,直接对应你编辑的文件。Git 并不主动监控它,只有执行 git status 或 git diff 时才会扫描文件变化,将文件内容与暂存区中的记录比对。新文件未被跟踪,修改过的文件在暂存区没有对应新版本,会显示为 ‘Changes not staged’;这些内容依然保留在工作区中,但未被 Git 纳入版本管理。
暂存区(index)是一个二进制文件,保存了每个路径对应的 blob 对象哈希和文件模式。git add 会把当前工作区文件压缩成 blob 对象写入对象库,并把路径与哈希记录到 index 中。git commit 则读取 index 生成 tree 对象,再创建 commit 对象指向该 tree,并更新当前分支指针。仓库中存放的是已提交的全部快照,每次提交都是一个不可变对象。
git checkout 从仓库取出提交,将文件写入工作区并重设 index;git reset 的 --mixed 模式默认把仓库某提交的内容同步到 index,但不动工作区;git restore 可分别指定 --staged 或 --worktree 来精准回退。理解 index 是暂存区的实现,才能解释为什么 git add 后还需 git commit。
修改后只部分暂存再提交
假设你编辑了 docs/readme.md,既修复了错别字又添加了新章节。你希望两次提交分开,以便历史清晰。此时工作区文件是合并后的状态,但暂存区仍是旧版本。执行 git add docs/readme.md 会把当前完整文件都暂存,这不行;正确做法是用 git add -p 进入交互模式,按 hunk 选择性地暂存。选择某个 hunk 后,该部分的新内容进入暂存区,未选择的修改仍留在工作区。
提交后,暂存区与仓库一致,但工作区仍有残留修改未暂存。git status 会显示 ‘Changes not staged’。继续 git add 剩余部分再 commit,两个提交分别包含你的错别字修复和新章节。如果误提交了全部,可用 git reset --soft HEAD~1 撤销提交并保留暂存区内容,但若已推送则需谨慎改写历史。
文件类型的边界与未跟踪文件处理
工作区中未被跟踪的文件(新文件)在没有 git add 前,仓库和暂存区都没有它们的记录。git commit 不会包含它们,因为提交只基于 index 内容。要强行包含,必须显式 add。同理,.gitignore 只影响未跟踪文件的扫描,对已跟踪文件无效。这是初学者常混淆的边界:修改已跟踪文件,仓库旧版本还在;工作区新版本只是差异。
若丢弃未暂存修改,git restore <file> 会用 index 内容覆盖工作区;若丢弃已暂存修改,需要 git restore --staged 后再 checkout。但如果修改已被提交并推送到远程,就不能依赖本地覆盖,因为其他协作者可能已基于该提交工作。此时应使用 git revert 生成反向提交。记住:仓库对象是不可变的,reset 只是移动指针,真正丢失提交前可用 reflog 找回。
容易答错的地方
- 认为 git commit 保存工作区所有修改
- commit 只保存暂存区的快照。你修改文件但不 add,commit 不会包含这些修改。必须先 add 或使用 git commit -a(仅限已跟踪文件)才会暂存并提交。
- 以为暂存区存放文件副本
- 暂存区存放的是每个文件的 blob 对象哈希,而非完整文件副本。文件内容以压缩对象形式存于对象库,index 只是引用。因此 add 一个巨大文件实际上先创建对象,这可能是 git add 慢的原因。
面试官还会怎么问?
git diff 与 git diff --staged 有何区别?
git diff 比较工作区与暂存区的差异,git diff --staged(或 --cached)比较暂存区与仓库 HEAD 的差异。没有参数时默认只看工作区中未暂存的修改。
git status 中 'Changes to be committed' 意味着什么?
它表示暂存区与当前 HEAD 提交有差异,这些差异将在下一次提交中生效。但工作区可能还有未暂存修改,git status 会分组显示。
一次 commit 后暂存区为何不再显示变化?
因为 commit 会用 index 创建新提交并移动 HEAD,随后 index 与 HEAD 内容一致,因此 status 显示工作区干净(除非有新的工作区修改)。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。