前言
最近在读《Pro Git》这本书,虽然距离第二版已经过去了好几年,Git 也在不断更新,但由于 Git 核心团队一直保持着良好的向后兼容性,所以书中关于 Git 的核心概念和命令依然有效。
本篇为我对 Git 文件状态及其状态转换的理解。
基本原理
上一篇文章讲过 Git 的分支与引用的原理,还有标签的引用方式。本篇文章将讲述的 Git 文件状态也是其核心内容。学习文件状态转换过程,会让你对使用 add、commit、checkout 等上层命令有更好的理解。
Git 文件状态
Git 的文件状态也是其核心内容,学习其文件状态转换过程,会让你对使用 add、commit、checkout 等上层命令时有着更好的理解。
对于 Git 来说,你工作目录下的每一个文件都可以被划分为两类:已跟踪或未跟踪。已跟踪的文件是指那些已加入版本控制的文件,在上一次的快照中有它们的记录,它们的状态可能处于未修改、已修改或已放入暂存区。除已跟踪文件以外的所有文件都是未跟踪文件,它们既没有存在于快照中,也没有被放入暂存区。如果在已有文件的目录中初始化 Git 仓库,则工作目录下所有的文件都会属于未跟踪文件。而通过 clone 拉取某个仓库的时候,工作目录中的所有文件都属于已跟踪文件,并处于未修改状态。
如果修改已提交的文件,Git 会将它们标记为已修改文件。我们可以通过 git add 等命令将修改过的文件放入暂存区,然后在某一时刻统一提交所有暂存的修改。如此反复,就是 Git 中文件的生命周期:

暂存区(Index)
Git 的暂存区是一个很重要的概念,前面曾多次提到,也使用过 update-index 命令将文件放入到暂存区中。一般说法是叫“暂存区(Staging Area)”,不过 Git 本身称之为“索引(Index)”。现在总结一下个人对暂存区的理解。
原子性提交
前面提到过,Git 基于快照实现的版本控制,通过我们的每一次提交(commit)而生成快照,形成了快照流。
Git 中的提交是原子性的。原子性提交指的是,由多个内容变动信息组合成提交后,每一个提交都被视为一个不可分割的最简整体,要么在同一次提交中保存全部修改,要么全部失败。Git 使用原子性提交所带来的好处就是,我们可以很方便的将工作目录中的文件内容切换至提交所生成的快照中,可能是某个阶段的内容,也可能是某个时间点的内容。
基于 Git 原子性提交的特性,我们的文件内容(例如:代码)也需要在提交中保持原子性。即,当代码变动需要提交时,这次的提交应该尽可能的小量并且是一个不可分割的整体,可以是特性(feature)、修复(fix)或是优化(improved)等等。更明确的说就是:一次提交只做一件事。
原子性提交是非常简单的,持续保持原子性提交的习惯,很对你的工作流很有好处;code review 会更加简单,代码更容易回滚,易于追踪变化等等,并且后续的开发者也会非常感谢你的。
暂存区的作用
暂存区就是为了原子性提交这个特性服务的。它可以随机的将各种文件的修改放进去,这样我们就可以用多个指令精确的挑选出我们需要提交的所有修改,从而更好的进行原子性提交。
暂存区原理
Git 将暂存区的内容存储于 .git/index 文件中,存储格式很复杂,具体可以看 Git 仓库的文档 index-format.txt。如果有需要,可以通过 git ls-files 命令查看暂存区中的文件列表信息。
三棵树
在 Git 中,文件在改变状态的同时,其所处位置也在变化。Git 管理着三棵不同的树,无论文件处于何种状态,都会位于某棵树中。“树”在这里的实际意思是“文件的集合”,而不是特指某种数据结构。
HEAD
HEAD 作为一个引用指针,它总是指向你的上一次提交,无论你是不是在一个分支上。最简单的理解,就是将它看做你的上一次提交的快照。这就表示它会是下一次提交的父节点。
使用 cat-file 及 ls-tree 命令可以查看快照信息及其文件目录列表:
$ git cat-file -p HEAD
tree a7b2c0a2a336f4c262ef7cc53e896ddab396edcd
author cnbailian <594647004@qq.com> 1590710985 +0800
committer cnbailian <594647004@qq.com> 1590710985 +0800
?
initial commit
$ git ls-tree -r HEAD
100644 blob e845566c06f9bf557d35e8292c37cf05d97a9769 README.md
100644 blob 8e695ec83aa8b1d596183b26206a514576570fff doc/doc.md
100644 blob 06ab7d0f9a35a7d1070711496d6ca1cb892a258f main.go
Index
Index 也就是暂存区,它是你的预期的下一次提交。当你提交时,它的文件内容就是你提交后生成的快照。
使用 ls-files 命令查看 Index 当前的样子:
$ git ls-files -s
100644 e845566c06f9bf557d35e8292c37cf05d97a9769 0 README.md
100644 8e695ec83aa8b1d596183b26206a514576570fff 0 doc/doc.md
100644 06ab7d0f9a35a7d1070711496d6ca1cb892a258f 0 main.go
工作目录
最后一棵树,就是你的工作目录。另外两颗树以一种高效但并不直观的方式将文件内容存储至 .git 目录中。工作目录会将它们解码为实际的文件以便编辑。
你可以将工作目录当作沙盒,在你将修改内容存储至暂存区并提交之前,可以随意更改。但这也意味着在沙盒中的修改,可能会被其他两颗树中检出的内容所覆盖,要注意提交自己想要保存的内容。
$ tree
.
├── README.md
├── doc
│ └── doc.md
└── main.go
?
1 directory, 3 files
工作流程
Git 主要的目的是通过操纵这三棵树来以更加连续的状态记录项目的快照。

总结
本篇为 Git 基本原理的第三篇,目的为理解 Git 文件状态及其转换流程。
至此基本原理篇已结束,下一步将是结合基础命令,讲解 Git 的使用实践。