作者 | 梁唐
出品 | 公众号:Coder梁(ID:Coder_LT)
大家好,日拱一卒,我是梁唐。
今天我们继续聊聊麻省理工的missing smester,消失的学期,讲述课堂上不会涉及,但又非常重要的知识和技能。
这一节课主要讲的内容是git的基本原理以及常见命令,git对于工程师的重要性相信不用我多说,绝对是所有程序员必学的技能之一。属于不一定要很精通,但至少得懂一点的领域之一。
B站视频链接:https://www.bilibili.com/video/BV1x7411H7wa?p=6
和之前一样,这节课的note质量同样非常高。
本文是基于本节课note以及老师上课演示的内容,还有我个人的一些理解做的翻译整理版本。日拱一卒,欢迎大家打卡一起学习。
版本控制系统(VCS)是用来追踪源代码(或其他文件、文件夹的集合)变更的工具。正如其名,这些工具帮助我们维护一个变更的历史。不仅如此,还让协同开发变得更加方便。VCS通过创建一系列快照的方式追踪一个文件夹和它当中所有内容的变更,每个快照都包含了文件/文件夹的完整的状态。VCS同样维护一些元信息,比如谁创建了快照,每个快照的备注信息等。
为什么我们要用版本控制呢?即使你独自工作,它也可以让你查看项目的历史版本,维护改动的历史,允许我们并行开发。当我们和其他人合作的时候,它更是无价之宝,因为我们可以看到其他人的修改,并且可以解决并行开发导致的冲突。
现代VCS同样让你能够很轻易地回答下列问题:
虽然还有其他的VCS,但事实上的标准是git。这里有一篇关于git的漫画,很有意思:

因为git的界面过于抽象(leaky abstraction),通过自顶向下的方式学习git充满了困惑。很多时候只能死记硬背一些命令,像是魔法一样使用它们。一旦遇到问题,就只能像是漫画里说的那样去处理了。
尽管git的界面有些简陋,它底层的设计和思想却非常出彩。丑陋的接口只能死记硬背,而优秀的设计值得花时间理解。因此,我们将提供一个自底向上的对于git的解释,从它的数据模型开始,然后再学习它的命令行接口。当数据模型被理解了之后,再理解命令以及它们是如何生成底层数据模型的就非常容易了。
进行版本控制的方法有很多,git拥有一个深思熟虑的模型是的它支持版本控制当中许多出彩的特性。比如维护历史、支持分支以及允许协同合作。
git将历史变更抽象成顶级目录下的一系列文件和文件夹的快照。在git术语中,文件被称为blob,会被视为是一系列字节(byte)。一个文件夹被叫做tree,它存储一系列blob和tree和名称的映射(文件夹可以包含文件夹)。快照是被追踪的最顶层的树,比如一个树看起来可能是这样的:

顶层的树包含两个元素,一个叫做foo的tree(foo当中又包含一个叫做bar.txt的blob),和一个叫做baz.txt的blob。
版本控制系统是如何关联快照的呢?一种简单的方式是线性历史。一个历史变更是由一系列快照按照时间顺序排列组成的。因为种种原因,git没有使用这样的模型。
在git当中,历史变更是一个快照构成的DAG(有向无环图)。这看起来似乎很高大上,但不用害怕只是一个很简单的概念。这表明了git当中的每一个快照可能有多个父节点。注意,快照有多个父节点而非一个,因为某一个快照可能是由多个父节点生成的,比如由于合并了两个并行开发的分支而创建的节点,就会有多个父节点。
git将这些快照叫做commit。将一个commit的历史可视化,看起来可能是这样的:

上面是一个由ASCII构成的简图,o表示一个独立的commit(快照)。箭头指向了每个commit的父节点(箭头方向是时间更早的方向)。第三个commit之后,历史记录分岔成了两个不同的分支。这可能由于两个独立的特性被并行开发。未来这些分支会被合并成一个新的快照,它会包含所有的特性。新的提交会创建一个新的历史记录,看起来像是这样,新创建的节点被加粗显示:

git中的commit是不可修改的。这不意味着错误不能被修改,而是我们修改变更历史实际上是创建的新的commit,而引用(参考下文)则被更新并指向这些新节点。
可能用伪代码的形式表示git中的数据模型更加清晰:

非常简洁易懂
object可以是一个blob、tree或commit:

在git数据存储当中,所有的objects都会基于它们SHA-1 哈希之后的结果进行寻址,SHA-1是一种哈希算法。会将传入的结果映射成一个字符串,算法会尽可能保证映射之后的字符串唯一。

blob、tree、commit就以这种方式被整合在了一起:它们都是object。当它们引用其他object时,它并没有真正将这些值存储下来,仅仅是引用了它们的hash值。
举个例子,刚才例子当中的tree可以通过git cat-file -p 698281bc680d1995c5f4caaf3359721a5a58d48d来进行可视化,看上去是这样的:

树本身会包含它当中内容的指针,baz.txt(blobl)和foo(tree)。如果我们查看baz.txt这个hash值中对应的内容,命令git cat-file -p 4448adbf7ecd394f42ae135bbeed9676e894af85,我们可以看到如下结果:

现在,所有的快照都可以通过它们的hash值来定位。这并不方便,因为对人类来说记住长度40的16进制字符串是非常不方便的。
git的做法是给这些hash值赋予人类能理解的名字,叫做引用。引用是指向commit的指针。和object不可变不同,引用是可变的,可以指向新的commit。比如,master是一个引用,通常用来指向主分支中最新的commit。

通过引用,git就使用了人类可读的诸如master这样的名字来指代历史中的快照了。
一个细节是,我们经常想要知道我们当前所在的位置。这样当我们创建新的快照时,我们就知道它关联哪些快照。在git当中,我们现在所在的位置也是一个特殊的引用,叫做HEAD。
最后,我们可以粗略地定义git仓库了:数据object和引用。
在磁盘上,所有记录都以object和引用的方式存储:因为数据模型当中只有这两个概念。所有的git命令都对应commit DAG上的一些操作,比如添加object,添加或更新引用。
当你输入命令的时候,思考一下命令背后对于底层数据结构进行的操作。相反,如果你做出对commit DAG进行具体的修改,比如抛弃未提交的变更,或者是让master引用指向5d83f9e commit,这通常都是有办法的。(上述的例子当中,可以使用git checkout master; git reset --hard 5d83f9e)
git当中还包含一个和数据模型不相关的概念,但它是创建commit接口的一部分。
你可能觉得上面说的创建快照的命令类似于create snapshot,一些VCS的确是这样,但git不是。我们希望干净的快照,每次都从当前状态创建快照在一些情况并不理想。比如,想象一个场景,你开发完了两个功能,你想要创建两个不同的分支。第一个分支包含第一个功能,第二个分支包含第二个。或者,你在代码当中加入了一些debug信息,在提交的时候你希望能不要带上这些debug代码。
git处理这些场景的方式是使用一种叫做暂存区(staing area)的机制,它允许你指定下一次快照会包含的内容。
为了避免重复信息,我们将不会详细解释下面的命令。强烈推荐阅读一下Pro Git这本书获得更多信息,或者观看课程视频。
喜欢本文的话不要忘记三连~