Git 的简单工作原理
在来到薄荷之前,我的工作中对代码审核等并没有一个规范的流程,所以对 Git 的使用也一直停留在几个基本的命令如add
,commit
的使用,而且对其背后的原理也不怎么理解。最近趁着每周坐公交车的时间,看了 Pro Git
这本小书的前三分之一的部分,算是对一些基本操作有了一些理解,今天就来与大家分享一下。
Git 简史
Git 诞生于一个极富纷争大举创新的年代。Linux 内核开源项目有着为数种广的参与者。绝大多数 Linux 内核维护工作都花在了提交补丁和保存归档的繁琐事物上。到 2002 年,整个项目组开始启用分布式版本控制系统 BitKeeper
来管理和维护代码。
到了 2005 年,BitKeeper
收回了 Linux 内核开源社区的免费使用权利。Linus 对此愤怒了,于是决定吸取教训,开发一套属于自己的版本控制系统。同时制定了如下的目标:
- 速度
- 简单的设计
- 完全分布式
- 对非线性开发模式的强力支持(允许上千个并行开发的分支)
自 2005 年诞生以来,Git 日臻完善成熟,在高度易用的同时仍然保留着初期设定的目标,即简单、速度。
Git 基础
在使用 Git 的时候,若是能对 Git 的思想和基本原理,用起来就会知其所以然、游刃有余。如果以前使用过 svn
之类的版本控制系统的经验的话,在初次使用 Git 的话应该会有种似曾相识的感觉,因为很多命令都很像。但是这些命令只是封装之后的结果,其思想和原理是截然不同的。
- 直接记录快照,而非差异比较
- Git 只关心文件数据的整体是否发生变化,而大多数其他文件系统则只关心文件内容的具体差异
svn
这类系统每次记录有那些文件发生了更新,以及都更新了哪些行的什么内容,大致是下图- Git 并不保存这些前后变化的差异数据。实际上,Git 更像是把变化的文件作快照后,记录在一个微型的文件系统中。每次提交更新,它会纵览一遍所有文件的指纹信息并对文件作一快照,然后保存一个指向这次快照的索引。为提高性能,若文件没有变化,Git 不会再次保存,而只针对上次保存的快照作一次链接
- 近乎所有操作都是本地执行
- 因为 Git 在本地磁盘上就保存着所有当前项目的历史更新,所以处理起来速度飞快
- 如果要浏览项目的历史更新摘要,Git 不用跑到外面的服务器上取数据回来,而直接从本地数据库读取后展示给你看
- 没有网络也可以非常愉快地频繁提交更新
- 时刻保持数据完整性
- 在保存到 Git 之前,所有数据都要进行内容的校验和(
checksum
)计算,并将此结果作为数据的唯一标识和索引 - Git 使用
SHA-1
算法计算数据的校验和,通过对文件的内容和目录的结构计算出一个SHA-1
哈希值,作为指纹字符串。该字符串由40个十六进制
(0-9 及 a-f)组成 - 所有保存在 Git 数据库中的东西都是用此哈希值来作索引的,而不是靠文件名
- 在保存到 Git 之前,所有数据都要进行内容的校验和(
- 多数操作仅添加数据
- 在 Git 里,一旦提交快照之后就完全不用担心丢失数据
- 文件的三种状态
- 已提交(
committed
),表示该文件已经被安全地保存在本地数据库中了 - 已修改(
modified
),表示修改了某个文件,但还没有提交保存 - 已暂存(
staged
),表示把已修改的文件放在下次提交时要保存的清淡中
- 已提交(
- Git 目录
- 每个项目都有一个 Git 目录(
.git目录
),它是 Git 用来保存元数据和对象数据库的地方。该目录非常重要,每次克隆镜像仓库的时候,实际拷贝的就是这个目录里面的数据 - 从项目中取出某个版本的所有文件和目录,这些文件实际上都是从 Git 目录中的压缩对象数据库中提取出来的,接下来就可以在工作目录中对这些文件进行编辑
- 暂存区域只不过是个简单的文件,一般都放在 Git 目录中
- 每个项目都有一个 Git 目录(
- 文件的状态变化周期
Git 收取的是项目历史的所有数据,服务器上所有的数据克隆之后本地也都有了。实际上,即便服务器的磁盘发生故障,用任何一个克隆出来的客户端都可以重建服务器上的仓库,回到当初克隆的状态。
Git 分支
几乎每一种版本控制系统都以某种形式支持分支。使用分支意味着你可以从开发主线上分离出来,然后在不影响主线的同时继续工作。在很多版本控制系统中,这是个昂贵的过程,常常需要创建一个源代码目录的完整副本,对大型项目来说会话费很长的时间。
而 Git 的分支模型被成为必杀技特性
,正是因为它,将 Git 从版本控制系统家族里区分出来。Git 的分支有何特性呢?Git 的分支可谓是难以置信的轻量级,它的新创建几乎可以在瞬间完成,并且在不同分之间切换起来也差不多是一样快。理解分支的概念并熟练运用后,你才会意识到为什么 Git 是一个如此强大而独特的工具,并从此真正改变你的开发方式。
何谓分支
在 Git 中提交时,会保存一个提交(commit
)对象,该对象包含一个指向暂存内容快照的指针,包含本次提交的作者等相关附属信息,包含零个或多个指向该提交对象的父对象指针:首次提交是没有直接祖先的,普通提交有一个祖先,由两个或多个分支合并产生的提交则有多个祖先。
下面举个例子:假设我们在工作目录中有三个文件,准备将它们暂存后提交。暂存会对每一个文件计算校验和(SHA-1哈希字符串
),然后把当前版本的文件快照保存到 Git 目录中(Git 使用 blob 类型
的对象存储这些快照),并将加入暂存区域。
当使用 git commit 新建一个提交对象前,Git 会先计算每一个子目录(本例中就是项目根目录)的校验和,然后在 Git 仓库中将这些目录保存为树(tree
)对象。之后 Git 创建的提交对象,除了包含相关提交信息以外,还包含着指向这个树对象
(项目根目录)的指针。如此它就可以在将来需要的时候,重现此次快照的内容了。
现在,Git 仓库中有五个对象:
- 三个表示文件快照内容的 blob 对象
- 一个记录着目录树内容及其中各个文件对应 blob 对象索引的 tree 对象
- 一个包含指向 tree(根目录)的索引何其他提交信息元数据的 commit 对象
两次提交后,仓库历史会变成下面这样
回来继续说分支。Git 中的分支,其实质上仅仅是个指向 commit 对象的可变指针
。Git 会使用 master
作为分支的默认名字。在若干次提交后,你其实已经有了一个指向最后一次提交对象的 master
分支,它在每次提交的时候都会自动向前移动。
Git 又是如何创建一个新的分支呢?答案很简单,创建一个新的分支指针。比如创建一个 test 分支
。
那么,Git 是如何知道你当前在哪个分支上工作的呢?其实答案也很简单,它保存着一个名为 HEAD
的特别指针。它是一个指向你正在工作的本地分支的指针,假设当前我们在 test 分支
,那么 HEAD
就指向了 test 分支
由于 Git 中的分支实际上仅是一个包含所指对象校验和(40个字符长度 SHA-1 字符串
)的文件,所以创建何销毁一个分支就变得非常廉价。其实,先见一个分支就是向一个文件写入 41 个字节
(外加一个换行符)那么简单,当然速度也就很快了。
这和大多数版本控制系统形成了鲜明的对比,它们管理分支大多采取备份所有项目文件到特定目录的方式,所以根据项目文件数目和大小不同,可能话费的时间也会有很大的差别,快则几秒,慢则几分钟。而 Git 的实现与项目复杂度无关,它永远可以在几毫秒内完成分支的创建和切换。同时,因为每次提交都保存了 parent 对象
,将来要合并分支时,寻找恰当的合并基础的工作其实已经自然而然地摆在那里了,所以实现起来非常容易。 Git 鼓励开发者频繁使用分支,正是因为有着这些特性保障。