Git 是如何工作的

2022-12-21 0 481

原副标题:Git 是怎样组织工作的

高度关注

JAVA爱家

,与一百万开发人员在一同

Git 是如何工作的

公司出品 | 政采云后端(ID:Zoo-Team )

http://zoo.zhengcaiyun.cn/blog/article/git-work

已赢得原社会公众号的许可转发

序言

Git 是两个分布式系统的版控制技术,这意味著它采用数个邻近地区存储库,主要就包括两个封闭式存储库和伺服器,它在从后端组织工作中抽象化出下层监督机制各方面做得十分出众。尽管 Git 早已演变成两个成形的版控制信息系统,但这并并非译者起初的企图,但并不负面影响它正式成为最当今世界上最出众、典雅的辅助工具众所周知。Git 的益处是,你能在整座生涯中都不晓得 Git 外部是怎样组织工作的,但你仍然能和它朝夕相处得较好。但当你介绍了 Git 怎样管理工作您的存储库将有利于关上你的观念形式,并让您更深入细致地介绍 Git 。

Git 的优点 差别

SVN 是封闭式版控制技术,它的版别列济夫分散放到华北局伺服器的,而干的这时候,用的都是他们的笔记本电脑,多于两台伺服器来保护和contacts,因此具体来说要从华北局伺服器那儿获得新一代的版,半天活后须要把他们略过的活发送到华北局伺服器。

Git 是分布式系统版控制技术,它没有华北局伺服器,每两台PS3都当做两台伺服器。

竞争优势

Git 和其它版控制技术的主要就差异是,Git 只关心文件数据的整体是否发生变化,而大多数其它系统则关心文件内容的具体差异。

Git 并不保存这些前后变化的差异数据。实际上,Git 更像是把变化的文件作快照后,记录在两个微型的文件系统中。每次提交更新时,它会纵览一遍所有文件的指纹信息并对文件做快照,然后保存两个指向这次快照的索引。为提高性能,若文件没有变化,Git 不会再次保存,而是直接引用上次保存的快照链接。

Git 近乎所有操作都是邻近地区执行,它的绝大多数操作都只须要访问邻近地区文件和资源,不用连网。但如果用 CVCS (封闭式版控制技术)的话,差不多所有操作都须要连接网络。且因为 Git 在邻近地区磁盘上就保存着所有当前项目的历史更新,因此处理起来速度飞快。

Git 实际上是怎样组织工作的

当我们要去探究 Git 是怎样组织工作的这时候我们该从何处下手呢?因为上文说过 Git 近乎所有操作都是邻近地区执行的,在邻近地区的文件中我们能找到他执行的记录,这就须要我们聚焦邻近地区文件的 Git 文件 —— .git 文件,那么接下来就来看看 Git 的邻近地区文件都有些什么。

Git 对象

.git 文件作为两个隐藏文件并不经常出现在我们的目录中,现在我们关上两个从代码仓库拉取的项目,关上终端程序并导航到存储库的主目录,再导航到存储库的.git目录:

cd.git

拉出 .git的目录列表,那么你至少能看到以下几个目录:

FETCH_HEAD/

HEAD/

config/

objects/

refs/

现阶段我们须要聚焦的是 objects 目录,在objects中,我们最常见的对象是以下三种(具体的下文会详细说明这三者):

Commits : 这将树对象链接在一同以形成历史,保存有关您的提交的元数据的结构,以及指向父提交和下面文件的指针。

Tree : 这相当于两个目录,记录着目录树内容及其中各个文件对应 blob 对象索引。

Blobs : 这是文件的内容,用于表示文件快照内容。

Commits 对象

直接进入 object 对象:

cdobjects

ls

控制台展示:

// 每个人的项目都不同,文件自然也不同,此处以笔者的两个项目为例

0c 57 85 b3

1b 60 94 c4

2a 67 98 cb

2c 6c 9a info

3c 73 a9 pack

49 82 af

52 83 b1

小朋友你是否有很多问号?在第一眼看到这么多两位字符的文件夹名时完全不晓得这些是啥。那么我们就须要转头来解释一下Git 的数据存储结构了。

当 Git 存储对象(也就是我们提交的记录)时,它不会将它们全部转储到两个目录中,因为这样会使得目录在不断的迭代提交后变得笨拙,因此它会将它们整齐地构造成一棵树——Git 将对象哈希的前 2 个字符用作目录名称,然后将剩余的 38 个字符用作对象标识符。当我们将以上的二位字符命名的文件夹展开时,我们就会获得这样两个树形结构的目录:

objects

├── 0c

│ ├── 8867d7e175f46d4bcd66698ac13f4ca00cf592

│ └── c8002da0403724dfaa6792885eaa97faa71bcf

├── 1b

│ └── 716fafdd3aeb3636222a0026d1d4971078db05

├── 2a

│ └── 14f7db6a6748cc98862960ff5d0e9b1d4a2f17

├── 2c

│ ├── 14f7db6a6748cc98862960ff5d0e9b1d4a2f17

├── 3c

│ ├── 121291ffc25ce6792f9350883b77cea2633048

.

.

.

为了验证上述 Git 存储对象的结构,我们能查看当前新一代的4次提交,并取第一条记录去提交记录的结构树中匹配:

command: git log -4 –oneline

9a5bf36 (HEAD -> master) feat: third commit

2c5331f feat: second commit

60814e1 feat: first commit

49942f3 Initial commit

我们能看到最近的4次提交,并且每次提交都会有两个 7 位长的哈希值以及提交时的描述。以 9a5bf36 这次提交为例,我们可能会有个疑问:这多于 7 位似乎跟我们说的不太一样呀。别急!我们须要转换一下,将他转换成完整的长哈希值,因为在树结构中是以长哈希值构建生成的。

git rev-parse 9a5bf36

Git 以等效的长哈希值响应: 9a5bf367f10390c64a3f7b3e738b78bd78a3d781 .

将其分解为目录名称和对象标识符:

目录: 9a

对象标识符: 5bf367f10390c64a3f7b3e738b78bd78a3d781

我们很容易就能看到找到:

objects

├── 0c

│ ├── 8867d7e175f46d4bcd66698ac13f4ca00cf592

│ └── c8002da0403724dfaa6792885eaa97faa71bcf

├── 1b

│ └── 716fafdd3aeb3636222a0026d1d4971078db05

.

.

.

├── 98

│ ├── ed6b3f02409778bc864d8897bc230c90cae445

├── 9a

│ ├── 5bf367f10390c64a3f7b3e738b78bd78a3d781 //====>在这

.

.

既然我们晓得了它的存储结构,那么我们自然就应该关上这个文件查看文件的内容,但是我们不能直接查看此对象,因为 Git 中的对象是经过压缩的。如果您尝试采用 cat 5bf367f10390c64a3f7b3e738b78bd78a3d781 或类似形式查看它,您可能会看到一堆像这样的乱码,以及计算机尝试从二进制对象读取控制字符时发出的咔呲声:

6?$?(?E9?z??nUmV?Em]?p??3?`??????q?Ţqjw????VR?O? q?.r???e|lN?p??Gq?)?????#???85V?W6?????

)|Wc*??8?1a?b?=?f*??pSvx3??;??3??^??O?S}??Z4?/?%J?

xu?Ko?0??̯?51??Ԯ

yB

??f?y?cBɯo?{ݝ?|ҌFL?:?@??_?0Td5?D2Br?D$??f?B??b?5W?HÁ?H*?&??(fbꒉdC!DV%?????D@?(???u0??8{?w

????0?IULC1????@(<?s

mO????????ƶe?S????>?K8 89_vxm(#?jxOs?u?b?5m????=w\l?

%?O??[V?t]?^??????G6.n?Mu?%

?̉?X??֖X

v??x?EX???:sys???G2?y??={X?Ռe?X?4u???????4oG??^”qݠ???$?Ccu?ml???vB_)?I?

`??*ގF?of??O

我们能采用命令:

git cat-file -p 9a5bf36

sanqius-MacBook-Pro:3c zcy$ git cat-file -p 9a5bf36

tree 85b9416a23f8fb018181f96e5c01ba4bd923b965

parent 2c5331fd7046e561aad8fdde3e3f21375a17549c

author 三秋 <sanqiu@***.com> 1665729807 +0800

committer 三秋 <sanqiu@***.com> 1665729807 +0800

feat: third commit

我们看到的这个文件外部的这些内容其实就是两个对象,两个包含了 tree、parent、author… 等数据的对象,这个对象就是 Commits 了。

Commits 对象是以键值对的形式展示的,这个 Commits 指向两个 Hash 值为 2c5331fd7046e561aad8fdde3e3f21375a17549c 的 parent ,其实这个parent 同样是两个 Commits 对象,这较好理解。但是这个 Commits 还有两个 Hash 值为 85b9416a23f8fb018181f96e5c01ba4bd923b965 的 tree 属性,也就是我们上面所说的第二个常用对象 Tree 。接下来我们须要聚焦的是 Commits 对象中的 Tree。

Tree 对象

这个提交的文章目录里面有什么?我们采用相同的命令关上这个哈希值指向的文件:

git cat-file -p 85b9416a23f8fb018181f96e5c01ba4bd923b965

100644 blob 0cc8002da0403724dfaa6792885eaa97faa71bcf README.md

040000 tree 3c121291ffc25ce6792f9350883b77cea2633048 src

Blobs 对象

接上文,现在这个 Tree 文件类型早已出现我们的第三类对象 Blob 了,打破砂锅问到底,继续看看这个Blob 是啥:

git cat-file -p 0cc8002da0403724dfaa6792885eaa97faa71bcf

MIT License

Copyright (c) 2019

Permission is hereby granted, free of charge, to any person obtaining a copy

of this software and associated documentation files (the“Software”), to deal

inthe Software without restriction, including without limitation the rights

to use, copy, modify, merge, publish, distribute, sublicense, and/or sell…

<snip>

我们能看到其实这就是我们在这个代码版下的文件内容,这也就意味著Blob其实就是存放文件的内容

总结

放一张图用来总结一下 Commits、Tree、Blob 三者之间的关系:

Git 是如何工作的

分支创建与合并

在上文中,我们不难晓得每一次提交记录其实就是向代码仓库提交一次 Commits 对象,还记得 Commits 对象中的 Parent 属性吗, Parent 属性指向的是当前基变的原型版。那么当有数个 Commits 提交后,我们能获得这样两个结构的 Commits 流:

Git 是如何工作的

用过图形化 Git 辅助工具的同学有没有觉得这个很眼熟,没错,图形化辅助工具就是将 Commits 关系视图化,就获得我们常用的 SourceTree 、GitKraken 这些常用的图像化 Git 辅助工具,当然这些软件肯定没这么简单,但基本原理还是一样的。

现在来谈分支,Git 中的分支,其实本质上仅仅是个指向 Commit 对象的可变指针。Git 会采用 Master 作为分支的默认名字。在若干次提交后,你其实早已有了两个指向最后一次提交对象的 Master 分支,它在每次提交的这时候都会自动向前移动。

Git 是如何工作的

当我们创建两个新的分支时,其实就是在当前 Commit 对象上新建两个分支指针。这也就是为什么当我们新建两个分支的这时候会如此迅速。

那么 Git 是怎样晓得你当前在哪个分支上组织工作的呢?其实答案也很简单,它保存着两个名为 HEAD 的特别指针。在 Git 中,它是两个指向你正在组织工作中的邻近地区分支的指针。因此当我们切换分支的这时候就是切换 HEAD 指针的指向,这和大多数版控制技术形成了鲜明对比,它们管理工作分支大多采取备份所有项目文件到特定目录的形式,因此根据项目文件数量和大小不同,花费的时间也会有很大的差异,快则几秒,慢则数分钟。而 Git 的实现与项目复杂度无关,它永远能在几毫秒的时间内完成分支的创建和切换。

当我们分别在 Master、testing 分支分别进行了一些修改,并将代码提交,那么我们就会获得这样结构的分支关系,当前 Master、testing 分支新一代的代码的父级记录指向的都是同两个。

读到这我们能总结出分支的本质:

当我们切换到两个命名分支,其实只是切换两个引用提交哈希的标签。

然后 Git 沿树对象递归,找到哈希对应的快照文件对象,然后解压缩文件对象。

您的组织工作目录现在代表该分支的状态,因为它存储在存储库中。

代码合并与冲突

当我们继续在 testing 分支进行开发,且 Master 与 testing 分支的开发是在两个不同文件中,那么当我们要将 testing 分支合并到 Master 分支中去时,Git实际上会将两个分支的末端(A5 和 A7)以及它们的共同祖先(A3)进行一次简单的三方合并计算。

这次,Git 没有简单地把分支指针右移,而是对三方合并后的结果重新做两个新的快照,并自动创建两个指向它的提交对象( A8 )。这个提交对象比较特殊,它有两个祖先( A5 和 A7 )。

此时我们晓得了代码的合并是怎样进行的,但当我们在两个分支都同时修改了同一处代码时,那么当你合并代码的这时候碰到这样的提示时,就意味著我们在进行代码合并时出现了代码冲突。

// 代码合并冲突提示

Auto-merging index.html

CONFLICT (content): Merge conflictinindex.html

Automatic merge failed; fix conflicts and thencommit the result.

当我们关上冲突的文件,你会看到类似于这种

<<<<<<< HEAD

<div id= “footer”>contact : [email protected]</div>

=======

<div id= “footer”>

please contact us at [email protected]

</div>

>>>>>>> iss53

能看到 ======= 隔开的上半部分,是 HEAD(即 Master 分支,在运行 Merge 命令时所切换到的分支)中的内容,下半部分是在 testing 分支中的内容。解决冲突的办法无非是二者选其一或者由你手动整合到一同。但是 Git 是怎样进行 Diff 的呢?

代码合并算法(Myers)

Git 的 Diff 是基于 Myers 算法进行的,那么先来介绍一下 Myers 算法。 Myers 算法由 Eugene W.Myers 在 1986 年发表的一篇论文中提出,是两个能在大部分情况产生”最短的直观的“diff的两个算法。

differ

Diff 就是寻找目标文本和源文本之间的差别,也就是将源文本变成目标文本所须要的操作。举两个 Myers 算法中最常用的例子,A1 = ABCABBA,A2 = CBABAC,那么通过怎样的操作才能使得由 A1 转变成 A2 呢。

例如:

1. – A 2. – A 3. + C

– B + C – A

C B B

– A – C – C

B A A

+ A B B

B – B – B

A A A

+ C + C + C

这三种都是有效的变动形式,其实这种转化过程有很多种,那么那种转换过程才是最高效的呢?我们在变动时有这么两个共识:

删除后新增,比新增后删除要好

当修改一块代码时,整块的删除然后新增,比删除新增交叉在一同要好

面对这个问题我们能将这个问题抽象化成两个数学问题,生成“直观”的 Diff 算法。抽象化的结果是: 寻找 Diff 的过程能被表示为图搜索

图搜索

还是以两个字符串,A1 = ABCABBA,A2 = CBABAC为例,根据这两个字符串我们能构造下面一张图,横轴是 A1 内容,纵轴是 A2 内容,要想从 A1 变换正式成为 A2抽象化的数学问题就是求一条从左上角到右下角的路径。图中每一条从左上角到右下角的路径,都表示两个 Diff。向右表示“删除”,向下表示”新增“,对角线则表示“原内容保持不动“。

将上述的共识再次进行数学抽象化化就对应为:

路径长度最短(对角线不算长度)

先向右,再向下(先删除,后新增)

就像走迷宫一样,我们就能摸索获得这么一条路径:

①. (0, 0) -> (1, 0) -> (2, 0)

②. (2, 0) -> (3, 1)

③. (3, 1) -> (3, 2)

④. (3, 2) -> (4, 3) -> (5, 4)

⑤. (5, 4) -> (6, 4)

⑥. (6, 4) -> (7, 5)

⑦. (7, 5) -> (7, 6)

这条路径代表的 diff 的操作为:

– A

– B

C

+ B

A

B

– B

A

+ C

代码 diff

我们以上文中的几次提交中的任意两次 2c5331f 和 60814e1 提交进行 diff:

command: git diff 2c5331f 60814e1

2c5331f 和 60814e1 表示两个文件的 Hash ,相当于它们的 HashID,这个 HashID 就代表了两个文件对象的特定版,最后的一串数字代表了两个文件的模式。

Git 会告诉你哪些行存在差异,它们被显示在两个 “@@” 符号之前,以上图示例中所表示的含义为:

来自文件 a (标记为 “-”),从第 1 行开始之后的 15 行代码。

来自文件 b (标记为 “+”),从第 1 行开始之后的 15 行代码。

@@ -1,15 +1,5 @@

– console.log( watch)

– const add = (a,c) => {

returna+c

– }

– const reduce=(a)=>{

if(a<0){

return“第一位不能为负数”

– } else{

returna-b

– }

+ const add = (a,b) => {

+ returna+b

}

add(4,8)

– console.log(reduce(-2,-9))

– console.log(new Date.getDate, 第二次提交)

而”@@”后面的紧跟着的部分就是其上下文信息,在每两个被改动过的代码行之前都会前置两个 “+” 或是 “-” 符号。这些符号能帮助你准确介绍版 a 和版 b ,例如前置了 “-” 符号的行就代表来自版 a ,反之带有符号 “+” 的行就代表来自于版 b 。

结尾

上述早已粗浅的为大家介绍了 Git 的一些简单原理,但这只是 Git 的冰山一角,如果大家还有兴趣能继续深入细致学习,相信大家能够为他们开拓出一块新的知识领域。

参考资料

《Pro Git》

《Advanced Git》

The Myers diff algorithm: part 1(https://blog.jcoglan.com/2017/02/12/the-myers-diff-algorithm-part-1/)

<END>

程序员专属卫衣

商品直购链接

👇👇

墙裂推荐!这才是专属程序员们的T恤!

亲自整理的Git命令汇总(建议收藏)

建议弃用C/C++后,Azure CTO再怼Git:最不直观、最笨重

git中怎样忽略文件和目录

我是怎么自学 Git / GitHub 的?

Office 2019/2021专业增强版,正版终身许可!

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务