likes
comments
collection
share

Git操作基本原理

作者站长头像
站长
· 阅读数 2

本文主要是学习Pro git中git内部原理章节git对象、git引用所做记录。

我们时常使用到的git命令以及流程都如下图

Git操作基本原理

  • workspace:工作区
  • staging/index area:暂存区/缓存区
  • local repository:版本库或本地仓库
  • remote repository:远程仓库

git项目初探

我们只需要执行

git init

就可以在当前目录创建一个.git目录,它包含了几乎所有GIT存储和操作的对象。 此时我们就可以查看.git文件夹包含的所有内容。

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的哈希值进行输出,并将该对象写入数据库中。

echo 'test content' | git hash-object -w --stdin
d670460b4b4aece5915caf5c68d12f560a9fe3e4

-w:将对象写入对象数据库。 -stdin:表示从标准输入读取,而不是从本地文件读取。

d670460b4b4aece5915caf5c68d12f560a9fe3e4是一个 SHA-1 哈希值——一个将待存储的数据外加一个头部信息(header)一起做 SHA-1 校验运算而得的校验和。 前文中提到,objects存储所有数据内容。此时我们可以查看objects内容。

find .git/objects -type f
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4

可以看到这就是git存储内容的方式,一个文件对应一条内容,加上特定的头部信息一起组成的SHA-1校验和为文件命名。校验和的前两个字符用作子目录,后38个字符做文件名。我们可以通过git cat-file读取显示这个对象的内容。

git cat-file d670460b4b4aece5915caf5c68d12f560a9fe3e4 -p
test content

git cat-file命令显示一个Git对象文件的内容。 - p:参数表示以易于阅读的格式显示。 - t:显示该对象的type而不是内容。

此时我们用相同的操作向文件中输入新内容并存入git数据库

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会记录下不同版本的信息。

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的值来查看该对象类型

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 将工作区的文件加入缓存区

git update-index --add --cacheinfo <mode>,<sha1>,<path>

--add:如果指定的文件不在index(staging)缓存区中,则添加该文件。默认行为是忽略新文件。 --cacheinfo <mode> <object> <path>:直接将指定的信息插入索引。 -mode: 10064,表示一个普通文件;100755,表示一个可执行文件;120000,表示一个符号链接。 -object: git对象

git update-index --add --cacheinfo 100644 \
  83baae61804e65cc73a7201a7252750c76066a30 test.txt

此时我们便可以通过 git write-tree命令用于根据当前缓存区域,生成一个树对象。 - p: 每一个 -p 代表了父提交对象的id。 -m: 提交信息。

git write-tree
d8329fc1cc938780ffdd9f94e0d364e0ea74f579
git cat-file -p d8329fc1cc938780ffdd9f94e0d364e0ea74f579
100644 blob 83baae61804e65cc73a7201a7252750c76066a30      test.txt
# 查看类型
git cat-file -t d8329fc1cc938780ffdd9f94e0d364e0ea74f579
tree

接着我们来创建一个新的树对象,它包括 test.txt 文件的第二个版本,以及一个新的文件:

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可以查看

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

接着将其当前的暂存区写入树对象

git write-tree
0155eb4229851634a0f03eb265b69f5a2d56f341

同时,我们也可以将第一个树对象加到新的树对象中,作为其子树

git-read-tree命令将树信息读入当前暂存区。 - prefix=<prefix>:读取目录下的命名树的内容。

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存储上述数据的结构如下图 Git操作基本原理

提交对象

目前我们的树对象仍然是SHA-1哈希值进行记录的。其次,我们并不知道是谁保存了这些快照,在什么时刻保存的,以及为什么保存这些快照。而以上这些,正是提交对象(commit object)能为你保存的基本信息。 我们可以通过commit-tree命令创建一个提交对象,并传入tree的SHA—1值。

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

接下来我们再将另外两个树对象进行提交,并分别指定其父提交对象为前一个提交对象。

echo "second commit" | git commit-tree 0155eb4229851634a0f03eb265b69f5a2d56f341 -p 83eef29acb18e5b5b3a607b9887829ac6fef4110
d74386019cd3faddea160d56d983e8dda37afbc9

# 沿用上一个提交对象为父对象
echo "third commit" | git commit-tree 3c4e9cd789d88d8d89c1073707c3585e41b0e614 -p d74386019cd3faddea160d56d983e8dda37afbc9
3f7f45c7ac907952572171979cfc04dcb170cbe5

现在我们可以查看一下提交记录

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 目录下。 下面列出了目前示例目录内的所有对象,辅以各自所保存内容的注释:

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引用

如上文,git log 3f7f45c7ac907952572171979cfc04dcb170cbe5可以浏览完整的提交历史,但仍然需要记住SHA-1值,所以在git中使用refs文件进行保存SHA-1值。我们可以通过git update-ref命令更新引用文件

git update-ref refs/heads/master 3f7f45c7ac907952572171979cfc04dcb170cbe5

# 此时再次执行以下命令,可以得到与git log 3f7f45c7ac907952572171979cfc04dcb170cbe5相同的结果
git log master

git branch命令

这基本就是 Git 分支的本质:一个指向某一系列提交之首的指针或引用。 当我们执行git branch命令时候其实就是使用update-ref并取得当前分支最新的SHA-1值进行创建引用。我们不妨试一下

# 使用update-ref对第二次提交创建引用
git update-ref refs/heads/second-branch d74386019cd3faddea160d56d983e8dda37afbc9
# 使用git branch命令创建引用
git branch branch-command

此时我们分别使用git branchupdate-ref命令创建了两个引用,它们都将被保存在refs/heads下,查看如下

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引用文件

# 此命令会相对于 .git 文件夹位置查找
git symbolic-ref HEAD
refs/heads/master

当然我们也可以传入第二个ref参数,进行修改当前

git symbolic-ref HEAD refs/heads/branch-command
git symbolic-ref HEAD          

refs/heads/branch-command

git checkout命令本质上便是修改HEAD的引用。

git checkout master
git symbolic-ref HEAD

refs/heads/master

参考

  1. Pro Git 中文版(第二版) - 本文更像是学习记录,Git内部原理章节。
  2. 阮一峰的Git教程 - 入门推荐。
  3. Git - Reference (git-scm.com) - api参考。