任意规模的 Git
大规模托管 Git 代码仓库是一场噩梦。Linus Torvalds 设计 来自地狱的信息管理器 的第一个版本时 (这其实是 Git 的标语,可以查证) ,心中有一个非常具体的使用场景:他自己。他想要替代当时用于开发 Linux Kernel 的分布式版本控制系统 BitKeeper。当然,替代方案也必须是分布式的。Kernel 是一个不同寻常的软件项目:它高度去中心化,众多维护者分别负责众多不同的子系统。分布式版本控制系统自然适合这样的工作流。
二十年后,Git 已成为行业标准,但事实上,它的分布式特性带来的阻碍多于优势。普通的开源软件项目并不采用去中心化的工作流,普通公司更是如此。它们享受分布式模型带来的诸多优势,例如可以离线工作、延后推送等,但同时也高度依赖集中式托管平台。而事实证明,托管 Git 代码仓库极其困难。
Git 的难点是什么?
大规模托管 Git 代码仓库所面临的挑战源于 Git 本身的设计:作为一种 分布式 版本控制系统,代码仓库的所有实例都完全相同。Git 服务器上的代码仓库与开发者笔记本电脑上的代码仓库没有本质区别。乍看之下,这似乎让托管 Git 代码仓库变得很简单 (只需在磁盘上的代码仓库前部署一个 HTTP 守护进程,就能运行 Git 服务器!) ,但实际上,许多严峻的可扩展性和可靠性挑战让情况恰恰相反。
在普通 Git 代码仓库中,您的代码和元数据 (文件、提交和树对象) 会被压缩并存储在 packfiles 中——这是一种简单的二进制序列化格式,便于在本地机器上处理,却不适合在服务器上进行大规模管理。packfile 是 Git 存储 以及 Git 网络通信的基础构件。当您向代码仓库推送数据或从中获取数据时,数据都会以 packfile 的形式传输。
Git 的设计就是如此,但认为它其实不必如此也很合理。毕竟,您无法控制 Git 客户端 (至少无法在不惹恼用户、增加大量使用阻力的情况下做到) ,但在自己的服务器内部,您可以做 任何 想做的事。没有什么规定您必须使用 packfile——Linus 不会亲自来检查。唯一的限制是,对于所有 Git 操作,您确实需要通过网络接收和发送 packfiles。
多年来,尝试大规模托管 Git 代码仓库的公司发现,这种基于 packfile 的设计严重限制了可用性和可扩展性。packfile 是大型二进制文件,Git 必须通过文件系统访问它们。在磁盘上的代码仓库前部署 HTTP 服务器这种简单方法,扩展能力的上限很低。理想情况下,您会希望代码仓库存在于多块磁盘和多台机器上 (这样可以并行运行许多 Git 操作,并在服务器崩溃时保持代码仓库可用) 。但该如何实现呢?
大致有三种实现方式,复杂度依次递增:分布文件系统、分布 packfile,或分布 Git 本身。
不使用 packfile 的 Git
Git 是一个内容寻址的数据存储。Git 代码仓库中的所有对象 (blob、tree、commit 等) 均以其内容的 SHA-1 作为键。这天然适合映射到分布式键值存储 (键为 SHA-1,值为实际对象) ,似乎能以一种简洁的方式扩展代码仓库的存储。但实际上并不可行。
问题在于:Git 代码仓库的实际结构是一个有向无环图 (简称 DAG) 。您可以通过 SHA 查找任何对象,但即使执行代码仓库中最简单的操作,也必须逐步遍历 DAG。
如果要列出代码仓库的近期变更,就必须处理其中的 commit。处理一个 commit 时,您会获得一个指向其 tree 根节点的指针。从该 tree 中,您会获得指向每个文件和子树的指针。从原始 commit 中,您会获得指向其父 commit 的指针 (即历史记录中位于它之前的 commit) 。关键在于,遍历的每一步中,必须先获取前一个指针,才能知道下一个指针的值。如果每次获取都需要与分布式存储进行一次往返通信,成本很快就会变得极高。
这种在对象级别分布 Git 的方法此前已被多次尝试,但往往难以在大规模场景下奏效。最有前景的一次实现,是我的前导师 Shawn Pearce 在 Google 版本控制系统团队工作时尝试的。他的方法是将对象存储在分布式哈希表中。这得益于 JGit——一个以 Java 编写的自定义 Git 实现。和经典的 Java 库一样,JGit 提供了大量接口、工厂和接口工厂,可抽象普通 Git 代码仓库的所有细节,包括用 DHT 替换磁盘上的 packfile。尽管该系统能够运行,且对于常规 Git 操作而言效果足够好,但 Git 协议的限制 (无论您如何在服务器上存储数据,它仍要求通过网络发送 packfile) 使得 git clone 的性能差到最终不得不放弃这一设计。
GitHub 与文件系统
Git 开始走出 Linux Kernel 的小圈子几年后,一家充满干劲的初创公司在旧金山诞生了。GitHub 于 2008 年成立,是一个社交化编程平台,标语极具远见:“Git 代码仓库托管:不再让人痛不欲生。”我不是在开玩笑,你可以查证。早在 2008 年,人们就已形成广泛共识:尽管 (或者正因为) Git 采用分布式设计,实际上仍需要以集中式方式托管 Git 代码仓库,才能让它易于使用,而这件事非常痛苦。GitHub 决心改变这一点。
其平台最初是 (如今大部分仍是) 一个 Rails 单体应用。最早的版本运行在一台性能强劲的机器上,上面运行着 Ruby 服务器,代码仓库副本则存放在同一台机器的磁盘上。扩展 Rails 应用很简单:部署更多实例即可。但在这个特定场景中,由于涉及 Git,他们很快遇到了我们试图在这里解决的老问题:如果 Rails 应用需要访问磁盘上的 Git 代码仓库,该如何部署更多副本?
作为一群精打细算的怪才,GitHub 早期的系统工程师尝试了最简单、或许能解决扩展问题的方法。他们的想法是,如果专注于分布式处理_文件系统_ (而不是 packfile 或 Git 本身) ,就可以保持 Rails 应用不变,把时间投入为不断增长的用户群交付更多功能,而不是对 Git 搞些奇技淫巧。非常务实。结果并不奏效。
团队尝试了许多用于存储 Git 数据的分布式文件系统方案:最显而易见的一种是使用 NFS 将所有代码仓库存储在一台集中式服务器上,但很快就被放弃了。Git 的默认实现对文件系统语义 (锁定、撕裂、读取、同步……) 做了大量假设,这些假设能确保在较慢的开发者笔记本电脑本地文件系统上获得不错的性能,却完全没有考虑它们在网络文件系统上的表现。它既慢又漏洞百出。
之后,他们还尝试了在如今看来坦率说相当可怕的技术,即在块级别复制文件系统。一次短暂的 GFS 部署,以及一次持续更久、基于 DRBD 的部署。它们全都碰壁了。它们的日常运维体验_糟糕透顶_,性能也不足以弥补这一点。这一切归根结底都在于磁盘上 packfile 的设计。
我们已经看到,Git 的图状数据结构如何让往返操作的成本高得难以承受。不幸的是,一个非常类似的原理同样适用于底层的磁盘数据。DAG 中对象的布局与它们在 packfile 中的存放方式之间没有关联。生成 packfile 时的关键启发式目标是尽可能缩小其体积;对象会随机分布在整个包中,它们会被压缩,而且至关重要的是,它们很少以完整形式存储。大多数对象都以同一 packfile 中另一个对象为基础的增量形式存储。在图数据结构中经过多次逻辑跳转后,读取单个对象还需要在磁盘格式中经历多次物理跳转。
这种跨越数 GB 数据的随机遍历,在代码仓库上执行的每一项 Git 操作中都会发生,根本无法与网络文件系统良好配合 (无论它是在文件级别还是块级别进行复制) 。要避免速度慢到近乎停滞,唯一的办法是将整个文件缓存在本地。但同一文件系统中有数十万个代码仓库时,缓存并不可行。
最终,GitHub 的系统工程师硬着头皮放弃了分布式文件系统。他们开始开发 RPC 系统,让代码仓库可以存放在专用文件服务器上,并改造 Rails 应用,使所有操作都能远程执行。这带来了相当可观的水平扩展能力,但并未解决可用性问题,也没有改善最繁忙代码仓库的性能。毕竟,每个代码仓库仍然只存储在一台机器上。
Spokes 与一致性
Spokes 最初由 GitHub 于 2013 年前后开发,如今已成为行业标准。大多数 Git 托管服务都在其架构中采用了 Spokes 方案的某种变体 (针对 Git 代码仓库的应用层复制) 。Spokes 能够长期有效运行,主要是因为它做出了三个根本性选择,并且这些选择后来被证明是最优的:
- 它不分发 Git 本身,而是在 packfile 层面运作。
- 它将所有数据以真正的 Git 代码仓库形式存储在本地 NVMe 磁盘上。
- 它复制 Git 数据,但始终让所有副本保持同步一致。
鉴于我们刚刚讨论过的 packfile 随机读取模式,要确保所有基本 Git 操作保持快速,将原生 Git 代码仓库存储在 NVMe 驱动器上几乎是必需的。这样也能让克隆保持高效,因为无需将数据转换为 Git 客户端所需的格式。此外,你可以专注于在 Git 之上构建产品,而不必自行维护一个能够处理特殊代码仓库的 Git 分支版本。
让所有数据副本始终保持同步一致同样至关重要。这一点往往要吃过苦头才会明白:Git 客户端 确实 无法很好地应对最终一致性。如果本地 Git 客户端推送了一个 commit,却在 fetch 后立即无法读取它,问题就严重了。Git 会对此非常困惑。如果你在一百个运行器上运行 CI 流水线,其中三个在克隆代码仓库后找不到本应测试的 commit,问题同样严重。这也会带来极差的用户体验。
无论是在客户端还是后端,面对 Git 代码仓库的最终一致性视图都会遇到许多棘手问题。因此,Spokes 不惜承担很高的复杂性成本,以确保系统始终完全一致。下面具体看看这意味着什么。
Spokes 是一个基于_共识_的分布式系统。它会在不同服务器上存储你的 Git 代码仓库的多个副本。每当你推送新数据时,协调器都会将该推送扇出,使代码仓库的每个实例都收到一份副本。“扇出”通过一种名为 3PC (三阶段提交) 的经典共识算法进行协调,因此只有在大多数节点确认后,推送才会被接受。
在进一步讨论 Spokes 如何使用 3PC 之前,我们需要先了解 Git 推送的工作原理。一次 Git 推送包含两个组成部分:一个 packfile 和一个 引用事务。前面已经介绍过的 packfile 包含要推送到代码仓库的对象 (blob、tree,以及包含更改的 commit) 。事务则通过更新一个或多个引用 (例如正在处理的分支) ,使其指向刚刚推送的 commit,从而真正将更改发布到代码仓库。
这种分离在这里非常方便,因为在指向它的引用更新之前,推送的 commit 不可见 (用 Git 术语来说,即“不可达”) 。这意味着,我们可以先同时将 packfile 扇出到所有主机 (此处无需同步) ,然后 对引用事务执行三阶段提交,以此为推送实现共识。引用事务比 packfile 小得多,因而同步也更快。Git 本身支持准备引用事务:它可以锁定引用,验证现有值是否符合预期,然后一直持有该锁,直到收到该事务的提交或中止命令。
通过这一设计,我们确保每次推送都在所有副本间完全同步。随后,读取操作 (fetch、clone) 可以安全地路由至_任意单个副本_,因为每个副本始终都是最新的。
这基本上就是 Spokes 的工作方式,过去 13 年来一直运行得相当不错。当然,Spokes 并不完美——没有任何系统是完美的。到 2026 年,人们使用 Git 代码仓库的方式已发生巨大变化,而我们也在此过程中积累了许多构建分布式系统的重要经验。时间和经验表明,Spokes 的哪些选择最终被证明是最优的,哪些则并非如此。
后来被证明至关重要的一个缺陷,是 3PC 水平扩展能力受限。Spokes 最初发布时,每个代码仓库三个副本是最佳平衡点。三个副本足以服务一般代码仓库,且还有富余容量;即使一台机器宕机,也具备足够的冗余,能够继续接受推送。
到了 2026 年,情况已大不相同。如今,企业的普通代码仓库已是庞大的单体仓库。三个副本不足以承载这类代码仓库的流量,尤其是 CI 流量。当然,除了令人畏惧的规模化尾延迟,没有任何因素会阻止 Spokes 使用超过三个副本运行。三阶段提交与 Git 事务模型的契合非常优雅,但作为一种共识算法,它存在根本性限制:每一步的延迟都受集群中最慢服务器的制约。集群中的副本越多,推送吞吐量就越低。
这种扩展性限制在另一个方向上同样存在。智能体大规模使用 Git 代码仓库时,往往不会使用单体仓库,而是创建海量的小型代码仓库,其中许多是一次性的,绝大多数几乎无人使用。Spokes 在这种场景下表现不佳,因为它仍要求每个代码仓库都使用三个副本。这三个大部分时间处于空闲状态的副本无法缩减,否则系统将不再完全一致,并可能发生数据丢失。对于三阶段提交而言,下限始终过高,上限又始终过低。
另一个缺陷,事先无法预见,却会在亲身经历后痛苦地显现出来:Spokes 在大规模运维时可能会 相当棘手。由于磁盘上的代码仓库始终是共识的事实来源,每个代码仓库的每个副本都 至关重要。您必须将代码仓库视为 宠物,而非牲畜。
这首先意味着,您需要确切知道每个代码仓库的位置。这会引入对外部数据库的依赖 (以及潜在的可用性问题) ;该数据库必须维护一张庞大的路由表,将每个代码仓库映射到存有其副本的每台机器。还必须为每个代码仓库计算校验和,并在该表中持续更新,以确保代码仓库在磁盘上保持有效。一旦代码仓库出现问题 (相信我,问题时常发生——Git 在实际使用中可能非常挑剔) ,您必须检测到问题并安排修复作业,使其恢复到健康状态。而且必须非常迅速地完成!因为,磁盘上的代码仓库仍是事实来源。损坏的副本与缺失的副本一样糟糕。如果三个副本中有两个损坏,系统将无法再接受推送:没有法定人数。
Continuity
Continuity (简称 Cnt) 是我们在 Cursor 开发的 Git 存储系统。其理念非常明确:借鉴 Spokes 的成功之处,并解决那些经过多年实践后我们已知的问题。
Cnt 是一个简单的系统 (系统若不简单,就不可能易于运维) 。其核心基础组件是预写日志,我们将其存储在兼容 S3 的对象存储中。在生产环境中,我们直接使用 S3,但它也可部署在任何云环境中。
当代码仓库收到推送时,我们会将其作为 WAL 条目存储到 S3。**在推送完全持久化之前,我们绝不会确认该推送。**每次推送都存储为独立对象;我们会将推送的 packfile 写入磁盘,同时上传到 S3。不过,上传 WAL 条目并不意味着将其发布。只有在代码仓库的本地副本上成功准备好引用事务,并在 WAL 索引文件中记录指向该 WAL 条目的指针后,推送才会可见;该索引文件本身也是存储中的一个对象。这使所有推送都可线性化。
我们尽量避免每次推送只执行一次 S3 写入,因为在繁忙的代码仓库中,这会因 S3 PUT 操作的延迟而为推送吞吐量设置硬性上限。借助精心调优的批处理实现,且只需将引用事务与单个本地代码仓库同步,而无需与法定人数的副本同步,我们的系统可以按磁盘允许的最高速度接收推送。
代码仓库的本地副本当然是存储在高速 NVMe 磁盘上的普通 Git 代码仓库。我们采用与 Spokes 相同的做法,因为我认为 Spokes 在这点上做得完全正确。这让我们能够复用 Git 社区出色的开源成果,包括上游 Git 客户端及其众多性能优化。我们因此能专注于交付新功能,而不是对 Git 做些奇怪的改造。
共识
我们已经看到,Spokes 集群难以运维的一个原因是:必须准确掌握每个代码仓库在各服务器上的位置。Cnt 的做法则截然不同。每个代码仓库位于哪里?答案是“任何地方”。这并不重要!我们将代码仓库视为磁盘上的热缓存,但唯一的事实来源始终是 S3 中的预写日志。系统是无状态的,没有路由表 (也无需运维关系型数据库——真是谢天谢地) 。如果在主机上访问代码仓库时,本地磁盘上没有该代码仓库,我们就从 WAL 将其还原出来。我们可以非常高效地完成这项操作,但当然不希望 一直 这么做,因为这会造成浪费。在生产环境中,我们使用 rendezvous hashing 将代码仓库 ID 映射到预期存放它的节点列表。路由代码仓库所需的全部状态,就是代码仓库 ID 和集群中当前健康节点的集合。但即使这些状态不同步 (例如某个节点变得不健康) ,也完全没问题。我们只需在下一个节点上还原该代码仓库。
那共识呢?选举呢?某个代码仓库由哪台服务器担任主节点?这同样不重要!这里没有状态,也没有共识。任何服务器都可以担任主节点。对预写日志的所有更新都会通过 S3 上的原子比较并交换 (CAS) 操作进行同步,因此任何代码仓库实例接收 push 都始终是安全的。同样,与路由一样,让任意服务器担任主节点并不是最高效的做法 (这会导致 CAS 重试,从而可能延迟 push) ,因此在实践中,我们始终选择同一台服务器作为主节点,即 rendezvous hashing 排名列表中的第一台服务器。但在边缘情况下——发生部署、故障转移或网络短暂波动时——我们并不在意究竟哪台服务器是主节点。该系统旨在降级时始终保持正确,在健康时始终保持快速。
复制
将预写日志存储在 S3 中,为系统扩展带来了无限可能。我们可以拥有_任意_数量的副本,因为 S3 的可扩展性无与伦比,所有副本都能直接从那里追赶进度。我们通过在集群中传播 UDP gossip 数据包来实现乐观复制。这些数据包包含每个副本在每次推送后直接从 S3 追赶进度所需的全部元数据。“这太疯狂了,”我仿佛听见您隔着时空在屏幕后嘀咕道,“UDP 不是可靠的传输协议。”当然不是。分布式系统中没有任何东西是可靠的!网络不可靠,路由不可靠,拓扑也不可靠。但没关系:这并不重要。每个副本都知道自己已追赶到的 WAL 索引最新版本的 ETag。当您在某个副本上执行读取操作时,我们会使用预期的 ETag 向 S3 发起条件 GET 请求。若返回不带正文的 304 响应 (这几乎是即时操作——平均不到 10ms,因为它只涉及 S3 元数据) ,就意味着副本已是最新状态,可以立即处理 fetch 或 clone 请求。若返回 200 响应,则会附带 WAL 索引的最新版本;我们会先据此追赶进度,再处理读取请求。
即使复制 UDP 数据包丢失,或因拓扑变化而到达错误的服务器,也无关紧要。所有副本上的所有读取都完全一致,因为它们都会根据作为事实来源的 S3 进行验证。系统的设计原则是:降级时始终保证正确,健康时始终保持快速。
这有两方面的意义。首先,由于系统始终保持一致,在其上构建基础设施轻而易举。我们 (智能体、Web 界面和客户端) 始终看到代码仓库的全局一致视图。而且,由于系统可向_两个_方向扩展,每个代码仓库都能获得恰到好处数量的副本。大型 单体仓库 可以部署到数百个副本上,以承载其 CI 任务产生的全部负载。由智能体创建的数百万个小型代码仓库则可各由一个副本提供服务;由于 S3 是事实来源,我们无需多个副本来确保可用性。实际上,空闲代码仓库甚至不需要副本:当某个副本一段时间未收到流量时,我们会从节点磁盘中将其垃圾回收;下次收到 fetch 请求时,再直接从 WAL 将其重新还原。
压缩
预写日志需要定期压缩。不能任由日志无限增长:完整恢复时需要重放每一条记录,记录越多,成本就越高。
巧合的是,普通 Git 代码仓库也需要定期压缩,尽管 Git 并不基于 WAL。我们已经看到,Git 代码仓库的基本存储单元是 packfile。每次向代码仓库的远程副本推送,或从远程副本拉取到本地时,都会创建一个新的 packfile。这无法无限扩展:每个 packfile 都有自己的索引,Git 可借此高效查找其中包含的对象,但这种查找效率仅限于单个 packfile。如果要查找某个特定对象,而代码仓库中有 100 个 packfile,就需要逐一打开它们的索引并查找该对象,直到在其中一个 packfile 中找到它。一项操作即使本身高效,若必须执行数百或数千次,也谈不上高效。
现代 Git 已经非常擅长规避这一问题;现在支持多 pack 索引和增量几何压缩。但最终还是必须对磁盘上的 Git 代码仓库进行重新打包。过去,这一直是 Spokes 等系统的可用性问题,因为重新打包是一项非常消耗 CPU 的操作,即使以增量方式执行也是如此,并且必须在系统的所有副本上执行。对于同一代码仓库,如果意外在两个或更多 Spokes 节点上触发维护操作,很容易导致代码仓库发生故障转移。
在这里,我们将压缩成本分摊到多个操作中。只有主节点执行压缩,压缩结果会同时应用于磁盘上的代码仓库 以及 WAL。由于所有副本都会跟随 WAL,它们也会跟随压缩事件。副本不会执行重新打包;它们只需从 S3 下载已压缩的 pack,以带宽换取 CPU。
扩展
复制和压缩是决定 Git 存储系统在负载下性能的两个关键因素。正如刚才所见,它们密切相关:代码仓库每秒接收的推送越多,读取性能下降得越明显,因为必须压缩每次推送产生的 packfile,才能保持 Git 操作的高效性。如果要复制这些推送,就必须复制压缩结果,或在每个副本上独立执行压缩。
Continuity 的 WAL 优先设计提供完全一致的水平扩展能力:您可以部署任意数量的副本,只读 Git 操作的吞吐量会随副本数量线性增长。由于集群中的所有副本完全一致,我们不仅能扩展 Git 协议 (克隆、fetch) ,还可扩展 Origin 在代码仓库之上执行的所有 RPC 操作 (Web UI 交互、REST API、所有智能体接口等) 。
我们已使用多达 100 个副本进行了合成压力测试,观察到读取性能稳定地线性扩展,且推送吞吐量没有任何回归。
集群的推送吞吐量取决于我们更新 S3 上 WAL 的延迟。使用 S3 标准版时,我们可以在压缩数据并将压缩后的数据复制到所有其他节点的同时,持续处理最高 120 次推送/秒。我们还在 S3 Express One Zone 上部署了高性能集群,其 PUT 操作的延迟低得多。在该环境中,我们可以接收超过 300 次推送/秒,实际瓶颈在于 Git 压缩磁盘上数据的速度。我们正在探索创新的数据磁盘布局方式,以降低压缩的影响:我们的目标是在不降低严格的持久性和一致性保障的前提下,持续优化 Git 代码仓库接收代码的速度。
- S3 Standard
- S3 Express One Zone
Cursor 的 单体仓库 everysphere 的推送/克隆吞吐量。
所有推送均可线性化,并会在确认前持久化到外部存储。
所有克隆均完全一致。
WAL 作为事实来源
S3 是一项出色的技术。由 S3 API 开创的对象存储理念,已成为大型数据存储系统中极其强大的基础构件,这无疑也适用于托管 Git 代码仓库。这里介绍的设计在许多方面都很新颖,但并非首个将 packfile 作为对象存储的设计。Azure DevOps (Microsoft 自家的 GitHub 竞争产品) 拥有一套非常成功的 Git 存储系统:将 packfile 存储在对象存储中,并将引用存储在关系型数据库 (MS SQL Server) 中。此类系统存在许多权衡。关系型数据库能够很好地扩展,以处理大型引用事务。但相应地,您也必须运维关系型数据库。我们坚信,Git 数据的一致性比任何其他因素都更重要。正是这一点让我们最终选择设计一个不依赖外部数据库、基于 WAL 的系统。
生产环境中的 Git 代码仓库可能出现许多问题:静态数据损坏、重新打包时的缺陷、push 期间的竞争条件。这其中充满了各种边缘情况。大多数问题已在 Git 上游得到解决,但并非全部如此。没有任何系统毫无缺陷,即使是开源且广泛部署的系统也不例外。我们的一致性模型确保能够跟踪代码仓库中发生的每一项基础操作。在 push 完全持久化到 WAL 之前,我们绝不会确认该 push。我们会将_所有_ push 线性化。我们访问的每个代码仓库的每个视图始终完全一致。 由于每次 push 都记录在 WAL 中,我们可以查看代码仓库曾经经历的每个状态。我们拥有所有 push 和重新打包操作的完整溯源数据。我们可以回退或快进每个副本。我们无需与任何外部数据库同步状态,无论该数据库仅存储引用,还是存储所有对象数据。当 (不是如果) 我们遇到 Git 中的缺陷时,都能精确定位发生了什么并将其还原。除了 Git 中已有的缺陷外,我们引入的新缺陷极少,因为在整个过程中,所有 Git 操作都是使用现成工具链在磁盘上的普通 Git 代码仓库中执行的。
Origin
我们深知,托管他人的源代码责任重大。我相信,读过并理解这篇博客文章的每个人都同样清楚这一点。如果开发人员无法向其 Git 代码仓库推送代码或从中拉取代码,公司就可能陷入停摆。CI 系统宕机五分钟造成的生产力损失很难用金额衡量,但无论从哪个角度看,都是一笔巨大的损失。
智能体从根本上改变了我们使用软件的方式,而且在许多方面让这一问题变得更加严重:更多代码、更多 PR、更多 CI 运行。版本控制是这一切的核心,也可能是最难在短时间内改变的系统。
过去数月,我们在 Cursor 内部一直面临这些难题,并投入了大量心力,构建了一个能为我们解决这些问题、也希望能为客户解决这些问题的平台。我们当前的重点是提供尽可能顺畅的迁移路径,帮助您获得更高的可靠性、更佳的性能和更大的规模,同时让迁移过程尽可能轻松无痛。
Origin 并非一项实验;它汇聚了在构建这类系统方面拥有数十年经验的人士的成果,他们深刻理解其中挑战的艰巨性。我们拥有经实践证明行之有效的工程与运营理念,并坚定承诺会随着版本控制领域的发展,持续演进这一理念。
我们希望您能信任我们和我们的平台。