白话Git提交树

版本管理是git的核心功能,是通过“提交树”实现的。最近遇到一个很有意思的网站Learn Git Branching,它用交互小游戏的方式,解释了git提交树的操作。这篇文章不包含任何命令和代码,只是从概念的角度,尝试用最简单的语言,记录下在挑战完关卡后,我对提交树的理解。

1. 基础

提交(Commit)是git的基本单位,代表了对代码库的一次修改。每一个提交,都基于上一次提交的结果,把提交用箭头连起来,就形成一棵提交树:

提交以哈希值命名,永久保存了当前状态。检出(Checkout)可以切换提交,即回到提交时的状态。为了方便,这里用C1、C2、C3代表三次提交。

分支(Branch)是指针,指向提交树上的某一个提交。通常分支名描述的是工作内容,代表基于当前的提交,进行特定的开发工作。在某个工作上提交修改时,提交树会向前生长,分支会指向新的节点。例如下方的master分支:

HEAD是一个特殊指针,它指向分支,分支的内容就是当前目录的内容。使用检出操作时,其实是将HEAD指向检出的节点或者分支,并且同步当前内容:

标签(Tag)也是指针,不过和分支不同,标签是永久指针,一旦指向某个提交,就不会再变动了。通常标签用来标识版本:

2. 分叉与合并

在同一个提交上,可以进行不同的工作。如果一个提交已经有子节点,继续基于它开发的话,会在同一个提交下,生成两个新节点,提交树便产生了分叉:

提交C2为代码库增加了A文件,提交C3为代码库增加了B文件,可是由于分叉,这两个提交都看不到对方新增的文件。为了让两个修改同时生效,git提供了合并(Merge)功能,将分叉的提交树重新合在一起,新的提交包含了两条分叉的所有修改内容:

子节点有两个父节点,所以严格来说应该叫提交图,不过提交树是约定俗成的叫法,这里也不修改了。

3. 修改

从合并后的提交树可以看到,代码曾经同时进行了两项工作,之后合并在了一起。但在实际项目中,很少人会关注工作是否同时进行,他们只关注进行过哪些工作。因此git提供了变基(Rebase)功能,就像名字所表达的意思,它可以改变基准提交,从而删除分叉。例如下面这张图,C3提交原本是基于C1做的修改,使用变基后,就变成了基于C2进行的修改:

挑选(Cherry Pick)是一个非常灵活的操作,可以把任意一个提交的修改内容,复制到当前的提交下面。例如下面图片中,对C2进行挑选操作,就会把C2复制后,接在C4的下面:

挑选通常用在修复bug上。当发布分支上出现bug时,根据Gitlab工作流,应当在主分支上进行修复,然后通过挑选功能,将修复节点复制到发布分支上。这样就不会把主分支上刚开发的新特性也一起复制了。

在提交树上,每一个分支都指向一个提交。从这个提交开始,回溯到根提交的所有提交,都被分支引用着。如果一个提交没有被任何分支引用,那么下次垃圾回收时,就会被删除:

重置(Reset)是将当前分支向父提交移动。利用上述原理,重置可以用来删除提交。重置命令移动分支后,中间经过的所有提交便会失去引用,最终被git删除:

变基、挑选和重置都修改了提交树。如果提交树已经共享给其他同伴,他们很可能已经基于提交树进行过开发。这时修改提交树,会引起同伴之间代码库不一致。通常情况下,只允许修改本地分支,而对于远程分支,git提供了还原(Revert)命令。还原实际上是创建一个新节点,这个节点的内容是撤销另一个节点的修改,不会影响到原始的提交树:

4. 远程

git是分布式版本管理,因此提供了对远程仓库的操作。克隆(Clone)用来将远程仓库复制到本地,其实是将提交树复制到了本地。但是在本地提交树上,所有的远程分支都加上了主机名,例如远程主分支就叫做“origin/master”:

在本地作出修改后,为了把修改推送到远程仓库,git提供了推送(Push)功能。推送可以将本地分支的内容,推送到某个远程分支:

如果远程仓库被其他人修改了,那么推送就会失败。因为推送功能要求,修改必须是基于远端分支的最新位置。git提供了取回(Fetch)功能,用来将最新的远程提交树,取回到本地来:

此时就可以用变基或者合并,将修改拉到最新远程节点下。

取回远程仓库后,最常见的下一步操作是将本地分支更新到最新位置。git提供了拉取(Pull)功能,把两步合为一起,先从远端取回提交树,再对本地分支进行变基或者合并。

每次推送和取回时,都需要手动指定本地分支和远程分支的对应关系,十分不方便。git提供了设置上流(Set Upstream)功能,自动关联本地分支和远程分支。此时在本地分支上执行推送、取回或者拉取时,会自动作用到对应的远程分支上。常规使用时其实不用担心,因为git在克隆的时候,就已经做好这项工作了。

全文完