我们知道 git 跟踪文件会经历三个阶段:工作区,暂存区和本地仓库(参考git:理解工作区,暂存区和本地仓库),在这些阶段文件如何被储存?理解 git 文件的存储方式能帮助我们掌握 git 的工作原理。
在上述三个阶段,文件会以对象(object)的形式存储在 .git/objects 目录下,对象主要有三类:commit,tree 和 blob。假设初始目录如下:
├── .git
├── file
│ └── c.txt
├── a.txt
└── b.txt
执行 git add .
将工作区所有文件存放到暂存区,查看目录 .git/objects,看到该目录下增加了3个子目录 2e,34,63,每个子目录下有一个由字母数字命名的文件。git 根据文件内容生成 SHA-1 哈希值作为文件的校验和,创建以该校验和前2个字符为名称的子目录,并以剩下的 38 个字符为文件命名。
.git/objects
├── 2e
│ └── 65efe2a145dda7ee51d1741299f848e5bf752e
├── 34
│ └── 10062ba67c5ed59b854387a8bc0ec012479368
├── 63
│ └── d8dbd40c23542e740659a7168a0ce3138ea748
├── info
└── pack
可以使用指令 git cat-file -t %校验和前六位
查看文件类型,git cat-file -p %校验和前六位
查看文件内容。
$ git cat-file -t 2e65ef
blob
$ git cat-file -t 341006
blob
$ git cat-file -t 63d8db
blob
$ git cat-file -p 2e65ef
a
$ git cat-file -p 341006
c
$ git cat-file -p 63d8db
b
因此,当保存工作区的文件到暂存区时,git 会复制每个文件并压缩为 blob 对象,保存在 .git/objects 下。
执行 git commit
将暂存区文件上传到本地仓库,.git/objects 目录下又增加了三个子目录 0c,b1,65,分别查看其对象和内容:
# 0c2dd9
commit
tree b15ad62733b40ee7f19be69f850fe5b575210edd
author XuanyuXiang 1665807213 +0800
committer XuanyuXiang 1665807213 +0800
first commit
# b15ad6
tree
100644 blob 2e65efe2a145dda7ee51d1741299f848e5bf752e a.txt
100644 blob 63d8dbd40c23542e740659a7168a0ce3138ea748 b.txt
040000 tree 653c8359fc980eb3a393a41a1f1cbe4e8ce458f8 file
# 653c83
tree
100644 blob 3410062ba67c5ed59b854387a8bc0ec012479368 c.txt
此时生成了一个 commit 对象,两个 tree 对象。commit 存放 b15ad6 的地址和一些用户信息;b15ad6 是一个 tree,存放其下的 blob 地址和 653c83 的地址;653c83 是一个 tree,存放其下的 blob 地址。分析得出,git commit
生成 commit 对象和 tree 对象,commit 指向整个项目目录,tree 用于构建整个项目的目录结构,同时暂存区的 blob 对象也会移入本地仓库,暂存区清空。其关系如下:
0c2dd9: commit
└── b15ad6: tree
├── 653c83: tree
│ └── 341006: blob
├── 2e65ef: blob
└── 63d8db: blob
现在我们修改 a.txt 的内容并提交一个新的 commit:
echo "helloGit" > a.txt
git add .
git commit -m "modify a.txt"
.git/objects 目录下又增加了3个子目录,7c,46,74,分别查看其类型和内容:
# 466622
commit
tree 7c0ef1dab56cd8d2ac5c3cda20ddadb4765e8311
parent 0c2dd90ef3827e608b3ea9fba424d1ff68353216
author XuanyuXiang 1665809170 +0800
committer XuanyuXiang 1665809170 +0800
new branch modified a.txt
# 7c0ef1
tree
100644 blob 7423c81faa8c815b6e8a1742fcc321d457cfaab0 a.txt
100644 blob 63d8dbd40c23542e740659a7168a0ce3138ea748 b.txt
040000 tree 653c8359fc980eb3a393a41a1f1cbe4e8ce458f8 file
# 7423c8
blob
helloGit
此时的 commit 多了一个 parent 对象,它指向上一个 commit;请注意 7423c8 虽然对应 a.txt,但由于修改了内容,7423c8 是一个全新的 blob 对象(此时 2e65ef 仍然存在)。本地仓库保存的 commit,tree,blob 是永远不会被删除的。新的指向关系如下:
0c2dd9: commit —————————————————— 466622: commit
└── b15ad6: tree └── 7c0ef1: tree
├── 653c83: tree ├── 653c83: tree
│ └── 341006: blob │ └── 341006: blob
├── 2e65ef: blob ├── 7423c8: blob
└── 63d8db: blob └── 63d8db: blob
HEAD 可以理解为指向当前分支的指针,查看 .git/HEAD 内容,确实指向当前的 master 分支:
$ cat .git/HEAD
ref: refs/heads/master
分支可以理解为指向当前 commit 的指针,查看 .git/refs/heads/master,它指向 466622,这就是上面的 commit 对象。分支指针通过指向不同 commit 实现版本的切换,HEAD 通过指向不同分支实现分支切换。
Git内部存储原理