 一等冲突(First-class Conflicts):可延迟、可继续变基的冲突处理机制深度指南)
Jujutsu (jj) 一等冲突First-class Conflicts可延迟、可继续变基的冲突处理机制深度指南【免费下载链接】jjA Git-compatible VCS that is both simple and powerful项目地址: https://gitcode.com/GitHub_Trending/jj/jj导读本文聚焦 Jujutsujj的核心设计之一——一等冲突first-class conflicts冲突不再阻塞提交操作而是作为一种逻辑状态被记录在提交中允许你随时变基、合并甚至继续背着冲突开发。你将掌握冲突标记conflict markers的物化语法与解析规则、ui.conflict-marker-style四种标记风格的配置与取舍、长标记与缺失结尾换行等边界情况的成因以及结合源码理解快照 diff格式为何比传统逐侧罗列更利于手工解决冲突。什么是冲突以及 jj 与其它 VCS 的根本差异当 Jujutsu 无法确定如何合并对同一文件做出的不同修改时冲突就发生了。典型场景是两人修改了同一文件的同一区域随后用jj new合并提交或用jj rebase把某个提交变基到另一个提交之上。与大多数 VCS 不同的是Jujutsu 可以把冲突状态直接记录进提交。例如变基一个提交若产生冲突冲突会被记录进变基后的提交变基操作本身照常成功你可以在任何方便的时候再去解决冲突处于冲突状态的提交可以继续被变基、合并或回退back out提交中存储的是冲突的逻辑表示而不是冲突标记markers文本本身因此反复变基冲突不会产生嵌套冲突标记其底层原理见 technical/conflicts.md。从源码结构看这一点体现在提交的树tree引用上普通提交只有一个根树而冲突提交会挂接一个有序的树对象列表lib/src/conflicts.rs中的MergedTreeValue即由此演变而来。核心数据模型见 core/src/merge.rs。一等冲突带来的七大优势Jujutsu 对冲突的更深入理解带来了多项实际收益消灭--continue系列命令。不再需要git rebase/merge/cherry-pick --continue。取而代之的是统一的三步流程检出冲突提交 → 解决冲突 → amend通常配合jj squash归并回原提交。自动变基auto-rebase。重写提交时其所有后代会被自动重写这一能力在大多数场景下取代了 Mercurial 的 Changeset Evolution 机制。合并且提交merge commit的正确变基。jj 将合并提交中的变更定义为与合并双方父提交对比因此可以正确变基合并提交——这是 Git 和 Mercurial 都做不到的包括合并提交中完成的冲突解决覆盖了git rerere的常见用例。由于合并提交中的变更可以按预期显示和变基所谓的 evil merges恶合并在 jj 中也不再那么邪恶。冲突解决可以无限期推迟。你可以把所有进行中的提交持续变基到上游最新而无需立刻解决冲突。交叉合并criss-cross merges与章鱼合并octopus merges在实现上变得平凡。部分 Git 目前无法处理、或会产出嵌套冲突标记的案例可以被自动解决。支持协作式冲突解决。冲突状态可以共享给他人共同处理前提是仓库参与者都使用 jj若有人通过 Git 与你的项目交互则不建议共享带冲突的提交。关于工作副本中冲突的具体处理方式参见 working-copy.md#conflicts下文在工作副本中物化与解决一节也会展开。冲突标记Conflict Markers物化与语法冲突在多种场景下会以冲突标记的形式物化materialized出来对带冲突的提交执行jj new或jj edit冲突被写入工作副本文件冲突出现在 diff 输出中例如用jj show查看一个引入或解决冲突的提交文件级命令如jj file show同样会物化冲突见 cli/src/commands/file/show.rs 中的marker_style传递。一个完整示例假设某文件原本内容全小写apple grape orange提交 A 中某人把 grape 改为 grapefruit提交 B 中另一个人把所有行改为大写。用jj new A B合并两者时jj 无法自动组合这两处修改于是产生冲突并在工作副本中物化如下 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 from: base\\\\\\\仅用于把标签拆成两行提高可读性to: sidediff 正文中以空格开头的行表示上下文-开头为删除行开头为新增行。因此解决该冲突就是把grape → grapefruit这个 diff 应用到大写快照上最终文件应为APPLE GRAPEFRUIT ORANGE多边冲突many-sided conflicts实践中绝大多数冲突是双边2-sided的即同时只合并两处修改但 jj 支持任意多边的冲突——当一次合并 3 个及以上提交时就会发生。此时你会看到一个快照 多个 diff 部分。相比逐侧罗列内容的传统标记jj 的快照 diff风格核心收益是无需人工逐行比对两侧差异。对于多边冲突尤其省力——解决过程就是依次把每个 diff 应用到快照上。该物化逻辑对应 lib/src/conflicts.rs 中的materialize_merge_result()与materialize_jj_style_conflict()先通过files::merge_hunks合并分块命中冲突后按行计算ContentDiffdiff from/to标签并基于diff_size自动挑选改动最小的一侧作为快照使整体标记最短、最易读。三种冲突标记风格ui.conflict-marker-style配置详解通过ui.conflict-marker-style配置项可切换标记风格。其合法值与默认值在 cli/src/config-schema.json 中定义枚举为diff、diff-experimental、snapshot、git默认值为diff默认值同时体现在 cli/src/config/misc.toml 的conflict-marker-style diff。底层枚举ConflictMarkerStyle定义于 lib/src/conflicts.rs。风格一diff默认上文示例即 diff 风格快照 一系列待应用 diff。也是唯一允许出现%%%%%%%标记的风格对应源码ConflictMarkerStyle::allows_diff()。风格二snapshot如果你只想看到每一侧的完整内容、不想要 diff可设置ui.conflict-marker-style snapshot同一冲突将物化为 conflict 1 of 1 rtsqusxu 2768b0b9 commit A apple grapefruit orange ------- vpxusssl 38d49363 merge base apple grape orange ysrnknol 7a20f389 commit B APPLE GRAPE ORANGE conflict 1 of 1 ends注意此处基线标记变为-------Remove每条 base/side 各自成段。风格三git部分外部工具编辑器、合并工具期望 Git 风格标记可设置ui.conflict-marker-style git产出与 Git diff3 一致的标记 rtsqusxu 2768b0b9 commit A apple grapefruit orange |||||||| vpxusssl 38d49363 merge base apple grape orange APPLE GRAPE ORANGE ysrnknol 7a20f389 commit B限制git 风格只支持双边冲突当冲突超过 2 侧时会回退为类似的 snapshot 风格标记见 lib/src/conflicts.rs 的materialize_conflict_hunks分支仅当[left, base, right]三元素恰好匹配时才走materialize_git_style_conflict。风格四diff-experimentalSchema 中还存在第四种取值diff-experimental与diff相似但总是选取第一侧作为快照。源码注释说明它可能在未来版本成为默认值目前属于实验性选项生产使用建议保持diff。此外某些内置合并工具会自动要求特定标记风格例如smerge、vscode、vscodium在 cli/src/config/merge_tools.toml 中均配置了conflict-marker-style git因为 VS Code 等工具对 Git 风格标记支持更好。长冲突标记消除歧义某些文件内容本身可能含有长得像冲突标记的行例如以开头的行。为保证哪些行是标记、哪些是文件内容永远无歧义jj 在必要时会使用比常规更长的冲突标记——只要某侧内容里检测到长度 N 的疑似标记jj 就用长度 N4 的标记且最短不低于 7。 conflict 1 of 1 %%%%%%%%%%%%%%% diff from: wqvuxsty cb9217d5 merge base \\\\\\\\\\\\\\\ to: kwntsput 0e15b770 commit A -Heading HEADING mpnwrytz 52020ed6 commit B New Heading conflict 1 of 1 ends实现证据见 lib/src/conflicts.rsMIN_CONFLICT_MARKER_LEN 7、CONFLICT_MARKER_LEN_INCREMENT 4而choose_materialized_conflict_marker_len()该文件 L420-L432扫描所有侧内容、统计已有疑似标记的最大长度再saturating_add(4).max(7)得到本次使用的标记长度。解析端parse_conflict_marker则要求标记长度不小于期望长度过短的标记会被当作普通内容从而保证内容中较短的同字符行不会被误解析。缺失结尾换行的冲突(no terminating newline)注释物化冲突时 jj 按行组织输出因此文本文件要么为空、要么应以\n结尾。遇到缺失结尾换行的冲突时jj 会为相关侧附加(no terminating newline)注释便于你理解文件状态如果你不关心文件是否以换行结尾通常可以直接忽略该注释并正常解决冲突。例如文件原本为不带结尾换行的grape一方把grape改为grapefruit另一方补上了结尾换行成为grape\n物化结果如下 conflict 1 of 1 tlwwkqxk d121763d commit A (no terminating newline) grapefruit %%%%%%% diff from: qwpqssno fe561d93 merge base (no terminating newline) \\\\\\\ to: poxkmrxy c735fe02 commit B -grape grape conflict 1 of 1 ends该冲突的合理解决方案可以是grapefruit\n补上结尾换行。底层实现上lib/src/conflicts.rs 与build_hunk_sides()当任一HunkTerm的最后一个字节不是\n时会在其标签末尾追加常量NO_ENDING_EOL_COMMENT即(no terminating newline)同时为了不让无结尾换行的内容与后续标记粘连jj 会给每个侧额外补一个换行作为分隔并从结束标记行省略结尾换行——parse_conflict在解析时会据此还原原始内容。在工作副本中物化与解决冲突的完整流程文件系统不理解冲突因此 jj 把冲突提交检出到工作副本时会以冲突标记形式写入文件并记录参与冲突的各部分通常是 3 个base、side 1、side 2。此后每次扫描工作副本jj 会解析标记并从中重建冲突状态。你可以用文本编辑器直接把标记替换为已解决文本——无需一次性解决全部冲突甚至可以只更新冲突标记中的某一部分用jj new commit在冲突提交之上创建工作副本提交解决后用jj diff检查改动、再jj squash把解决方案并入原冲突提交或用jj edit commit直接在带冲突的提交内修改缺点是难以单独审查解决方案用jj resolve调用外部合并工具处理2 侧 1 基的冲突见下文用jj restore直接选取冲突的某一侧但该方式无法查看各侧来源。使用jj resolve调用外部合并工具jj resolve实现见 cli/src/commands/resolve.rs支持对可 3-way 合并的冲突逐文件调用外部合并工具jj resolve --list或-l列出所有冲突而不实际解决jj resolve --tool NAME指定合并工具内置的:ours与:theirs可直接选取冲突的第 1、2 侧jj resolve FILESETS只解决指定路径的冲突退出合并工具而不做任何修改即可中止解决流程。外部合并工具通过 cli/src/config/merge_tools.toml 配置内置了vimdiff、vscode、vscodium、smerge、mergiraf等模板支持$base、$left、$right、$output、$path、$marker_length等占位符。底层原理冲突的数据模型与化简理解标记格式后值得了解其背后的逻辑表示详见 technical/conflicts.md冲突在提交中被记录为有序的树对象列表数量恒为奇数首个树为起始树后续每两棵树构成一个应用到起始树上的 diff。例如树 A、B、C、D、E 表示内容 A(C-B)(E-D)三方合并 A 与 C、基为 B记为A(C-B)。树内容按需计算检出工作副本时只需合并与上一次不同的部分列出冲突路径时只需遍历无法平凡解决的子树若仅一侧修改了lib/则该子树内无需查找冲突。合并树时无法通过 tree id 平凡解决的子树冲突会递归深入文件冲突则递归到文件内 hunk 粒度。冲突化简嵌套冲突表达式先展开Merge::flatten()再消去可抵消项Merge::simplify()两处实现均位于 core/src/merge.rsflatten见 L695-L710。例如提交 B 基于 A、被变基到 C 产生冲突C(B-A)且未解决再次变基到 D 得到D((C(B-A))-C)化简为D(B-A)——完全抹去 C 的痕迹。这正是长期背着冲突持续变基也不会积累递归混乱冲突的根本原因同理回退一个冲突提交E C(B-A)等价于E(C-E)化简后恰好回到无冲突的C。same-change rule同改规则当冲突各方做出完全相同的修改时默认自动视为已解决为该值与 Git、Mercurial 一致Darcs 则视为冲突。该自动解决在冲突代数意义上是有损的——把提交变基到包含相同或子集改动的提交上再变基回来会丢失改动jj 选择这一行为是因为它在绝大多数场景下更符合用户直觉。相关实现位于 lib/src/merge.rs 的SameChange::Accept与 lib/src/conflicts.rs 的resolve_file_executable()等调用处。总结Jujutsu 的一等冲突把冲突从阻碍操作的中断事件转变为可持久化、可传递、可延迟解决的数据。理解冲突标记的物化语法/%%%%%%%//、四种ui.conflict-marker-style风格的适用场景、长标记的歧义规避机制与缺失结尾换行的特殊处理能让你在日常多分支协作中把冲突处理成本降到最低而树列表数据模型与flatten/simplify化简算法则为持续变基不解决的工作流提供了坚实的底层保证。【免费下载链接】jjA Git-compatible VCS that is both simple and powerful项目地址: https://gitcode.com/GitHub_Trending/jj/jj创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考