跳转到内容
← 返回核心概念
软件工程实践计算机科学 · 软件工程18 分钟阅读

版本控制

Version Control

1991 年,Linus Torvalds 在赫尔辛基大学宿舍里开始写 Linux 内核,协调方式是通过邮件列表发补丁文件(.patch)。贡献者把修改写成差异文件,发给 Linus,他手工合并。 这种方式在全球数千名开发者同时贡献代码时完全行不通。

版本控制Git协作历史分支

1991 年,Linus Torvalds 在赫尔辛基大学宿舍里开始写 Linux 内核,协调方式是通过邮件列表发补丁文件(.patch)。贡献者把修改写成差异文件,发给 Linus,他手工合并。

这种方式在全球数千名开发者同时贡献代码时完全行不通。

2005 年 4 月,Linux 社区与 BitKeeper(当时使用的专有版本控制工具)决裂。Torvalds 只用了几天就写出 Git 的雏形——一个专为大规模分布式协作设计的版本控制系统。第一个提交记录的时间戳是 2005 年 4 月 7 日;约三个月后,他就把日常维护权交给了沿用至今的维护者滨野纯(Junio Hamano)。今天,Git 是地球上使用最广泛的软件开发工具之一。

破除误解:版本控制不是"存档"

许多初学者把版本控制理解为"定期存档"——就像游戏存档一样,不满意了就退回去。这是一个过度简化。

版本控制的核心价值不是回退,而是追踪意图。每一次提交(commit)不只是一个快照,还携带: - 做了这个改动(作者信息) - 什么时候做的(时间戳) - 为什么做这个改动(提交消息) - 改了什么(差异)

这个元数据让团队能够回答:"三个月前我们为什么删掉了这段代码?"——这个问题的答案往往决定了今天一个 bug 的修复方向。

Git 把文件内容做成哈希寻址的对象。改一个字,哈希变,历史里旧快照仍在。分支便宜,是因为分支只是指向某次提交的指针,不是整棵目录的拷贝。分布式的意思是:每人硬盘上都有一份完整历史,不必问中央服务器才能 diff。BitKeeper 决裂之后,Torvalds 要的就是这个:内核开发者不能把仓库锁在一家公司的许可证里。十天写出能跑的原型,三个月交给滨野纯,剩下的是社区把工具磨成基础设施。版本控制的政治,写在"谁拥有历史"这一句上。提交信息若只写"fix",历史就变成无意图的快照,追踪意图的承诺落空。工具能记下 diff,不能替你写出为什么。代码审查是把为什么公开化。Pull Request 把审查做成门禁,于是分布式仓库又有了守门人。守门人可以是质量,也可以是瓶颈。工作流的选择,是在这两种风险里站队。

Git 的核心模型

Git 把每次提交表示为一个有向无环图(DAG) 中的节点,每个节点指向它的父节点。分支只是指向某个节点的指针。

A --- B --- C --- D  (main)
            \
             E --- F  (feature)
```

这个模型带来了几个重要性质:

历史是不可篡改的:每个节点的 ID 是其内容的 SHA-1 哈希(Git 2.29 起可选 SHA-256)。改变任何内容会导致哈希变化,从而连锁改变所有后续节点的哈希。这使得 Git 仓库本质上是一个内容寻址的不可变数据结构

不过"不可篡改"是有前提的:它依赖底层哈希的抗碰撞性,而 SHA-1 的这道防线在 2017 年被攻破。2017 年 2 月 23 日,荷兰 CWI 研究所与 Google 公布了 SHA-1 的首个实际碰撞(代号 SHAttered),构造出两个内容不同、SHA-1 却完全相同的 PDF。这次碰撞耗费约 2 的 63 次方次哈希运算,折合 6500 个 CPU 年加 110 个 GPU 年的算力。

这意味着理论上有人能伪造一个与正常提交同哈希的恶意对象。Git 的回应不是连夜更换算法,而是在 2.13.0 版(2017 年 5 月)默认启用 Marc Stevens 与 Dan Shumow 的 sha1collisiondetection——一种能嗅出碰撞攻击痕迹、一旦发现就主动报错的强化版 SHA-1。彻底迁移到 SHA-256 的工作至今仍未完成,因为全球海量仓库的对象哈希都要重算,是一次牵一发而动全身的迁移。

Git 的对象库其实只有几种积木:blob 存文件内容,tree 存目录结构,commit 把树、父提交、作者与说明捆在一起,tag 给某次提交加注释。全部用内容哈希寻址,所以相同文件在仓库里只存一份。工作区、暂存区、仓库三层,是人为加上的提交门槛:你可以改完先不提交,也可以把准备提交的内容和还在试验的内容隔开。初学者觉得 Git 难,常常难在这三层和"HEAD 只是指针"叠在一起,而不是难在差分算法。

分支极其廉价:一个分支只是 40 字节的哈希值(一个指针),创建分支的代价几乎为零。这与 SVN 等早期工具形成对比——在 SVN 中,创建分支需要复制整个项目目录。

合并是一等公民:Git 的合并算法(三路合并,three-way merge)理解"我在哪里分叉",能自动合并两个人在不同地方做的修改,只在真正冲突时要求人工介入。这套算法本身仍在进化:2021 年的 Git 2.34 把沿用了二十年的默认合并策略 recursive 换成了 Elijah Newren 重写的 ort(名字意为 "Ostensibly Recursive's Twin"),在某些大型仓库的合并上快出三个数量级。

工作流演进

集中式工作流:所有人直接推送到 main 分支,适合个人项目或极小团队。

功能分支工作流(Feature Branch):每个新功能在独立分支开发,完成后通过 Pull Request / Merge Request 合并。这是当今最主流的开源协作模式,GitHub 在 2008 年推出 Pull Request 功能后将其普及。有意思的是,2008 年最初的 Pull Request 不过是 git request-pull 命令的网页包装——发一条"请拉取我的提交"的通知而已;真正接近今天形态(带代码审查与行内评论的协作界面)的版本,要到 2010 年 8 月的 "Pull Request 2.0"。

GitFlow:Vincent Driessen 在 2010 年提出的规范化分支策略,定义了 main、develop、feature、release、hotfix 五类分支及其交互规则。适合有计划发布节奏的产品。

Trunk-Based Development(主干开发):所有人每天至少向主干提交一次,通过功能开关(Feature Flag)控制功能是否对用户可见。Google 和 Facebook 内部采用的方式,配合持续集成效果最佳。分支活得越长,合并越像考古。主干开发把冲突提前到每天,用开关把未完成功能藏起来。Git 让分支便宜,并不等于长分支免费——便宜的是创建,贵的是对不上的历史。工作流是在用社会规则补上工具不管的那一截。

分布式版本控制的革命

Git 和 Mercurial(2005 年,同期由 Matt Mackall 开发)属于分布式版本控制系统(DVCS),与此前的 CVS、SVN 有根本区别:

每个开发者本地有完整的仓库副本,包括全部历史。这意味着: - 本地提交、查看历史、创建分支无需网络连接 - 每个仓库都是完整备份,没有单点故障 - 开发者可以在私下积累提交,选择合适时机推送

邮件补丁时代,Linus 是单点。Git 把单点拆成每个人硬盘上的图。中央服务器仍常在,那是协调和备份的便利,不是协议的必需。GitHub 把便利做成了产品,于是分布式工具长出了新的中心。协议开放不保证权力开放——HTTP 篇写过同一句。版本历史仍在你的笔记本上,发现与社交却折进了平台。

相比之下,SVN 的每次提交都需要连接中央服务器,断网就无法工作。

版本控制系统的演进历史

了解版本控制的历史,有助于理解 Git 的设计决策:

SCCS(Source Code Control System,1972):贝尔实验室 Marc Rochkind 开发,世界上第一个版本控制系统。只支持单文件锁定(一次只有一人可以编辑一个文件),避免冲突的方式是强制串行化。

RCS(Revision Control System,1982):Walter Tichy 在普渡大学开发,目标是给当时收费的 SCCS 提供一个免费替代(RCS 后来才并入 GNU 项目)。它同样是文件锁定模型,但存储方式更高效——用"反向差分"(reverse delta)只完整保存最新版本,旧版本靠逐步回退的编辑指令还原。

CVS(Concurrent Versions System,1986):革命性地引入了乐观并发——多人可以同时编辑同一文件,提交时系统尝试自动合并,有冲突时提示人工解决。这使团队协作不再需要严格协调。

Subversion(SVN,2000):CVS 的替代,改进了目录移动、原子提交等操作,成为 2000 年代开源项目的主流版本控制系统。Apache、GCC、Python 等都曾使用 SVN。

BitKeeper(商业,2000 年首次公开发布):由 Larry McVoy 的 BitMover 公司开发,Linux 内核于 2002 年至 2005 年初使用,是第一个大规模应用的分布式版本控制系统,对 Linux 贡献者免费。2005 年因许可证争议(一位内核开发者试图逆向工程 BitKeeper 协议)撤销了免费使用权,直接导致 Torvalds 写出了 Git。

导火索写在 2005 年春天。安德鲁·特里格尔(Andrew Tridgell)试图摸清 BitKeeper 的网络协议,BitMover 随即收回 Linux 内核的免费使用权。Torvalds 不能接受把内核历史锁进一家公司的许可证,于是自己写替代品。Git 从第一天起就把完整性放在哈希上、把协作放在补丁和拉取上,而不是放在中央锁文件上。工具的政治选择先于市场份额:先保证没有人能把历史收走,再谈好不好用。

Git(2005)Mercurial(2005):几乎同时诞生于 Linux 社区,两者设计目标类似,但 Git 赢得了市场(GitHub 的支持是关键因素),Mercurial 在 Facebook 等少数公司使用。

这段历史说明:版本控制系统的每一次革新,都是对前一代局限性的直接回应——从串行到并发,从集中到分布。

代价与争议

Git 的学习曲线:Git 的概念模型(工作区、暂存区、仓库;HEAD、分支、标签;rebase vs merge……)对初学者来说相当陡峭。"我不小心 force push 了 main" 是许多工程师噩梦的来源。reflog 把"我刚弄丢的提交"从哲学问题变成可恢复的指针日志:HEAD 移动过的位置暂时还在,force push 之前往往还能救。真正危险的是已经推送到别人正在基于其上工作的远端历史。本地整理和远端改写不是同一件事,工具把它们做成了同一组命令,于是伦理争论被按键触发。提交信息写清楚"为什么",是给未来的 diff 阅读者留一条不必考古的路。

大文件问题:Git 为文本代码优化,但游戏资产、机器学习模型等大型二进制文件在 Git 中存储效率极低。Git LFS(Large File Storage)是一个补丁方案,但体验依然不完美。

历史重写的伦理git rebasegit push --force 允许重写已发布的历史,这在团队环境中可能导致他人工作丢失。何时应该保留"真实历史"(包括所有混乱的中间提交),何时应该整理为整洁的叙事,是持续的工程文化争论。

签署提交(signed commit)把"谁"从可伪造的字符串升级成可验证的密钥,但密钥管理本身又变成新的社会问题。仓库托管平台后来用分支保护、必审人数和状态检查,把分布式的推送重新收成门禁。这不是背叛 Git 的设计,而是承认:开放协议解决不了"错误的历史被推上主干"这件事。质量门禁与瓶颈是同一套机制的两张面孔,团队要选的是阈值,不是要不要守门。

Monorepo vs Polyrepo:把公司几乎所有代码存进一个超大仓库(Monorepo),声称有利于跨项目代码复用和原子性变更。Google 是最著名的例子:据 Potvin 与 Levenberg 2016 年的论文,其单一代码库约有 20 亿行代码、约 900 万个源文件、占用 86 TB,每个工作日逾 1 万名工程师提交约 4 万次。

这里有个常被误传的细节:这种量级的 Monorepo 大多并不跑在 Git 上。Google 的代码库由自研系统 Piper 管理(在 Piper 之前,它靠单台机器上的一个 Perforce 实例撑了十多年);Meta 用的是从 Mercurial 演化、2022 年才开源的 Sapling。真正"用 Git 装下整个公司主干"的标杆是微软:2017 年它把约 350 万个文件、约 300 GB 的 Windows 源码迁上 Git,成为当时地球上最大的 Git 仓库,还为此专门造了虚拟文件系统 VFS for Git(按需下载文件,免得每个开发者一次拉取整个仓库)。

反对者认为 Monorepo 带来了工具链复杂性和不必要的耦合——上面这些公司无一例外都得自研基础设施才能让它跑起来,这本身就是一种成本信号。两种方式都有成功案例。

跨域连接

  • 社会资本:提交元数据把「谁在何时为何改了什么」变成可验证的公共记录,于是声誉能在陌生人之间积累和查证,合作成本随之下降。这解释了提交历史的社会功能——它不只是技术档案,也是贡献者的履历。改写已发布的历史之所以引发伦理争论,正因为它同时抹掉了别人据以主张贡献的证据。
  • 史学方法之争:整洁历史与真实历史之争,史学早就给出过更好的问法:史料要求原样保存,叙述要求可读,两者功能不同,不能互相替代。推论是这道题不该在「要不要变基」上二选一,而应同时提供两层——保留原始分支记录以备追溯,另附整理过的合并说明供阅读,就像档案与专著各司其职。
  • 共识算法:分布式版本控制刻意不做共识:每个副本可以持有不同的历史,冲突推迟到人工合并时才解决。这与要求全序的共识协议正好相反,也直接解释了它为何能离线工作而数据库副本不能。代价同样清楚——哪一份是权威主干无法由协议判定,只能由社会约定,也就是大家承认谁的仓库。
  • 语系:代码分支与方言分化机制相同:分离时间越长,累积的改动越多,且分布在多个互不重叠的层面上,于是重新合并的代价随分歧量超线性增长。这给出主干开发有效的真正理由——它不是纪律洁癖,而是把分歧窗口压到最小,使冲突数量保持在人能逐个判断的量级,而不是留给算法去猜。
  • 产业组织:单一仓库把跨项目的原子变更变成内部操作,代价是必须自研工具链,正文里那几家公司无一例外。这是把协调成本内部化、同时承担规模不经济的取舍。推论解释了争论为何双方都对:可行性门槛由工具投入决定,因此它对能养专职团队的大公司是选项,对小团队不是。

参考文献

  • Potvin, R. & Levenberg, J. Why Google Stores Billions of Lines of Code in a Single Repository. Communications of the ACM 59(7), 2016. (Google Monorepo 实践与规模数据的权威描述)
  • Stevens, M., Bursztein, E., Karpman, P., Albertini, A. & Markov, Y. The First Collision for Full SHA-1. Advances in Cryptology – CRYPTO 2017, LNCS 10401. (SHAttered 攻击的原始论文)
  • Tichy, W. F. RCS — A System for Version Control. Software: Practice and Experience 15(7), 1985. (RCS 反向差分设计的一手论文)
  • Harry, B. The Largest Git Repo on the Planet. Microsoft DevBlogs, 2017-05-24. (Windows 源码迁移到 Git 与 VFS for Git 的工程记录)
  • Meta Engineering. Sapling: Source Control That's User-Friendly and Scalable. engineering.fb.com, 2022-11-15. (Meta Monorepo 版本控制系统 Sapling 的官方介绍)

延伸阅读

  • Chacon, S. & Straub, B. Pro Git. Apress, 2014. (免费在线阅读:https://git-scm.com/book)
  • Driessen, V. A Successful Git Branching Model. nvie.com, 2010.
  • Hammant, P. Trunk Based Development. trunkbaseddevelopment.com. (持续更新的在线指南)
  • Loeliger, J. & McCullough, M. Version Control with Git. O'Reilly, 2012.
  • O'Sullivan, B. Mercurial: The Definitive Guide. O'Reilly, 2009. (Mercurial 的官方指南,对理解 DVCS 设计思想有帮助)