本文主要是学习Pro git中git内部原理章节git对象、git引用所做记录。
我们时常使用到的git命令以及流程都如下图
- workspace:工作区
- staging/index area:暂存区/缓存区
- local repository:版本库或本地仓库
- remote repository:远程仓库
git项目初探
我们只需要执行
bash
git init
就可以在当前目录创建一个.git
目录,它包含了几乎所有GIT存储和操作的对象。 此时我们就可以查看.git
文件夹包含的所有内容。
bash
cd .git
ls -F
HEAD
config
hooks/
objects/
branches/
description
info/
refs/
其中,HEAD
简单讲就是当前所在分支;config
是项目中的配置选项文件;hooks
存储的是钩子脚本文件;objects
存储所有数据内容;branches
存储的是分支信息;description
通常用作项目描述的文本可选文件;info
目录包含一个exclude
全局排除性的文件,放置的是不被.gitignore
所记录的忽略模式;refs
目录存储指向数据(分支)的提交对象的指针;
git对象
Git 是一个内容寻址文件系统。核心部分是一个简单的键值对数据库(key-value data store)。向数据库插入内容后会返回一个键值,通过该键值又可以检索(retrieve)到插入的内容。
我们可以使用git hash-object
命令来进行演示。
git hash-object
计算一个文件的git对象ID,即SHA1的哈希值进行输出,并将该对象写入数据库中。
bash
echo 'test content' | git hash-object -w --stdin
d670460b4b4aece5915caf5c68d12f560a9fe3e4
-w:将对象写入对象数据库。
-stdin:表示从标准输入读取,而不是从本地文件读取。
d670460b4b4aece5915caf5c68d12f560a9fe3e4
是一个 SHA-1 哈希值------一个将待存储的数据外加一个头部信息(header)一起做 SHA-1 校验运算而得的校验和。
前文中提到,objects
存储所有数据内容。此时我们可以查看objects
内容。
bash
find .git/objects -type f
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4
可以看到这就是git存储内容的方式,一个文件对应一条内容,加上特定的头部信息一起组成的SHA-1校验和为文件命名。校验和的前两个字符用作子目录,后38个字符做文件名。我们可以通过git cat-file读取显示这个对象的内容。
bash
git cat-file d670460b4b4aece5915caf5c68d12f560a9fe3e4 -p
test content
git cat-file
命令显示一个Git对象文件的内容。
-
p:参数表示以易于阅读的格式显示。
-
t:显示该对象的type而不是内容。
此时我们用相同的操作向文件中输入新内容并存入git数据库
bash
echo 'version 1' > test.txt
git hash-object -w test.txt
83baae61804e65cc73a7201a7252750c76066a30
# 重复输入新内容并存入git数据库
echo 'version 2' > test.txt
git hash-object -w test.txt
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
此时我们便完成了一次对test.txt的版本的更新,git会记录下不同版本的信息。
bash
find .git/objects -type f
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30
# 此时我们可以查看不同版本的内容
git cat-file 83baae61804e65cc73a7201a7252750c76066a30 -p
version 1
git cat-file 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a -p
version 2
上述的git对象都称之为blob
对象,我们可以使用如下命令通过传递SHA-1的值来查看该对象类型
bash
git cat-file 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a -t
blob
而前文中提到的SHA-1的值是由头部信息(header)和待存储数据的内容一起SHA-1 校验运算而得到的。其中header信息就是特定的带有存储数据对应类型格式的的文本。
但现在存在的问题是我们不可能记录每一个SHA-1的值;且当前的方式并没有存储对应文件的名字信息而只存了内容。
树对象
树对象(tree object)可以解决没有存储文件名的问题。git中所有的数据均以树对象和数据对象的形式存储。树对象一般对应为目录项,数据对象则大致上对应了 inodes 或文件内容(对比UNIX操作系统,构通常使用inodes-索引节点来表示文件和目录,每个文件或目录都有一个对应的inode)。
通常,Git 根据某一时刻暂存区(即 index 区域,下同)所表示的状态创建并记录一个对应的树对象,如此重复便可依次记录(某个时间段内)一系列的树对象。 因此,为创建一个树对象,首先需要通过暂存一些文件来创建一个暂存区。 可以通过底层命令 update-index
为一个单独文件------我们的 test.txt 文件的首个版本------创建一个暂存区。 利用该命令,可以把 test.txt 文件的首个版本人为地加入一个新的暂存区。 git update-index
将工作区的文件加入缓存区
bash
git update-index --add --cacheinfo <mode>,<sha1>,<path>
--add:如果指定的文件不在index(staging)缓存区中,则添加该文件。默认行为是忽略新文件。
--cacheinfo <mode> <object> <path>:直接将指定的信息插入索引。
-mode: 10064
,表示一个普通文件;100755
,表示一个可执行文件;120000
,表示一个符号链接。
-object: git对象
bash
git update-index --add --cacheinfo 100644 \
83baae61804e65cc73a7201a7252750c76066a30 test.txt
此时我们便可以通过 git write-tree
命令用于根据当前缓存区域,生成一个树对象。
- p: 每一个
-p
代表了父提交对象的id。
-m: 提交信息。
bash
git write-tree
d8329fc1cc938780ffdd9f94e0d364e0ea74f579
git cat-file -p d8329fc1cc938780ffdd9f94e0d364e0ea74f579
100644 blob 83baae61804e65cc73a7201a7252750c76066a30 test.txt
# 查看类型
git cat-file -t d8329fc1cc938780ffdd9f94e0d364e0ea74f579
tree
接着我们来创建一个新的树对象,它包括 test.txt 文件的第二个版本,以及一个新的文件:
sql
echo 'new file' > new.txt
git update-index --cacheinfo 100644 \
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt
git update-index test.txt
git update-index --add new.txt
现在暂存区包含了 test.txt 文件的新版本,和一个新文件:new.txt。我们使用高级命令git status
可以查看
bash
git status
On branch master
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: new.txt
new file: test.txt
接着将其当前的暂存区写入树对象
bash
git write-tree
0155eb4229851634a0f03eb265b69f5a2d56f341
同时,我们也可以将第一个树对象加到新的树对象中,作为其子树
git-read-tree
命令将树信息读入当前暂存区。
- prefix=<prefix>:读取目录下的命名树的内容。
bash
git read-tree --prefix=bak d8329fc1cc938780ffdd9f94e0d364e0ea74f579
git write-tree
3c4e9cd789d88d8d89c1073707c3585e41b0e614
git cat-file 3c4e9cd789d88d8d89c1073707c3585e41b0e614 -p
040000 tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579 bak
100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new.txt
100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt
可以认为Git存储上述数据的结构如下图
提交对象
目前我们的树对象仍然是SHA-1哈希值进行记录的。其次,我们并不知道是谁保存了这些快照,在什么时刻保存的,以及为什么保存这些快照。而以上这些,正是提交对象(commit object)能为你保存的基本信息。 我们可以通过commit-tree
命令创建一个提交对象,并传入tree的SHA---1值。
bash
echo "first commit" | git commit-tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579
83eef29acb18e5b5b3a607b9887829ac6fef4110
# 查看类型
git cat-file 83eef29acb18e5b5b3a607b9887829ac6fef4110 -p
tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579
author QiTao Tang <email> 1709799689 +0800
committer QiTao Tang <email> 1709799689 +0800
first commit
接下来我们再将另外两个树对象进行提交,并分别指定其父提交对象为前一个提交对象。
bash
echo "second commit" | git commit-tree 0155eb4229851634a0f03eb265b69f5a2d56f341 -p 83eef29acb18e5b5b3a607b9887829ac6fef4110
d74386019cd3faddea160d56d983e8dda37afbc9
# 沿用上一个提交对象为父对象
echo "third commit" | git commit-tree 3c4e9cd789d88d8d89c1073707c3585e41b0e614 -p d74386019cd3faddea160d56d983e8dda37afbc9
3f7f45c7ac907952572171979cfc04dcb170cbe5
现在我们可以查看一下提交记录
bash
git log 3f7f45c7ac907952572171979cfc04dcb170cbe5
commit 3f7f45c7ac907952572171979cfc04dcb170cbe5
Author: QiTao Tang <email>
Date: Thu Mar 7 16:32:01 2024 +0800
third commit
commit d74386019cd3faddea160d56d983e8dda37afbc9
Author: QiTao Tang <email>
Date: Thu Mar 7 16:31:03 2024 +0800
second commit
commit 83eef29acb18e5b5b3a607b9887829ac6fef4110
Author: QiTao Tang <email>
Date: Thu Mar 7 16:21:29 2024 +0800
first commit
到此时,我们没有借助任何上层命令,仅凭几个底层操作便完成了一个 Git 提交历史的创建。 这就是每次我们运行 git add
和 git commit
命令时, Git 所做的实质工作------将被改写的文件保存为数据对象,更新暂存区,记录树对象,最后创建一个指明了顶层树对象和父提交的提交对象。这三种主要的 Git 对象------数据对象、树对象、提交对象------最初均以单独文件的形式保存在 .git/objects
目录下。 下面列出了目前示例目录内的所有对象,辅以各自所保存内容的注释:
bash
find .git/objects -type f
.git/objects/3c/4e9cd789d88d8d89c1073707c3585e41b0e614 # tree3
.git/objects/d7/4386019cd3faddea160d56d983e8dda37afbc9 # second commit
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 # test content
.git/objects/d8/329fc1cc938780ffdd9f94e0d364e0ea74f579 # tree1
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a # test.txt version 2
.git/objects/3f/7f45c7ac907952572171979cfc04dcb170cbe5 # third commit
.git/objects/01/55eb4229851634a0f03eb265b69f5a2d56f341 # tree2
.git/objects/fa/49b077972391ad58037050f2a75f74e3671e92 # new.txt
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30 # test.txt version 1
.git/objects/83/eef29acb18e5b5b3a607b9887829ac6fef4110 # first commit
上述大致关系图如下(图来自Pro git。commit SHA-1 可能对不上,主要看关系):
GIT引用
如上文,git log 3f7f45c7ac907952572171979cfc04dcb170cbe5
可以浏览完整的提交历史,但仍然需要记住SHA-1值,所以在git中使用refs文件进行保存SHA-1值。我们可以通过git update-ref
命令更新引用文件
bash
git update-ref refs/heads/master 3f7f45c7ac907952572171979cfc04dcb170cbe5
# 此时再次执行以下命令,可以得到与git log 3f7f45c7ac907952572171979cfc04dcb170cbe5相同的结果
git log master
git branch命令
这基本就是 Git 分支的本质:一个指向某一系列提交之首的指针或引用。
当我们执行git branch
命令时候其实就是使用update-ref
并取得当前分支最新的SHA-1值进行创建引用。我们不妨试一下
bash
# 使用update-ref对第二次提交创建引用
git update-ref refs/heads/second-branch d74386019cd3faddea160d56d983e8dda37afbc9
# 使用git branch命令创建引用
git branch branch-command
此时我们分别使用git branch
和update-ref
命令创建了两个引用,它们都将被保存在refs/heads
下,查看如下
bash
find .git/refs/heads
.git/refs/heads
.git/refs/heads/second-branch
.git/refs/heads/master
.git/refs/heads/branch-command
git checkout
上面提到git branch
命令会取得当前分支最新的SHA-1值,而如何知道当前分支便是通过HEAD文件引用。我们可以通过git symbolic-ref <name> <ref>
对 查看HEAD引用文件
bash
# 此命令会相对于 .git 文件夹位置查找
git symbolic-ref HEAD
refs/heads/master
当然我们也可以传入第二个ref参数,进行修改当前
bash
git symbolic-ref HEAD refs/heads/branch-command
git symbolic-ref HEAD
refs/heads/branch-command
而git checkout
命令本质上便是修改HEAD的引用。
bash
git checkout master
git symbolic-ref HEAD
refs/heads/master
参考
- Pro Git 中文版(第二版) - 本文更像是学习记录,Git内部原理章节。
- 阮一峰的Git教程 - 入门推荐。
- Git - Reference (git-scm.com) - api参考。