jj 与 Sapling 对比指南:从工作副本快照到冲突处理、撤销与 Forge 工作流的全面差异分析
【免费下载链接】jjA Git-compatible VCS that is both simple and powerful项目地址: https://gitcode.com/GitHub_Trending/jj/jj
本指南围绕 Jujutsu(jj)官方文档中的《Comparison with Sapling》展开,系统梳理 jj 与 Meta 开源的版本控制系统 Sapling 在设计理念上的共同点与关键差异。Sapling 是 Mercurial 的重度分支,而 jj 大量借鉴了 Mercurial 的思想,两者表面相似,却在工作副本、冲突建模、撤销机制、Git 互操作与 Forge 集成等核心环节走了截然不同的路线。读完本文,你将理解 jj 的"自动快照工作副本""可提交冲突""操作日志撤销"等设计如何带来更一致的 CLI 体验,并能直接上手jj git push --change、jj op log、jj resolve等命令完成实战操作。
背景:两条从 Mercurial 出发的技术路线
根据 docs/sapling-comparison.md,Sapling 是 Meta 开发的版本控制系统,在 jj 启动开发约三年后才对外发布。它本质上是 Mercurial 的重度修改分支;而 jj 也把 Mercurial 的很多理念吸收为自身设计的基础。因此两款工具存在大量相似之处,主要体现在:
- 用户友好的 CLI:都试图降低日常版本操作的心智负担;
- revset 语言:都提供函数式语言用于选择修订集(jj 的实现见 revsets 文档,支持符号、运算符、函数与别名,例如
jj log -r @-查看工作副本的父提交); - 对堆叠提交(stacked commits)的良好支持:都支持跟踪"匿名 heads",没有 Git 那种"detached HEAD"状态;都提供
split等命令;在修改(amend)一个提交后会自动 rebase 其后代提交; - 灵活的模板自定义输出:都允许通过模板定制日志、diff 等输出格式(jj 的模板语法见 templates.md)。
这些相似点说明两款工具面对的是同一类痛点。真正的分歧在于实现哲学,下面逐条展开。
差异一:工作副本是否"自动提交"
这是 jj 与 Sapling(以及绝大多数 VCS)最根本的分歧点。
- Sapling 的方式(传统方式):用户必须显式告诉工具何时创建提交、包含哪些文件。存在"暂存"环节,且工作副本有未提交改动时某些操作会失败。
- jj 的方式:工作副本由每个命令自动快照(见 working-copy.md)。新文件被自动跟踪,删除的文件被自动取消跟踪。
自动快照带来三个直接收益:
- 每次运行命令都等于备份了工作副本。即使你忘了提交,改动也不会停留在"无状态"的工作区里。
- 没有任何命令会因为工作副本有改动而失败。不会出现 Sapling 中
abort: 1 conflicting file changes: ...之类的报错,也不需要sl shelve这类"暂存避难所"命令。 - CLI 更简单、更一致:工作副本被当作普通提交对待,
@符号就是一个指向工作副本提交的 revset 表达式。
实战:控制自动跟踪的范围
snapshot.auto-track配置项控制哪些路径会被自动跟踪(语法见 filesets 文档)。默认情况下,被忽略文件(匹配 ignore 规则)永远不会被自动跟踪;如果你把snapshot.auto-track改为非默认值,未被自动跟踪的未跟踪文件可以用jj file track手动跟踪,而jj file untrack可以在保留文件于工作副本的同时取消跟踪。
ignore 规则沿用 Git 生态的.gitignore格式(jj 目前没有.jjignore),支持工作副本任意目录下的.gitignore、$XDG_CONFIG_HOME/git/ignore与$GIT_DIR/info/exclude(详见 git-compatibility.md 中关于.gitignore: Yes的说明)。注意:已被跟踪的文件即使匹配 ignore 规则也仍保持跟踪,需要jj file untrack才能排除。
实战:多个工作副本与陈旧工作副本
由于快照机制与操作日志绑定,jj 还支持单个仓库挂多个工作副本(jj workspace add),以及用jj workspace update-stale修复"陈旧工作副本"。后者常用于你在工作区 B 重写了工作区 A 的提交、或命令被^C中断导致第三步(更新工作副本)未完成的场景——详见 working-copy.md 的 "Stale working copy" 一节。
差异二:冲突能否被"提交"
传统 VCS(包括 Sapling)要求用户在提交前必须解决冲突。jj 则允许你把冲突提交到提交对象里(详见 conflicts.md)。需要强调的是,提交的是冲突的逻辑表示,而不是<<<<<<<之类的冲突标记文本本身。
这一设计带来一系列连锁优势:
- 合并冲突不会阻止你检出任一提交。你可以"带着冲突"继续切换分支、继续开发。
- 你可以在想解决的时候再解决。比如把全部进行中的工作持续 rebase 到上游头部,冲突先放着。
- Rebase 后代提交永远成功。Sapling 虽然也自动 rebase,但一旦遇到冲突就会失败;jj 则把冲突记录下来并继续。
- 合并提交可以被正确 rebase(Sapling 有时会失败)。jj 把合并提交中的改动定义为相对"自动合并后的父提交"的变更,因此连在合并提交里做的冲突解决也能被 rebase,覆盖了 Git 生态中
git rerere的大部分使用场景。 - 你可以 rebase"冲突本身"以及"冲突的解决结果"。因为冲突是数据对象而非标记文本,冲突被 rebase 时不会产生嵌套的冲突标记(技术原理见 technical/conflicts.md)。
实战:冲突标记的三种风格
冲突在写入工作副本或出现在 diff 输出中时会被"物化"为冲突标记。默认(diff 风格)标记长这样:
<<<<<<< conflict 1 of 1 %%%%%%% diff from: vpxusssl 38d49363 "merge base" \\\\\\\ to: rtsqusxu 2768b0b9 "commit A" apple -grape +grapefruit orange +++++++ ysrnknol 7a20f389 "commit B" APPLE GRAPE ORANGE >>>>>>> conflict 1 of 1 ends<<<<<<</>>>>>>>标记冲突起止,+++++++标记快照(snapshot)起始,%%%%%%%标记要应用到快照上的 diff 起始,\\\\\\\只是让标签换行更易读。解决冲突 = 把 diff 应用(或不应用)到快照上。
如果你更喜欢直接看每一侧的内容,可以把ui.conflict-marker-style设为"snapshot";如果团队工具期望 Git 风格,则可设为"git"(即 diff3 风格),但该风格只支持两侧冲突,多于两侧时自动回退到 snapshot 风格。此外,当文件内容可能被误认为冲突标记时,jj 会使用更长的冲突标记以保证无歧义;当冲突涉及缺失结尾换行符的文件时,jj 会给每个冲突项补换行并从>>>>>>>结尾标记省略终止换行——这些边界细节都记录在 conflicts.md 中。
实战:解决冲突的命令流程
- 用
jj new <commit>在冲突提交之上创建工作副本提交,冲突会同步出现在工作副本中;解决后用jj diff检查,再jj squash把解决结果并入原冲突提交。 - 或直接
jj edit <commit>在冲突提交上编辑(缺点是难以单独检查解决结果)。 - 两侧冲突可用
jj resolve调用外部合并工具;目录/文件/符号链接之间的冲突目前还没有很好的解决方式。jj restore可用于直接选择冲突的一侧。
差异三:撤销能力——操作日志 vs MetaLog
jj 的撤销由**操作日志(operation log)**驱动(见 operation-log.md),它记录了仓库随时间变化的历史:每次操作都保存一份"view"快照(bookmark、tag、Git ref 指向、repo heads、各工作区的工作副本提交),外加父操作指针与时间戳、用户名、主机名、描述等元数据。Sapling 的 MetaLog 功能看似接近,但关键区别在于:
- jj 通过
jj op log把日志暴露给用户,你可以明确看到要回退多少步;Sapling 的sl debugmetalog更像是展示单次提交的历史,而非整个仓库的演变。 - 得益于工作副本快照,jj可以撤销工作副本的改动:
jj undo掉一次jj commit后,jj diff仍显示提交前的改动;而sl undo掉sl commit后工作副本是干净的(改动已丢失在提交里)。
围绕操作日志的命令:jj undo(逐条撤销最近操作)、jj op revert(回退某个非最新操作)、jj op restore(把整个仓库恢复到某次操作的状态)。顶层选项--at-op/--at-operation可把仓库加载到指定操作时刻(此时自动快照被跳过),适合分析"仓库怎么变成现在这样的"。操作引用中@表示当前操作,x-表示其父操作,x+表示子操作。
实现证据:并发安全
从源码看,操作日志也是 jj 实现无锁并发的基础(operation-log.md 的 "Divergent operations" 一节):并发命令各自从最新操作加载仓库、各自写入新操作,随后由jj st/jj log提示分叉(divergence)。在 Git 后端下,git的改动会被记录为 "import git refs" 操作,因此也可以用jj undo/jj op restore撤销 Git 命令的结果(见 git-compatibility.md)。
差异四:Git 互操作——colocated 工作区
两者都支持从远程 Git 仓库克隆、推送、拉取。jj 的独特之处在于可以与 Git 仓库共享同一个工作副本,让jj和git在同一仓库中互换使用。
这得益于 Git 后端与"colocated 工作区"设计(详见 git-compatibility.md):jj git init <name>创建同时含.jj与.git的 colocated 工作区,每个jj命令会自动与 Git 仓库互相导入导出 refs;jj git init --git-repo=<path>可基于已有 Git 仓库建 jj 仓库(行为类似 Git worktree);jj git clone <URL>则克隆远程仓库,默认 remote 名为origin(可用--remote改名)。
实践建议与注意事项(来自 git-compatibility.md):
- 混合使用
jj与git命令时,推荐多用只读 Git 命令,改动交给 jj;jj 通常会让 Git 仓库处于 detached HEAD 状态,执行会改动的 Git 命令前可能需要先git switch。 - 可用
--no-colocate或git.colocate = false关闭 colocation;jj git colocation status/enable/disable可随时查看与切换。 - 关闭 colocation 可避免 IDE 后台
git fetch引发的分支冲突/change-id 分叉、巨型 refs 仓库下每次命令的自动 import 开销,以及 Git 工具面对冲突提交(以.jjconflict-base-*/、.jjconflict-side-*/根目录和非标准jj:treescommit header 存储)时的困惑。在 NFS/Dropbox 上共享 colocated 仓库的并发韧性也较弱。
差异五:打磨程度与 Web UI
文档坦率地承认:Sapling 更成熟、功能更完整("Polish: Sapling is more polished and feature-complete")。Sapling 内置了名为 Interactive Smartlog(ISL)的优质 Web UI,支持拖拽提交进行 rebase 等操作。这是 Sapling 在开箱即用体验上的显著优势;jj 目前没有对等的内置图形界面。
差异六:Forge 工作流——sl pr submit --stackvsjj git push --change
在代码托管平台(forge)集成上,两者取向完全不同:
- Sapling 的
sl pr submit --stack可以把一叠提交分别推成独立的 GitHub PR,并自动设置 base 分支,但只支持 GitHub。 - jj没有与 GitHub 或其他 forge 的直接集成,但提供
jj git push --change:为指定提交自动创建分支(bookmark),再按常规流程推送成 PR。
实战:jj git push --change的用法
文档给出的工作流要点:
- 为每个需要分支的提交显式指定:
jj git push --change X --change Y ...; - base 分支需要你在 GitHub(或 GitLab 等)的 UI 里手动设置;
- 后续推送可一次更新全部:
jj git push -r main..@(推送当前提交栈从main分叉点以来的所有分支)。
从源码可以印证命令行为(cli/src/commands/git/push.rs):
GitPushArgs定义了互斥的分组:--bookmark/--tag/--revision/--change/--named属于specific组,--all/--tracked/--deleted属于what组(push.rs 第 119-238 行)。--change的参数注释明确写道:"Push this commit by creating a bookmark",生成的 bookmark 名可用templates.git_push_bookmark模板自定义,默认是"push-" ++ change_id.short();新生成的 bookmark 会被自动跟踪(push.rs 第 210-221 行)。- 命令注释还说明:默认推送范围是
remote_bookmarks(remote=<remote>)..@,并强调 jj 不像 Git 那样从 tracked remote bookmark 推断推送目标,必须用--remote显式指定(push.rs 第 87-106 行)。 - 集成测试覆盖了
--change的多场景(cli/tests/test_git_push.rs,如jj git push --change @、--change=(@|root()+)、同 bookmark 重复--change等用例),说明"按 change 生成分支并推动"是经过验证的稳定路径。
总结:如何理解 jj 与 Sapling 的选择
一句话概括:Sapling 更贴近传统 VCS 的心智模型并做了精致打磨,而 jj 选择重构底层模型以换取一致性与可组合性。四条核心差异环环相扣——工作副本自动快照是撤销工作副本改动的前提,可提交冲突让 rebase 永不因冲突失败,操作日志为撤销与无锁并发提供统一基础,Git 互操作让 jj 可以无缝嵌入既有 Git 生态。如果你的团队深度依赖 GitHub 的 PR 栈工作流并看重开箱即用的 Web UI,Sapling 的sl pr submit --stack更具吸引力;如果你更在意"无论仓库处于什么状态命令都不失败、改动永不丢失、事后随时可撤销"的一致性体验,并且愿意接受 forge 集成需要手动拼装,那么 jj 的这些设计值得认真评估。
进一步阅读:本文所有结论均可回溯至仓库内的原始文档与源码——sapling-comparison.md(本文蓝本)、working-copy.md、conflicts.md、operation-log.md、git-comparison.md、git-compatibility.md、revsets.md 以及 push.rs 与对应测试。
【免费下载链接】jjA Git-compatible VCS that is both simple and powerful项目地址: https://gitcode.com/GitHub_Trending/jj/jj
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考