发明 Git(一) Posted by xmpace on 2019-09-01

我是个程序员,会点前端,最近接了个私活,给一个小公司做官网,纯静态页面。

下载好必要的库文件后我就开始开发了。我把页面做了一下拆分,一步步开发,先布局,再做 Banner,接着导航栏、边栏,最后主体。

我有时候会比较纠结,觉得这种样式也好看,那种样式也好看,索性就多套方案都尝试一下,然后从中选一个。

所以每开发完一个部分,我就会将文件夹设置为只读,不再修改。开发下一个部分的时候,再从这份文件夹复制一份新的出来,在这个新的文件夹中改。这样,我想尝试多种方案的话,就复制多份文件夹出来尝试就好了。我管这些文件夹叫快照,每一个文件夹都是一个快照。

是的,我的版本管理水平还停留在大学时做毕业设计的水平上。

没多久,项目就变成这样了。在做边栏的时候,我尝试了两种方案。图中,我在 main.css 和 index.html 后面标记了个 M,表示我每次都只修改这两个文件,其他文件并不改动,毕竟纯静态的前端页面就是拖拖控件,调调样式嘛。

在版本管理中,上面提到的 快照 其实就是 版本,一个快照就是一个版本。

作为一个有追求的程序员,我觉得用这样的版本管理方式有点 low,但我又觉得这种复制文件夹的方式还挺实用的,所以我在想能不能基于复制文件夹这个比较蠢的思想做一个版本管理软件呢?

能不能做出来不知道,反正名字得起好,就叫它 Git(蠢货)吧!

我想了下,其实现在复制文件夹最大的问题是 版本之间没有建立关联。我要是不说,你肯定不知道 website-v3-1 是从 website-v2 还是从 website-v3 复制过去的,说不定过段时间,我自己都忘了。

另外还有个问题,版本没有说明。当然我可以在每个目录下放个 README,说这次干了啥,跟上一个版本什么变化啥的。但这毕竟不是项目本身的内容,放在项目目录里还是不太好。

这两个问题很好解决,我把版本之间的关联和说明记下来不就完了吗?但是存哪呢?存到 Git 的安装目录吗?那样的话,我把项目复制到别的电脑去,版本信息就全没了。

不如直接存到项目根目录!我专门建个 git 的文件夹,用来存版本的各种信息。当然,我的软件要对这个文件夹做特殊处理,不要把它当成项目的内容。对了,这个文件夹正常情况下不应该被用户看到,以免给用户带来困扰,不如在文件夹名字前加个点吧,在 Linux 这样的系统中,文件名前带点的会被当做隐藏文件,正常设置下是不会展示的。

另外我工作的时候其实只会在一个版本上工作,其他版本都是备用的,有需要的时候才会把那些版本翻出来,不如将那些暂时不用的版本也都放 .git 文件夹里去,只保留我当前工作的内容在项目根目录里就好。这个项目根目录就是 工作目录,我只会对工作目录的内容做修改。

版本之间的关系应该怎么记呢,我只要记每个版本是从哪个版本复制来的就行了,也就是记录每个版本的 父版本,就把它记到 log 文件里吧。

如图,log 文件第一列记录的是版本的名称,第二列是它的父版本,没有父版本就记个 null,最后一列是这个版本的说明。

好了,我的 Git 雏形好像就已经完成了,够简单的,来看一下 work 不 work。

假设我现在在边栏 0的版本上,工作目录内的内容都是边栏 0 的内容,但最终我还是决定用边栏 1的样式,并在边栏 1 的基础上开发网页主体,我会怎么操作呢?首先,我会把版本切到边栏 1,执行 git checkout website-v3-1,这条命令会将 .git 目录中的 website-v3-1 的内容拿出来复制到工作目录,现在工作目录中的文件就都被换成 website-v3-1 的文件了。也就是说现在切到了快照 website-v3-1。

然后开发网页主体部分,编辑完 main.css 和 index.html 文件后保存,执行 git commit '网页主体',我管这条命令叫提交命令,因为它提交了一个新版本。提交命令会在 .git 文件夹中新建一个文件夹 website-v4,然后将工作目录的文件全部复制到 website-v4 中。

至于 website-v4 这个文件名的生成规则是怎样的,其实不重要,它只要是唯一的就行了,暂时就用版本号递增的生成规则吧。

如果我此时执行 git log,则会看到如下的历史版本记录,注意,website-v3 是看不到的,因为它不在这条版本线上。

这样,整个版本的全局关系就变成了一颗树,树的每个节点都可以有多个 分支

如果我想看看 website-v3,我可以执行 git checkout website-v3,这条命令会将项目目录的内容切到 website-v3 这个版本去,此时执行 git log,则会看到如下的历史版本记录。

版本号

目前为止,这个版本管理软件工作得还算不错,但我一直在用快照文件名做版本号,我觉得这不妥。因为版本不止包含快照内容,还包含版本的父版本以及版本说明等信息,也就是说快照只是版本的部分信息。因此我决定用快照内容以及版本说明来生成版本号,我把快照内容、父版本、版本说明以及快照时间合在一起,做一个 SHA1 摘要算法,生成一个指纹,用这个指纹做版本号。

由于 SHA1 生成的指纹比较长,所以我这里只取了指纹的前 5 位来表示。

现在 git checkout 命令的行为有所改变了,之前是 git checkout 快照文件名,现在是 git checkout 版本号 了,这条命令首先会去 log 文件中找相应版本号的行,然后取该行的快照文件名,再根据文件名取快照文件夹中的内容复制到工作目录。

分支名

假设我现在在边栏 0 版本上,我要切到边栏 1 版本,该怎么办呢?我无法通过 git log 命令找到边栏 1 的版本号,因为边栏 1 版本不在我当前所在的分支上,遍历父版本是找不到边栏 1 版本的。那除非我能记住边栏 1 的版本号,否则就只能把整个版本树全列出来才能知道版本 1 的版本号了,这显然不科学。

由此,我意识到,分支是需要命名的。每个分支在创建的时候就应该指定一个名字,那么分支是什么时候创建的呢?

我回顾了一下,当我开发完边栏 0 的版本后,我想尝试另一个边栏方案,于是,我切到上一个版本“导航栏”,然后在“导航栏”版本上做开发,开发完成后提交新版本,得到了边栏 1 版本,这个时候边栏 1 版本的分支就形成了。

这里面实际包含了一个创建分支的动作,只是这个步骤被省略了,还原的话应该是这样的:先切到版本“导航栏”,然后创建分支“边栏 1”(此时分支的“枝”还没长出来),开发完成后提交新版本,得到边栏 1 版本(此时分支的“枝”长出来了)。

创建分支(分支的“枝”还没长出来)

提交新版本(分支的“枝”长出来了)

于是我搞了个 git branch 分支名 的命令用来创建分支,这个命令会建立一个分支名与分支的映射关系,映射关系以文件的形式存在 .git 中的 branches 文件夹中,我就用分支的最新版本号来指代分支,每次 git commit 生成新版本时,就更新一下分支的最新版本。比如,我用 git branch sidebar-v1 在“导航栏”版本上创建一个 sidebar-v1 的分支,如下。

更新分支的最新版本

sidebar-v1 的文件内容很简单,就是一行版本号,指向相应的版本。

提交对象

好了,让我再看看还有没有其它可改进的地方。我把目光放在了 log 文件的实现上,如果我要切到某个版本,首先就要找到这个版本在 log 文件的哪一行,怎么找呢?就我现在这个文件组织方式,只能从上到下一行行扫。这个效率差了点,是,我知道我这小破项目不值得费这个劲,但咱要 think big 啊,万一将来 Linux 源码要用我这个 Git 来管理呢?

要怎么优化呢?我想了想,这不就是个 key-value 的存储场景么?版本号做 key,其他信息做 value。那难道我写个 key-value 数据库?那就真是杀鸡用牛刀了。何不直接利用文件系统呢?文件系统天然不就是个 key-value 的系统么,文件名是 key,文件内容是 value。对应到 log 文件就是用版本号做文件名,版本信息做文件内容。这么改造后,log 文件就变成了一堆版本文件。现在每次执行 git commit 提交新版本时就会生成一个这样的文件,所以我把这样的文件叫提交对象,存在 .git 目录下的 commit-objects 目录下。

现在目录变成了这样

普通对象

我已经迫不及待想发布 Git 这个产品了,但总觉得哪里不对,哦,空间!因为我是基于复制文件夹的思路来做的版本管理,所以对于很多文件来说,会有多份快照。比如 lib 文件夹里的 jquery 和 bootstrap 文件,从头至尾内容都没动过,但却被复制了 N 次,在我的版本管理库中存了 N 份,这也太太太浪费空间了!

按理来说这些文件只应该存一份,每个版本中搞一个链接指向那独一份的文件就行了,这个思路一下击中了我,对啊,像 Windows 的快捷方式,Linux 的软链接那样不就行了么。

开干,我现在不在快照文件夹中存实际的文件了,我把这些实际的文件都存到一个统一的地方,先前我搞了个 commit-objects,这次就再搞个 plain-objects 吧,因为它们就是普通对象,它的内容是啥 Git 并不关心。我用文件内容做 SHA1 得到的指纹作为文件名,这样,文件内容相同的文件就只会存在一份,然后快照文件夹中再用软链接指向这些文件。

快照文件夹中使用软链接

我已经把空间的问题解决了!只是我发现还不够完美,因为我用到了软链接,这玩意儿是操作系统的东西,等于跟操作系统耦合起来了。有没有更好的方法?我想了想,其实软链接这种东西我完全可以自己实现啊,不就是个指针嘛。我用普通文件来代替软链接,就叫这种文件做链接文件好了,链接文件的内容是真正文件内容的 SHA1 值,指向 plain-objects 里对应的文件。

树对象

好了,现在普通文件链接过去了,我发现,其实目录也可以像普通文件一样链接过去啊,并且目录链接过去后,普通文件的链接文件就没必要创建了,普通文件的信息可以写在目录链接文件中,这样就能少创建好多文件啦。区别于普通对象,我把目录文件叫树对象,存在 tree-objects 下,快照中的每个目录都对应一个树对象,目录的名称也可以像上面那样用目录内容的 SHA1 值来命名。

项目的根目录也是个目录,我们不需要特别对待它,把它当普通目录处理就行了。与普通目录不同的地方是,普通目录的名称会存在上级目录文件中,而根目录没有上级目录,所以它的名称自然也没地方保存,不过根目录名称本来也没存的必要,使用者想将它命名成啥都行,所以这不是个问题。

有了根目录对应的树对象,通过层层遍历,我们就能得到整个快照的结构和内容。

好了,到这里,我觉得我的 Git 可以公开发表了,至少,它现在作为一个单机的版本管理软件可以工作得很好。

但我不满足于此,因为,我发现自己的设计功底比较差,我准备找我设计功底比较牛的基友来帮我合作开发。所以,下一个迭代,我准备把 Git 打造成可多人协作的版本管理软件,尽请期待吧~