Jujutsu(jj)Git 专家实战指南:同仓共置、提交式暂存区与操作日志驱动的六类工作流升级
【免费下载链接】jjA Git-compatible VCS that is both simple and powerful项目地址: https://gitcode.com/GitHub_Trending/jj/jj
作为 Git 的熟练用户,你可能会问:Jujutsu(jj)到底带来了什么实际好处?本文正是围绕这个问题展开的实战指南。它面向已经精通 Git 的开发者,逐一讲解 Jujutsu 在共置仓库、暂存区替代、历史编辑、撤销、变更演化追踪、补丁栈维护六个场景中如何让日常工作更简单、更安全、更高效,并配合当前仓库的源码实现,说明这些能力背后的底层原理。读完本文,你将能够把jj无缝接入现有 Git 工作流,并用更少的命令完成过去需要多步操作才能完成的 Git 任务。
同一仓库内与 Git 并排使用:colocation
Jujutsu 与 Git 仓库存放在同一个目录中,因此你可以在同一个工作区里同时使用jj和git命令,这就是所谓的colocation(共置工作区)。如果你遇到某个场景用 Git 处理更顺手,直接运行git命令即可,处理完再回到jj;如果发现某个场景缺少对应的jj支持,还可以向项目提交功能请求。
共置模式让迁移变得非常平滑:你可以只为那些确实被改进的工作流采用 Jujutsu,而不会失去你已经熟悉的 Git 命令和工具链。
共置工作区的判定与创建
从术语定义看,当使用 Git 后端、且底层 Git 仓库的.git/目录与.jj/目录互为同级(sibling)时,该工作区即称为 colocated(参见 glossary.md 中的定义)。用jj git init <name>或jj git clone <URL>创建的工作区默认就是共置的,此时jj会在每次命令执行时自动从 Git 仓库导入(import)并导出(export)引用(参见 git-compatibility.md)。
共置模式对构建工具等"期望看到一个 Git 仓库"的工具非常友好。文档允许在共置工作区中任意混用jj与git命令,但更推荐的做法是:大部分读操作(如git log、git diff)用 Git,写操作交给jj。原因在于jj没有"当前分支"的概念,执行jj命令后 Git 仓库通常处于 detached HEAD 状态,因此在运行会修改仓库的 Git 命令前,你可能需要用git switch先告诉 Git 当前分支是什么。
你还可以用jj git colocation status查看共置状态,用jj git colocation enable/jj git colocation disable在共置与非共置之间切换(详见 git-compatibility.md 中"Converting a workspace into a colocated workspace"一节)。
混用命令时的撤销保障
共置模式有一个非常实用的特性:可以用jj undo和jj op restore撤销修改性 Git 命令的后果。在jj op log中,由 Git 命令造成的变更会以一条名为 "import git refs" 的操作呈现。这意味着即使是 Git 侧的误操作,也能纳入 Jujutsu 的统一撤销体系。
用提交取代 Git 的暂存区(index/staging area)
Jujutsu 没有 Git 那样的 index/暂存区。因为重写提交在 Jujutsu 中既快又简单,所以很自然地用提交来充当暂存区的角色。
你不需要再为操作暂存区使用独立命令(git add、git rm --cached),而是用jj split和jj squash来移动进行中的工作,移动它们就像移动已完成的工作一样容易:
# 将工作区提交拆分成两个连续提交,file1 和 file2 进入第一个提交 jj split file1 file2 # 或者交互式地选择要拆分的变更 jj split # 把 file3 中的变更移入父提交 jj squash file3 # 或者交互式地选择: jj squash -i拆分(split)的源码实现视角
jj split的完整参数定义位于 cli/src/commands/split.rs。从源码可以看到几个关键行为:
- 默认拆分目标是当前工作区提交(
--revision的默认值为@),未提供文件参数时会自动进入交互模式(interactive || paths.is_empty())。 - 拆分后选中变更留在原提交,剩余变更进入新的子提交;也可以用
--parallel让两部分变成兄弟提交。 - 描述处理有明确规则:如果被拆分的提交有描述,会分别询问两个提交的描述;如果没有描述,则第二个提交不获得描述,只为第一个提交询问(split.rs 的注释描述了这一点)。
- 拆分空提交不被支持,因为同样的效果可以用
jj new实现。
挤压(squash)的源码实现视角
jj squash的完整参数定义位于 cli/src/commands/squash.rs。从注释和参数可以确认:
- 不带任何选项时,把变更从工作区提交移动到父提交;
-r指定从某个修订移动到其父提交;若目标提交有多个父提交(即合并提交)则会报错。 --from/--into(别名-t/to)可显式指定源与目标;省略时默认是工作区提交。例如jj squash --into @--把变更从工作区提交移动到祖父提交。- 移动后若源提交相对于其父提交为空、且未指定
--keep-emptied,源提交会被abandon(废弃)。若源与目标都有非空描述,会询问合并后的描述;若其中一方为空则直接采用另一方的。 - 每次 squash 完成后,Jujutsu 会自动rebase 所有后代提交(源码中
rebase_descendants()的调用可见于 squash.rs),这也是下文"自动且更安全的历史编辑"的基础。
这正是 Git 专家最需要转变的观念:不需要 index,提交本身就是可移动、可拆分、可合并的"暂存区"。
自动且更安全的历史编辑
如果你经常执行 amend、reorder、squash,Jujutsu 通常能用更少的命令完成同样的操作。
假设你想修改一个较早的提交。用 Git 可能需要三步:
git add file1 file2 git commit --fixup abc git rebase -i --autosquash而用 Jujutsu,你只需直接把变更 squash 进目标提交,所有后代提交会自动 rebase 到修改后的提交之上:
jj squash --into abc file1 file2从 squash.rs 的文档注释可以确认:--into的作用是把变更移入指定修订;而 squash 完成后,lib/src/rewrite.rs 中的rebase_descendants()机制会负责把受影响的后代提交全部自动重放。这意味着你永远不会因为改写中间提交而弄丢下游工作,也不需要像 Git 那样手动计算 fixup 顺序或担心rebase -i后的冲突残留。
这种"自动 rebase 后代"是 Jujutsu 的全局设计:无论 amend、rebase、split 还是 squash,只要一个提交被重写,它的所有后代都会自动跟上(参见 git-comparison.md 中关于 "Descendant commits are automatically rebased" 的概述)。
比 reflog 更强大的撤销:操作日志(operation log)
Git 的 reflog 功能强大,但它是按引用(per-ref)记录的,当涉及多个 ref 和多个操作时使用起来比较笨拙。
Jujutsu 的operation log(操作日志)记录的是整个仓库的状态:每一次变更都是一条可以检视的操作,你可以用一条命令恢复到任意更早的状态。从 glossary.md 的定义看,操作日志本身是由 operation 对象构成的 DAG(有向无环图),与提交形成的历史 DAG 同构——顺序发生的操作形成一条直线,并发发生的操作则形成分支与合并。这种设计让操作日志能够原子性地记录所有 ref 的更新,而不是像 Git reflog 那样按 ref 分开记录。
操作日志的常见用法:
jj undo:一步撤销最后一次操作,无需费心判断要 reset 哪个 ref。可以重复执行jj undo继续向过去回溯。jj op log -p:显示带有 diff 的操作,让你能检视"当时到底发生了什么"。--at-operation ID:让命令在仓库处于过去某个状态的前提下运行(即"如果仓库停留在之前的某个状态会怎样")。
undo 的源码实现视角
jj undo的实现位于 cli/src/commands/undo.rs,其文档注释确认了关键行为:连续执行jj undo会逐步恢复到越来越旧的操作;与之互补的jj redo则在一系列 undo 之后向"未来"方向前进。
实现上有几个值得 Git 专家注意的细节:
- 不能撤销根操作:
jj undo对根操作(root operation)会报错"Cannot undo root operation";对合并操作(merge operation)会建议改用jj op restore。 - 撤销栈的跳跃优化:源码中有一段精心设计的逻辑(undo.rs),避免重复执行
jj new ; jj undo时形成不断增长的撤销链——如果目标操作本身是 undo 操作,会直接恢复到最原始的操作,从而"跳过"旧的撤销栈。 - 撤销 push 的警告:撤销 push 类操作往往会导致 bookmark 冲突,因此源码会提示"Undoing a push operation often leads to conflicted bookmarks",并建议立即运行
jj redo(undo.rs)。
从 git-comparison.md 的对比表述看,操作日志之于 reflog 的改进,正如当年 Subversion 之于 RCS 的按文件历史——从"按 ref 记录"升级为"原子地记录整个仓库"。它还驱动了撤销功能的实现。
演化日志(evolog):追踪单条变更的完整历史
Git reflog 展示的是 ref 如何随时间移动,却很难看出某个特定提交(commit)是如何随时间演化的。Jujutsu 的演化日志("evolog")恰恰回答这个问题:每当一条变更(change)被重写,更新都会在 evolog 中可见。
你可以用 evolog 找到某个历史版本,然后用jj restore把完整或部分内容恢复到当前版本。
从 Change ID 到演化追踪
理解 evolog 需要先理解 Jujutsu 的双 ID 模型(参见 glossary.md 与 glossary.md):
- Change ID:标识一条"变更"(change)的稳定身份。重写提交(修改描述、rebase 等)会产生新的 commit ID,但 change ID 一般保持不变。
- Commit ID:标识某个具体快照;使用 Git 后端时就是 Git 的 commit ID。
evolog 正是沿着change ID 的 predecessor 链回溯,展示这条变更经历过的所有版本。实现上,cli/src/commands/evolog.rs 调用jj_lib::evolution::walk_predecessors来遍历前置版本;jj evolog支持-n/--limit限制条数、-G/--no-graph切换扁平列表、-T/--template自定义渲染模板,以及-p/--patch显示与上一版本的 diff(evolog.rs)。
值得注意的是,-p模式下如果新旧版本父提交不同,会临时 rebase 到新版本的父提交上再计算 diff,避免无关变更污染差异视图(evolog.rs)。
用 jj restore 恢复旧版本内容
jj restore的完整行为定义在 cli/src/commands/restore.rs:
- 不带参数时,
jj restore从父提交恢复内容到工作区提交,类似于jj abandon,但会保留描述等元数据。 --from指定源(默认工作区),--into(别名-t/to)指定目标(默认工作区);两者都省略时等价于jj restore --changes-in @,即撤销当前提交相对父提交合并结果的变更。- 只提供文件参数时,仅恢复这些路径的内容;
-i可交互式选择要恢复的部分。 - 恢复后同样会自动 rebase 后代提交;
--restore-descendants可以在重放后代时保留其内容而非保留 diff。
配合 evolog,你就能实现"找到旧版本 → 恢复全部或部分内容"的完整回滚闭环。
jj absorb:让补丁栈(patch stack)的更新更轻松
当需要修改一摞提交(stack)中的多个提交时,Git 要求你在运行git rebase --autosquash之前,为每一个要修改的提交至少执行一次git commit --fixup <ID>。
jj absorb适用于这样的场景:你在工作区里做了一些小的修正,希望把它们吸收进最近的若干提交。它自动把工作区中的每处变更移动到"上一次修改了该行"的那个提交中。
参数与行为
jj absorb的参数定义在 cli/src/commands/absorb.rs:
--from:源修订,默认@(工作区提交)。--into(别名-t/to):目标修订集合,默认mutable(),只会考虑源提交的祖先。paths:只吸收这些路径的变更。-i/--interactive:交互式选择要吸收的部分;--tool可指定 diff 编辑器(隐式启用交互)。- 如果源提交的所有变更都被吸收且源提交没有描述,源提交会被 abandon。
底层算法:基于标注(annotation)的 hunk 归属
jj absorb的核心算法位于 lib/src/absorb.rs,模块注释将其概括为:"把单个源提交中的变更拆分到最相关的祖先提交中,将其'吸收'掉"。核心流程在split_hunks_to_trees(lib/src/absorb.rs):
- 对源提交相对其父提交的每个文件 diff,计算父文件内容的行级标注(annotation)——即每一行最后一次是由哪个祖先提交修改的(使用
FileAnnotator)。 - 将 diff 切成 hunk,根据标注区间把每个 hunk 归属到对应的祖先提交。
- 为每个目标提交构造一棵"父内容 + 选中 hunk"的新树,最终由
absorb_hunks(lib/src/absorb.rs)合并进目标提交并重写后代。
明确的边界:它不解决所有情况
文档明确指出了jj absorb的局限:如果栈中多个提交都修改了工作区中改动的同一行,则该变更不会被移动。从源码看,当某个 hunk 无法被无歧义地归属到单个标注区间时(例如跨两个区间的新增行、或修改了被"遮盖"的行),split_file_hunks会跳过它(lib/src/absorb.rs 中有大量针对"ambiguous"情况的判定与单元测试)。这些边界情况在 lib/src/absorb.rs 的test_split_file_hunks_*系列测试中被系统地覆盖。
所以正确的预期是:jj absorb帮我们处理掉琐碎的常见情况,剩余无法自动归属的变更会留在工作区,由你决定如何 squash。
用jj op show -p检视 absorb 的改动
由于jj absorb本身也是一次 operation,你可以用jj op show -p以 diff 形式检视它到底修改了哪些提交(cli/src/commands/absorb.rs 的文档注释明确推荐了这一检视手段)——这再次体现了"操作日志 + 可检视操作"这一统一设计。
总结:六类工作流升级一览
| 场景 | Git 传统做法 | Jujutsu 做法 | 收益 |
|---|---|---|---|
| 暂存/挑选变更 | git add -p+git commit | jj split/jj squash -i | 无需暂存区,提交即暂存区 |
| 修改较早提交 | git commit --fixup+rebase -i --autosquash | jj squash --into abc | 单条命令,后代自动 rebase |
| 撤销操作 | 定位并git reset对应 ref | jj undo/jj op restore | 仓库级原子撤销 |
| 查看发生了什么 | 逐个 ref 翻阅 reflog | jj op log -p | 带 diff 的全局操作历史 |
| 追踪单条变更演化 | 难以从 reflog 还原 | jj evolog+jj restore | 按 change ID 完整回溯 |
| 更新补丁栈 | 为每个提交--fixup再 autosquash | jj absorb | 自动按行归属到最近修改点 |
对于 Git 专家而言,Jujutsu 的价值不是"另一个版本控制工具",而是一套保留了 Git 兼容层(共置仓库、Git 后端、jj git命令族)的、以提交和操作日志为核心的新交互模型。你可以从 colocation 开始逐步迁移,把上述六类高频工作流逐步交给jj,其余场景继续使用熟悉的git命令——二者可以在同一仓库里长期和平共处。
延伸阅读
- Git 兼容性详情(colocation、导入导出、禁用共置等)
- Jujutsu 与 Git 的概念对比
- 操作日志与撤销机制
- 术语表(change / commit / operation / rewrite 等核心概念)
jj absorb的命令测试:cli/tests/test_absorb_command.rsjj squash与jj split的命令测试:cli/tests/test_squash_command.rs、cli/tests/test_split_command.rs
【免费下载链接】jjA Git-compatible VCS that is both simple and powerful项目地址: https://gitcode.com/GitHub_Trending/jj/jj
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考