Git操作基本原理

本文主要是学习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 addgit 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 branchupdate-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

参考

  1. Pro Git 中文版(第二版) - 本文更像是学习记录,Git内部原理章节。
  2. 阮一峰的Git教程 - 入门推荐。
  3. Git - Reference (git-scm.com) - api参考。
相关推荐
但老师41 分钟前
Git遇到“fatal: bad object refs/heads/master - 副本”问题的解决办法
git
秃头女孩y43 分钟前
git创建分支
git
研究是为了理解6 小时前
Git Bash 常用命令
git·elasticsearch·bash
DKPT6 小时前
Git 的基本概念和使用方式
git
Winston Wood9 小时前
一文了解git TAG
git·版本控制
喵喵先森10 小时前
Git 的基本概念和使用方式
git·源代码管理
xianwu54311 小时前
反向代理模块
linux·开发语言·网络·git
binishuaio13 小时前
Java 第11天 (git版本控制器基础用法)
java·开发语言·git
会发光的猪。14 小时前
如何在vscode中安装git详细新手教程
前端·ide·git·vscode
stewie616 小时前
在IDEA中使用Git
java·git