jj:一个比 Git 更简单、也更强大的版本控制系统

今天 HN 上另一篇特别值得读的文章,是 Steve Klabnik 写的 jj 教程。jj 是 Jujutsu 的命令行工具,Jujutsu 是一个新的分布式版本控制系统。它的核心观点很直接:jj 既比 Git 简单,也比 Git 强大。

嗯,这个说法听起来很冒险,因为 Git 已经在很多团队里根深蒂固。但 jj 的价值,不在于它要取代 Git,而在于它提供了一种不同的概念模型,让很多在 Git 里很麻烦的操作,变得自然。

jj 最核心的设计选择,是把"变更"和"分支"解耦。在 Git 里,你通常会先创建一个分支,然后在上面做一系列提交,最后再合并或变基。这个流程本身没问题,但它带来的副作用是:分支一旦创建,就变成了一种"承诺"。你要么把它合并,要么把它删掉,要么把它重命名。而在这之间,你的工作历史就被绑定在这个分支上,很难再灵活调整。

jj 的做法是,默认所有变更都是"匿名"的。你可以先做一堆修改,把它们组织成多个独立的变更,每个变更都有自己清晰的描述,但还没有被绑定到任何分支上。只有当你准备提交给远程仓库时,才把它们关联到分支。这意味着你可以在本地自由重组、拆分、合并、调整顺序,而不必担心破坏分支结构。

这种设计带来的直接好处,是工作流变得更像"编辑",而不是"提交"。你不再需要为了保持分支干净而提前规划好每一步,也不再需要为了合并方便而频繁变基。你可以先专注于把问题拆开,把每个小改动做成独立的变更,然后在最后再决定怎么组织它们。

jj 还通过 revset(变更集合表达式)让很多查询和过滤操作变得更自然。比如,你可以用 jj log -r 'working_copy()' 只看工作区相关的变更,用 jj log -r 'conflict()' 只看有冲突的变更,用 jj log -r 'desc("fix")' 只看描述里包含"fix"的变更。这些表达式在 Git 里要么需要复杂的 git log 参数组合,要么需要额外工具,而在 jj 里是原生支持的。

另一个值得注意的点,是 jj 对堆叠 PR(stacked PRs)的原生支持。在 Git 里,堆叠 PR 通常需要借助 git rebasegit cherry-pick 和一些手动操作,过程容易出错,也容易被破坏。jj 通过变更的父子关系和分支的灵活绑定,让堆叠 PR 变成一种自然的工作方式。你可以先提交第一个 PR,然后在它的顶端创建第二个变更,再在第二个的顶端创建第三个,最后一次性推送,它们会自动形成依赖链。

当然,jj 并不完美。它的社区还小,文档还在完善,很多团队对 Git 的依赖也深。但 jj 提供了一个很重要的信号:版本控制系统的概念模型,仍然有改进空间。Git 的成功,部分来自于它在分布式协作上的突破;jj 的尝试,则是在"本地工作流"上再做一次突破。

从工程角度看,这篇文章最有价值的建议,其实不是"立刻从 Git 迁移到 jj",而是"先理解 jj 的设计思路,再决定哪些部分值得借鉴"。比如,你可以继续在 Git 里工作,但尝试在本地用更小的变更粒度、更频繁的重组、更灵活的分支策略。或者,你可以在新项目里尝试 jj,利用它的匿名变更和 revset,让本地工作流更顺畅。

从更大的角度看,我觉得 jj 真正提醒我们的,是另一件常被忽略的事:工具的形状,会反过来塑造我们的工作习惯。Git 的分支模型,让很多人习惯先规划再行动;jj 的变更模型,则鼓励先行动再规划。这两种习惯没有绝对的对错,但它们会导致不同的工程文化。

所以今天真正值得记住的,不是"jj 比 Git 好",而是另一句更根本的话:版本控制系统的价值,不仅在于它能记录什么,还在于它让你愿意以什么方式工作。而这件事,目前还有很多改进空间。