深入理解与解决 Jujutsu(jj)的 Divergent Changes:从成因到五种处理策略
深入理解与解决 Jujutsujj的 Divergent Changes从成因到五种处理策略【免费下载链接】jjA Git-compatible VCS that is both simple and powerful项目地址: https://gitcode.com/GitHub_Trending/jj/jj导读本文以 docs/guides/divergence.md 为核心主体系统讲解 Jujutsujj中分歧变更divergent changes的产生机制——当多个可见提交共享同一个 change ID 时会发生什么并结合jj converge、jj abandon、jj metaedit --update-change-id、jj squash五种实战策略给出完整解决方案。读完本文你将能识别日志中的 divergence 标记、用 change offset 精确定位提交并根据保留哪个版本的内容这一核心诉求选择最合适的收敛手段同时理解其底层可见性模型与收敛算法。什么是 Divergent Changes分歧变更Jujutsu 的数据模型与其他版本控制系统最大的不同之一是引入了change ID变更 ID与commit ID提交 ID的分离。一个 change 代表一次变更随时间的演化而 change ID 是 change 的属性在提交被重写rewrite时保持不变commit ID 则每次重写都会变化。关于这对概念的精确定义可参考 术语表 与 Rewrite 条目。一个divergent change分歧变更发生在多个 可见提交 拥有同一个 change ID时。此时这些提交是同一 change 的多个并存版本在jj log输出中会以 change ID 后面的 change offset 和 divergent 标签来标识$ jj log mzvwutvl/0 test.userexample.com 2001-02-03 08:05:12 29d07a2d (divergent) │ a divergent change上面的输出包含三个关键信息mzvwutvl是 change ID默认以 12 位k-z字母序列显示详见 术语表 Change ID 条目/0是change offset用于在同 change ID 的多个提交之间消歧最靠近当前的一个为/0更早的依次为/1、/2…(divergent)标签明确告诉你这个 change 当前有不止一个可见提交。从源码角度看divergent()是一个内置 revset 函数定义于 lib/src/revset.rs其求值逻辑在 lib/src/default_index/revset_engine.rs 中检查某 change 的目标集合是否is_divergent()。也就是说divergence 不是某种坏状态的报错而是数据模型中的一种普通状态——类似于文件冲突jujutsu 允许你决定何时、以何种方式去处理它。Divergence 是如何产生的要理解 divergence 的成因首先必须理解 jujutsu 的可见性模型正常重写提交时原版本predecessor前驱会被隐藏新提交successor后继变为可见因此同一 change ID 在同一时刻通常只有一个可见提交。可见性的具体定义是可见提交是那些可以从 view视图中的匿名 headanonymous head到达的提交可见提交的祖先隐式可见见 术语表 Visible commits 条目。实现上匿名 head 集合记录在 lib/src/view.rs 的View::heads()中。情况一隐藏提交重新变得可见一个已隐藏的提交可能重新变为可见文档明确列出了四种触发方式本地新增可见后代例如jj new REV会使REV即使此前已隐藏也重新可见从远端抓取到可见后代如果某个隐藏提交曾被推送到远端其他人可能基于它创建新提交当这些新提交被 fetch 回本地时它们的可见性会使那个隐藏提交重新变为可见将其设为工作副本jj edit REV会使REV及其所有祖先如果尚未可见变为可见其他使隐藏提交可见的操作例如给隐藏提交添加 bookmark书签其隐含语义是你要重新开始使用这个提交了。这些场景的共同点都可以归结为 view 中的 head 集合被扩充——参见 lib/src/view.rs 的add_head/replace_heads等操作。情况二两个用户或进程同时改写同一个 change即使提交从未隐藏divergence 也可能因为同一 change 被双写而出现两个可见后继他人修改了你也修改过的分支你在本地改写了某分支上的提交远端其他人也改写了它在同一仓库的不同工作区中对同一 change 执行操作每个工作区都有独立的工作副本提交跨工作区操作很容易制造双写两个程序同时修改仓库例如你运行jj describe正在编辑提交描述期间一个 IDE 集成执行了 fetch 并 rebase 了你正在处理的分支——两个操作基于同一旧提交各自生成了后继。设计文档 docs/design/jj-converge-command.md 还补充了更多真实场景在终端 A 打开jj describe编辑器后到终端 B 改文件并触发快照然后回到终端 A 保存描述最终会得到两个可见提交任何交互式命令jj split -i、jj squash -i等都可能以类似方式引入 divergence在使用 Git 后端时change ID 存储在提交头中jj git fetch之后也可能出现第二个同 change ID 的提交。值得注意的是divergent 提交之间可以有不同的提交描述、不同的文件树、不同的父提交、不同的作者甚至可能出现除时间戳外完全相同的两个提交——这正是收敛算法需要逐一处理多个属性的原因。解决 Divergence 前的准备如何引用分歧提交在动手解决之前有一个关键规则必须牢记revset 中引用分歧提交时要么使用它的 commit ID要么使用带 change offset 的 change ID如/0、/1与 log 中显示一致因为裸 change ID 本身是有歧义的。change offset 的详细语义见 术语表 Change offset 条目当 change ID 无法唯一确定一个提交提交被隐藏或 change 分歧时在 change ID 后追加偏移量即可精确定位/0表示最近的提交/1表示更早的一个以此类推。处理策略的选择取决于你的核心诉求保留哪个提交的内容、两个都要、还是合并它们。下面逐一展开。策略一使用jj converge自动收敛实验性jj converge是 jujutsu 提供的实验性命令目标是自动解决或减少divergence它把某个 change 的两个或多个可见提交替换为单一提交并尽可能避免引入新的 divergence。基本用法与默认搜索范围直接运行jj converge命令首先在一个搜索空间内按 change ID 分组查找分歧提交。默认搜索空间来自配置项revsets.converge其默认值定义在 cli/src/config/revsets.tomlconverge mutable() divergent()即默认只处理mutable可变历史中且处于分歧状态的提交——这保证了不可变历史如已推送或被保护的分支不会被自动改写。你也可以用--revisions别名--revisions短选项-r显式指定搜索范围jj converge --revisions mutable() divergent() -:: jj converge -r author(alice) -r ::main对应参数定义在 cli/src/commands/converge.rs。无交互模式--no-interactive指示命令不向用户提问如果无法自动求解它将打印警告并退出而不做任何修改divergence 仍然保留jj converge --no-interactive当搜索范围内有多个divergent change 且处于非交互模式时命令无法自行决定收敛哪一个会直接报错并提示要么用交互模式运行要么指定一个只解析出单个 change ID 的 revset——见 cli/src/commands/converge.rs。命令的工作流程从源码 cli/src/commands/converge.rs 可以还原出完整流程解析搜索空间 revset并检查其中提交是否可重写check_rewritable_expr第139-141行调用库函数find_divergent_changes()找出所有分歧 changelib/src/converge.rs求值 revset 后按 change ID 分组只保留成员数大于 1 的分组若无分歧直接打印 No divergent changes found. 并成功返回对应测试 cli/tests/test_converge_command.rs若存在分歧打印每个分歧 change 及其提交列表只有一个时直接选定有多个时交互式询问用户要收敛哪一个对选中的 change 运行收敛算法见下文底层原理必要时就作者author、提交描述description、父提交parents向用户提问创建解决方案提交把 divergent 提交的后代 rebase 到解决方案上并更新指向这些提交的本地 bookmark打印结果摘要创建的解决方案提交 ID、被 rebase 的后代数量若范围内还有其他分歧 change会提示可再次运行命令。jj converge一次只收敛一个 change。测试 cli/tests/test_converge_command.rs 展示了最简单的单分歧 change、无需用户输入的场景日志中zsuskuln/0与zsuskuln/1两个 divergent 提交被收敛为单一提交zsuskuln同时Rebased 1 descendants。收敛算法如何工作底层原理库层实现位于 lib/src/converge.rs。核心思路是以演化历史而非提交图为依据来求解TruncatedEvolutionGraph截断演化图只包含该 change ID 的提交并沿 predecessor 链回溯到所有分歧提交的最近共同支配点closest common dominator见 lib/src/converge.rs。它本质上是对jj evolog演化图的截断版本。作者、描述、父提交的求解对每个属性在演化图上用 dominator value支配值即演化分叉点处该属性的取值作为合并基底构造base (A - base) (B - base)形式的三方合并能平凡求解则自动解决否则标记为Unsolved并转入交互式提问见 lib/src/converge.rs 与create_value_merge第591-628行。树的收敛把每个分歧提交的树 rebase 到选定父提交之上rebase_tree_onto_solution_parents第698-721行再以支配值为基底做多路合并。因此解决方案中可能存在文件冲突即使原本没有冲突——这是该命令文档明确声明的行为。落地apply_solution()第192-215行创建新提交保持原 change ID将其标记为所有分歧提交的后继set_predecessors并set_rewritten_commit后rebase_descendants()完成后代迁移。重要注意事项与安全网该命令使用启发式算法寻找好的解决方案但没有任何算法能永远做到用户想要的事因为用户想要什么本身没有客观定义。因此请务必记住三个检查手段# 查看命令究竟做了什么查看操作差异 jj op show -p # 用 evolog 观察 change 的演化历史 jj evolog # 对结果不满意时回退 jj undo这三条同样在 cli/src/commands/converge.rs 的注释中被强调且jj op show -p、jj undo也是原文档推荐的检查与回退路径。策略二抛弃其中一个提交jj abandon如果某个分歧提交已经明显过时或错误直接抛弃它即可# 用 commit ID或带 offset 的 change ID抛弃不需要的提交 jj abandon unwanted-commit-id # 可以一次抛弃多个 # jj abandon abc def 123 # jj abandon abc::这是你已经知道该保留哪个版本时最简单的解决方案。注意jj abandon并不会删除提交对象只是将其从可见 head 集合中移除、使其变为隐藏——这与可见性模型完全一致。策略三为其中一个提交生成新的 change IDjj metaedit --update-change-id如果希望把两个版本都保留下来作为拥有不同 change ID 的独立 change可以给其中一个提交生成全新 change IDjj metaedit --update-change-id commit-idmetaedit命令用于修改提交元数据而不改变其内容--update-change-id标志的说明见 cli/src/commands/metaedit.rs其实现调用commit_builder.generate_new_change_id()第230-231行。生成新 change ID 后两个提交各自拥有唯一 change IDdivergence 即告解除同时两份内容都被完整保留。策略四将提交合并在一起jj squash当你想把两个分歧提交的内容合并到单个提交时使用 squash# 把一个提交压进另一个 jj squash --from source-commit-id --into target-commit-idsquash 会将源提交的变更合并进目标提交形成单一提交源提交随即被抛弃变为隐藏。这与保留一个、抛弃另一个策略二的区别在于策略二只保留单一版本的内容而 squash 是把两份内容都并入最终提交。策略五忽略 divergenceDivergence 不是错误。如果它没有立即引起问题完全可以保持原样尤其当两个提交都处于不可变历史immutable history中时忽略可能是唯一可行的选项。不过忽略也伴随着实际代价你无法仅凭裸 change ID 无歧义地引用分歧提交——每次都必须附带 commit ID 或 change offset这会在日常操作中持续带来不便。如何选择策略决策速查你的目标推荐命令效果自动解决启发式必要时询问jj converge单提交替换全部分歧提交后代自动 rebase只保留其中一个版本jj abandon id抛弃其余提交内容不合并两个版本都保留为独立 changejj metaedit --update-change-id id解除歧义两份内容并存合并两个版本的内容jj squash --from src --into dst内容合并源提交被抛弃不想处理 / 不可变历史不操作divergence 保留引用需带 offset需要留意的是jj converge是实验性命令其行为细节例如启发式规则可能随版本演进使用前可查看 converge 命令的设计文档 了解完整设计动机与更细致的场景分析。深入理解可见性模型与演化历史是理解 divergence 的关键最后从仓库实现层面总结两个理解 divergence 的核心机制1. 可见性由 view 中的匿名 head 集合决定。lib/src/view.rs 中的heads()返回当前 view 的 head 集合lib/src/view.rs 的normalize_heads()保证 head 集合保持规范化移除有后代的 head。可见提交是这些 head 的后代闭包。当新的 head 被加入如jj new、jj edit、bookmark 添加、fetch原本隐藏的提交就可能重新进入可见集合从而与另一个可见后继撞车形成 divergence。2. 演化历史predecessor/successor被记录在操作日志中。lib/src/evolution.rs 定义了CommitEvolutionEntry通过遍历操作operation历史读取predecessors_for_commit来还原提交演化链jj evolog展示的正是这条链。converge 的启发式算法也正是基于这条演化链上的支配值来寻找合并基底因此它对两个分歧提交从哪个共同祖先分叉而来非常敏感。理解这两点后你会明白divergence 本质上不是数据损坏而是 jujutsu变更change与提交commit解耦模型下多人/多进程协作的正常副产品。选择jj converge自动收敛、jj abandon丢弃、jj metaedit分流、jj squash合并或干脆忽略它——这五种策略覆盖了所有常见诉求且都有jj op show -p、jj evolog、jj undo三重安全网兜底。【免费下载链接】jjA Git-compatible VCS that is both simple and powerful项目地址: https://gitcode.com/GitHub_Trending/jj/jj创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考