资讯动态

Jujutsu(jj)架构深度解析:数据模型、后端抽象与库/CLI 分层设计

发布时间:2026/9/10 13:07:27 来源:尧图企业网站定制
Jujutsujj架构深度解析数据模型、后端抽象与库/CLI 分层设计【免费下载链接】jjA Git-compatible VCS that is both simple and powerful项目地址: https://gitcode.com/GitHub_Trending/jj/jj导读本文基于仓库 docs/technical/architecture.md 展开全面解析 Jujutsujj这一 Git 兼容版本控制系统的内部架构。你将掌握 jj 的提交数据模型与 Git 对象模型的差异、jj-lib与jj-cli双 crate 分层设计的原则、五大存储后端commit / operation / op heads / index / working copy的抽象边界、以及GitBackend、SimpleBackend、StackedTable、revset 求值管线等核心组件的实现原理。读完本文你既能理解 jj 各目录与类型间的调用关系也能看懂其支持多用户服务器、可替换存储层的设计动机。数据模型与 Git 对象模型的异同Jujutsu 的提交数据模型与 Git 的对象模型 相似但有若干关键差异这些差异正是 jj 架构设计的出发点。在 Git 中提交commit是一个不可变对象由作者、提交者、树、父提交和提交信息组成任何重写amend、rebase都会生成一个全新的提交 ID且原提交从逻辑上消失。而 Jujutsu 的数据模型在CommitId之外引入了ChangeId这一稳定标识ChangeId会跟随一次变更的多次重写不会因提交被改写而变化。这一点在 lib/src/backend.rs 中有明确注释CommitId基于提交内容生成的标识符提交被重写后CommitId会变化ChangeId稳定标识符提交被重写后保持不变用于追踪同一变更的演进即 jj 的 evolution 机制基础。此外jj 的提交模型中还包含 Git 没有的前驱predecessors列表——记录当前提交由哪些旧提交演化而来——以及以MergeTreeId形式表达根树冲突的能力参见extract_root_tree_from_commit对jj:treesheader 的解析lib/src/git_backend.rs。树、文件、符号链接等对象同样以内容寻址方式存储但由后端的抽象接口承载而非像 Git 那样绑定单一格式。库与 UI 分离双 crate 架构jj二进制由两个 Rust crate 组成库 cratejj-lib对应仓库中的 lib/ 目录Cargo 包名在 lib/Cargo.tomlCLI cratejj-cli对应仓库中的 cli/ 目录Cargo 包名在 cli/Cargo.toml。目前库 crate 只被 CLI crate 使用但它的定位是可复用于 GUI、TUI甚至可运行在服务端为多用户提供请求服务。为此库层必须遵守两条硬性约束不直接与用户交互所有终端输入/输出都由 CLI crate 负责库层不得通过终端或其他方式与用户直接交互不读取用户级配置与环境变量因为库可能运行在服务器环境中不能依赖用户主目录下的配置或用户特定的环境变量如$HOME。唯一的例外是仓库格式自动升级期间打印的消息见原文档脚注。从源码结构看cli/src/ 中的命令实现大量通过UserSettings等对象将配置注入库层调用而 lib/src/settings.rs 等模块只接收显式传入的设置印证了这一边界。库 crate 的 API 易用性经过了大量设计投入但作者坦诚细节层面如使用哪种集合类型、API 暴露哪些符号尚未投入太多精力——这暗示了库 API 仍处于演进期。存储无关的 API五大后端抽象架构中一个贯穿始终的原则是数据存在哪里应当易于更换。设计目标是在本地磁盘存储默认与云端存储如 Google 内部场景也对所有人开放之间自由切换。为此系统将存储职责拆分为五类每类有独立后端与独立的接口存储内容后端类型类型文件.jj/下提交commits、树trees、文件files等对象commit backend.jj/repo/store/type操作operations与视图viewsoperation backend.jj/repo/op_store/type操作日志头部op headsop heads backend.jj/repo/op_heads/type提交索引commit indexindex backend.jj/repo/index/type工作副本working copyworking copy backend—尚无独立 trait 文件接口全部以普通 Rust 数据类型定义不绑定任何特定格式加载仓库时具体使用哪个后端由.jj/repo/下的type文件指定。工作副本目前还没有自己的 trait但其接口很小将来需要时很容易为其创建 trait见 lib/src/working_copy.rs 中WorkingCopy相关定义。这一抽象直接体现在仓库目录结构中lib/src/下存在 backend.rs、op_store.rs、op_heads_store.rs、index.rs、working_copy.rs 五个核心接口模块以及对应的默认实现。库 crate 设计核心类型与关系图库 crate 中若干重要类型及其关系可概括为给定一个Workspace可以获取WorkingCopy或RepoLoaderTransaction是获取MutableRepo的必要途径。仓库中的 types.svg 给出了完整的类型关系图。下面逐节讲解每个组件。Backend提交后端接口Backendtrait 定义了每个提交后端必须实现的接口涉及提交、树、文件等对象的最低层读写。仓库内目前有两个提交后端实现GitBackend把提交存储在 Git 仓库中见下节SimpleBackend概念验证实现见后文。由于还存在非提交类后端op store、op heads store、index 等原文档指出Backendtrait 或许应更名为CommitBackend更贴切——从 lib/src/backend.rs 的模块注释看它确实是最低层的读写提交、树、文件等对象的 trait。GitBackend基于 Git 仓库的提交后端GitBackend把提交存储在 Git 仓库中通过gitoxide纯 Rust 的 Git 实现读写提交与引用这也是 jj 不与 libgit2 绑定的原因。它需要解决两个 Git 模型与 jj 模型的差异问题1. 防止 Git GC 误删操作日志可达的提交操作日志中可达的每个提交GitBackend都会在refs/jj/keep/命名空间下保存一个引用防止 Git 的垃圾回收删除这些提交。该常量定义在 lib/src/git_backend.rs/// Ref namespace used only for preventing GC. const NO_GC_REF_NAMESPACE: str refs/jj/keep/;对应的to_no_gc_ref_update()会创建refs/jj/keep/{commit_id}引用recreate_no_gc_refs()则为新 head 重建并清理不可达的非 head 引用。lib/src/git_backend.rs的测试约第 2401-2457 行验证了写提交后生成refs/jj/keep引用GC 删除引用后重新导入会重建引用的行为。2. 存储 jj 模型独有、Git 模型没有的数据Git 模型无法表达 change ID 与前驱列表这些数据存放在.jj/repo/store/extra/下的StackedTable中。对于在该表中没有任何数据的提交即任何由git创建的提交前驱列表使用空列表change ID 使用比特反转bit-reversed的 commit ID。synthetic_change_id_from_git_commit_id()lib/src/git_backend.rs对此有清晰注释取 commit ID 的最后 16 字节并逐字节反转比特位生成 change ID。这样做的原因是避免哈希前缀在 commit ID 与 change ID 之间产生歧义同时降低用户依赖两者关系的可能性算法本身被标注为不应被依赖。3. 相同内容冲突时报错由于 commit ID 采用 Git Object ID两个仅在 change ID 上有差异的提交会得到相同的 commit ID因此写入第二个这样的提交时会直接报错。从源码看jj 会把 change ID 写入 Git commit 的jj:change-idheaderextract_change_id_from_commit负责回读lib/src/git_backend.rs从而在多数情况下避免冲突。SimpleBackend概念验证实现SimpleBackend仅是概念验证proof of concept对象按其哈希寻址每个对象一个文件。它证明了存储无关 API的可行性——换一个提交后端整个系统的其余部分无需改动。Store对 Backend 的包装与缓存Store类型包装了Backend并返回包装后的提交、树类型以方便使用。关键在于包装后的对象持有指向Store自身的引用因此可以写出commit.parents()这样的调用而无需把Store作为参数层层传递。Store还提供提交与树的缓存lib/src/store.rs 中可见commit_cache: MutexCLruCacheCommitId, Arcbackend::Commit字段。ReadonlyRepo某次操作下的仓库快照ReadonlyRepo表示仓库在特定操作operation时点的状态并保存与该操作关联的 view 对象lib/src/repo.rs。注意仓库并不知道各工作副本在磁盘上的位置它只通过 view 对象知道每个工作区应该对应哪个工作副本提交。MutableRepo可变的仓库MutableRepo是ReadonlyRepo的可变版本它持有对基础ReadonlyRepo的引用但拥有自己的 view 对象副本允许调用方修改lib/src/repo.rs 中可见base_repo: ArcReadonlyRepo与view: View字段。Transaction事务与操作日志写入Transaction对象包含一个MutableRepo和将要写入操作日志的元数据lib/src/transaction.rs。当事务提交时MutableRepo变为磁盘操作日志中的 view 对象Transaction对象变为操作对象operation在内存中Transaction::commit()返回一个新的ReadonlyRepo。这就是每次jj命令都会在操作日志中追加一条操作这一用户可观察行为的底层机制。RepoLoader指向.jj/repo/的指针RepoLoader表示仓库处于未指定操作的状态可视为指向.jj/repo/目录的指针给定一个操作 ID它可以创建对应的ReadonlyRepolib/src/repo.rs。TreeState工作副本文件状态TreeState表示工作副本中文件的状态为每个被追踪文件记录mtime 与 size并知道自己所代表的TreeId。它提供两个核心方法snapshot()利用记录的 mtime/size 检测工作副本变化若有变化则返回新的TreeIdcheckout()按请求的TreeId更新磁盘上的文件。TreeState支持稀疏检出sparse checkout。事实上所有工作副本都是稀疏的——大多数情况下只是恰好追踪整个仓库而已lib/src/local_working_copy.rs 附近的TreeState定义与此一致sparse 模式的用户配置见 docs/sparse-v2.md 设计文档。WorkingCopyTreeState 工作区元信息WorkingCopy除持有TreeState外还知道自己的工作区名称WorkspaceName以及最近一次更新发生在哪个操作上。它决定工作副本的实际状态与 view 中记录的期望状态相对应。Workspace仓库 工作副本的组合Workspace表示一个仓库与一个工作副本的组合类似 Git 的worktree概念lib/src/workspace.rs。当前操作下的仓库 view 决定每个工作区期望的工作副本提交WorkingCopy决定工作副本实际的内容。如果工作副本提交在另一个工作区被更改或更新进程崩溃工作副本就会变得 stale过期。Git 模块高层 Git 互操作git模块lib/src/git.rs提供比GitBackend更高层的 Git 仓库互操作能力。GitBackend受Backendtrait 约束只能做对象级操作而git模块专门服务于 Git 后端仓库提供从 Git 仓库导入 refs向 Git 仓库导出 refs与 Git 远程仓库进行 push / pull。Revsets从字符串到 Revset 的五阶段求值用户提供的 revset 表达式字符串要经过以下阶段才能完成求值详见 docs/technical/revset-evaluation.md解析把表达式解析为RevsetExpression接近 AST此阶段同时展开别名符号解析把tags()等符号与函数解析为具体提交此阶段后表达式仍是RevsetExpression但不再包含任何CommitRef变体优化优化表达式以便更高效地求值可见性解析解析visible_heads()与all()产出ResolvedExpression求值把ResolvedExpression求值为Revset。第 5 步由Index::evaluate_revset()执行接口见 lib/src/index.rs从而允许Revset实现利用特定索引实现的优势前四步则与索引实现无关。这是索引后端可替换性在 revset 引擎上的直接体现。StackedTable自定义持久化键值格式StackedTable实际为ReadonlyTable与MutableTable见 lib/src/stacked_table.rs是一种简单的磁盘格式用于存储按键排序的键值对。键必须等长值可以变长。选择自研格式是因为希望获得无锁并发参见 docs/technical/concurrency.md而现有键值存储无法满足该需求。文件格式查找表lookup table后跟拼接的值。查找表是按序排列的键列表每个键后跟其在拼接值中的偏移量。父表链与合并策略一个表可以有父表查找键时若当前表没有则去父表查找。表从不就地更新若新增条目数少于父表条目数的一半则创建新表并指向父表否则将父表条目与新条目一起拷贝进新表并以祖父表作为新表的父表该策略递归执行保证父表至少是子表大小的 2 倍。因此插入与查找的摊还复杂度均为O(log N)。目前还没有对不可达表的垃圾回收。表以哈希命名并像操作日志那样用一个独立目录保存指向当前叶表的指针参见 docs/technical/concurrency.md#storage。CLI crate 设计模板Templates模板概念借鉴自 Mercurial但语法不同。核心区别是顶层表达式就是模板表达式而不是 Mercurial 那样的字符串同时不存在字符串插值例如 Mercurial 中的Commit ID: {node}写法在 jj 中不可用。模板的解析与构建实现在 cli/src/template_parser.rs、cli/src/template_builder.rs 中默认模板配置见 cli/src/config/templates.toml。差异编辑Diff-editingjj diffedit的工作方式很有特色它创建两个极其稀疏的工作副本只包含用户需要编辑的文件让用户编辑 diff 的右侧新版本然后简单地对该工作副本做快照以生成新树。对应命令实现位于 cli/src/commands/diffedit.rs。这正体现了前面TreeState的snapshot()能力——把编辑工作副本文件这一朴素操作直接升级为提交内容重写。架构脉络小结从整体看jj 的架构围绕三条主线展开数据模型差异驱动后端抽象ChangeId、前驱列表、冲突树等 Git 没有的概念决定了必须把存储抽象为接口而非直接依赖 Git 仓库格式库/CLI 严格分层jj-lib面向多前端CLI、GUI、TUI、服务器因此不碰终端、不读用户配置jj-cli负责所有交互与输入输出可替换性贯穿全局commit / operation / op heads / index / working copy 五大后端各有type文件指定实现StackedTable与自定义索引则为无锁并发与多用户服务器场景铺路。想要继续深入可依次阅读类型关系图 docs/technical/types.svg、操作日志与无锁并发设计 docs/technical/concurrency.md、revset 求值细节 docs/technical/revset-evaluation.md、冲突模型 docs/technical/conflicts.md以及核心接口源码 lib/src/backend.rs、lib/src/op_store.rs、lib/src/index.rs。【免费下载链接】jjA Git-compatible VCS that is both simple and powerful项目地址: https://gitcode.com/GitHub_Trending/jj/jj创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价