腾讯云技术分享专栏 想晋级高级工程师只知道表面是不够的!Git 内部原理介绍

匿名 · 2019年01月18日 · 730 次阅读

本文由云 + 社区发表

作者:腾讯工蜂用户:王二卫

从不一样的视角了解 git,以便更好的使用 git

一、git & git 版本库认识

git 是一个内容寻址的文件系统,其核心部分是一个简单的键值对数据库 (key-value data store),可以向该数据库插入任意类型的内容,它会返回一个 40 位长的哈希键值。并在此基础上提供了一个版本控制系统的用户界面。

git 版本库其实只是一个简单的数据库,其中包含所有用来维护与管理项目的修订版本和历史信息。其不同于 subversion,git 版本库不仅提供版本库中所有文件的完整副本,还提供版本库本身的副本。在 git 版本库中,git 维护两个主要数据结构:对象库 (object store),索引 (index)。

从整体来看,一个项目的 git 仓库,就如一张带节点的渔网(该渔网是一张有向网),随着项目的不断推进,该渔网也将不断的向四周扩散。

渔网上的节点就像一个个的提交,从某一个正常的节点都能漫游至项目最开始的起点。而分支就如该网上不同节点上的一个特殊标记,分支的演变就是该标记不断的移至其他节点。 分支的合并,根据合并方式的不同,使得这一张网的交叉紧密度越来越高。

1.1git 对象类型

对象库是 git 版本库实现的心脏,包含四种类型:

块 (blob,binary lare object),文件的每一个版本表示为一个块。一个 blob 被视为一个存储任意数据,且内部结构被程序忽略的变量或文件的黑盒。一个 blob 保存一个文件的数据,但不包含任何关于这个文件的元数据 (Metadata,描述数据的数据)。

目录树 (tree), 一个目录树对象代表一层目录信息。它记录 blob 标识符、路径名和在一个目录里所有文件的一的元数据。它也可以递归引用其他目录树或子树对象,从而建立一个包含文件和子目录的完整层次结构。

提交 (commit),一个提交对象保存版本库中每一次变化的元数据,每一个提交对象指向一个目录树对象,这个树对象在一张完整的快照中补货提交时版本库的状态。

标签 (tag) ,一个标签对象分配一个可读的名字给一个特定的对象,通常是一个提交对象。

为了有效的利用磁盘空间和网络带宽名,git 把对象压缩并存储在打包文件 (pack file) 里,这些文件也在对象库里。

1.2 索引

索引是一个临时的、动态的二进制文件,不包含任何文件内容,它仅仅追踪你想要提交的那些内容。使得开发的推进与提交的变更之间能够分离开来。

1.3 引用

引用 (ref) 是一个保存 SHA-1 值的文件,该文件的名字指针来替代原始的 SHA-1 值,一般指向提交对象。本地分支名称、远程跟踪分支名称和标签名都是引用。

.git/refs
.git/refs/heads
.git/refs/tags

1.3.1 创建一个引用

$ echo “1a410efbd13591db07496601ebc7a059dd55cfe9” > .git/refs/heads/master

现在可以通过新建的引用来代替 SHA-1 的值: $ git log —pretty=oneline master 1a410efbd13591db07496601ebc7a059dd55cfe9 third commit cac0cab538b970a37ea1e769cbbde608743bc96d second commit fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit

不提倡直接编辑引用文件,可以通过update-ref更新某个引用 $ git update-ref refs/heads/master 1a410efbd13591db07496601ebc7a059dd55cfe9

比如新建一个分支(git 分支的本质:一个指向某一系列提交之首的指针或引用)$git update-ref refs/heads/feature-zhangsan cac0ca

1.3.2 符号引用

符号引用 (symbolic reference),间接指向 git 对象,其实际也是一个引用,不像普通引用那样包含一个 SHA-1 值,它是一个指向其他引用的指针。 git自动维护几个用于特定目的的特殊符号引用,这些引用可以在使用提交的任何地方使用。

  • HEAD 始终指向当前分支的最近提交,不像普通引用那样包含一个 如: $ cat .git/HEAD ref: refs/heads/master

若执行 $ git checkout test,git 会这样更新 HEAD 文件 ref:refs/heads/test

  • ORIG_HEAD 某些操作 (如:merge、reset),会把调整为新值之前的先前版本的 HEAD 记录到 OERG_HEAD 中,只用其可以恢复或回滚之前的状态或做个比较
  • FETCH_HEAD git fech 命令将所有抓取分支的头记录到.git/FETCH_HEAD 中
  • MERGEHEAD 正在合并进 HEAD 的提交

1.3.3 远程引用

如果你添加了一个远程版本库并对其执行过推送操作,Git 会记录下最近一次推送操作时每一个分支所对应的值,并保存在 refs/remotes 目录下。 如:$cat .git/refs/remotes/origin/master ca82a6dff817ec66f44342007202690a93763949 发现添加的远程 origin 远程库的 master 分支锁对应的 SHA-1 值,就是最近一次与服务器通信时 master 分支所对应的 SHA-1 值。 远程引用和分支(位于 refs/heads 目录下的引用)之间最主要的区别在于,远程引用是只读的。 虽然可以git checkout 到某个远程引用,但是 Git 并不会将 HEAD 引用指向该远程引用。 因此,你永远不能通过commit 命令来更新远程引用。 Git 将这些远程引用作为记录远程服务器上各分支最后已知位置状态的书签来管理。

二、git 底层命令

  • cat-file 展示 git 仓库对象实体的类型、大小和内容
  • ls-remote 显示远程库信息
  • ls-files 显示由工作目录中添加到缓存中的文件的相关信息
  • ls-tree 列出树对象内容
  • read-tree 将给出的树写入索引但不写入缓存
  • write-tree 按照索引区内容创建树对象
  • symbolic-ref 同步引用信息
  • update-index 更新树对象内容至索引

三、.git 结构说明

  • HEAD 指示目前被检出的分支
  • index 保存暂存区信息
  • config* 包含项目特有的配置选项
  • description 仅供 gitweb 程序使用,用户一般不需要关注。
  • hooks 包含客户端和服务端的钩子
  • info 包含全局排除 (global excude) 文件,存放那些不希望被记录在.gitignore 中的忽略模式
  • objects 存储所有数据内容
  • refs 存储指向数据 (分支) 的提交对象的指针

四、git 版本演变

准备工作:创建一个没有任何文件的 git 初始库 $ git init test Initialized empty Git repository in /data/work/test/test/.git/

4.1 git 数据存储演示

  • hash-object 存储任意类型数据至数据库,并返回 hash 键值

$ echo ‘test conten’ | git hash-object -w —stdin

d670460b4b4aece5915caf5c68d12f560a9fe3e4

-w 执行写入数据库操作若不指定该选项只会返回hash,不会写入数据库

--stdin 标准输入输出读取

默认存入是blob类型通过-t 参数指定

$ find .git/objects/ -type f .git/objects//d6/870460b4b4aece5915caf5c68d12f560a9fe3e4

  • 一个文件对应一条内容,这个内容的名称以该文件内容加上特定头部信息一起的 sha-1 校验和。

头部信息 - 对象类型(blob 或 tree 或 commit)+ 一个空格 + 数据内容长度 + 一个空字节 git 会通过 zlib 将文件内容和头部信息拼接一起的内容进行压缩写入磁盘某个对象,并用计算出的 sha-1 值的前两个字符串作为目录名称,后 38 个字符串作为子目录内文件的名称。

$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4

test content

4.2 简单版本控制演示

4.2.1 创建初始版本

$ echo ‘version 1’ > test.txt

$ git hash-object -w ./test.txt 83baae61804e65cc73a7201a7252750c76066a30

4.2.2 更新版本

$ echo ‘version 2’ > test.txt

$ git hash-object -w ./test.txt 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a

此时数据库已经存储了 test.txt 两个不同的版本,如下:

$ find .git/objects/ -type f .git/objects//1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a .git/objects//83/baae61804e65cc73a7201a7252750c76066a30

可以通过cat-file -p查看内容,以上都是数据 (blob) 对象。可以使用 cat-file -t查看。

4.3 树对象引入

树对像 (tree object) 解决文件名和目录保存问题。一个树对象包含了一条或多条树对象记录,每条记录包含一个指向数据对象或子树对象的 sha-1 指针,以及相应的模式/类型/文件信息。

如下所示:


img

$ git cat-file -p master{tree}

100644 blob a906cb2a4a904a152e80877d4088654daad0c859 README 100644 blob 8f94139338f9404f26296befa88755fc2598c289 Rakefile 040000 tree 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0 lib

$ git cat-file -p 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0 100644 blob 47c6340d6459e05787f644c2447d2595f5d3a54b simplegit.rb

master{tree}指向 master 分支最新提交所指的树对象。 数据对象几种类型

  • 100644: 表示一般文件
  • 100755: 表示可执行文件
  • 120000: 表示 指针
  • —add: 将未跟踪文件加入缓存区
  • —cacheinfo 将数据对象文件加入工作区

4.3.1 将文件加入暂存区

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

4.3.2 生成树对象

创建第一个树 $ git write-tree 将暂存区内容生成一个树对象,并输出树对象 SHA-1 d8329fc1cc938780ffdd9f94e0d364e0ea74f579

4.3.3 演变一个复杂的树

$ echo ‘new file’ > new.txt

$echo ‘test file2’ > test.txt

$git update-index —cacheinfo 100644 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt

$ git update-index test.txt

$ git update-index —add new.txt

创建第二个树

$ git write-tree 0155eb4229851634a0f03eb265b69f5a2d56f341

$ git cat-file -p 0155eb4229851634a0f03eb265b69f5a2d56f341 100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new.txt 100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt

此时发现,第一个树丢了,并没有跟第一个树有关系,通过 read-tree 进行链接 $ git read-tree —prefix=bak d8329fc1cc938780ffdd9f94e0d364e0ea74f579

$ git write-tree 3c4e9cd789d88d8d89c1073707c3585e41b0e614

$ git cat-file -p 3c4e9cd789d88d8d89c1073707c3585e41b0e614 040000 tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579 bak 100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new.txt 100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt

4.3.4 查看我们生成的树

img

4.4 提交对象引入

通过 commit 对象将这些树对象串起来。 创建第一个提交 $ echo ‘first commit’ | git commit-tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579 fdf4fc3344e67ab068f836878b6c4951e3b15f3d

创建第二个提交 $ echo ‘second commit’ | git commit-tree 0155eb -p fdf4fc3 cac0cab538b970a37ea1e769cbbde608743bc96d

创建第三个提交 $ echo ‘third commit’ | git commit-tree 3c4e9c -p cac0cab 1a410efbd13591db07496601ebc7a059dd55cfe9

版本库目录变化` **$ find .git/objects -type f** .git/objects/01/55eb4229851634a0f03eb265b69f5a2d56f341 # tree 2 .git/objects/1a/410efbd13591db07496601ebc7a059dd55cfe9 # commit 3 .git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a # test.txt v2 .git/objects/3c/4e9cd789d88d8d89c1073707c3585e41b0e614 # tree 3 .git/objects/83/baae61804e65cc73a7201a7252750c76066a30 # test.txt v1 .git/objects/ca/c0cab538b970a37ea1e769cbbde608743bc96d # commit 2 .git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 # ‘test content’ .git/objects/d8/329fc1cc938780ffdd9f94e0d364e0ea74f579 # tree 1 .git/objects/fa/49b077972391ad58037050f2a75f74e3671e92 # new.txt .git/objects/fd/f4fc3344e67ab068f836878b6c4951e3b15f3d # commit 1 `提交版本图

img

没有执行read-tree
$ git log --stat 92387
commit 923879712b02f980a2edbe1cee315d883ee72503
Author: erweiwang <erweiwang@tencent.com>
Date:   Tue Jul 17 15:55:53 2018 +0800

    second commit

 new.txt  | 1 +
 test.txt | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

commit e624badd39a25484a08ae74231be65ea50a0fe32
Author: erweiwang <erweiwang@tencent.com>
Date:   Tue Jul 17 15:54:20 2018 +0800

    first commit

 test.txt | 1 +
 1 file changed, 1 insertion(+)

五、包文件

Git 最初向磁盘中存储对象时所使用的格式被称为 “松散(loose)” 对象格式。 但是,Git 会时不时地将多个这些对象打包成一个称为 “包文件(packfile)” 的二进制文件,以节省空间和提高效率。 当版本库中有太多的松散对象,或者你手动执行 git gc 命令,或者你向远程服务器执行推送时,Git 都会这样做。

git 打包对象时,会查找命名及大小相近的文件,并只保存文件不同版本之间的差异内容和文件最新版本的完整内容。

六、引用规格

引用规格的格式由一个可选的 + 号和紧随其后的 : 组成,其中 是一个模式(pattern),代表远程版本库中的引用; 是那些远程引用在本地所对应的位置。 + 号告诉 Git 即使在不能快进的情况下也要(强制)更新引用。

[remote "origin"]
  url = https://github.com/schacon/simplegit-progit
  fetch = +refs/heads/*:refs/remotes/origin/*

如果想让 git 每次只拉取远程 master 分支,而不是所有分支,可以将引用规格那一行修改为: fetch = +refs/heads/master:refs/remotes/origin/master

七、git clone 代码库过程

执行 git clone 后,

  • 拉取 info/refs 文件 => GET info/refs ca82a6dff817ec66f44342007202690a93763949 refs/heads/master
  • 确定 HEAD 引用,明确检出至工作目录的内容 => GET HEAD ref: refs/heads/master 以上说明完成抓取后需要检出 master 分支
  • 从 info/refs 文件中所提到的 ca82a6 提交对象开始 => GET objects/ca/82a6dff817ec66f44342007202690a93763949 (179 bytes of binary data)
  • 根据 ca82a6 提取的的父提交对象和树对象开始遍历整个完整版本库。

在遍历过程中,若是未能直接找到(非松散对象)某些对象,会去替代版本库或某个包文件获取。

八、git 推送远端库过程

为了上传数据至远端,Git 使用 send-pack 和 receive-pack 进程。 运行在客户端上的 send-pack 进程连接到远端运行的 receive-pack 进程。

九、扩展知识

9.1 维护

git gc —auto //整理松散对象并放置包文件,将多个包文件合并为一个大的包文件,移除与任何提交不相关的陈旧对象

9.2 数据恢复

  • 确定需要恢复的版本 git reflog 查看 git 默默记录的每一次你改变的 HEAD 的值。 git log -g 可以详细的查看引用日志中各个版本的信息,风方便确定要恢复的提交。 如下所示 commit 1a410efbd13591db07496601ebc7a059dd55cfe9 Reflog: HEAD@{0} Reflog message: updating HEAD third commit commit ab1afef80fac8e34258ff41fc1b867c702daa24b Reflog: HEAD@{1} Reflog message: updating HEAD modified repo.rb a bit
  • 创建用于恢复的临时分支

$ git branch recover-branch ab1afef

  • 通过 git fsck 检查数据库的完整性(当 reflog 也不存在需要恢复的版本)

当引用日志所在目录.git/logs/ 被不小心清空时

$ git fsck —full Checking object directories: 100% (256/256), done. Checking objects: 100% (18/18), done. dangling blob d670460b4b4aece5915caf5c68d12f560a9fe3e4 dangling commit ab1afef80fac8e34258ff41fc1b867c702daa24b dangling tree aea790b9a58f6cf6f2804eeac9f0abbe9631e4c9 dangling blob 7108f7ecb345ee9d0084193f147cdad4d2998293

9.3 移除对象

该操作使用须谨慎,会导致提交历史不被重写。应用场景,必须对已上库的某些文件(因文件太大或保密信息)进行彻底移除可以使用。

  • 定位出问题文件名 保密文件一般是已知的,若是误提交的文件较大需要删除,但又不知道是哪些文件,且又执行过 git gc 可以通过类似以下命令定位: $ git verify-pack -v .git/objects/pack-29…69 .idx | sort -k 3 -n | tail -3 dadf7258d699da2c8d89b09ef6670edb7d5f91b4 commit 229 159 12 033b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5 blob 22044 5792 4977696 82c99a3e86bb1267b236a4b6eff7868d97489af1 blob 4975916 4976258 1438 $ git rev-list —objects —all | grep 82c99a3 82c99a3e86bb1267b236a4b6eff7868d97489af1 git.tgz
  • 从过去所有树中移除这个文件 查看哪些提交对这个文件做过改动 $ git log —oneline —branches — git.tgz dadf725 oops - removed large tarball 7b30847 add git tarball 从 7b30847 之后的所有提交历史中完全移除该文件 $ git filter-branch —index-fileter ‘git rm —ignore-unmatch —cached git.tgz’ — 7b30847 Rewrite 7b30847d080183a1ab7d18fb202473b3096e9f34 (1/2) rm ‘git.tgz’ Rewrite dadf7258d699da2c8d89b09ef6670edb7d5f91b4 (2/2) Ref ‘refs/heads/master’ was rewritten --index-filter 只修改暂存区或索引中的文件 --cached 需要从索引中移除,使得在运行过滤器是,并不会将每个修订版本检出到磁盘 --ignore-unmatch 如果尝试删除的模式不存在时,不提示错误 filter-branch 用于指定从那个提交以来的历史
  • 重新打包日志 执行上面操作,本地历史不在包含那个文件的引用,但是,引用日志和 .git/refs/original 通过 filterbranch 选项添加的新引用中还存有对这个文件的引用,必须移除它们后重新打包数据库。 $ rm -Rf .git/refs/original $ rm -Rf .git/logs/** $ git gc
  • 彻底移除 $ git prune --expire now $ git count-objects -v

此文已由腾讯云 + 社区在各渠道发布

获取更多新鲜技术干货,可以关注我们腾讯云技术社区 - 云加社区官方号及知乎机构号

暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册