资讯详情

jj 复制/重命名追踪设计:CopyHistory DAG、快照模型与 diff/merge 实现解析

📅 2026/9/10 16:27:09 | 华诺云谱 👁 阅读
jj 复制/重命名追踪设计:CopyHistory DAG、快照模型与 diff/merge 实现解析
jj 复制/重命名追踪设计CopyHistory DAG、快照模型与 diff/merge 实现解析【免费下载链接】jjA Git-compatible VCS that is both simple and powerful项目地址: https://gitcode.com/GitHub_Trending/jj/jj本文基于 jjJujutsu仓库中的设计文档 copy-tracking.md 展开系统讲解 jj 如何在快照模型中记录文件的“复制/重命名”信息从CopyId/CopyHistory的 DAG 数据结构到 diff 算法、merge 阶段的命名冲突处理再到 Git 后端与云端后端的表示方案。读完后你将理解 jj 与 Git/Mercurial 在复制检测上的本质差异并能对照当前仓库中 lib/src/copies.rs、lib/src/backend.rs 的实际实现验证设计文档中各例子的落地情况。一、设计目标为什么快照模型需要复制信息jj 的设计文档首先明确了复制信息至少要支撑的四个使用场景Diff如果文件被复制过diff 应该与“源版本”比较而不是显示为整个文件的“新增”Merge当合并或 rebase的一侧重命名了文件、另一侧修改了该文件时修改要传播到重命名后的路径还有其他大量边界情况需要处理Log应该能执行类似jj log -p file的操作在文件是被复制出来的时候沿复制链向前追溯Annotateblame与 log 类似遇到复制创建的文件时也要向前追溯历史。同时文档对性能提出了两条约束方案必须同时适配Git在任意两棵树之间即时合成复制信息与自定义后端可能显式记录复制信息并在任意大的提交范围内回取API 的定义要让自定义后端在“还没准备好实现复制追踪”时可以完全忽略复制信息。一个关键的设计取舍是文档认为没有必要区分 copy 和 rename——重命名就是“复制 删除”。代价是无法区分“把foo复制为bar并把foo重命名为baz”与“把foo复制为baz并把foo重命名为bar”。这一取舍在后文中与当前仓库代码的对应关系会再次出现lib/src/copies.rs 中正是用一个CopyOperation { Copy, Rename }枚举来表达“源路径是否被删除”。二、期望的用户体验Desired UX一组可验证的行为不变式设计文档用大量具体场景定义了“理想行为”这些场景本质上是行为不变式也是后续实现与测试的验收标准。2.1 restore 必须保留复制关系且传递性复制要“压平”例如jj new X--; jj restore --from X应该把X-和X中做的复制恢复到新的工作副本。传递性的复制要压平如果X-把foo重命名为barX又把bar重命名为baz那么恢复出来的提交应该表现为foo直接重命名为baz。这一行为同样适用于一般的 reparenting如社区讨论过的 “verbatim rebase” 场景。2.2 restore 之后再 diff 应当为空jj restore --from X; jj diff --from X至少就文件内容而言应当是空的即使它可能提示重命名过的文件有不同历史。2.3 rebase 应支持无损往返除了A(A-B)A这条冲突代数中的“同变规则”对应 lib/src/merge.rs 中的实现rebase 目前从不损失信息把一个提交 rebase 走再 rebase 回来应当得到相同内容。文档给出的例子$ jj log C rename bar-baz | B rename foo-bar | A add foo $ jj rebase -r C -o A $ jj rebase -r C -o B # 回到上述状态2.4 revert 父提交应当是 no-op补丁必须可逆做完一个修改再 revert两个提交之间的 diff 都应为空$ jj log B rename foo-bar | A add foo $ jj revert -r B -o B $ jj diff --from B- --to B # 应为空2.5 parallelize / serialize 是“无损 rebase”的特例$ jj log E edit qux | D rename baz-qux | C rename bar-baz | B rename foo-bar | A add foo $ jj parallelize B::D # E 不应出现冲突看起来仍像一次普通编辑 $ jj rebase -r C -A B $ jj rebase -r D -A C # 现在回到与之前相同的图2.6 合并提交中的复制命名冲突要可解决、可回退两侧把不同源文件重命名到同一目标时应能解决命名冲突且解决方案可以被撤销、回到命名冲突状态$ jj log D resolve naming conflict by choosing foo as the source |\ C | rename bar-baz || | B rename foo-baz ||/ A add foo and bar $ jj file annotate baz # 不应包含来自 C 的修改还要能重命名“只在一侧存在”的文件$ jj log D rename foo2-foo3 and bar2-bar3 |\ C | rename bar-bar2 || | B rename foo-foo2 ||/ A add foo and bar2.7 跨越合并提交的复制diff 结果必须与图拓扑一致$ jj log D delete baz |\ C | rename foo-baz || | B rename foo-bar ||/ A add foo此时jj diff --from C --to D应当显示baz-bar的重命名就像jj diff --from C --to B那样而jj diff --from B --to D不应显示任何重命名——尽管 C 里确实发生了一次重命名。三、高层设计把“过去的路径名”写进树对象文档指出jj 使用与 Git 类似的快照模型其一级冲突的代数也建立在快照之上补丁即两个状态之间的差。因此复制信息也必须塞进快照模型——这一点文档作者自嘲花了数月才真正想通。3.1 数据结构CopyHistory DAG 与 CopyId提案是更新树对象使其包含文件“过去路径”的信息如果文件foo在一个提交里被重命名为bar又在另一个提交里被重命名为baz那么就记录baz曾经叫过bar和foo。为了支持“两个文件合并为一个”过去名字列表实际上是一个DAG——合并可以在合并提交中发生两侧把不同源文件复制到同一目标而模型层面支持它之后普通提交中“多合一”也顺带被支持了。为了避免在树条目中存储全部历史路径设计把复制历史写成独立对象树只通过 ID 引用每个 ID 指向复制历史 DAG 中的一个节点——正如提交 ID 指向提交 DAG 的节点。文档给出的原始数据结构是// 当前 TreeValue::File 变体 File { id: FileId, executable: bool }, // 新的 TreeValue::File 变体 File { id: FileId, executable: bool, copy_id: CopyId }, // CopyId 是以下结构的哈希 struct CopyHistory { path: RepoPath, parents: VecCopyId }这里还有一个值得注意的细节ID 的输入只含文件名时树 ID 是确定性的而如果给复制图节点加入一个“盐”salt则可以表达“文件被从零重写”——例如foo在前一个提交的 copy ID 是123当前提交中文件被整体重写后即使没有任何复制发生也拿到新的 copy ID456。文档作者当时对盐的实用性持保留态度但这个设计已经落地当前仓库 lib/src/backend.rs 中CopyHistory结构体确实包含salt: Vecu8字段注释说明它“可以让一个提交声明某文件被其新化身取代”结构体还有current_path与parents新建文件无父普通复制/重命名有一个父多文件合并有多个父与文档的 DAG 描述一一对应。文档还留下两个未决问题符号链接是否参与复制追踪其历史对 blame 意义不大但有助于检测整目录重命名以及是否追踪目录级别的复制文档认为可能很复杂但坦承没有深入思考。3.2 与当前实现的对应trait 的三个复制接口设计文档要求“自定义后端在准备好之前可以忽略复制信息”。在 lib/src/backend.rs 中这一要求落实为ReadCopy/WriteCopy相关的三个 trait 方法read_copy(self, id: CopyId) - BackendResultCopyHistorywrite_copy(self, copy: CopyHistory) - BackendResultCopyIdget_related_copies(self, copy_id: CopyId) - BackendResultVecRelatedCopy文档注释明确写道“不支持复制追踪的后端可以返回BackendError::Unsupported”并且get_related_copies的语义是“返回给定复制历史的所有祖先以及这些祖先的全部后代子节点必须先于父节点返回顺序应确定”。这正是第 5 章云端方案中“按 copy ID 取整图”查询的 trait 化实现。与此同时当前仓库中的 Git 后端与 simple 后端都尚未支持lib/src/git_backend.rs 中这三个方法均返回The Git backend doesnt support tracked copies yetlib/src/simple_backend.rs 同样返回Unsupported。也就是说文档“实现计划”中 Git 后端复制追踪一项第 8 步在当下仍走“即时启发式检测”路线——jj diff/jj status通过 cli/src/diff_util.rs 中的get_copy_records从后端按 DAG 区间取得启发式CopyRecord每个CopyRecord记录 target/target_commit/source/source_file/source_commit见 lib/src/backend.rs仓库还提供了 cli/src/commands/debug/copy_detection.rs 调试命令用于观察检测结果。四、Diff 算法先比树再沿复制图追溯文档描述的 diff 流程是先不考虑复制信息对两棵树做常规 diff对 diff 中所有发生变化的 copy ID遍历其复制图弄清它们之间的关系确定哪个源文件对应哪个目标文件细节留给实现文档用三个例子证明可行性。4.1 例一分叉的复制与重命名场景假设新文件在两提交中内容不同M rename foo-baz, create bar | | L copy foo-bar, create baz |/ K add foo各提交树中记录如下id是内容哈希即FileId2:bar-1:foo表示 copy ID 2 的文件bar复制自 copy ID 1 的fooCommit K: name: foo, id: K, copy_id: 1:foo Commit L: name: bar, id: L, copy_id: 2:bar-1:foo name: baz, id: L, copy_id: 3:baz name: foo, id: K, copy_id: 1:foo Commit M: name: bar, id: M, copy_id: 4:bar name: baz, id: M, copy_id: 5:baz-1:foo对应地copy ID 与提交的包含关系是一幅图节点2:bar与5:baz都指向1:foo而4:bar孤立。diff K→M树 diff 发现 copy ID 1、4、5 受影响。遍历复制图发现 1 与 5 相关、4 不相关。对于涉及 1 和 5 的复制图由于foo在目标端不存在而baz在源端不存在判定为重命名。diff L→M等价于jj new L; jj bookmark create M2; jj restore --from M --to M2后对M2做 diff发现 copy ID 1、2、3、4、5 全部变化。遍历复制图后1、2、5 相关3、4 不相关。bar与baz在两侧的复制图互不相交因此把它们的 diff 拆成两个独立文件级 diff。剩下的 copy ID 中复制图上最短路径连接的是源端foo与目标端baz且foo不在目标端、baz不在源端以相关 copy ID 计判定为重命名。源端剩下的bar其最近亲是baz但baz已作为foo的重命名目标被占用于是把bar视为复制进baz。最终 diff 结果baz被删除删除内容Lbar被创建内容Mfoo重命名为baz显示 K 到 M 的 diffbar合并进baz显示 L 到 M 的 diff这段推理与当前实现惊人一致lib/src/copies.rs 中CopyHistoryDiffStream的poll_next正是“copy ID 相同则走普通条目不同则调用find_diff_sources_from_copies沿复制图查找源”后者会先按拓扑序构建祖先/后代映射collect_descendants再依次尝试“父节点/祖先在树中 → 父节点的后代在树中 → 祖先的后代在树中”三级查找最后用classify_source把每个源分类为Normal/Copy(path)/Rename(path)。is_ancestor函数则负责复制图的祖先判断。文档中“先找最短路径、已占用目标不再复用”的策略在代码中体现为按 parents 顺序每父至多取一个源代码中的 TODO 注释也保留了“当父节点自身有多个父时如何取最接近亲缘”的开放问题。4.2 例二最佳重命名目标的选取N copy baz-qux | M rename foo-baz | | L rename foo-bar |/ K add foodiff L→N 时所有文件都相关。由于bar在目标端不存在需要为它找重命名目标baz在图上比qux更近所以选baz。结果bar重命名为bazbar复制为qux4.3 例三向已删除文件的同一路径复制M copy foo-bar | L delete bar | K add foo, bardiff K→Mbar在两侧是不同的、无关的 copy ID。呈现两条记录bar被删除bar由foo复制而来。diff M→K反过来呈现bar被创建bar合并进foo。这一“同一目标路径拆成删除 复制两条记录”的行为在 lib/src/copies.rs 中有对应注释当 before/after copy ID 不同时会先发出一个“删除”条目再做复制追溯NOTE[deletion-diff-entry]文档承认这可能比旧 diff 流多出条目并标注“目前计划改进”。五、Merge先做“命名冲突阶段”再做内容合并文档规定合并时要在内容级合并之前增加一个复制处理阶段先根据输入树构建完全未解析的合并树查看各 diff 中变化的文件逐个查询其完整复制图沿复制图找出候选目标路径在合并的另一侧树中查找该路径——若路径存在且 copy ID 匹配则两文件相关由于 base 与冲突首项之间的差异可能非常大不查看那个 diff只要后端能按 copy ID 查询完整复制图就不需要该 diff 也能保证正确性找到所有参与合并的复制后进行分析找出冲突例如两侧把文件重命名到同一目标有冲突时树保持不变由用户通过jj resolve解决命名冲突文档还预留了“在提交上写标志位表示存在未解决命名冲突以便后续跳过该阶段”的优化合并树时先把每个 diff 改写成目标树中的路径名。例如树冲突为A(B-C)(D-E)时把(B-C)与(D-E)两个 diff 改写为A中的路径先从C到A计算重命名再把这组重命名同时应用到C和B这可能产生冲突。如果某文件的 copy ID 本身处于冲突状态那么物化时它表现为“不存在”因此在用户解决冲突前不会出现在工作副本中。文档给了两个 rebase 的具体例子说明“相关才传播、不相关不动”例 1K add foohello→L rename foo-bar另一侧M set foobye。把Mrebase 到L上时把foo-bar重命名应用到M及其父的树上。例 2K add fooK→L delete foo另一侧M create fooMN rename foo-bar。把Mrebase 到N上时虽然N里有foo-bar重命名但它与M中新建的foo假设使用了不同的 salt不相关因此不做任何重命名新foo文件在 rebase 后的M中直接创建与 rebase 前一样。5.1 决策点是否把修改传播到复制目标文档讨论了三条路线自动传播Mercurial 的行为Git 不做修改了foo再 rebase 到一个把foo复制为bar的提交时修改也应用到bar。在“文件被一分为二”的场景下特别有用——各修改在其中一个文件里成功应用在另一个文件里产生 modify/delete 冲突可以较容易地向删除侧解决。不传播则修改只会以 modify/delete 冲突形式出现在原文件里需要手工拷到复制文件。副作用是“rebase 走再 rebase 回来”不再恒等同一修改会应用到foo两次但同变规则下不构成冲突完全不传播询问用户在冲突提交中保持输入树的相关路径不变每次检查该提交时重做复制追踪由jj resolve用简单的 yes/no 逐个复制目标询问是否传播。文档的最终决策是询问用户“这避免了意外并让冲突代数在更多场景下成立”。对应的例子M fooM | | L copy foo-bar |/ K add fooK把Mrebase 到L时由于不自动传播M(L-K)树保持未解析。若用户不解决冲突而是把Lrebase 回K冲突会按常规冲突简化规则自动消失。5.2 多个复制目标四元冲突N fooN | | M fooM, foo2M2, foo3M3 | | | L copy foo-foo2, copy foo-foo3 |/ K add fooK把Mrebase 到N时foo、foo2、foo3的修改全部应用到foo上得到一个四元冲突。5.3 收敛性重命名复制图上的 merge$ jj log C rename bar-baz | | B rename foo-baz |/ A add foo, add bar $ jj new B Cbaz的复制图应同时继承foo与bar在复制图中产生一个 merge。各提交树Commit A: name: foo, id: aaa111, copy_id: 1:foo name: bar, id: aaa111, copy_id: 2:bar Commit B: name: bar, id: aaa111, copy_id: 2:bar name: baz, id: aaa111, copy_id: 3:baz-1:foo Commit C: name: foo, id: aaa111, copy_id: 1:foo name: baz, id: aaa111, copy_id: 4:baz-2:bar Merge commit: name: baz, id: aaa111, copy_id: 5:baz-{3:baz-1:foo,4:baz-2:bar}示例中foo与bar用了相同内容以便简化若内容不同内容会冲突但 copy ID 依然清晰。这正是第 3 章把 past names 做成 DAGparents: VecCopyId允许多父的直接原因。5.4 rebase 示例两次 rebase 回到原图$ jj log C rename bar-baz | B rename foo-bar | A add foo $ jj rebase -r C -o Arebase 后C变为rename foo-baz、与B分叉再执行jj rebase -r C -o B即回到原线性图——与第 2.3 节的无损往返不变式呼应。5.5 Mercurial 的经典难题重命名“新增文件”$ jj log C rename foo-bar | | B modify foo |/ A add foo $ jj squash --from C --into AMercurial 的问题在于squash C 进 A 之后新的 A 有文件bar但没有记录它曾叫foo。本文档的设计之所以能处理它是因为 squash 后保留了bar的 copy ID从而能检测到 B 对foo的修改应传播到bar。5.6 发散性重命名与 Git / Mercurial 的对比$ jj log C rename foo-baz | | B rename foo-bar |/ A add foo $ jj new B C普通三方树合并不考虑复制信息会得到一个无冲突的树但用户可能合理期待需要在bar与baz之间做出选择。文档引用了 Git 的输出$ git merge main CONFLICT (rename/rename): foo renamed to baz in HEAD and to bar in main. Automatic merge failed; fix conflicts and then commit the result. $ git st HEAD detached from ab0b8e3 You have unmerged paths. (fix conflicts and run git commit) (use git merge --abort to abort the merge) Unmerged paths: (use git add/rm file... as appropriate to mark resolution) added by them: bar added by us: baz both deleted: foo值得注意的是 Git 似乎通过索引中“正常冲突不会产生的状态”来代表这一情形。而 Mercurial 的输出$ hg merge main note: possible conflict - foo was renamed multiple times to: bar baz 1 files updated, 0 files merged, 0 files removed, 0 files unresolved (branch merge, dont forget to commit)Mercurial 没有地方记录这个状态只打印提示了事。本文档描述的模型与算法则会通过传播重命名在两个路径上产生 copy ID 冲突——冲突状态被显式记录进快照这正是 jj 与 Mercurial 的差别所在。5.7 一个未完成的测试用例文档保留了 jonathantanmy 的测试用例标注 TODO 待补充$ jj log E bazbaz (resolves conflict) | D conflict |\ C | rename bar-baz || | B rename foo-baz ||/ A add foofoo and barbar $ jj rebase -r E -o C $ jj new D E -m F约束是若 F 为空自动合并它应处于与 rebase 前的 E 相同的状态。六、Log 与 Annotate 的支持Log复制图包含文件的所有历史路径与 copy ID因此jj log filename可以翻译成一个类似files()的 revset只不过匹配的是特定的(path, copy ID) 对而非特定路径。Annotate设计文档中这一节标记为 TBD——即当时 blame 沿复制链追溯的具体算法尚未设计。仓库中已存在独立的 lib/src/annotate.rs 实现但从设计文档的口径看annotate 的复制感知能力仍属于待完善范围读者引用时应以当前实际行为为准。七、后端表示Git 后端与云端后端的落地难题7.1 Git 后端要不要、如何回填文档提出一系列开放问题与数据点是否要在 Git 后端里记录重命名若记录大概要像存储 change id 一样存到 Git 对象之外对于没有复制图记录的树用什么填充若直接按当前路径新建复制图调用方将永远发现不了任何复制是否在jj git init时做一遍全库重命名检测索引代价很高——文档给出了作者机器上的实测参照git log --summary --find-copies-harder在 git.git 仓库约165 秒在 Nixpkgs 仓库约13 小时备选克隆后在后台做复制索引。代价是复制信息要过一段时间才可见且实现更复杂同一内容但不同 FileId 的两棵树如何处理把附加数据挂在提交对象上——但工作副本状态指向的树并不来自提交此路不通还有一种思路Git 里完全不存复制信息把前述模型作为后端实现细节原生后端与 Google 后端使用Git 后端继续即时检测。但若想向用户展示“冲突的 copy ID”细节以便其决定如何解决就必须在抽象层面表达它。对照当前仓库如前所述lib/src/git_backend.rs 中read_copy/write_copy/get_related_copies目前全部返回Unsupported(The Git backend doesnt support tracked copies yet)——即文档所讨论的“Git 后端是否回填复制图”在当下仍未定案日常 diff 中的复制/重命名来自按区间计算CopyRecord的启发式路径get_copy_records走后端root..head区间查询见 lib/src/backend.rs 的接口注释与 cli/src/diff_util.rs 的实现。7.2 云端仓库如 Google 后端按 copy ID 查整图场景你有一个包含若干修改文件的提交现在要把它同步rebase到更新后的主干。如果你修改的某些文件在主干上已不存在要判断它们是否被重命名、从而把你的修改传播到新位置。方法是找出“自上次与主干同步以来 copy ID 发生变化的文件”——但如果主干上有 1000 万个新提交可能有数以万计这样的文件散布在全树计算非常昂贵因此必须让自定义后端帮上忙。由于只关心“rebase 的提交中变化过的文件”涉及的复制图后端只需提供一个方法给定一个或一组copy ID取回其完整复制图。流程是先找出 rebase 提交 diff 涉及的所有 copy ID再向后端查询完整复制图然后遍历复制图看是否有节点存在于目标树中。该方案的弱点是相关文件非常多时查询变贵——“实践中问题不大”。文档还提醒服务器可能只希望为 public/immutable 提交建索引否则用户可以创建大量复制故意或误操作来“污染”索引使之后对这些文件的所有查询都变贵。这段设计直接对应到了 lib/src/backend.rs 中get_related_copies的契约“返回指定 copy 历史的祖先及其全部后代允许多返回兄弟甚至无关的复制历史但属于浪费”以及 lib/src/copies.rs 中diffs_from_copies的真实调用链拿到 after 文件的copy_id→backend().get_related_copies(copy_id)→ 构建CopyGraph→ 祖先/后代分析 → 分类源。八、实现计划设计文档给出的粗略实现顺序原文编号即如此在测试后端中实现复制追踪支持实现 diff 算法并测试实现 merge 算法并测试实现 blame 算法并测试实现“文件跟随”的 log 算法并测试把部分查询提取到 commit backend trait让基于数据库索引的云端后端如 Google 后端可以提供自己的版本在 Git 后端中实现复制追踪可能涉及惰性的回填也可能需要在 commit backend trait 中引入新的抽象实现用于记录复制、以及解决复制冲突的 CLI。对照仓库现状第 3 步的 diff 算法已落地CopyHistoryDiffStream及配套的源查找/分类逻辑第 7 步的 trait 接口也已定义含BackendError::Unsupported逃生舱而第 8 步Git 后端与第 9 步CLI 侧的复制记录/冲突解决在当前代码中尚未完成这与 docs/changelog.md 等文档中“复制检测仍为启发式”的现状一致。九、被否决的替代方案9.1 即时检测Git 模型Git 不记录复制信息而是在比较两棵树时推断。难点在于超大仓库的可扩展性例如 rebase 本地提交到一个领先 100 万提交的上游时要找本地提交的文件是否在上游被复制过对比新旧基树极其昂贵。而本文档定义的查询 API以提交而非树为输入允许后端利用历史做索引后端可以基于本地提交中的文件建立索引判断其是否被复制而无需对比整树。9.2 树中记录逻辑文件标识BitKeeper 模型BitKeeper 为每个路径记录一个文件 ID标识逻辑文件。比较任意两棵树时找出新增与删除的文件比较它们的 ID 即可判断哪些是重命名。问题是该模型似乎无法扩展到复制只能表达重命名跨百万提交 rebase 时不想 diff 整棵树可能有百万个修改文件或许可以二分查找“删除了被 rebase 提交所改文件”的提交另一个难题是 Git 后端如何合成文件 ID——可以从根提交开始遍历并持久化索引。9.3 把复制信息并入 FileIdMercurial 模型Mercurial 把复制信息存在文件内容自身的元数据段里复制历史一变文件就拿到新的内容 ID——与本设计相当接近。差别在于 Mercurial 只记录最近一次复制的信息文件一旦修改就获得新文件 ID要找前一个名字必须遍历文件历史由于 Mercurial 在提交级修订 DAG 之外还有每文件的修订 DAG这通常不是大问题。文档脚注还指出Mercurial 从某个版本起也支持把复制信息存进提交而那恰好是下一种“快照/补丁混合模型”被论证为行不通。9.4 混合快照/补丁模型把复制信息存进提交团队认真考虑过把复制/重命名信息存在提交对象里但它对数据模型有显著影响没有复制信息时线性链 A..D 的总 diff 只需 diff D-A因为 (B-A)(C-B)(D-C) 可化简为 D-A而复制信息使总 diff 涉及复制信息若挂在单个提交上就必须做某种聚合从另一棵树 restore 不再只是拷贝那棵树还需要弄清新旧树之间的复制关系冲突状态由“要加要减的一组状态”表示与补丁式复制信息不兼容。作者团队花了很多时间尝试找到可行解结论是快照式冲突模型与补丁式复制信息模型无法调和因此不追踪冲突的复制信息例如foo→baz与bar→baz两个重命名之间的冲突由于复制记录是相对于自动合并后父提交的记录会依赖合并算法未来合并算法的改动可能使某些复制记录失效因此实现上不能假设复制源一定存在。他们还尝试过用下面的表示来表达冲突提交中的状态struct MergedTree { snapshot: Tree, diffs: Diff } struct Diff { before: Tree, after: Tree, /// Copies from before to after copies: VecCopyInfo, /// Copies from before to snapshot copies_to_snapshot: VecCopyInfo, } struct CopyInfo { source: RepoPathBuf, target: RepoPathBuf, // Maybe more fields here for e.g. do not propagate }它能算出结果树但做不了现有的冲突代数——这意味着“把提交并行化再串行化”之类的操作会丢失复制信息最终被放弃。十、小结设计要点与仓库中的验证路径这篇设计文档的核心贡献可以归纳为三点复制信息快照化把“过去的路径名”表示为CopyHistoryDAG 对象树条目只持CopyId从而与 jj 的一级冲突代数基于快照差兼容salt字段现已见诸 lib/src/backend.rs还额外支持“同路径文件被逻辑重写”的表达行为不变式驱动restore 压平传递复制、diff 在 restore 后为空、rebase/parallelize 无损往返、revert 为 no-op、命名冲突可解可回退——每一条都是可写测试的断言后端无关的查询契约以提交为输入、按 copy ID 取整图的get_related_copies接口让 Git即时检测与云端数据库后端索引查询都能接入且未就绪的后端可用BackendError::Unsupported平滑共存当前 Git 与 simple 后端正是如此见 lib/src/git_backend.rs、lib/src/simple_backend.rs。延伸阅读建议diff 算法细节读 lib/src/copies.rstrait 契约读 lib/src/backend.rs启发式检测的 CLI 侧入口读 cli/src/diff_util.rs 与 cli/src/commands/debug/copy_detection.rs冲突代数读 lib/src/merge.rs。【免费下载链接】jjA Git-compatible VCS that is both simple and powerful项目地址: https://gitcode.com/GitHub_Trending/jj/jj创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。