Git核心原理
1 Git简介
什么是Git?首先来一段官方介绍:
Git is a free and open source distributed version control system designed to handle everything from small to very large projects with speed and efficiency.
Git(读音为/gɪt/)是一个开源的分布式版本控制系统,可以有效、高速地处理从很小到非常大的项目版本管理。
看来要了解什么是Git,首先我们要了解什么是版本控制了。
版本控制是一种记录一个或若干文件内容变化,以便将来查阅特定版本修订情况的系统。
由于现代软件开发项目具有周期长,参与人员多的特点,版本控制便成了现代软件开发过程中必不可缺的一部分。通过版本控制我们可以轻松实现:
- 优雅备份
- 多人协作
1.1 版本控制系统
1.1.1 集中式版本控制系统
在一个单一的服务器中保存所有文件的修订版本,协同工作的人们通过客户端连接到服务器,获取最新的文件或者提交更新。
- 单点故障
- 数据丢失
如图所示,完整的版本信息保存在中央服务器中,而每个客户端所保存的只是某个版本的内容。
1.1.2 分布式版本控制系统
每一个客户端保存完整的代码仓库镜像。如果服务器发生了故障,就可以用本地仓库的镜像恢复。每一次克隆操作其实是对整个版本库的完整备份。
如图所示,每一个客户端中都保存有版本信息。
1.2 Git的特点
Git 特点:1.记录快照而非比较差异;2.几乎所有操作都是本地执行;3.保证完整性;4.一般只添加数据
- 记录快照而非比较差异
以文件变更列表的形式存储信息:保存一组基本文件和每个文件积累的差异,当需要查看某个版本的数据时,通过计算得出完整的文件内容。
以快照流的方式存储信息:把数据当作是对小型文件系统的一组快照,每个版本都保存完整的文件信息。为了提高效率,如果文件没有修改,Git 不再重新存储该文件,而是只保留一个链接指向之前存储的文件。
- 几乎所有操作都是本地执行
可以在没有网络的情况下查看版本信息,提交版本信息,只不过在多人协作的场景下,不一定能同步最新的版本信息。 - 保证完整性
在存储数据的时候,Git会使用SHA-1散列机制根据文件内容或者目录结构计算出校验和,并以校验合引用。如果在传送过程中信息发生丢失或者改变,Git就能发现。 - 一般只添加数据
几乎所有的Git操作都是在往Git数据库中添加信息。一旦将数据推送到Git仓库,就很难再丢失数据。即使是“回退”等操作,也不是将不要的commit删除,所有的commit信息仍然保存在数据库中。
2 Git 核心原理
2.1 Git区域
Git 大致可以划分为三个工作区域:Git仓库、工作目录、暂存区。Git管理的文件会处于三种状态之一:已提交(committed)、已修改(modified)和已暂存(staged)。
三种状态:已提交(committed)、已修改(modified)和已暂存(staged)
1.已提交:数据已保存在数据库中。
2.已暂存:对已修改文件的当前版本做了标记,使之包含在下一次提交的快照中。
3.已修改:文件已被修改,但是尚未保存到数据库中。
三个区域:Git 仓库、工作目录以及暂存区域
Git仓库:Git 用来保存项目的元数据和对象数据库的地方。
工作目录:对项目的某个版本独立提取出来的内容。
暂存区:一个保存了下次将提交的文件列表信息的文件。
git status 显示工作目录和暂存区的状态
下面为新建三个文件a.txt,b.txt,c.txt后add b.txt,c.txt,随后修改c.txt后执行git status得到的结果
git status
# On branch master
#
# Initial commit
#
# Changes to be committed:
# (use "git rm --cached <file>..." to unstage)
#
# new file: b.txt
# new file: c.txt
#
# 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: c.txt
#
# Untracked files:
# (use "git add <file>..." to include in what will be committed)
#
# a.txt
其实更为准确地说该命令是展示文件在三个工作区域中的差异。仅在工作目录中,不在暂存区的文件状态为Untracked files,表示未被Git跟踪;在工作区和暂存区中都存在但是内容不同为Changes not staged for commit,在工作区和暂存区中都相同但是不在数据库中则为Changes to be committed,表示待提交。我们可以根据Git的提示,使用不同的命令撤回操作,如git checkout -- <file>
,撤销工作区中的修改,将其恢复到暂存区中的版本。
2.2 Git 目录
当我们在一个目录中执行git init,Git会创建一个.git目录,几乎所有的Git对象都会存储在这个目录中,我们后续也会沿着目录中的内容展开讲解。
以下是在一个空目录中执行git init
后生成的.git
文件夹中的内容。
[root@vm-20-8-centos git-demo]# git init
Initialized empty Git repository in /usr/local/git-demo/.git/
[root@vm-20-8-centos git-demo]# tree .git
.git
|-- branches
|-- config
|-- description
|-- HEAD
|-- hooks
| |-- applypatch-msg.sample
| |-- commit-msg.sample
| |-- post-update.sample
| |-- pre-applypatch.sample
| |-- pre-commit.sample
| |-- prepare-commit-msg.sample
| |-- pre-push.sample
| |-- pre-rebase.sample
| `-- update.sample
|-- info
| `-- exclude
|-- objects
| |-- info
| `-- pack
`-- refs
|-- heads
`-- tags
下面对主要目录进行介绍:
- hooks:Git钩子脚本。在特定的重要动作发生时触发自定义脚本。有两组钩子:客户端的和服务器端的。客户端钩子由诸如提交和合并这样的操作所调用,而服务器端钩子作用于诸如接收被推送的提交这样的联网操作。
- info
- exclude:全局性排除文件,放置不希望被记录在.gitignore文件中的忽略模式 。.gitignore配置文件会提交到仓库,所以常用于对公共文件的控制。exclude文件的修改不会被提交到仓库。
- objects:存储所有的数据内容,此文件夹中的内容大多是不可变的 。里面存放着三种git object。
- refs:存放一系列引用。包括heads 本地分支,remote 远程分支,tags 标签。
- config:项目配置文件。Git一共有三个级别的配置,分别是 --system系统级, --global用户级和仓库级。git config -l
- HEAD:当前检出的分支
- index:暂存区信息。使用git ls-files --stage 查看该文件内容
- description:⽤于GitWeb程序,显示对仓库的描述。
git help gitrepository-layout 查看官方目录说明
2.3 Git Object
我们先来看一下objects文件夹中的内容。
[root@vm-20-8-centos git-demo]# tree .git/objects
.git/objects
|-- 08
| `-- fe7365fb4dc9e21ea09747e2b81d6470895dd5
|-- 0c
| `-- ca7e1d6fb5a88ccbf8430fa2ba841edcf70ec6
|-- 37
| `-- 057b2e8a9041ef88b805a5b7c4e0e668a03be4
|-- 72
| `-- 943a16fb2c8f38f9dde202b7a70ccc19c52f34
|-- cc
| `-- c3e7b48da0932cc0f7c4ce7b4fd834c7032fe1
|-- ee
| `-- da65ae8f26bbc7beb16283653afcd5f29a190b
|-- info
`-- pack
.git/objects/目录下存放着三种Git object,分别是数据对象(blob),树对象(tree objec),提交对象(commit object)。
数据对象(blob):内容无固定格式,比较简单。存放数据内容。
树对象(tree object):树对象描述了工作区的内容。存储的信息包括指向数据对象或者子树对象的指针,以及相应的模式、类型、文件名信息。
提交对象(commit object):在 Git 中提交时,会保存一个提交(commit)对象,该对象包含一个指向暂存内容快照的指针,包含本次提交的作者等相关附属信息,包含零个或多个指向该提交对象的父对象指针:首次提交是没有直接祖先的,普通提交有一个祖先,由两个或多个分支合并产生的提交则有多个祖先。
git cat-file
-p 查看object内容
-t 查看对象类型
Blob
[root@vm-20-8-centos git-demo]# git cat-file -p 7294
aaa
可以看到blob的内容就是文件的内容。
Tree
[root@vm-20-8-centos git-demo]# git cat-file -p 31fd
100644 blob ccc3e7b48da0932cc0f7c4ce7b4fd834c7032fe1 a.txt
040000 tree b937120dc3951277017602a630e50b0a271f6182 next
[root@vm-20-8-centos git-demo]# git cat-file -p b937
100644 blob f761ec192d9f0dca3329044b96ebdb12839dbff6 b.txt
每一行从左到右依次是:文件模式、对象类型、对象的SHA-1指针、文件/目录名。
Git的文件模式参考了常见的 UNIX 文件模式:
100644:表明这是一个普通文件。(blob对象的文件模式一般都为100644)
100755:表示一个可执行文件。
120000:表示一个符号链接。
tree中的文件类型只会是blob或者tree。
Commit
[root@vm-20-8-centos git-demo]# git cat-file -p 08fe
tree 0cca7e1d6fb5a88ccbf8430fa2ba841edcf70ec6
parent eeda65ae8f26bbc7beb16283653afcd5f29a190b
author XianYu <1468399787@qq.com> 1666013606 +0800
committer XianYu <1468399787@qq.com> 1666013606 +0800
2.4 对象存储
文件名的生成:将头部信息和原始数据拼接起来,并计算出这条新内容的 SHA-1 校验和,得到一个长度为40个十六进制数组成的字符串。其中头部信息=对象类型(如blob)+空格+内容长度+空字节(null byte)。
SHA-1是一种数据加密算法,该算法的思想是接收一段明文,然后以一种不可逆的方式将它转换成一段(通常更小)密文,也可以简单的理解为取一串输入码(称为预映射或信息),并把它们转化为长度较短、位数固定的输出序列即散列值(也称为信息摘要或信息认证代码)的过程。
一旦数据内容发生改变,生成的SHA-1值就会发生改变。
随后Git将这条新内容通过Zlib压缩,并写入到磁盘中,以SHA-1的前两个字符为子目录名称,剩下的字符为文件名。
3 Git引用
在大多数时候我们并不想记住这一长串无规则的字符串,所以我们需要一个文件来保存 SHA-1 值,并给文件起一个简单 的名字,然后用这个名字指针来替代原始的 SHA-1 值。 这就是Git引用,它们被保存在.git/refs文件夹中。Git中的引用大致可以分为三种类型,分支、标签以及HEAD。
D:.
├─heads
│ feature_221012_ant
│ master
│
├─remotes
│ └─origin
│ │ feature_221008_refund
│ │ feature_221012_ant
│ │ HEAD
│ │ master
│ │
│ └─release
│ 20220822-175752501755152_release_1644114_55
│ 20220822-175820569111328_release_1644114_55
│
└─tags
tag_2022-09-06-16-42-26
tag_2022-09-22-19-52-53
3.1 分支
我们可以看到,refs目录中有两个文件夹,分别是heads和remote,它们存储着不同的类型的分支。
- heads:本地分支
- remote:远程分支
我们可以查看一下分支文件中的内容:
PS D:\IDEAPoject\unified-proxy-server\.git\refs> cat .\heads\feature_221012_ant
d8703d9b794305fe10c924853bab7c56d84f020b
这一串字符看上去有点眼熟是不是?我们可以查看一下提交记录。
git log 查看历史提交记录
PS D:\IDEAPoject\unified-proxy-server\.git\refs> git log --pretty=oneline feature_221012_ant -5
d8703d9b794305fe10c924853bab7c56d84f020b (HEAD -> feature_221012_ant, origin/feature_221012_ant) feat:升级api版本号
959ace250e57b7dd4a8a91427385fbab280fcf27 feat:****接口
1ab30f1f95bd2bf29fb73aadc236d75cb9ce66b1 ***进件
ec736f01d55d260afa7a1b8da02548824cdec74d feat:***接口
286552fcf9cf17a65efab13acc5c95e7c0b39a0c feat:***对接
我们发现,feature_221012_ant中存储的内容就是最新提交的commit id。通过cat-file查看该id的内容,便可以看到提交信息。
PS D:\IDEAPoject\unified-proxy-server\.git\refs> git cat-file -p d8703
tree cc4fa320f69ee511d9e2ba47103c9bc0771d3780
parent 959ace250e57b7dd4a8a91427385fbab280fcf27
author lu**wei <lu**wei@zj.tech> 1665995722 +0800
committer lu**wei <lu**wei@zj.tech> 1665995722 +0800
feat:升级api版本号
由上内容,我们不难得出Git 分支的本质:一个指向某一系列提交之首的指针或引用。由此不难理解,当我们使用git branch创建一个分支的时候,Git实际上是创建了一个指向当前提交对象的可移动的指针。
—— 创建一个新分支就相当于往一个文件中写入 41 个字节(40 个字符和 1 个换行符),如此而已。
所以在 Git 中,任何规模的项目都能在瞬间创建新分支。 同时,由于每次提交都会记录父对象,所以寻找恰当的合并基础(即共同祖先)也是同样的简单和高效。
并且,当产生新的提交时,分支指针会自动移动,指向最新的提交对象。
3.2 HEAD
新的问题来了,当执行 git branch (branchname) 时,Git 如何知道最新提交的 SHA-1 值呢?答案是 HEAD 文件。HEAD 文件是一个符号引用(symbolic reference),指向目前所在的分支。所谓符号引用,意味着它并不像普通引用那样包含一个 SHA-1 值——它是一个指向其他引用的指针。
我们可以看一下HEAD文件的内容:
PS D:\IDEAPoject\unified-proxy-server\.git> cat HEAD
ref: refs/heads/feature_221012_ant
HEAD文件格式形如:ref:xxxx。可以看到,它保存着目前所在的分支的名称。
当执行git checkout 命令切换分支的时候,实际上是修改HEAD文件的内容;而当执行git commit的时候,会创建一个提交对象,并用 HEAD 文件中那个引用所指向的 SHA-1 值设置其父提交字段。
3.3 标签引用
前文我们刚讨论过 存放在objects文件夹下的三种主要对象类型,事实上还有第四种。标签对象(tag object)非常类似于一个提交对象——它包含一个标签创建者信息、一个日期、一段注释信息,以及一个指针。主要的区别在于,标签对象通常指向一个提交对象,而不是一个树对象。它像是一个永不移动的分支引用——永远指向同一个提交对象,只不过给这个提交对象加上一个更友好的名字罢了。
来看一下tag文件中的内容:
PS D:\IDEAPoject\unified-proxy-server\.git> cat .\refs\tags\tag_2022-08-15-22-47-55
b8839b545f83f33a67d82a99eb18deee33bc1304
我们得到了一串id,那么这一串id又是什么呢?
PS D:\IDEAPoject\unified-proxy-server\.git> git cat-file -t b8839
commit
可以看到,tag文件中的内容就是一个commit的指针。
标签又分为普通标签和附注标签。普通标签就是如上所展示的,它只是一个固定的引用。而附注标签除了指针之外,还包含了标签的说明信息。创建一个附注标签时,Git会创建一个标签对象,并记录一个引用来指向该标签对象,而不是直接指向提交对象。
可以通过git tag -a
创建一个附注标签:
[root@vm-20-8-centos git-demo]# git tag -a v1.1 50297f184aec540d9bd998857f2ac4b1164e9f34 -m 'first tag'
查看该tag的类型和内容:
[root@vm-20-8-centos git-demo]# git cat-file -t c6e031
tag
[root@vm-20-8-centos git-demo]# git cat-file -p c6e031
object 50297f184aec540d9bd998857f2ac4b1164e9f34
type commit
tag v1.1
tagger XianYu <1468399787@qq.com> 1666097176 +0800
first tag
4 底层命令
由于 Git 最初是一套面向版本控制系统的工具集,而不是一个完整的、用户友好的版本控制系统,所以它还包含了一部分用于完成底层工作的命令,称之为底层命令(plumbing)。我们日常使用到的例如checkout,add,commit,push这种命令我们称其为高层命令(porcelain)。当执行一个高层命令时,Git实际上是执行了一系列的底层命令。
4.1 git add
git add 将文件添加到暂存区
- 生成文件对应的blob object
git hash-object -w fileName
#该命令输出一个长度为40字符的校验和。是通过对头部信息和内容进行SHA-1校验运算得出的校验和。
#-w表示存储数据对象,若无次此参数,则该命令仅仅返回对应的键值。
[root@vm-20-8-centos git-demo]# echo 'aaa' > a.txt
[root@vm-20-8-centos git-demo]# git hash-object -w a.txt
72943a16fb2c8f38f9dde202b7a70ccc19c52f34
[root@vm-20-8-centos git-demo]# tree .git/objects/
.git/objects/
|-- 72
| `-- 943a16fb2c8f38f9dde202b7a70ccc19c52f34
|-- info
`-- pack
3 directories, 1 file
[root@vm-20-8-centos git-demo]# git cat-file -p 7294
aaa
由于Git生成校验和时主要根据内容来生成,并且blob对象并不会记录文件名,所以如果内容相同而文件名不同的情况下,Git并不会重新生成数据对象。
[root@vm-20-8-centos git-demo]# echo 'aaa' > b.txt
[root@vm-20-8-centos git-demo]# echo 'aaa' > c.txt
[root@vm-20-8-centos git-demo]# git hash-object -w b.txt
72943a16fb2c8f38f9dde202b7a70ccc19c52f34
[root@vm-20-8-centos git-demo]# git hash-object -w c.txt
72943a16fb2c8f38f9dde202b7a70ccc19c52f34
[root@vm-20-8-centos git-demo]# tree .git/objects/
.git/objects/
|-- 72
| `-- 943a16fb2c8f38f9dde202b7a70ccc19c52f34
|-- info
`-- pack
3 directories, 1 file
- 更新暂存区
git update-index --add --cacheinfo <mode> <object> <path>
#update-index 修改索引或目录缓存。
#–add 向暂存区中添加新文件
#–cacheinfo 直接将指定的信息插入索引。
将步骤1中新建的文件更新到暂存区:
[root@vm-20-8-centos git-demo]# git update-index --add --cacheinfo 100644 72943a16fb2c8f38f9dde202b7a70ccc19c52f34 a.txt
[root@vm-20-8-centos git-demo]# git update-index --add --cacheinfo 100644 72943a16fb2c8f38f9dde202b7a70ccc19c52f34 b.txt
[root@vm-20-8-centos git-demo]# git update-index --add --cacheinfo 100644 72943a16fb2c8f38f9dde202b7a70ccc19c52f34 c.txt
[root@vm-20-8-centos git-demo]# git ls-files
a.txt
b.txt
c.txt
[root@vm-20-8-centos git-demo]# git ls-files -s
100644 72943a16fb2c8f38f9dde202b7a70ccc19c52f34 0 a.txt
100644 72943a16fb2c8f38f9dde202b7a70ccc19c52f34 0 b.txt
100644 72943a16fb2c8f38f9dde202b7a70ccc19c52f34 0 c.txt
4.2 git commit
- 根据暂存区内容生成tree对象
git write-tree
# 使用当前索引创建树对象。新树对象的名称被打印到标准输出。
[root@vm-20-8-centos git-demo]# git write-tree
0c58b56cf28846b55c20db49447e8836e62fe54b
[root@vm-20-8-centos git-demo]# git cat-file -t 0c58
tree
[root@vm-20-8-centos git-demo]# git cat-file -p 0c58
100644 blob 72943a16fb2c8f38f9dde202b7a70ccc19c52f34 a.txt
100644 blob 72943a16fb2c8f38f9dde202b7a70ccc19c52f34 b.txt
100644 blob 72943a16fb2c8f38f9dde202b7a70ccc19c52f34 c.txt
- 生成commit对象
git commit-tree <tree> [(-p <parent>)…] [(-m <message>)…]
#基于提供的树对象创建一个新的提交对象,输出生成的提交对象的id
#可指定 父提交对象 必须指定提交信息
[root@vm-20-8-centos git-demo]# git commit-tree 0c58 -m 'first commit'
f5a767fccb416e145dd5ef4fea6fcfea592fc45e
[root@vm-20-8-centos git-demo]# git cat-file -t f5a767
commit
[root@vm-20-8-centos git-demo]# git cat-file -p f5a767
tree 0c58b56cf28846b55c20db49447e8836e62fe54b
author XianYu <1468399787@qq.com> 1666100555 +0800
committer XianYu <1468399787@qq.com> 1666100555 +0800
first commit
- 更新分支使其指向最新的提交对象
git update-ref
更新引用
[root@vm-20-8-centos git-demo]# git update-ref refs/heads/master f5a767
[root@vm-20-8-centos git-demo]# cat .git/refs/heads/master
f5a767fccb416e145dd5ef4fea6fcfea592fc45e
[root@vm-20-8-centos git-demo]# git log master
commit f5a767fccb416e145dd5ef4fea6fcfea592fc45e
Author: XianYu <1468399787@qq.com>
Date: Tue Oct 18 21:42:35 2022 +0800
first commit
4.3 分支
- 创建分支
[root@vm-20-8-centos git-demo]# git update-ref refs/heads/test f5a767
[root@vm-20-8-centos git-demo]# ll .git/refs/heads/
total 8
-rw-r--r-- 1 root root 41 Oct 18 21:50 master
-rw-r--r-- 1 root root 41 Oct 18 22:02 test
- 切换分支
git symbolic-ref
读取,修改和删除符号引用
git symbolic-ref [-m <reason>] <name> <ref>
将<name>引用修改为<ref>
git symbolic-ref [-q] [--short] <name>
查看<name>符号引用内容
git symbolic-ref --delete [-q] <name>
删除<name>符号引用
[root@vm-20-8-centos git-demo]# git symbolic-ref HEAD refs/heads/test
[root@vm-20-8-centos git-demo]# cat .git/HEAD
ref: refs/heads/test
[root@vm-20-8-centos git-demo]# git branch
master
*test
参考资料:
Pro Git 2nd Edition:
https://git-scm.com/book/zh/v2
Git 中文参考:
https://apachecn.gitee.io/git-doc-zh/#/