20 年前,Git 横空出世。这款原本只是为了解决“内容追踪”问题而诞生的小工具,最终却成为改变全球开发者工作方式的关键基建。本文作者是 GitHub 前联合创始人 Scott Chacon,他将带我们回顾 Git 从一行代码开始的故事——那些疯狂的开端、奇妙的演变,还有它至今仍令人惊叹的生命力。
作者 | Scott Chacon
翻译工具 | ChatGPT 责编 | 苏宓
出品 | CSDN(ID:CSDNnews)
二十年前的 4 月 7 日,Linus Torvalds 做下了 Git 的第一次提交,也由此拉开了这个“来自地狱的信息管理器”的序幕。

在过去的二十年里,Git 从一个小型、简单的个人项目逐渐成长为有史以来最具统治力的版本控制系统。
对我个人来说,这段旅程就像是坐了一趟惊心动魄的软件过山车。
还记得 Linus 在第一次提交 Git 代码的几个月后,我就开始使用它了,当然我用它来做的事情可能出乎你的想象——完全不是它最初的用途。比如,我与他人共同创立了 GitHub,还写了一本关于 Git 的书——《Pro Git》(https://git-scm.com/book/en/v2?ref=blog.gitbutler.com),搭建了该项目的官方网站,发起了每年的开发者大会……这个小小的项目不仅改变了软件开发的世界,更深刻地改变了我的人生轨迹。

今天,Git 正式走进了第三个十年,我想借这个机会回顾一下它早期岁月,也聊聊为什么我会对这个项目如此着迷。

补丁和压缩包:Git 为什么会诞生?
在讲 Git 的历史以及我和它的关系之前,我想先聊聊一个关键问题:Git 为什么会诞生?它最初的出发点是什么?
Git 最早是因为 Linux 内核开发社区在版本控制和协作方面感到非常不满而诞生的。
在这个社区里,大家长期以来都是靠邮件列表来协作的。这种方式其实很有意思——它非常适合大规模分布式开发、支持离线操作、能详细讨论每个代码改动,还能通过加密确保安全性。
这种“邮件列表协作”的流程大概是这样的:
维护者发布一个“tarball”(可以理解为压缩包,像 zip 文件),里面是某个时刻的完整项目代码。
开发者下载这个压缩包,在本地解压。
然后他们在本地添加功能或修复 bug。
用 GUN diff 工具创建一个“补丁”(patch),这个补丁描述了他们修改了哪些地方。
通过电子邮件把补丁发到邮件列表里。
大家在邮件里讨论这些改动。
如果没问题,维护者就把这个补丁合并进项目里,准备下一个版本的发布;如果有问题,会请开发者修改。
循环往复。
其实我特别想单独写一篇博客来讲讲邮件列表协作多酷,但今天先按下不表。
不过问题来了:在这种协作方式下,当时市面上的版本控制工具并不好用,甚至其功能可以说是“倒退”。它们的访问控制很麻烦、不支持分布式,而且速度还慢得吓人。
所以社区里的开发者还是主要靠“补丁 + 压缩包”这个方式工作,而当时那些所谓的版本控制系统,根本跟不上。
如果你仔细想想,这套“补丁 + 压缩包”的工作方式其实就是最早的分布式版本控制系统——每个人都有一份完整的本地代码,可以在本地进行更改,任何人都能访问“合并”并将新的 tarball 推送到服务器。
但是,这个过程仍然有些麻烦,比如:
补丁不好管理
经常忘记哪些改动已经合并了,哪些还没
多个开发者同时在改代码会冲突
有时候还需要“重做”之前的改动(也就是 rebase)
后来出现了一个叫 BitKeeper 的工具,专门为 Linux 内核这种开发方式设计的。Linus 其实挺喜欢它的,但它的授权协议不太符合开源社区的理念,最终没能继续用下去。
这也正是 Git 出现的背景 —— 它最初并不是为了做一个“版本控制系统”而设计的,而是为了更好地处理“补丁 + tarball”这套协作方式。
它的核心思想就是:保存一组文件的快照,并能清楚地展示改动,方便大家讨论。
Git 的底层结构就是为这个目标设计的(比如树形结构、内容寻址存储、链式提交记录),而从最初的提交到今天,这些设计几乎没有变过。

第一次提交:Git 的起点是什么样的?
既然聊到这儿,不妨看看 Git 的第一次提交到底长啥样?它在诞生的那一刻,到底能做些什么?
说实话,最开始的 Git,其实只是一个“傻瓜式的内容追踪器”。Linus Torvalds 当时是这么形容它的:
这是一个傻乎乎的(但非常快的)目录内容管理器。它功能不多,但它能做的,就是高效地追踪目录里的内容变化。

https://github.com/git/git/commit/e83c5163316f89bfbde7d9ab23ca2e25604af290?ref=blog.gitbutler.com
最初的提交内容其实很简单:就是一组七个独立的小工具。它们还不是我们今天熟悉的 git commit 那种命令,而是一些非常底层的数据操作工具,比如 write-tree 和 commit-tree。(几周后,Linus 才把它们统一改名,加上 git- 前缀,比如 git-write-tree。)
这些工具里,有些后来演变成今天依然存在的“底层命令”,比如 git cat-file 和 git write-tree;还有一些当年和现在的用法已经完全不同了,比如当时的 read-tree 更像今天的 git ls-files。不过不管名字怎么变,核心概念从第一天起就已经确立了。
那最早的 Git 到底能干嘛?简单来说,它可以完成这样几件事:
用 update-cache 把当前目录的内容缓存起来(本质上是个 tarball),然后用 write-tree 把这份“快照”写进 Git 的数据库里。
用 commit-tree 创建一次提交(changeset),附带说明它是基于哪个之前的快照,并记录这次改动的说明文字。就像是在构建一条“压缩包历史”。
用 cat-file 从数据库里读取某个对象;用 read-tree 查看缓存区的内容;用 show-diff 看缓存和当前目录之间有哪些改动。
从一开始,Linus 的思路就很清晰:我只想做好底层“管道”工具,至于用户界面(也就是所谓的“瓷器层”),以后谁爱做谁做去。
他自己当时是这么说的:
在我眼里,Git 从一开始就只是底层的“管道”系统,仅此而已。比如像 arch 这样的工具,主要是基于“补丁和压缩包”的(我记得 darcs 在这方面也差不多),它们完全可以把 Git 当作一个超级好用的“压缩包历史记录器”来用。——Linus
换句话说,他最初的目标其实不是要造一个完整的版本控制系统,而只是想做一个超高效的“压缩包历史数据库工具集”。他觉得其他人会基于它再开发出好用的 UI 层。
当然,这个故事还没完,我们接着讲……

Scott 初识 Git
我第一次接触 Git,还是在我和朋友兼同事 Nick Hengeveld 一起待在一家名叫 Reactrix、后来倒闭的初创公司时。
有意思的是,我们最早用 Git 的方式,其实更接近 Linus 最初的设想 —— 把它当作一个分布式的内容管理工具,而不是传统意义上的“版本控制系统”。
简单说,我们当时的公司更像是一家广告公司,管理着一大批数字广告屏。这些屏幕要播放的素材又大又复杂,而且每台设备上播放的广告组合都不一样。更麻烦的是,大部分设备还用的是较慢的蜂窝数据网络,而且广告更新频率也挺高。
所以我们得想办法让广告分发更高效。比如说:
设备 A 要播放广告 1、2 和 3(v1 版本);
设备 B 要播放广告 2、3( v2 版本)和广告 4。
当某个广告版本更新后,我们还要只更新那一小部分内容,不能每次都传完整的素材。
于是我们用了 Git——不是来管理代码,而是当作内容分发系统用。我们写了个脚本,根据广告排期自动生成“这台设备需要的广告素材树”,把这个树提交到 Git 的某个分支上,然后让每台设备每天晚上去拉取自己的分支,强制切换到对应版本。
这种方式带来了不少好处:
如果某个广告素材更新了,Git 只传改动的部分,而且还能利用现有对象进行增量压缩;
所有共用素材都只存一份,无论在哪个设备上都能重复利用;
即使我们有上千种广告组合,Git 也能帮我们做到既不重复存储内容,也不重复传输数据,极大节省了带宽和存储。
当时 Nick 还为 Git 的早期开发做了不少贡献,好让它更适合我们这种用法。他为 HTTP 拉取添加了 SSL 支持、可断点续传和并发下载,还写了第一个基于 HTTP 的推送功能。他的第一个补丁是在 Linus 创建 Git 的 6 个月后提交的,也算是 Git 的早期开发者之一了。
他把 Git 介绍给我,我一开始完全没整明白,研究了好久才慢慢领悟过来:“这玩意其实还挺酷的!”也正是这个“突然开窍”的瞬间,激发了我去写文章、做教程,想帮更多人搞懂 Git。
这促使我后来去整理《Git Community Book》,制作《Git Internals》的 Peepcode PDF,搭建 git-scm.com 网站,并撰写《Pro Git》一书——这一切最终让我走上了 GitHub 的这条路。


Git 传说
那么,一个“愚蠢的内容追踪器”是怎么变成全球最广泛使用的版本控制系统的?
我在之前的一篇博文里详细讲过为什么 Git 和 GitHub 能“胜出”,但我觉得还是值得快速回顾一下 Git 为何会长成今天这个样子。顺便也讲几个你可能熟悉的功能是怎么诞生的小故事。
你可能也发现了,Git 的一些命令不太友好,甚至有些晦涩、前后不统一——这是因为 Git 并不是一开始就从用户体验角度精心设计出来的系统。
最初的几个月里,Git 提供的命令都是非常底层的。哪怕你熟悉现在的 Git plumbing 命令,当时(2005 年 6 月)的那些命令可能一个都没见过,比如 rev-tree、mkdelta、tar-tree。
从一开始就很清楚,Git 的设计目标是成为一个低层级的数据库/文件系统工具集,供上层的工具去调用,而不是一个“版本控制系统”的成品。
“有必要把构建在 Git 上的版本控制系统与 Git 本身区分开来,以免引起混淆。后续可能会出现多个基于 Git 构建的 SCM,它们可以各自起个更有创意的名字。”
——Steven Cole
既然 Linus 和早期的 Git 团队一开始就只是想造“管道”,那我们今天熟悉的这些“瓷器”命令(也就是用户友好的 Git 命令)又是从哪里来的?
答案是——它们是逐渐“混”进来的,最开始都是一堆为了解决小问题而写的 shell 脚本。
早期有不少 UI 项目尝试在 Linus 的底层工具上加一层人类友好的界面。最早也是当时最流行的一个是 Petr Baudis 写的 git-pasky,后来演化成了“Cogito”。这个项目的第一个版本发布在 Git 诞生仅几天后。
在那些早期的发布邮件列表里,你可以看出一些今天的 Git 工具已经开始萌芽。

几个月之后,“瓷器”和“管道”之间的界限就开始变得模糊。Git 本身内置的工具和上层脚本开始互相竞争。
“像 git diff 和 git commit 这类命令是为了提高基本可用性才加进来的,我不反对它们成为 Git 核心工具的一部分。但与此同时,我认为 Git 的核心不应该和那些上层瓷器工具竞争,这条线应该划清。”
——Junio
接下来的一两年里,越来越多的脚本被移植进了 Git 的核心代码库。到最后,大家逐渐达成共识,与其维持一个“核心 vs 瓷器”的区分,不如直接把精力投入到 Git 官方工具的开发中去。
2007 年,Cogito 宣布“出售”,这也标志着“Git 会有其它主流瓷器工具替代其默认体验”这个想法的落幕。
回头看 20 年前的那些 commit 和邮件记录,你会发现很多今天我们习以为常的工具,其实就是在那个时候诞生的。

Git log 的第一个版本
最早的 git log 是一个 shell 脚本,它其实就是调用 git-rev-list --pretty,然后通过 less 分页显示,默认从 HEAD 开始。这是最原始的 git log 程序全文:
#!/bin/sh
git-rev-list --pretty HEAD | LESS=-S ${PAGER:-less}
rev-list 是一个遍历提交的工具,只打印提交的 SHA 值。现在你仍然可以在项目中运行 git rev-list。
实际上,现在很多命令都是这么“从脚本走向内建”的。一开始就是几行 shell 或 Perl 脚本封装底层命令,后来逐步用 C 重写,变成了 Git 的内建命令,增强了可移植性。
$ git-log-script
commit d9f3be7e2e4c9b402bbe6ee6e2b39b2ee89132cf
Author: Junio C Hamano
Date: Fri Apr 5 12:34:56 2024 -0700
Fix bug
第一个“git log”看上去很熟悉

第一个 git rebase
这些“第一次”的故事还有很多,我想再讲一个,因为它实在太有趣了。大名鼎鼎的 rebase 命令,正是诞生于 2005 年 6 月 Junio 和 Linus 一次关于开发流程的对话中。
当时,Junio 向 Linus 描述了他平时的工作流程:
从 Linus 的 HEAD 开始。
重复开发和提交的循环。
使用 git format-patch(这个命令当时还不在 Linus 的代码库中)生成补丁。
把补丁发出去,看看哪些能被接受。
从 Linus 那边拉取最新代码。
丢弃我当前的 HEAD,把 Linus 的 HEAD 设为新的 HEAD,同时保留我从分支出来后所做的更改。我是用 jit-rewind 实现这一点的。
检查那些被 Linus 拒绝的补丁,挑出我仍然觉得有价值的,逐个提交。我用的是 jit-patch 和 jit-commit -m。
回到第 2 步,继续循环。
Linus 指出,开发者真正想要的,其实不是传统的合并操作,而是“变基”工作流:
这类似于当时的 git-merge-script,但不一样的是,它不是基于共同的祖先提交去做合并,而是把你本地从那个共同点开始的所有提交,整体搬到远程分支的最新进度上。对开发者来说,这样的做法更合理,因为你可以让自己的修改保持清晰,又能跟上远程分支的最新变化。
随后 Junio 提出了一个简单的脚本,调用新命令 git cherry 来“re-base”一系列提交。
据我所知,这可能是版本控制历史上第一次使用“rebase”这个词。看着这种历史的诞生,真的很有意思。

Git 里的 “章鱼” 是怎么来的?
很多人问过我:“GitHub 的吉祥物 Octocat(章鱼猫)是怎么来的?”

Octocat
这其实也得从 Git 的早期邮件说起。最早出现“octopus”这个词,是 Junio 回邮件时说 Linus 的 patch 是“顺序合并”的,而不是“octopus”式的合并。
所谓“octopus merge”,指的是一次性合并多个分支(一个 merge commit 有多个 parent)。后来,这种合并方式正式成为 Git 的一个合并策略。(顺便一提,Git 曾经还有一个合并策略叫“stupid”)
到了 GitHub 创立初期,联合创始人 Tom 想找一个可以具象化、好做吉祥物的 Git 术语,结果看来看去,只有“octopus”像点样子。他去网上搜了张章鱼的插画,找到了 Simon Oxley 的那只可爱章鱼——于是 Octocat 就诞生了。

Git 的未来
20 年过去了,很多人会问:这个曾经“愚蠢的内容追踪器”未来还会怎样?
好玩的是,我现在还在用 Git 做一些它最初设计时就擅长的事。比如在 GitButler 里,我们不仅用 Git 记录代码变更,还用它的数据库记录整个项目的历史。说到底,它依然是 Linus 最初设想的那个——“好用的内容追踪器”。
所以,生日快乐,Git。你依旧古怪,依旧强大。


财经自媒体联盟

4000520066 欢迎批评指正
All Rights Reserved 新浪公司 版权所有