Git 原理

前言

在本章中,我们将介绍一个分布式版本控制系统的设计思路,以及它与集中式版本控制系统的不同之处。除此之外,我们还将带你了解分布式版本库的具体工作方式,以及为什么我们会说,在Git中创建分支和合并分支不是个大不了的问题。

集中式版本控制系统

先说集中式版本控制系统,版本库是集中存放在中央服务器的,而干活的时候,用的都是自己的电脑,所以要先从中央服务器取得最新的版本,然后开始干活,干完活了,再把自己的活推送给中央服务器。中央服务器就好比是一个图书馆,你要改一本书,必须先从图书馆借出来,然后回到家自己改,改完了,再放回图书馆。

分布式版本控制系统

在分布式版本控制系统中,开发者环境与服务器环境之间是没有分隔的。每一个开发者都同时拥有一个用于当前文件操作的工作区与一个用于存储该项目所有版本、分支以及标签的本地版本库(我们称其为一份克隆)。每个开发者的修改都会被载入成一次次的新版本提交(commit), 首先提交到其本地版本库中。然后,其他开发者就会立即看到新的版本。通过推送(push)和拉回(pull)命令,我们可以将这些修改从一个版本库传送到另一个版本库中。这样一来,从技术上来看,这里所有的版本库在分布式架构上的地位是同等的。因此从理论上来讲,我们不再需要借助服务器,就可以将某一台开发工作机上所做的所有修改直接传送给另一开发工作机。相较于SVN 的每一次 commit 都需要联网,这就需要网络的等待。 Git则可以在无网络情况下本地提交,他把提交版本与推送服务器这两个概念分开了。Git只有在Push、Pull 的时候需要联网,而我们平时更多的操作应是commit。
Git是去中心,它的服务器并非必须的,我们可以把Git服务器(Github)看成一个始终在线的参与者,它不写代码,只接受大家的推送与合并,方便大家交流使用。当然在具体实践中,Git中的服务器版本库也扮演了重要的角色。

1.Git文件快照

Git 和其他版本控制系统的主要差别在于,Git 只关心文件数据的整体是否发生变化,而大多数其他系统则只关心文件内容的具体差异。这类系统(CVS,Subversion,Perforce,Bazaar 等等)每次记录有哪些文件作了更新,以及都更新了哪些行的什么内容,请看图。

其他系统在每个版本中记录着各个文件的具体差异

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

Git 保存每次更新时的文件快照

这是 Git 同其他系统的重要区别。它完全颠覆了传统版本控制的套路,并对各个环节的实现方式作了新的设计。Git 更像是个小型的文件系统,但它同时还提供了许多以此为基础的超强工具,而不只是一个简单的 VCS。稍后在第三章讨论 Git 分支管理的时候,我们会再看看这样的设计究竟会带来哪些好处。

image.png

这是项目的三个版本,版本1中有两个文件A和B,然后修改了A,变成了A1,形成了版本2,接着又修改了B变为B1,形成了版本3。

如果我们把项目的每个版本都保存到本地仓库,需要保存至少6个文件,而实际上,只有4个不同的文件,A、A1、B、B1。为了节省存储的空间,我们要像一个方法将同样的文件只需要保存一份。这就引入了Sha-1算法。

可以使用git命令计算文件的 sha-1 值。

echo 'test content' | git hash-object --stdin
d670460b4b4aece5915caf5c68d12f560a9fe3e4

SHA-1将文件中的内容通过通过计算生成一个 40 位长度的hash值。

Sha-1的非常有特点:

  • 由文件内容计算出的hash值
  • hash值相同,文件内容相同

对于上图中的内容,无论我们执行多少次,都会得到相同的结果。因此,文件的sha-1值是可以作为文件的唯一 id 。同时,它还有一个额外的功能,校验文件完整性。

有了 sha-1 的帮助,我们可以对项目版本的存储方式做一下调整。

图片

2.Git 文件的三种状态

对于任何一个文件,在 Git 内都只有三种状态:已提交(committed),已修改(modified)和已暂存(staged)。已提交表示该文件已经被安全地保存在本地数据库中了;已修改表示修改了某个文件,但还没有提交保存;已暂存表示把已修改的文件放在下次提交时要保存的清单中。

由此我们看到 Git 管理项目时,文件流转的三个工作区域:Git 的工作目录,暂存区域,以及本地仓库。

image.png

每个项目都有一个 Git 目录(译注:如果 git clone 出来的话,就是其中 .git 的目录;如果 git clone --bare 的话,新建的目录本身就是 Git 目录。),它是 Git 用来保存元数据和对象数据库的地方。该目录非常重要,每次克隆镜像仓库的时候,实际拷贝的就是这个目录里面的数据。

从项目中取出某个版本的所有文件和目录,用以开始后续工作的叫做工作目录。这些文件实际上都是从 Git 目录中的压缩对象数据库中提取出来的,接下来就可以在工作目录中对这些文件进行编辑。

所谓的暂存区域只不过是个简单的文件,一般都放在 Git 目录中。有时候人们会把这个文件叫做索引文件,不过标准说法还是叫暂存区域。

加入暂存区的原因有以下几点:
  • 为了能够实现部分提交
  • 为了不再工作区创建状态文件、会污染工作区。
  • 暂存区记录文件的修改时间等信息,提高文件比较的效率。
基本的 Git 工作流程如下:
  1. 在工作目录中修改某些文件。
  2. 对修改后的文件进行快照,然后保存到暂存区域。
  3. 提交更新,将保存在暂存区域的文件快照永久转储到 Git 目录中。

所以,我们可以从文件所处的位置来判断状态:如果是 Git 目录中保存着的特定版本文件,就属于已提交状态;如果作了修改并已放入暂存区域,就属于已暂存状态;如果自上次取出后,作了修改但还没有放到暂存区域,就是已修改状态。到第二章的时候,我们会进一步了解其中细节,并学会如何根据文件状态实施后续操作,以及怎样跳过暂存直接提交。

为了理解 Git 分支的实现方式,我们需要回顾一下 Git 是如何储存数据的。或许你还记得第一章的内容,Git 保存的不是文件差异或者变化量,而只是一系列文件快照。

image.png
  • git仓库(版本库):git仓库就是一个.git文件夹。这个文件夹内包含了很多文件(见插图2),其中有一个很重要的文件夹objects,保存了暂存区的所有文件对象,包括blob对象、tree对象、commit对象等,这些对象都是一以文件的形式来保存的。还有HEAD文件,保存着最新的提交的指针。当然很多人到这里可能还是不理解objects中的文件对象和HEAD中保存的指针到底是什么意思,没关系,下面会详细讲解。

  • 工作区:在一个项目目录中,除了.git文件的其他所有文件的集合就是工作区。

  • 暂存区:暂存区可以理解为文件从修改到最后提交到git版本库之间的一个缓存,为了防止一次提交了不必要的文件,有回退的余地,便有了暂存区。

  • HEAD:HEAD在.git文件夹中是一个文件,文件的内容是一个32位的16进制数,这只是一个指针,他指向最近一个提交点、这个提交点实质是一个commit对象,对象里包含里多个属性,包括最后一个提交点目录结构索引、上一次提交点id、提交人、提交时间等。

3.Git版本库

3.1Git对象

Git版本库(实际上就是一个数据库)不仅仅提供版本库中所有文件的完整副本,还提供版本库本身的副本。
Git定义了4种对象:blob、tree、commit和tag,它们都位于.git/objects/目录下。git对象在原文件的基础上增加了一个头部,即对象内容 = 对象头 + 文件内容。这种格式无法直接通过cat命令读取,需要使用git cat-file这个底层命令才能正确读取。
对象头的格式为:对象头 = 对象类型 + 空格 + 数据内容长度 + null byte,例如一个文件内容为“hello world”,其blob对象头为"blob 11\000"。

  • blob:文件快照,每个blob代表一个(版本的)文件,blob只包含文件的数据,而忽略文件的其他元数据,如名字、路径、格式等。
  • tree:一个目录树对象代表一层目录信息。它记录blob标识符,路径名和一个目录里的所有文件的一些元数据。(也就是说,一个tree目录对象包含一个文件的许多不同的blob,保存blob信息和元数据)
  • commit:一个提交对象保存版本库中每一次变化的元数据,包括作者,提交者,提交日期和日志消息。每一个提交对象指向一个目录树对象。(也就是说,文件提交一次就会产生一个提交对象,保存提交的信息,并将该次提交指向一个目录树对象中。)
  • tag:一个标签对象分配一个任意的且人类可以读懂的名字给一个特定对象,通常是一个提交对象。commit ID 很难理解,所以可以通过tag对象来制定。(也就是说,提交一次,产生一个提交ID,就可以对应一个tag对象。)

    对于所有的数据,它们都会被计算成一个十六进制散列值(例如像1632acb65b01 c6b621d6e1105205773931bb1a41这样的值)。这个散列值将会被用作相关对象的引用,以及日后恢复数据时所需的键值。

    也就是说,一个提交对象的散列值实际上就是它的“版本号”,如果我们持有某一提交的散列值,就可以用它来检查对应版本是否存在于某一版本库中。如果存在,我们就可以将其恢复到当前工作区相应的目录中。如果该版本不存在,我们也可以从其他版本库中单独导入(拉回)该提交所引用的全部对象。

接下来,我们来看看采用这种散列值(hash值)和这种既定的版本库结构究竟有哪些优势。

高性能:通过散列值来访问数据是非常快的。
冗余度—释放存储空间:相同的文件内容只需存储一次即可。
分布式版本号:由于相关散列值是根据文件,作者和日期来计算的,所以版本也可以“离线”产生,不用担心将来会因此而发生版本冲突。
版本库间的高效同步:当我们将某一提交从一个版本库传递给另一个版本库时,只需要传送那些目标版本库中不存在的对象即可。而正是因为有了散列值的帮助,我们才能很快地判断相关对象是否已经存在。
数据完整性:由于散列值是根据数据的内容来计算的,所以我们可以随时通过Git来查看某一散列值是否与相关数据匹配。以检测该数据上可能的意外变化或恶意操作。
自动重命名检测:被重命名的文件可以被自动检测到,因为根据该文件内容计算出的散列值并没有发生变化。也正因为如此,Git中并没有专用的重命名命令,只需移动命令即可。

注:散列值即hash值
3.2 索引

索引描述整个版本库的目录结构,它捕获项目在某个时刻的整体结构的一个版本。Git的关键特色之一在于它允许你用有条理的、定义好的步骤来改变索引的内容。所谓的索引就是你用git add file后添加到缓存的文件的一个版本,你可以通过GIT命令再索引中暂存变更(即添加、删除或者编辑某个文件或某些文件),索引会记录和保存好这些变更直到你准备好要git commit了。索引跟踪文件的路径名和相应的blob。

3.3 Git追踪内容

首先要注意的是。Git追踪的是内容而不是文件,Git并不追踪那些与文件次相关的文件名或者是目录名。如果两个文件的内容完全一样,Git在对象库里只保存一份blob形式的内容副本,并且该文件具有唯一的SHA1值。文件内容改变,则Git会计算一个新的SHA1值,识别出它现在是一个不同的blob对象并把这个blob对象添加到对象库里。因为Git使用一个文件的全部内容散列值作为文件名,所有它必须对每个文件的完整副本进行操作。GIT用户所说的SHA1、散列码和对象ID都是指同一个东西。在互联网上,文件或者任意大小的blob都可以通过仅比较他们的SHA1标识符来判断是否相同。

内容寻址
  1. 依赖底层命令git hash-object命令,对文件内容增加头信息后计算hash值并返回,增加-w参数后在git仓库内创建blob对象(blob对象 = 对象头 + 文件内容)。
  2. blob对象存储到git仓库目录(.git/objects/)时,依据40位(16进制字符)长度的hash串指定存储目录(hash串前2位)和命名文件(hash串后38位)。例如某blob对象的hash值为62/0d4582bfbf773ef15f9b52ac434906a3cdf9c3,那么它在git仓库中的路径为.git/objects/62/0d4582bfbf773ef15f9b52ac434906a3cdf9c3
  3. Git内容寻址本质是:Git根据由文件内容(增加文件头)产生的Hash值来标识和索引文件,另外进行命令操作时没有必要写完整的hash串,只要输入的hash串长度是唯一可识别和索引的即可。
  4. 无需考虑Hash碰撞的情况,在大型项目上也可以放心使用Git。因为在概率上SHA-1产生的哈希值碰撞的机会可以小到忽略。
3.4 打包文件

Git并不是每次修改一个文件都会完全储存这两个版本文件的全部内容,而是采用一种叫做打包文件的储存机制。要创建一个打包文件,Git会定位内容非常相似的全部文件,为其中之一储存整个内容,然后计算相似文件之间的差异并只储存差异。

4 Git分支

为了理解 Git 分支的实现方式,我们需要回顾一下 Git 是如何储存数据的。或许你还记得第一章的内容,Git 保存的不是文件差异或者变化量,而只是一系列文件快照。

在 Git 中提交时,会保存一个提交(commit)对象,该对象包含一个指向暂存内容快照的指针,包含本次提交的作者等相关附属信息,包含零个或多个指向该提交对象的父对象指针:首次提交是没有直接祖先的,普通提交有一个祖先,由两个或多个分支合并产生的提交则有多个祖先。

为直观起见,我们假设在工作目录中有三个文件,准备将它们暂存后提交。暂存操作会对每一个文件计算校验和(即第一章中提到的 SHA-1 哈希字串),然后把当前版本的文件快照保存到 Git 仓库中(Git 使用 blob 类型的对象存储这些快照),并将校验和加入暂存区域:

$ git add README test.rb LICENSE
$ git commit -m 'initial commit of my project'
单个提交对象在仓库中的数据结构

作些修改后再次提交,那么这次的提交对象会包含一个指向上次提交对象的指针(译注:即下图中的 parent 对象)。两次提交后,仓库历史会变成 的样子如下图


多个提交对象之间的链接关系

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


分支其实就是从某个提交对象往回看的历史
新建分支

多个分支指向提交数据的历史

那么,Git 是如何知道你当前在哪个分支上工作的呢?其实答案也很简单,它保存着一个名为 HEAD 的特别指针。请注意它和你熟知的许多其他版本控制系统(比如 Subversion 或 CVS)里的HEAD 概念大不相同。在 Git 中,它是一个指向你正在工作中的本地分支的指针(译注:将 HEAD 想象为当前分支的别名。)。运行 git branch 命令,仅仅是建立了一个新的分支,但不会自动切换到这个分支中去,所以在这个例子中,我们依然还在 master 分支里工作(参考图 3-5)。

HEAD 指向当前所在的分支
切换分支

这样 HEAD 就指向了 testing 分支

HEAD 在你转换分支时指向新的分支
在分支上提交
每次提交后 HEAD 随着分支一起向前移动
再切换到master分支
HEAD 在一次 checkout 之后移动到了另一个分支
在master分支上提交
不同流向的分支历史

由于 Git 中的分支实际上仅是一个包含所指对象校验和(40 个字符长度 SHA-1 字串)的文件,所以创建和销毁一个分支就变得非常廉价。说白了,新建一个分支就是向一个文件写入 41 个字节(外加一个换行符)那么简单,当然也就很快了。

这和大多数版本控制系统形成了鲜明对比,它们管理分支大多采取备份所有项目文件到特定目录的方式,所以根据项目文件数量和大小不同,可能花费的时间也会有相当大的差别,快则几秒,慢则数分钟。而 Git 的实现与项目复杂度无关,它永远可以在几毫秒的时间内完成分支的创建和切换。同时,因为每次提交时都记录了祖先信息(译注:即 parent 对象),将来要合并分支时,寻找恰当的合并基础(译注:即共同祖先)的工作其实已经自然而然地摆在那里了,所以实现起来非常容易。Git 鼓励开发者频繁使用分支,正是因为有着这些特性作保障。

5 Git是如何物理存储对象的

所有的对象都以SHA值为索引用gzip格式压缩存储, 每个对象都包含了对象类型, 大小和内容.

Git中存在两种对象 - 松散对象(loose object)和打包对象(packed object).

5.1. 松散对象(对应未改动文件)

松散对象是一种比较简单格式. 它就是磁盘上的一个存储压缩数据的文件. 每一个对象都被写入一个单独文件中.

如果你对象的SHA值是ab04d884140f7b0cf8bbf86d6883869f16a46f65, 那么对应的文件会被存储在:

GIT_DIR/objects/ab/04d884140f7b0cf8bbf86d6883869f16a46f65
Git使用SHA值的前两个字符作为子目录名字, 所以一个目录中永远不会包含过多的对象. 文件名则是余下的38个字符.

5.2. 打包对象(对应休改动文件)

另外一种对象存储方式是使用打包文件(packfile). 由于Git把每个文件的每个版本都作为一个单独的对象, 它的效率可能会十分的低. 设想一下在一个数千行的文件中改动一行, Git会把修改后的文件整个存储下来.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,265评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,274评论 1 288
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,087评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,479评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,782评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,218评论 1 207
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,594评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,316评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,955评论 1 237
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,274评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,803评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,177评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,732评论 3 229
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,953评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,687评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,263评论 2 267
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,189评论 2 258

推荐阅读更多精彩内容

  • Git 学习笔记 GitHub浏览器快捷键:t: 查找 先丢一堆图。。就是这么任性 目录Git对象说明 1.Git...
    歌白梨阅读 907评论 0 1
  • Git 基础 基本原理 客户端并不是只提取最新版本的文件快照,而是把代码仓库完整的镜像下来。这样一来,任何一处协同...
    __silhouette阅读 15,751评论 5 147
  • 林有木兮木有枝,心悦君兮君不知。 暗恋的种子发了芽,扎了根。成了她生命的一部分。 做早操时,她下意识得搜寻他的身影...
    毛毛虫也有春天阅读 194评论 0 0
  • 70年代初,我上初中,学校在逍遥津西南角,一个巷道里。 初二时,班主任是教政治的男教师。他高个,長脸,一双豆荚小眼...
    言午57阅读 280评论 0 1
  • 对的人是不是兜兜转转还会回到你的身边?没有回来的大多都不是对的人……吧. 从你的全世界路过,让我们变成更好的...
    奕先生阅读 265评论 0 1