Git 理论与实践:能做什么以及为什么可以这么做

作为几乎已经一统天下的版本控制系统 Git,其精妙之处却不在版本控制本身,这真是一件顶有趣的事。

Linus,作为 Git 的设计和开发者,也是 Linux 的缔造者,有这样一句蔑视天下大部分程序员的名言:

程序本身不重要,数据结构才是关键。

在了解过 Git 的精髓之后,对于此话有了更深的理解。作为凡人的我,大部分时间都是在和程序缠斗,能写出易读易维护的程序已是难上加难的事。但 Linus 说了,数据结构才是关键。

事实也证明,Git 的灵魂的确是它那简单清晰,灵活轻盈的数据结构。正是有了这样的数据结构,才能让 Git 可以轻轻松松的支撑起成千上万条分支的并行开发,分支之间的切换,迅如闪电。

毫无疑问,Git 的学习曲线异常陡峭,遥想自己当初刚刚接触 Git 时,愣是看不明白那一个个简单又灵活的指令应该怎么用。后来,岁月流转,经过一次次尝试使用,终于基本掌握了几个最基础的 Git 命令的使用。比如:

所有这些命令看着不少,其实也不多,但能做的事情已经非常多。大部分使用 Git 进行代码版本控制的程序员也都多少对其中大部分命令有所耳闻,有所使用。但是其中有多少命令,只是会用而不知个中门道呢?

我自己的情况是,在了解 Git 内部原理之前,以上几乎所有命令的掌握均是来自于死记硬背和用成惯性,而个中门道,只对个别的有些模糊又说不出来的直觉性感知,并且不知对错。

稍微生僻一些的命令,非要用到的时候,要么依赖同事的把关,要么到网上找到几乎一模一样的案例,然后照抄其解决方案。

真要自己写,心里是一点底也没有。

但是呢,我现在心里有底了。因为我已经初步窥探到 Git 内部原理的精髓之处。记在这里,一是为了巩固,二是为了方便自己日后查看。

以下便是 Git 内部原理的精髓之处:

尝试使用自己的语言简单描述一番。首先,最核心之处在于,Git 本质上不是版本控制系统,它是一个键值型文件存储系统。对于存储而言,只使用两种文件对象:tree 和 blob。tree 的作用是我们熟悉的文件夹,blob 的作用是我们熟悉的文件,所有类型的文件在 git 存储体系下都是 blob 对象。

在此基础之上,git 的版本控制通过一次次的文件对象快照来实现。为了实现版本控制,又额外多了两种类型的对象:commit 和 tag。其中最核心也最频繁使用的是 commit 对象。每一次 commit 都会新生成一个 commit 对象,此 commit 对象指向当前的文件对象快照。虽然 commit 前后文件不变的话,两次快照是共用一个文件对象,但每一次文件快照都是完备的,相互之间都是独立的。

是的,git 的版本控制是通过文件快照实现。聪明的你,或许开始质疑其存储空间的利用率。如果一个文件特别大,我只改变一点点时,git 的快照系统便会保存两个内容相近的大文件。这岂不是特别浪费且低效?

不用担心,git 有自己的解决办法:delta 压缩。这和版本控制本身无关,所以此处可以略过不讲。你只需知道,git 的存储一点也不浪费,一点也不低效就可以了。

还是回到版本控制系统,git 使用一次又一次的 commit 生成一个个相互间独立的文件对象快照,这一句话便是其核心所在。

接下来,就是如何通过一些辅助性手段来让这个版本控制系统更加灵活且易用。

首先,在这样一个 commit 对象和文件快照构成的版本控制网中,到底哪一个 commit 才是这个网络的入口呢?

答案是:任何一个都可以。

因此,我们需要一个单独的文件来存储这个入口,这个文件的名字就是 HEAD,具体来说是 .git/HEAD。

我们新增 commit 时会通过这个 HEAD 确定相应的挂靠点。

所以,HEAD 会关联到一个具体的 commit 上。

此处,该到分支出场了。什么是分支?分支在 git 里面非常简单:仅仅只是一个指向某个 commit 对象的指针。

因为大部分情况下,我们都是在某个分支的头部开展工作,这些个分支也就被放在了 .git/refs/heads/ 下。默认情况下,git 会建立一个 master 默认分支,因此会有一个 .git/refs/heads/master 引用文件存在。当我们新建一个develop分支时,git 会新建一个 .git/refs/heads/develop 引用文件。这些引用文件指向自己所在分支的头部 commit 对象。

而之前提到的 HEAD 在大部分情况下会指向某一个分支引用。因此,当我们切换分支时,其实只是把 HEAD 关联到相应的分支引用上。

到这里,整个 git 版本控制系统的大部分主要功能所需的数据结构均已到位。剩下的事情不过是具体的实施细节而已。

比如 checkout 命令,当作用到 commit 对象时,只是操作一下 HEAD 指针的指向。

比如 reset 命令,只是操作一下相应分支引用所指向的 commit 对象,并根据具体参数做一些额外处理工作而已。

比如 commit 命令,不过是在当前分支尝试新建一个 同时指向两个分支头部 commit 对象的 commit 提交而已。

比如 rebase 命令,不过是将某些历史提交(也就是一些文件快照)拎出来,重新接续到目标分支的头部 commit 对象后面,并允许我们在这个过程中重新整理、编辑这些历史提交而已。

至于 cherry-pick,那就不用我说了吧?

除了这些之外,再稍微理解一下工作区,暂存区,stash 缓存区和 commit 历史这几个概念,便能清楚的明白所谓 add、commit、stash、stash pop、reset HEAD、checkout file等等命令的作用和原理。

有了这些理解,对 git 才算是真正入门。

最后,欢迎来到强大无比又异常灵活的 Git 世界。


不知是该恭喜,还是该怎样,总之阅读到该文的,你是第 人。每一次刷新,都是不同的自己。