在命令行工具的设计领域,「功能丰富」与「易于上手」往往被视为难以兼得的取舍。Git 作为版本控制领域的标准工具,其强大的功能背后是陡峭的学习曲线和大量需要记忆的子命令与参数。然而,由 Rust 编写的 Jujutsu(以下简称 jj)尝试打破这一惯性思维 —— 它不仅在功能上比 Git 更加强大,同时在接口设计上追求更高的表达力和一致性。这种设计理念的核心,正是 Revsets 查询语言与 Templates 渲染机制的有机结合。
从「命令堆砌」到「小型语言」
传统的 CLI 工具通常采用「动词加名词」的模式,例如 git commit -m "message" 或 git log --oneline --graph。这种模式的优势在于直观易懂,但随着功能增加,命令和选项的数量会急剧膨胀,用户不得不依赖搜索引擎或 man 手册来回忆特定操作的语法。jj 的设计者选择了一条不同的路径:将 CLI 视为一种小型编程语言来构建,而非简单的命令集合。
jj 的核心设计哲学可以概括为两点:其一是「选择」(Selection),即如何精确指定你想要操作的提交;其二是「呈现」(Presentation),即如何将查询结果以你期望的方式展示。这两个维度分别由 Revsets 和 Templates 两个子系统来实现,它们各自独立又相互配合,共同构成了 jj 区别于其他版本控制工具的独特接口层。
Revsets:面向提交的选择器语言
Revsets 是 jj 中用于描述「哪些提交」的查询表达式。你可以将其理解为一种专门针对版本历史的功能性查询语言,它允许用户使用符号、运算符和函数来构造精确的提交集合。与传统 CLI 中通过通配符或多个过滤参数不同,Revsets 在语法层面就支持组合和嵌套,这使得复杂的历史查询变得可读且可复用。
在实际使用中,Revsets 的表达力体现在多个层面。首先是基本符号的丰富性:@ 代表当前工作空间提交的父提交,@-| 表示从当前提交出发沿第一条父链回溯的所有提交,而 @+ 则指向正在操作中的所有非空提交。这些基础符号构成了构建更复杂查询的积木。其次是内置函数的强大能力:函数如 ancestors(...) 返回给定提交的所有祖先提交,heads(...) 返回一组提交中最新的那些,而 mutable() 则专门指向可被安全修改的「可变」提交 —— 这与 jj 底层 immutable commits 的设计理念紧密相连。
运算符的支持进一步释放了 Revsets 的灵活性。你可以使用 + 计算两个提交集合的并集,用 - 计算差集,用 :: 表示祖先链关系。例如,heads(all()) 可以找出所有分支的最新提交,而 ancestors(@..@-|) 则可以定位当前提交与某个历史提交之间的所有节点。更为强大的是,Revsets 支持在配置文件中定义别名,这意味着团队可以共享常用的查询模式,例如将 mine() 映射为 author(_current_user_) & visible_heads(),从而在日常工作中使用更具语义的命令。
这种设计带来的直接好处是:用户不再需要记住数十个独立的过滤选项,而是学习一套统一的查询语法,然后在不同的命令中复用这一能力。jj log、jj diff、jj rebase 等命令都接受 Revsets 作为输入,真正实现了「一次学习,多处受益」。
Templates:可编程的输出渲染
如果说 Revsets 解决了「操作什么」的问题,那么 Templates 则负责解决「如何展示」的问题。jj 的模板系统同样借鉴了编程语言的设计思路:它是一种类型化的函数式模板语言,允许用户定义格式化规则,将查询结果转化为可读的文本输出。
Templates 的核心要素包括关键字、函数和方法。关键字提供对提交元数据的直接访问,例如 commit_id 获取提交哈希,description 获取提交信息,author 获取作者信息。函数则提供了更高级的转换能力,例如 format_datetime() 格式化时间戳,indent() 为多行文本添加缩进,if_() 实现条件渲染。方法则允许在特定对象上调用操作,例如 tags() 返回与提交关联的标签列表。
在实际工程实践中,Templates 的价值体现在多个场景。默认的 jj log 输出已经经过精心设计,展示了关键的提交信息;但当需要自定义输出时,用户可以通过 -T 或 --template 参数指定模板。例如,定义一个简洁显示提交哈希前七位和描述的模板:format_id(self.commit_id_short) ++ " " ++ self.description_first_line。更进一步的,可以将常用的模板保存为命名模板,在配置文件中声明,然后在命令行中通过名称引用,实现输出样式的标准化。
Templates 的函数式特性也使得复杂的输出逻辑变得可控。你可以使用 if_() 进行条件渲染,使用 try_() 处理可能的空值,使用 map() 对集合进行迭代。这些构造在编程语言中耳熟能详,被引入到 CLI 工具的模板系统中,让熟悉编程的用户能够快速上手,同时也为构建自动化脚本提供了坚实基础。
工程实践中的接口设计启示
jj 的 CLI 设计对其他命令行工具开发者具有重要的参考价值。首先,将查询语言与呈现语言分离是一个值得借鉴的模式。传统工具往往将这两个维度混在一起,通过不断增加参数来实现过滤和格式化的需求;而 jj 通过 Revsets 和 Templates 的清晰划分,让用户可以用一致的思路处理「选什么」和「怎么显示」这两个独立问题。
其次,默认值的设计同样体现了深思熟虑。jj 的 jj log 默认展示的是完整的提交历史而非仅当前提交,这一选择反映了工具设计者对「有用优先于简洁」的坚持。用户可以随时通过 Revsets 缩小范围,但不需要从零开始学习如何获取完整信息。这种「慷慨的默认」策略有助于降低新用户的入门门槛。
第三,配置与代码的边界被合理地模糊。jj 支持在配置文件中定义 Revsets 别名和命名模板,这意味着用户可以将自己的领域知识编码为可重用的工具。这种「可扩展性内置于核心」的设计思路,让工具在保持一致接口的同时,能够适应不同团队和项目的个性化需求。
最后,jj 与 Git 的兼容性是其工程可行性的关键保障。用户可以独立采用 jj 进行日常开发,而无需强制整个团队迁移。这种「渐进式采纳」的策略,为新工具的引入提供了安全着陆点,也是在企业环境中推广创新工具的现实路径。
资料来源
Steve Klabnik 在其 Jujutsu 教程中详细阐述了 jj 的设计理念与使用方法,该教程可在 steveklabnik.github.io/jujutsu-tutorial 获取。Jujutsu 官方文档提供了完整的 CLI 参考、Revsets 语法说明及模板语言规范,详见 jj-vcs.dev 官方站点。