**Git** 二十年:依旧怪异,依旧精彩
3 小时前,由 Scott Chacon 发布 — 11 分钟阅读
Git 二十年:依旧怪异,依旧精彩
二十年前,Git 诞生了。这个不太寻常的“信息管理器”是如何征服世界的?
就在二十年前的今天,Linus Torvalds 对 Git 进行了第一次提交,这个来自地狱的信息管理器。
在过去的 20 年里,Git 从一个小型、简单、个人的项目发展成为有史以来最具统治地位的版本控制系统。
我个人也在这款软件的过山车上经历了一段非凡的旅程。
在它首次提交后的几个月,我就开始使用 Git 做一些你可能无法想象的事情。然后,我共同创立了 GitHub,写了可以说是阅读最广泛的 关于 Git 的书,建立了该项目的官方网站,发起了年度开发者大会等等 - 这个小项目改变了软件开发的世界,但更重要的是,它极大地改变了我的人生轨迹。
我认为今天,当 Git 项目进入第三个十年时,回忆 Git 的早期以及解释我为什么觉得这个项目如此令人着迷会是一件有趣的事情。
希望您喜欢 Kiril 和我一起品酒和构建 Git 的第一个提交,探索它从一开始就能做什么。
Patches 和 Tarballs
在我们深入探讨 Git 的历史以及我与它的关系之前,我想先谈谈 Git 存在的原因以及它开始时的心态。
Git 的诞生源于 Linux 内核开发社区对版本控制和协作的失望。
内核社区一直使用邮件列表进行协作。实际上,这是一种非常吸引人的协作方式 - 它具有大规模可扩展性、高度分布式、本地优先、能够对补丁进行细粒度讨论、可加密保护等等。
邮件列表协作流程的要点是:
- 发布项目已知状态的 tarball (某种 zip 文件)
- 人们下载并将其在本地解压
- 使用他们想要更改的任何功能或修复程序对其进行修改
- 在其上运行 GNU diff 以创建一个补丁,维护者可以将该补丁应用于初始已知状态以添加该功能
- 将该补丁或一系列补丁通过电子邮件发送到邮件列表
- 列表讨论更改
- 维护者应用该补丁以用于下一个 tarball 版本或要求进行更改
- 重复
我很乐意写一篇完整的博客文章来介绍邮件列表协作的工作方式以及它的各个方面有多么酷,但这要留到以后再谈。
然而,在这个世界里,当时的版本控制系统根本没有帮助 - 它们似乎在功能上倒退了一步。它们具有笨拙的访问控制机制,它们不是分布式的,而且速度非常慢。
社区主要使用 patches 和 tarballs,而现有的 SCM 根本不够好。
如果你仔细想想,patches 和 tarballs 工作流程有点像是第一个_分布式_版本控制系统 - 每个人都有一个本地副本,可以在本地进行更改,拥有 "merge" 权限的人可以将新的 tarball 推送到服务器。
然而,这个过程仍然有点繁琐 - 管理 patches,记住应用了什么以及谁贡献了它,保持多个系列处于活动状态,处理冲突或 rebasing 更改。
Bitkeeper 工具是专门为内核的用例开发的,旨在构建一个适用于此工作流程的版本控制系统,Linus 很喜欢它,但他们想要使用的许可与为其构建的社区不协调。
如果你想了解更多关于 Bitkeeper 的信息,请查看这一集 Bits and Booze,我们在其中设置了它并向您展示了它的使用方式。
重要的是要理解这就是 Git 被创建的原因。实际上并不是作为一个版本控制系统,而从根本上说是一种更好的 patches 和 tarballs 的方式 - 对一组文件进行快照并显示可以讨论的差异。
这主要是它的数据结构的设计方式 (文件树的链接列表,内容可寻址的 blob 存储),并且从第一次提交到今天,这种结构从根本上没有改变。
第一次提交
既然我们正在讨论这个话题,那么第一次提交是什么样的?从它存在的第一个时刻开始,Git 可以做什么?
好吧,它是一个 stupid content tracker。正如 Linus 本人从第一天所说的那样:
这是一个愚蠢 (但速度极快) 的目录内容管理器。它没有做很多事情,但它所_做_的是有效地跟踪目录内容。
第一次提交是七个简单的独立工具的集合。它们不是像 git commit
这样的东西,它们是非常低级的数据库工具,比如 write-tree
和 commit-tree
(这种情况发生了改变 在项目开始几周后,当所有东西都开始以 git-
作为前缀时)。
其中一些演变成了至今仍然存在的底层命令,比如 git cat-file
和 git write-tree
,其他的则从根本上不同 (例如,git read-tree
是当前的 Git 底层命令,但最初的 read-tree
更像是当前的 git ls-files
),然而,在底层,这些概念仍然存在。
基本上,通过第一次提交,Git 可以:
- 通过使用
update-cache
构建内容缓存 (本质上是一个 tarball) 来构建一个“快照”,并使用write-tree
将其作为对象写入数据库。 - 使用
commit-tree
编写一个“变更集” (commit),该变更集注释了使用新的 tarball 引入的变更以及它所基于的父级,以便建立“tarballs”的历史记录。 - 使用
cat-file
(从数据库中提取一个对象),read-tree
(列出缓存的样子) 和show-diff
(显示缓存到工作目录的差异) 读取这些数据库结构。
从一开始,Linus 就提到他真的只想构建这个底层工具,并让它成为一些 UI (“porcelain”) 的后端,并在此基础上编写脚本。
这真的是我个人一直认为“git”是什么,只是表面之下的底层工具。例如,像 arch 这样的东西,它基于 "patches 和 tar-balls" (我认为 darcs 在这方面类似),可以使用 git 作为一个_非常_好的 "tar-balls 的历史"。 - Linus
他打算构建一个高效的 tarball 历史数据库工具集,而不是一个真正的版本控制系统。他认为其他人会编写该层。
稍后会详细介绍。但首先…
Scott 遇到 Git
我个人第一次接触 Git 大概是在这个时间线上,由我在倒霉的创业公司 Reactrix 的朋友和同事 Nick Hengeveld 介绍的。
Nick 和我负责将这些交互式广告的资源发送到全国各地购物中心和剧院的计算机上。
有趣的是,我们使用 Git 的方式更像是 Linus 认为的工具 - 作为分布式内容跟踪器 - 而不是你今天可能主要认为的版本控制系统。
我们本质上是为一家广告公司工作,该公司管理着许多数字标牌显示器,这些显示器具有相当重量级的资源。我们的数百个显示器中的每一个都需要运行独特的广告组合,大多数都通过缓慢的蜂窝数据上行链路连接,并且广告经常更改。因此,我们需要一种有效的方式来说“对于机器 A,我们需要广告 1、2 和 3 (v1)。对于机器 B,我们需要广告 2、3 (v2) 和 4”,并在现有广告有新版本时逐步更新它们。
我们使用 Git - 不是为了跟踪源代码中的更改,而是作为一种内容分发机制。我们将使用一个脚本来查看即将到来的调度,编写每个机器只需要的广告的唯一树,将该树提交到机器的分支,然后让每台机器每晚都获取并硬 checkout。
这种方法有许多有趣的优点。
- 如果一个广告更新了,我们只传输更改的文件,并且这些更改是根据可能已经在机器上的对象进行 delta 压缩的。
- 所有共享资源都有一个可以在多个上下文中 checkout 的单个 blob - Git 的内容可寻址文件系统方面非常适合这一点。
- 我们可以拥有数千个数百个资源的组合,而无需在任何地方存储任何内容两次或通过网络多次传输相同的内容。
Nick 是早期 Git 项目的相当重要的贡献者,使其能够为我们的用例工作 (向 http-fetch 添加 SSL 支持,添加可恢复和并行 HTTP 传输,第一个基于 HTTP 的推送解决方案等)。他的第一个补丁是在 9 月份,距离 Linus 的第一次提交仅仅 6 个月。
他向我介绍 Git,我努力理解它,以及我最终突然意识到它非常酷的时刻,这些都促使我写关于它的文章,并努力让人们更容易学习它。
这促使我汇编了 Git 社区书,Git 内部 Peepcode PDF,构建了 git-scm.com 网站 并编写了 Pro Git 书 - 所有这些最终都引导我加入了 GitHub。
我于 2008 年首次发布的 git-scm.com 版本。它是一个吃 "trees" 的 "blob"。
Git Lore
那么,这个愚蠢的内容跟踪器是如何成为世界上使用最广泛的 SCM 的?
好吧,我在之前的博客文章中介绍了我认为 Git 和 GitHub “获胜” 的许多原因,但我确实认为值得快速浏览一下为什么 Git 本身最终会变成今天的样子。也许还会讲一些关于你所知道和喜爱的东西的起源的有趣轶事。
正如你可能从 Git 命令偶尔不友好、晦涩或不一致的性质中推断出来的那样,这不是一个有人从一开始就从可用性的角度坐下来精心设计的系统。
在最初的几个月里,git 命令都非常底层 - 即使你了解现有的底层命令,你可能也无法识别 2005 年 6 月存在的任何一个命令 (rev-tree
, mkdelta
, tar-tree
?)
从一开始就很明显,这种方法是 Git 只是这种非常底层的数据库/文件系统类型的工具集,并且 (可能是一些) 其他工具将使用 Git 作为其基础设施。
区分建立在 git 之上的 SCM 与 git 本身可能值得避免混淆。以后可能会开发其他建立在 git 之上的 SCM,这些 SCM 可以提出自己巧妙的名称。 - Steven Cole
因此,如果 Linus 和早期的 Git 团队最初并没有想象 Git 成为一个实际的版本控制工具,而只是想构建底层工具,那么我们今天所知的 "porcelain" 命令实际上是从哪里来的呢?
答案是,它们在几年时间里慢慢地渗透进来,主要是作为为了解决一系列问题而演变而来的 shell 脚本。
在早期,有许多用户界面编写了 Linus 的后端工具的脚本,使其更加用户友好。最早的也是最初几年最受欢迎的是 git-pasky
,它很快被 Petr Baudis 称为 “Cogito”。这些脚本的第一个版本是在 Git 发布几天后发布的。
在早期 发布 公告中,你可以感受到将开始成为 Git 的工具。
几个月后,尝试保持 porcelain 和 plumbing 之间的界限开始崩溃,因为 git 中的工具开始与 porcelain 脚本中的工具竞争。
当为使 Bare Plumbing 的可用性而添加 "git diff"、"git commit" 和朋友时,这种趋势就开始了。这些基本命令是必须的,我对在 "core GIT" 套件中拥有它们没有任何异议,但与此同时,我认为核心不应该与 Porcelains 竞争,并且认为应该在某个地方划一条线。 - Junio
在接下来的几年里,越来越多的脚本继续进入核心 Git 代码,直到最终人们清楚地认识到,与其试图在工具中保持这种 plumbing/porcelain 的区别,不如将时间花在开发 Git 附带的工具上。
2007 年,Cogito 最终被“挂牌出售”,并且某种其他 porcelain 成为使用 Git 的主要方式的想法或多或少被放弃了。
回顾 20 年前的这些提交和电子邮件,看到我们许多人每天使用的某些臭名昭著的工具的诞生真是令人着迷。
第一个 git log
git log
的第一个版本是一个包装脚本,它调用 git-rev-list --pretty
,通过寻呼机将其管道传输,并被硬编码为从 HEAD
开始。这是原始的 "git log" 程序的完整内容:
#!/bin/sh
git-rev-list --pretty HEAD | LESS=-S ${PAGER:-less}
💡
如果你没有听说过 rev-list
,它是一个简单的 walker,它只打印 sha。它仍然存在 - 你现在仍然可以在你的项目中运行 git rev-list
。
实际上,_许多_当前的命令都是以这种方式开始的 - 几行长的 shell 或 Perl 脚本,它运行一些核心 plumbing 命令。最终几乎所有东西都用 C 语言作为内置程序重写,以提高可移植性,但是在这些脚本语言中有很多命令的第一个版本。
$ git-log-script
commit d9f3be7e2e4c9b402bbe6ee6e2b39b2ee89132cf
Author: Junio C Hamano <gitster@pobox.com>
Date: Fri Apr 5 12:34:56 2024 -0700
Fix bug
第一个 "git log" 看起来会非常熟悉。
第一个 git rebase
有很多这样的“第一次”,但我只做一个,因为我觉得它非常有趣。臭名昭著的 “rebase” 命令诞生于 Junio 和 Linus 在 2005 年 6 月之间关于工作流程的对话。
Junio 告诉 Linus 他的工作流程是什么:
仅供参考,这就是我一直在做的事情:(1) 从 Linus HEAD 开始。(2) 重复开发和提交周期。(3) 运行 "git format-patch" (不在 Linus 树中) 以生成补丁。(4) 将它们发送出去并等待查看哪个补丁有效。(5) 从 Linus 拉取。(6) 丢弃我的 HEAD,使 Linus HEAD 成为我的 HEAD,同时保留我自他分叉以来所做的更改。我使用 "jit-rewind" 来执行此操作。(7) 检查 Linus 拒绝的补丁,并应用我仍然认为好的补丁,每个补丁进行一次提交。我使用 "jit-patch" 和 "jit-commit -m" 来执行此操作。(8) 返回到步骤 2。
Linus 评论说,开发人员真正想要的合并类型是“re-base”工作:
它有点类似于当前的 git-merge-script,但是它不是基于公共父级进行合并,而是尝试将从公共父级开始的所有本地提交重新基于新的远程 head 之上。从想要将其工作更新到远程 head 的单个开发人员的角度来看,这通常更有意义。
然后 Junio 使用一个名为 git cherry
的新命令回复一个简单的脚本,以 "re-base" 一系列提交。
据我所知,这是 "rebase" 一词首次在版本控制中使用。看到历史的诞生很有趣。
"octopus" 是如何成为 Git 的一部分的?
很多人问过我 GitHub 是如何想出 "Octocat" 的,答案也在这些早期的档案中。
我在 Git 邮件列表中看到的 "octopus" 一词的第一次使用是 Junio 告诉 Linus 他的补丁是按顺序应用的,而不是 "octopus"。
这指的是创建一个具有多个父级的合并提交,这是合并不同补丁的另一种方式。最终,"octopus merge" 成为 Git 箭袋中有效的合并策略之一。(有趣的事实,Git 曾经有一个 "stupid" 作为合并策略)
在 GitHub 的早期,Tom 正在寻找任何可以拟人化地用作 Git 图腾的东西,而 "Octopus" 是 Git 词典中似乎符合要求的唯一术语。Tom 搜索了以 "octopus" 为主题的剪贴画,而这张 Simon Oxley 的图片是符合要求的图片中最可爱的一张。所以 "octocat" 就诞生了。
Git 的未来
二十年零一天后,有人可能会问这个不太可能的英雄的未来是什么。
有趣的是,我仍然在以某种方式以最初的目的使用 Git。GitButler 不仅使用 Git 进行跟踪代码更改的普通提交,还使用 git 数据库来跟踪项目的历史。最后,正如 Linus 最初设想的那样,它仍然是一个非常好的愚蠢的内容跟踪器。
所以,Git 生日快乐。你仍然很奇怪。你仍然很精彩。感谢所有的鱼。
我们正在招聘!
我们正在寻找一位经验丰富的 TypeScript 开发者加入我们在柏林的团队,以帮助我们构建下一代 Git 客户端。
全职 - 柏林