我不知道 GitHub 究竟是如何做到的,但这是一种可能的方法。它需要了解 git 存储数据的方式。
简短的回答是 repos 可以共享数据库,objects
但每个都有自己的引用。
我们甚至可以在本地对其进行模拟以进行概念验证。
在裸仓库的目录中(.git/
如果不是裸仓库,则在子目录中)有三件事是仓库工作的最低要求:
objects/
存储所有对象(提交、树、blob ...)的子目录。它们可以单独存储为名称等于对象哈希的文件,也可以存储在.pack
文件中。
refs/
子目录,它存储简单文件,例如其refs/heads/master
内容是它引用的对象的哈希值。
- 该
HEAD
文件,其中说明了当前提交是什么。它的值要么是原始散列(对应于分离的头,即我们不在任何命名的分支上),要么是指向可以找到实际散列的参考的文本链接(例如ref: refs/heads/master
- 这意味着我们在分支上master
)
假设有人orig
在 Github 上创建了他的原始(非分叉)回购。
为了模拟,我们在本地做
$ git init --bare github_orig
我们假设上述情况发生在 Github 服务器上。现在有一个空的 github 存储库。然后我们想象从我们自己的 PC 上克隆 github repo:
$ git clone github_orig local_orig
当然在现实生活中github_orig
我们会用https://github...
. 现在我们已经克隆了 github 仓库local_orig
。
$ cd local_orig/
$ echo zzz > file
$ git add file
$ git commit -m initial
$ git push
$ cd ..
this 之后github_orig
的object
目录将包含我们推送的提交对象、一个 blob 对象file
和一个树对象。该refs/heads/master
文件将包含提交哈希。
Fork
现在让我们想象一下当有人按下按钮时会发生什么。我们将手动创建一个 git repo :
$ mkdir github_fork
$ cd github_fork/
$ cp ../github_orig/HEAD .
$ cp -r ../github_orig/refs .
$ ln -s ../github_orig/objects
$ cd ..
请注意,我们复制 HEAD
了,refs
但我们为objects
. 正如我们所见,制作叉子非常便宜。即使我们有数十个分支,每个分支都只是refs/heads
目录中的一个文件,其中包含一个简单的十六进制散列(40 字节)。因为objects
我们只链接到原始对象目录——我们不复制任何东西!
现在我们模拟创建分叉的用户,在本地克隆分叉的 repo:
$ git clone github_fork local_fork
$ cd local_fork
$ # ls
.git/ file
我们可以看到我们已经成功克隆了虽然我们克隆的 repo 没有自己的objects
但链接到原始 repo 的 repo。
现在分叉用户可以创建分支、提交,然后将它们推送到github_fork
. 对象将被推送到与!objects
相同的目录中。github_orig
但是refs
andHEAD
将被修改并且将不再匹配github_orig
.
所以底线是属于同一分叉树的所有存储库共享一个公共对象池,而每个存储库都包含自己的引用。任何将提交推送到自己的分叉存储库的人都会修改自己的引用,但会将对象放入共享池中。
当然,要真正可用,必须注意更多的事情——最重要的是,除非调用它的 repo 知道所有引用的知识,否则不得调用 git 垃圾收集器——而不仅仅是它自己的。否则,它可能会丢弃共享池中无法从其引用访问但可以从其他 repos 的引用访问的对象。