目录
本文将从0开始构建一个git项目,介绍.git目录下各个文件夹/文件的含义,并且通过添加/编辑文件的过程介绍git对象的原理。本文只涉及单分支master,对于多分支的情况将在下一篇文章 深入理解git分支(二)中介绍。
初始化项目
新建项目文件夹并在文件夹路径下运行命令初始化项目:
git init
项目初始化后的文件结构如下:
|—— .git
|—— hooks 一些钩子脚本
|—— info 存放仓库的一些信息
|—— objects git数据库,存放所有git对象
|—— info
|—— pack
|—— refs 保存指向各分支commit对象的指针
|—— heads
|—— tags
|—— config 文件,配置文件
|—— description 文件,描述信息
|—— HEAD 文件,指向当前分支
添加文件并提交
在项目中添加一些文件和文件夹:
|—— .git
|—— dir1
|—— file2.txt
|—— file1.txt
运行命令:
git add .
git commit -m "add files and dir"
此时查看.git文件夹:
|—— .git
|—— hooks
|—— info
+ |—— logs
|—— refs
|—— heads
|—— master
|—— HEAD
|—— objects
+ |—— 2d
|—— e2ca1110497724be5af3322611d2c4b2c79a9d
+ |—— 6e
|—— 44180b8f0cb06279626d54a8d0b7a77feefbd9
+ |—— 21
|—— 895d3c9a9d047fce8cc40c86a8274f2096541f
+ |—— b0
|—— 62f1906d5c772df480b171bd52e5c15415c1e1
+ |—— e6
|—— 9daa91f260c60a293ed816277b26b8d5420849
|—— info
|—— pack
|—— refs
|—— heads
+ |—— master
|—— tags
+ |—— COMMIT_EDITMSG
|—— config
|—— description
|—— HEAD
+ |—— index
变化:
- logs文件夹,保存日志
- objects文件夹,由于进行了git commit操作,git数据库发生了变化,objects文件夹中产生的每个文件都对应一个git对象
- refs文件夹中新增的master文件保存了最近一个commit对象的索引,本例中为"e69daa91f260c60a293ed816277b26b8d5420849"(objects中最后一个git对象)
- HEAD文件的内容被更新为"ref: refs/heads/master",因此也是"e69daa91f260c60a293ed816277b26b8d5420849"
- COMMIT_EDITMSG文件记录了git commit时输入的信息,在本例中即"add files and dir"
- index文件保存暂存区的信息,关于git文件状态和分区,请参考git文件状态
关于git对象
上述操作结束后objects文件夹/git数据库中新增了5个对象,文件夹名+文件名
(共40个字符)即为对象的哈希值(git使用的哈希算法是SHA-1)。这5个对象可以分为3类:
- blob对象,对应于每个
文件
,对于一个文件,每次变更都会产生一个新的blob对象。本例中有两个文件变更即file1.txt和file2.txt,因此有2个blob对象 - tree对象,对应每个
文件夹
(根文件夹也算),对于一个文件夹,每次变更都会产生一个新的tree对象,该对象包含一些其内部文件夹/文件对应的tree对象和blob对象的指针。本例中有两个文件夹变更即根目录和dir1,因此有2个tree对象 - commit对象,对应每次
commit
,每次commit都会产生一个新的commit对象,该对象包含作者信息(用户名和邮箱,可通过git config user.name和git config user.email查看)、提交时输入的信息(-m后的信息)、指向当前路径对应的tree对象的指针和父commit对象指针(可为空)。本例commit了一次,因此有1个commit对象
那么问题来了,如何知道objects下的对象的具体信息呢?
查看git对象
通过哈希值查看
在已知对象哈希值的情况下,使用git cat-file命令查看对象信息,上述5个对象分别如下:
// commit对象,包含tree指针,author, committer和输入的信息,父commit对象为空,因为这是第一个commit
git cat-file -p e69daa91f260c60a293ed816277b26b8d5420849
tree 2de2ca1110497724be5af3322611d2c4b2c79a9d
author test <test@test.com> 1580282713 +0800
committer test <test@test.com> 1580282713 +0800
add files and dir
// 根目录对应的tree对象,包含指向dir1 tree对象和file1.txt blob对象的指针
git cat-file -p 2de2ca1110497724be5af3322611d2c4b2c79a9d
040000 tree 6e44180b8f0cb06279626d54a8d0b7a77feefbd9 dir1
100644 blob b062f1906d5c772df480b171bd52e5c15415c1e1 file1.txt
// 目录dir1对应的tree对象,包含指向file2.txt blob对象的指针
git cat-file -p 6e44180b8f0cb06279626d54a8d0b7a77feefbd9
100644 blob 21895d3c9a9d047fce8cc40c86a8274f2096541f file2.txt
// file1.txt对应的blob对象,包含file1.txt的内容
git cat-file -p b062f1906d5c772df480b171bd52e5c15415c1e1
This is file1.
// file2.txt对应的blob对象,包含file2.txt的内容
git cat-file -p 21895d3c9a9d047fce8cc40c86a8274f2096541f
This is file2.
通过master^{type}语法查看
还可以通过master^{type}语法查看git对象:
// 查看最近的commit对象
git cat-file -p master^{commit}
tree 2de2ca1110497724be5af3322611d2c4b2c79a9d
author test <test@test.com> 1580282713 +0800
committer test <test@test.com> 1580282713 +0800
add files and dir
// 查看最近的tree对象
git cat-file -p master^{tree}
040000 tree 6e44180b8f0cb06279626d54a8d0b7a77feefbd9 dir1
100644 blob b062f1906d5c772df480b171bd52e5c15415c1e1 file1.txt
git对象的关系
简单来说,3类git对象的关系是:commit对象指向tree对象,tree对象指向其他tree对象或blob对象,blob对象代表文件。本例中的5个git对象关系是:
HEAD, index和文件状态
.git文件状态中提到,git项目中的文件有3种状态:modified, staged和committed,那么git如何知道每个文件处于何种状态呢?HEAD文件和index文件就是用来做这件事的。
HEAD文件
本例中HEAD文件的内容是ref: refs/heads/master,引用了refs下的一个文件,打开refs/heads/master文件,其内容是e69daa91f260c60a293ed816277b26b8d5420849,这是最近的一个commit对象,而通过这个commit对象git可以拿到所有文件的最新版本(上一节也介绍了如何通过git cat-file来查询),所以可以认为HEAD文件实际上保存了所有处于committed
状态的文件信息。
index文件
.git/index文件是一个二进制文件,它保存了所有文件最近一次暂存(git add)的信息,可以使用命令git ls-files --stage查看index文件的内容:
git ls-files --stage
100644 21895d3c9a9d047fce8cc40c86a8274f2096541f 0 dir1/file2.txt
100644 b062f1906d5c772df480b171bd52e5c15415c1e1 0 file1.txt
因此,通过对比index文件和git项目中的文件的哈希值,就可以知道哪些文件处于modified状态
,即还未git add;通过对比index文件和HEAD文件就可以知道哪些文件处于staged状态
,哪些文件处于committed状态
,即git add后还未git commit。此外,如果index文件不存在(初始化项目时),则说明所有文件处于untracked状态
,也还未git add。
如果用一段伪代码来表示判断文件状态的逻辑:
遍历git项目中的所有文件:
if index文件不存在:
return "untracked"
else:
var oldHash = 从index文件中读取
var newHash = SHA1(文件)
if oldHash != newHash:
return "modified"
else:
var committedHash = 通过HEAD文件找到
if newHash != committedHash:
return "staged"
else:
return "committed"
编辑file1.txt文件
下面通过编辑file1.txt文件看一看文件状态和git对象的变化。
-
编辑file1.txt文件,然后运行git status,file1.txt文件的状态是modified
git status On branch master Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git checkout -- <file>..." to discard changes in working directory) modified: file1.txt no changes added to commit (use "git add" and/or "git commit -a")
-
运行git add命令,再用git ls-files --stage查看index文件内容,发现file1.txt的哈希值更新了,如果再运行git status,发现file1.txt的状态变为staged
git ls-files --stage 100644 21895d3c9a9d047fce8cc40c86a8274f2096541f 0 dir1/file2.txt - 100644 b062f1906d5c772df480b171bd52e5c15415c1e1 0 file1.txt + 100644 8210a3da97594bfc4c427dd481907895f98a9144 0 file1.txt
-
运行git commit,然后查看refs/heads/master,文件内容已经更新为新的commit对象
-
查看objects文件夹,发现多了3个对象(下图加框),这3个对象是一个blob对象、一个tree对象和一个commit对象,commit对象的parent指向上一个commit对象
小结
git项目的.git文件夹保存了所有关于对象、工作区、文件状态的数据,其中objects文件夹保存了所有git对象(commit对象、tree对象和blob对象);refs文件夹保存了所有分支最新commit的指针;HEAD文件通过引用refs下的文件掌握了所有.git仓库目录下的文件(即committed状态的文件);index文件保存了所有暂存区的文件(即staged状态的文件),通过HEAD和index文件就可以知道每个文件的状态。
本文的介绍基于单分支master,下一篇文章深入理解git分支(二)将继续介绍多分支的情况。