Jujutsu(jj)相关项目对比指南:git-branchless、Sapling、GitUp 等同类工具横向解析
【免费下载链接】jjA Git-compatible VCS that is both simple and powerful项目地址: https://gitcode.com/GitHub_Trending/jj/jj
Jujutsu(jj)是一款兼容 Git 的版本控制系统,官方文档在 docs/related-work.md 中用一页篇幅梳理了六款「试图解决类似问题」的同类工具。本篇技术指南以该文档为核心骨架,逐一解析每款工具的定位、核心特性与设计取舍,并结合本仓库中的 README.md、docs/sapling-comparison.md、docs/git-comparison.md、docs/working-copy.md、docs/conflicts.md、docs/operation-log.md 等文档,以及cli/、lib/源码,深入说明 jj 与它们各自的异同。读完本文,你将掌握:这六款工具各自解决什么问题、与 jj 在「工作副本快照、冲突建模、操作日志、Git 互操作」等核心设计上的差异,以及如何结合 jj 的jj git push --change、jj op log、jj workspace等命令对照评估与选型。
为什么会有「相关工作」这份清单
Jujutsu 并非凭空出现。在 README.md 中,作者明确列出它的设计灵感来源:Git(追求速度与 Git 仓库互操作)、Mercurial 与 Sapling(revset 语言、无 index/staging area、匿名分支、历史重写原语、模板化输出)、Darcs(将冲突作为一等公民对象)。相关工具清单正是这一「站在巨人肩膀上」思路的延续——jj与其他工具共享大量目标,但用不同的方式实现。
从实现层面看,这种「借鉴与差异化」贯穿整个代码库:revset 解析器位于 lib/src/revset_parser.rs,模板系统位于 cli/src/template_parser.rs 与 cli/src/commit_templater.rs,冲突存储与合并位于 lib/src/merge.rs 与 lib/src/tree_merge.rs。理解下面每款工具,本质上就是理解 jj 在这些关键设计点上「抄了什么、改了什么、另起炉灶了什么」。
逐款解析:六款同类工具
git-branchless:在 Git 仓库内实现无分支工作流
git-branchless 的定位是在你现有的 Git 仓库内帮助你使用无分支(branchless)工作流,官方文档的描述是「Helps you use a branchless workflow in your Git repo」。它支持:
- 匿名分支(anonymous branching):不需要为每个小改动起分支名;
- 撤销(undo):与 jj 类似提供操作级撤销;
- 更快的变基(
git move):将git rebase封装为更快、更顺滑的操作。
文档同时指出它「处于重度开发中,新功能增长很快」(Under heavy development and quickly gaining new features)。
与 jj 的核心差异:git-branchless 构建在 Git 之上,必须通过git命令与 hook 机制工作;而 jj 从根本上把工作副本建模为一个提交(working copy as a commit),匿名分支是数据模型的内生属性(参见 docs/git-comparison.md 中「no current branch」一节)。也就是说,jj 不依赖 Git 的「detached HEAD」概念来维持匿名分支——Git 中的 detached HEAD 是一种特殊状态,而 jj 中是常态,且所有可见提交图的叶子(heads)都被跟踪,不会丢失或被 GC 回收。
Sapling:Meta 的 Mercurial 深度改造分支
Sapling 是 Meta 开发并内部使用的、对 Mercurial 深度改造的 fork。它与 jj 有最多的相似点,因为 jj 本身就从 Mercurial 借鉴了大量设计。官方专门为其撰写了一整篇对比文档 docs/sapling-comparison.md,核心相似点包括:
- 对用户友好的 CLI;
- 用 revset 语言选择修订版本;
- 对堆叠提交(stacked commits)的良好支持,包括跟踪「匿名 head」(没有 Git 那种 detached HEAD 状态)、
split命令、以及修改提交后自动变基其后代提交; - 用 templates 灵活定制输出。
两者的关键差异正是 jj 的招牌设计,逐条展开如下。
工作副本:自动快照 vs 显式提交。使用 Sapling(与大多数 VCS 一样),用户要明确告诉工具何时创建提交、包含哪些文件;而 jj 的每个命令都会自动把工作副本快照成提交(见 docs/working-copy.md),新文件自动被跟踪、删除的文件自动被取消跟踪。文档列举了三个好处:工作副本在每个命令执行时都被有效备份;不会有命令因为工作副本有改动而失败(不再需要sl shelve);CLI 更简单一致,因为工作副本被当作普通提交对待。
冲突:可提交 vs 必须解决后提交。Sapling(与大多数 VCS)要求用户在提交前解决冲突;而 jj 允许把冲突提交进提交里(见 docs/conflicts.md)。注意提交的是冲突的逻辑表示,而不是<<<<<<<之类的冲突标记。这带来连锁优势:合并冲突不会阻止你检出另一个提交;可以想什么时候解决就什么时候解决;后代变基永远成功(Sapling 也会自动变基,但有冲突时会失败);合并提交可以被正确变基(Sapling 有时会失败);冲突本身和冲突解决都可以被变基。
撤销:操作日志 vs MetaLog。jj 的撤销由 operation log 驱动,记录仓库随时间如何变化。Sapling 也有类似机制 MetaLog。两者功能看似相近,但 jj 通过jj op log把日志暴露给用户,让你能精确指定要回退多远;Sapling 的sl debugmetalog只展示单个提交的历史而非整个仓库的历史。此外,由于 jj 快照工作副本,撤销对工作副本的改动也是可能的:例如jj undo一个jj commit后,jj diff仍会显示与jj commit之前相同的改动;而sl undo一个sl commit后工作副本是干净的。
Git 互操作与打磨程度。Sapling 支持从 Git 远程克隆、推送、拉取,jj 也支持,且 jj 还支持与 Git 仓库共享一个工作副本(colocated workspace),可以在同一仓库中互换使用jj与git(详见 docs/git-compatibility.md)。文档也坦承 Sapling 更精致、功能更完整:它内置了很棒的 Web UI「Interactive Smartlog」,可以拖放提交进行变基等操作。
Forge 工作流。Sapling 的sl pr submit --stack可以把一串提交作为独立的 GitHub PR 推送(包括设置 base 分支),但只支持 GitHub。jj 没有对 GitHub 或其他 forge 的直接集成,但有jj git push --change为指定提交自动创建分支——见下文「从源码看jj git push --change」一节。
GitUp:Mac 平台的 Git GUI
GitUp 是一款仅限 Mac的 Git GUI。它与 jj 的相似点在于:都支持撤销,并能把仓库恢复到更早的快照。它由 GitUpKit 库 支撑。它是 GUI 工具,与 jj 的 CLI 定位互补。
Gitless:无 index 概念的 Git 简化接口
Gitless 是「为 Git 提供更简单接口」的另一尝试。它与 jj 一样没有 index/staging area 概念。但 Gitless 不会在工作副本改动之间移动(切换分支时改动不会随身带走);而 jj 由于把工作副本建模为一个提交,「工作副本改动随分支移动」是水到渠成的自然结果。用 jj 的体验是:工作副本改动永远待在当前提交上,你重写提交图时,工作副本提交会跟着被自动变基到新位置(参见 docs/working-copy.md 中「stale working copy」与自动快照机制)。
Breezy:多存储后端的 VCS
Breezy 是另一款 VCS,与 jj 的相似点在于支持多种存储后端:既有自己的格式,也支持.git(即把 Git 仓库作为后端之一)。jj 在 README.md 中同样强调其「storage-agnostic」设计——把用户界面与版本控制算法从存储系统抽象出来,目前只有 Git 后端达到生产可用(基于 gitoxide Rust 库),但未来可以接入 Mercurial、Breezy 格式乃至 Google 的 Piper/CitC 这类混合系统。
GitButler:多虚拟分支的 Git 客户端
GitButler 是一款 Git 客户端,核心特性包括:同时操作多个虚拟分支(virtual branches)、一等公民的冲突处理、操作历史(operations history)。它的「虚拟分支」概念与 jj 的「匿名分支 + bookmark 手动推进」思路不同:GitButler 仍以 Git 仓库为事实来源,而 jj 把整个仓库状态(含书签、各工作区的工作副本提交)都记录在自定义存储中。
从源码看jj git push --change
jj git push --change是相关文档中唯一出现的具体命令示例,其行为在 cli/src/commands/git/push.rs 中有完整定义。命令头部注释(第 87–93 行)说明:
默认推送指向
remote_bookmarks(remote=<remote>)..@的跟踪书签与标签;可用--bookmark/--tag推送特定书签或标签;用--all推送所有书签和标签;用--change根据指定提交的 change ID 生成书签名。
--change参数的定义(第 209–214 行)进一步说明:它通过创建书签来推送该提交(可重复),创建的书签会被自动跟踪,生成的默认书签名可用templates.git_push_bookmark设置定制,默认为"push-" ++ change_id.short()。默认模板定义在 cli/src/config/templates.toml 第 30 行:
git_push_bookmark = '"push-" ++ change_id.short()'底层实现中,create_change_bookmarks函数(第 1189 行附近)会解析每个变更参数、解析提交模板文本并生成书签名;随后推送逻辑会对--change/--named创建的书签做「不移动既有书签」的处理(第 393–395 行注释),并对--change隐含「允许创建远程书签、不允许删除」(第 426–427 行)。对应测试见 cli/tests/test_git_push.rs(如git push --change @后重复推送、-c=@ -b=...混用、以及自定义模板templates.git_push_bookmark的用例)。
实际使用示例(对应相关文档描述):
# 为某个提交创建书签并推送(书签名默认形如 push-<change id 短前缀>) jj git push --change X --change Y ... # 后续一次性更新整条栈上的所有书签(从 main 分叉点到 @) jj git push -r main..@注意与 Git 的差异:jj 不会从「跟踪远程书签」自动推导推送目标,必须用--remote指定远程仓库名(cli/src/commands/git/push.rs 第 104–106 行),且不支持一次推送到多个远程。
对照维度总结:jj 与同类工具的分水岭
把六款工具放在 jj 的设计维度上横向对照,可以提炼出四条分水岭:
| 设计维度 | jj | git-branchless | Sapling | GitUp | Gitless | Breezy | GitButler |
|---|---|---|---|---|---|---|---|
| 工作副本自动快照 | 是(作为提交) | 否 | 否 | 否 | 否 | 否 | 否 |
| 冲突可提交/一等公民 | 是 | 否 | 否 | 否 | 否 | 否 | 一等公民冲突 |
| 操作日志驱动撤销 | 是(jj op log) | 有撤销 | MetaLog | 快照式撤销 | 否 | 否 | 操作历史 |
| 无 index/staging area | 是 | 否 | 否 | 否 | 是 | 否 | 否 |
| 支持 Git 远程互操作 | 是(甚至 colocated) | 是(构建于 Git 内) | 是 | 是(GUI) | 是 | 是(.git 后端) | 是 |
| 多存储后端 | 是(抽象设计) | 否 | 否(Mercurial fork) | 否 | 否 | 是 | 否 |
(此表依据相关文档、docs/sapling-comparison.md、docs/git-comparison.md、README.md 中可确认的陈述整理;「否」表示相关文档未声称具备该能力。)
与 Git 本身的对比(补充语境)
要真正评估上述工具,还需要理解 jj 相对 Git 的概念性差异,官方在 docs/git-comparison.md 中做了系统总结,其中与本文主题直接相关的几条:
- 工作副本自动提交:导致 CLI 更简单一致,工作副本被当作普通提交;
- 没有 index(暂存区):Git 的 index 既是文件系统信息缓存又被暴露给用户,造成 CLI 不必要地复杂。Git 老手可能认为需要 index 才能只提交部分改动,但 jj 提供了更直接的命令:用
jj split把工作副本提交拆成两个;用jj squash -i挑选要移入父提交的改动,或用jj squash <file>移动特定文件; - 没有当前分支:Git 的「当前分支」是为了防止丢失新提交而必须存在的概念;jj 没有对应概念,书签手动推进;
- 冲突可以提交:没有任何命令会因合并冲突而失败,冲突被记录进提交,之后随时解决。这也让冲突与冲突解决可以被变基,覆盖了
git rerere的大多数使用场景; - 后代提交自动变基:重写提交(如
jj rebase)时,所有后代提交自动被变基到新提交之上,指向它们的书签与工作副本也会同步更新; - 操作日志取代 reflog:操作日志同时记录所有引用的一次性原子更新,比 Git 逐引用的 reflog 强大得多,并驱动撤销功能;
- 单一虚拟根提交:像 Mercurial 一样存在一个全零哈希的虚拟根提交,作为所有提交的共同祖先,消除了 Git 的「unborn branch」状态及相关命令行标志(如
git rebase --root、git checkout --orphan)。
这些设计正是 jj 在相关文档中被反复与其他工具比较的底层原因,也是评估替代方案时的核心维度。
如何进一步研究
如果本文激发了你的兴趣,建议按以下路径深入本仓库:
- 想快速掌握 jj 命令与 Git 命令的对应关系,看 docs/git-command-table.md;
- 想了解 jj 与 Git 的互操作边界(colocated workspace、Git 特性支持矩阵、冲突在 Git 中的表示、change-id header 等),看 docs/git-compatibility.md;
- 想从测试验证
jj git push --change等行为,读 cli/tests/test_git_push.rs; - 想了解多工作副本(
jj workspace add/list/forget)这类其他工具不具备的能力,看 docs/working-copy.md 的 Workspaces 一节; - 想理解操作日志如何支撑并发安全与撤销,看 docs/operation-log.md 与 docs/technical/concurrency.md。
一句话总结:git-branchless、Sapling、GitUp、Gitless、Breezy、GitButler 各自以「修补 Git」「fork Mercurial」「GUI 化」「简化接口」「多后端」「多虚拟分支」等不同路径接近同一类问题,而 jj 的答案是用「工作副本即提交 + 可提交冲突 + 操作日志 + 存储抽象」这套自洽模型,从根上重新组织版本控制的基本操作。
【免费下载链接】jjA Git-compatible VCS that is both simple and powerful项目地址: https://gitcode.com/GitHub_Trending/jj/jj
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考