本文由云 + 社区发表

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

从不一样的视角了解 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自动维护几个用于特定目的的特殊符号引用,这些引用可以在使用提交的任何地方使用。

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

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 底层命令

三、.git 结构说明

四、git 版本演变

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

4.1 git 数据存储演示

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

d670460b4b4aece5915caf5c68d12f560a9fe3e4

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

--stdin 标准输入输出读取

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

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

头部信息 - 对象类型(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 分支最新提交所指的树对象。 数据对象几种类型


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 后,

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

八、git 推送远端库过程

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

九、扩展知识

9.1 维护

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

9.2 数据恢复

$ git branch recover-branch ab1afef

当引用日志所在目录.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 移除对象

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

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

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


↙↙↙阅读原文可查看相关链接,并与作者交流