Jujutsu 是一个新的版本控制系统,它看起来非常不错!
最初尝试使用时,我被其文档劝退了,因为在我理解整体概念之前,它提供了过多的细节。其他人可能也有类似的体验,并撰写了替代教程,但这些教程风格散漫,过于专注于命令,对我来说也不够友好。
我怀疑,就像编写单子教程一样,理解的路径实际上是将其写下来。因此,我尝试编写一个入门/教程。
也许与其他教程不同,我的目标是使其足够高级,以便阅读和思考,而不会提供太多细节以至于让人难以理解。这里无需记忆命令,它们只是用来传达想法。如果最后您有兴趣尝试,我建议参考其网站上的文档。
**概述**
忽略细节,您可以将 Jujutsu(以下简称“jj”)视为一个新的 Git 前端。底层数据仍然存储在 Git 中。不同之处在于您在本地与文件交互的方式,它采用了不同的概念模型和一组不同的命令。
Git 问答:提交是文件状态的快照还是差异?技术答案很微妙——作为用户,您通常将它们视为差异,而在概念上它们是快照,但在具体实现上则存储为增量。更有用的答案是,思考细节会模糊概念模型。
类似地,用 Git 中发生的事情来描述 jj 很有诱惑力,但我认为最终会使解释变得模糊。
在实践中,这意味着尝试搁置您对 Git 的了解,但也请注意,您可以使用 jj 并继续与更大的 Git 生态系统交互,包括例如推送到 GitHub。
**核心思想:一切皆为提交**
版本控制系统的目的是跟踪代码的历史记录。但有趣的是,在大多数系统中,一旦您在本地工作副本中编辑文件,该新的历史记录(“我已从版本 Y 开始编辑文件 X”)就会处于系统外部的某种中间状态,并单独管理。
这非常普遍,以至于几乎很难察觉。但请考虑像 git diff 这样的命令,它有一种模式需要两个提交来进行比较,然后是一堆其他模式和标志来操作它跟踪的其他各种事物。您可以获取与工作副本的差异,但无法在 diff 命令中命名“工作副本”。(特别是 Git 还增加了索引的非提交状态,并附带了更多种类的命令。终极的 Git 问答:git reset 的不同软/硬/混合行为是什么?)
另一个例子:考虑如果您有一个工作副本更改,并且想要检出其他代码,您要么必须将其放在新位置(git stash,一个与其他位置分开的第四个位置),要么进行临时提交。或者,如果您有一个想要移植到其他地方的工作副本更改,您会使用 git checkout -m,但要移动已提交的更改,则使用 git rebase。
相反,在 jj 中,您的工作副本状态始终是一个提交。进行新的更改时,这将是一个新的(无描述)提交。您在磁盘上进行的任何编辑都会立即反映在当前提交中。
如此多的东西都源于这个简单的决定!
* jj diff 显示提交的差异。如果没有参数,则为当前提交的差异,即当前工作副本的差异;否则,您可以指定要查看的历史提交。许多其他 jj 命令也具有类似的令人愉悦的对称行为。
* 在您完成工作之前,您可以为正在进行的提交起草提交消息,使用与编辑任何其他提交消息相同的命令。没有最终的 jj commit 命令,提交是隐式的。(相反,您在完成后使用 jj new 创建一个新的空提交。)
* 您无需“存储”当前工作以执行其他操作,它已存储在当前提交中,并且易于返回。
* 在 Git 中,要修复旧提交中的错别字,您可能会创建一个新的提交,然后使用 git rebase -i 将补丁移动到适当位置。在 jj 中,您可以直接检出旧提交(因为工作副本 == 提交),并在没有其他命令的情况下编辑文件。
(这篇博客文章并排展示了使用 Git 和 jj 进行类似的实际操作。)
从 Git 的角度来看,jj 非常“rebasey”。编辑文件就像 git commit --amend,而在上面的“修复错别字”操作中,编辑会隐式地重新整理任何下游提交。为了使该功能正常工作,在冲突处理和分支方面还有一些其他的概念性飞跃,这些将在介绍基础知识后进行说明。
**基本工作流程**
在 Git 仓库中:
```bash
$ jj git init --colocate
```
这会创建一个 .jj 目录,它与 Git 仓库一起工作。Git 命令仍然可以工作,但可能会令人困惑。
普通的 jj 命令运行 jj log,显示最近的提交。这是来自此博客仓库的输出:
```
$ jj
@ zyqszntn [email protected] 2024-12-12 11:58:52 21b06db8
│ (no description set)
○ pmnzyyru [email protected] 2024-12-12 11:58:48 86355427
│ unfinished drafts
◆ szzpmvlz [email protected] 2024-09-18 09:08:15 fcb1507d
│ syscalls
~
```
最左边的字母字符串是“更改 ID”,它是您在 diff 等命令中用来引用更改的标识符。它们在编辑过程中保持稳定,与右侧的 Git 哈希不同。在终端中,更改 ID 会使用颜色进行显示,以显示在命令中唯一引用它们所需的必要前缀(单个字母)。
最顶层的提交 zyqszntn 是当前提交,包含我在撰写此博客文章时的内容。正如预期的那样,如果我运行 jj status,它会显示我编辑的文件列表,如果我运行 jj diff,它会显示差异。
我现在可以为其提供描述,或者在完成后再提供:
```bash
$ jj desc -m 'post about jujutsu'
```
然后为下一个更改创建一个新的提交:
```bash
$ jj new
```
**迭代更改**
这足以处理简单的更改,但通常我处理更重要的更改,我可能会在几天内丢失上下文。您可以根据自己的工作方式选择两种方法。
第一种方法是像上面那样描述您的更改并继续编辑它,而无需运行 jj new。每次后续编辑都会更新更改。这操作起来很简单,但意味着 jj diff 将始终显示整个差异。在 Git 中,这类似于在工作副本中保留大量编辑。
另一种选择称为教程书中的“squash 工作流程”。在这种情况下,当您进行新的工作时,您使用 jj new 从现有工作创建新的独立提交,当您对它满意时(例如,通过检查 jj diff,它只显示工作副本的新更改),您运行 jj squash 将这些新更改刷新到之前的提交中。对我而言,这感觉非常类似于使用 Git 索引作为复杂更改的暂存区域,或者可能重复使用 git commit --amend。
**移动和编辑历史记录**
这些命令(如 jj diff 和 jj desc)作用于当前提交(或通过 -r 标志显式请求的任何提交)。
要将工作副本切换到现有更改,请使用 jj edit <changeid>。
同样,您在此处对文件或描述进行的任何更改,或者通过进行新的更改并将其压缩,都会直接作用于您正在编辑的历史提交。我重复这一点,因为它在回顾时既奇怪又显而易见。
**冲突**
对历史记录的任何操作都会导致隐式重新整理,这些操作会静默发生。重新整理可能会发生冲突。jj 对此有有趣的处理方式。
在 Git 中,重新整理的解决通过您的工作副本发生,因此在“正在进行的重新整理”和 git rebase --continue 方面又存在额外的状态。相反,在 jj 中,冲突的提交仅记录为冲突,并在历史记录中标记为冲突,因此即使重新整理产生一系列冲突的提交,它们也会始终“成功”。
如果您要修复冲突提交(通过上述 jj edit),则照常编辑文件,一旦冲突标记被移除,它就不再被视为冲突。
照常,一旦您进行历史记录编辑,下游更改将再次重新整理,可能在您编辑后解决其冲突状态。同样,“所有相关信息都建模在提交中”,而无需使用具有状态等的单独重新整理模式,这是 jj 反复出现的强大主题。
我对此还没有太多经验,所以我无法评论它运行的效果如何,除了我遇到它的几次让我感到惊喜之外。jj 文档似乎对这里的建模和行为感到自豪,这让我觉得它可能很复杂。
**分支**
jj 没有命名分支,而只是跟踪提交。由于 jj 处理提交的方式,在历史记录中的任意点开始添加提交非常容易,因此分支名称没有那么有用。根据我的经验,到目前为止,拥有有用的提交描述足以跟踪我的工作内容。来自 Git 的用户会对缺少命名分支感到惊讶,但我相信这对 Mercurial 用户来说很舒适,Monotone 也类似地工作(我认为?)。
值得强调的是缺少分支,因为特别是在与 Git 交互时,您仍然需要分支,即使只是为了推送。jj 支持这一点(其中“书签”是指向特定提交的指针),但感觉有点笨拙。另一方面,我对 git push 语法可能患上了斯德哥尔摩综合症。
**缺少的功能:VSCode**
使用 jj 使我意识到我有多依赖 VSCode 的 Git 支持,无论是查看差异还是合并。
在 jj 中编辑给定提交时,Git 认为提交中的所有文件都在工作树中,而不是索引中。换句话说,在 VSCode UI 中,当前差异显示为挂起的更改,就像在 Git 中一样。这运行得很好,这正是我所期望的。我还没有触碰与 Git 索引交互的按钮,因为害怕 jj 会如何处理它。
由于我不太了解的技术原因——可能是 VSCode 仅执行三方文件合并,而 jj 需要三方目录合并?——两者在解决冲突方面并未完全配合。jj 文档推荐 meld,我过去也使用过 meld,但我并没有意识到 VSCode 如何让我上瘾,直到我错过使用它进行合并。
**未来**
jj 的作者在 Google 工作,并且可能正在为 Google 内部版本控制系统开发它。(上面我写道 jj 是一个 Git 前端,但官方它具有可插入的后端;我可能永远不会看到非 Git 的后端。)
当我在三年前离开 Google 时,我记得他们当时正在尝试确定如何解决 Git 的扩展性问题,或者采用 Mercurial,或者其他什么。我记得与该领域的人员交谈过,并认为“现实地讲,您的用户必须使用 Git 与外部世界协作,因此您所做的任何其他事情都是纯粹的成本”。我发现这篇来自 Mercurial 粉丝关于 jj 的帖子很有趣,因为它谈到了它修复的 Mercurial 的缺点。从这个角度来看,它非常有趣:它可以取代您目前使用 Git 的地方,同时还提供更优秀的 UI。
总而言之,jj 看起来非常完善,已经存在多年了,并且如果出现问题,有一个非常简单的退出策略——只需退出到 Git 仓库即可。我打算继续使用它。
附注:每次我读到“jujutsu”这个名字时,我总是认为它是“jiu-jitsu”(武术)的拼写错误。但日语单词是 じゅうじゅつ,实际上是 jiu-jitsu 拼写错误!
阅读一篇关于它的更长文章。