https://steveklabnik.github.io/jujutsu-tutorial/

hello world

首先要说明的是:与 git 不同, jj 没有暂存区。每次运行 jj 命令时,它都会检查工作副本(磁盘上的文件)并生成快照。这里它已检测到我们 A 添加了新文件。你还会看到 M 被修改的文件,以及 D 被删除的文件。

jj
Working copy : qzmzpxyl bc915fcd (no description set)
Parent commit: zzzzzzzz 00000000 (empty) (no description set)

首要的一点是,每个仓库始终包含一个 zzzzzzzz 00000000 变更,且它始终为空。这被称为“根提交”,是整个仓库的基础。由于它是空的, jj 在其之上创建了第二个变更,本例中即 qzmzpxyl ,它正在追踪工作副本的内容。由于它非空,其行末不像根变更那样带有 (empty) 标记。

随时都可以用 jj describe 来描述我们的更改。最简单的使用方式是通过 -m (即 “message” 标志),这允许我们在命令行中直接传递描述:

➜  hello-world jj desc -m "hahaha"
Working copy  (@) now at: xqotnkml 8d49c669 hahaha
Parent commit (@-)      : zzzzzzzz 00000000 (empty) (no description set)

如此配置编辑器

export EDITOR="code -w"

这么新建一个变更

➜  hello-world jj
Hint: Use `jj -h` for a list of available commands.
Run `jj config set --user ui.default-command log` to disable this message.
@  xqotnkml multya77@gmail.com 2025-05-07 23:05:22 0a765773
│  hahaha
◆  zzzzzzzz root() 00000000
➜  hello-world jj st  
Working copy changes:
A .gitignore
A Cargo.lock
A Cargo.toml
A src/main.rs
Working copy  (@) : xqotnkml 0a765773 hahaha
Parent commit (@-): zzzzzzzz 00000000 (empty) (no description set)
➜  hello-world jj new
Working copy  (@) now at: qmwymuwt c6573838 (empty) (no description set)
Parent commit (@-)      : xqotnkml 0a765773 hahaha
➜  hello-world jj st 
The working copy has no changes.
Working copy  (@) : qmwymuwt c6573838 (empty) (no description set)
Parent commit (@-): xqotnkml 0a765773 hahaha
➜  hello-world jj                 
Hint: Use `jj -h` for a list of available commands.
Run `jj config set --user ui.default-command log` to disable this message.
@  qmwymuwt multya77@gmail.com 2025-05-07 23:11:26 24b41aa5
│  (empty) oh yes
○  xqotnkml multya77@gmail.com 2025-05-07 23:05:22 0a765773
│  hahaha
◆  zzzzzzzz root() 00000000

接下来我们看到变更 ID。最右侧是提交 ID。因为以这种方式查看时,我们几乎不关心提交 ID:我们关注的是稳定的变更标识符序列。两者之间是作者和时间,下方一行则是描述说明。

在最底部,我们有根提交(root commit),但不同于常规的作者和时间信息,这里显示的是 root() 。这是一个修订集表达式(revset),我们稍后会深入探讨这个功能。简而言之: jj 拥有极其灵活的方式来筛选修订列表。 root() 是该语言中的一个函数(没错,它支持函数),用于返回根提交。

So here is our current workflow: 这是我们当前的工作流程:

  1. Create new repositories with jj git init. 使用 jj git init 创建新仓库。
  2. To start working on a new change, use jj new. 要开始进行新变更,请使用 jj new 。
  3. To describe a change so humans can understand them, use jj describe. 为了让人类理解变更内容,使用 jj describe 进行描述。
  4. We can look at our work with jj st. 我们可以通过 jj st 查看工作内容。
  5. When we’re done, we can start our next change with jj new. 完成后,可以用 jj new 开启下一项变更。

Finally, we can review our repository’s contents with jj log. 最后,我们可以通过 jj log 审查仓库内容。

在 git 中,我们通过提交完成代码变更集,而在 jj 中,我们通过创建变更来开启新工作,再修改代码。相比事后编写提交信息,先写出预期变更的初始描述,再在工作中逐步完善,这种方式更为实用。

其实应该也可以回过头来再修改的,用 desc 就可以了

real-world workflow

squash

The workflow goes like this: 该工作流的具体步骤如下:

  1. We describe the work we want to do. 首先描述我们要完成的工作内容。
  2. We create a new empty change on top of that one. 然后在其上方创建一个新的空白变更集。
  3. As we produce work we want to put into our change, we use jj squash to move changes from @ into the change where we described what to do. 当我们产出需要纳入变更的工作成果时,使用 jj squash 命令将 @ 中的变更移动到事先描述任务的变更集中。

In some senses, this workflow is like using the git index, where we have our list of current changes (in @), and we pull the ones we want into our commit (like git add). 从某种角度看,这个工作流类似于使用 git 索引功能——我们维护当前变更列表(存放在 @ ),然后将需要的变更拉取到提交中(如同 git add 的操作)。

➜  hello-world jj new
Working copy  (@) now at: uluzqzkr 20dc5641 (empty) (no description set)
Parent commit (@-)      : qmwymuwt 24b41aa5 (empty) oh yes
➜  hello-world jj    
Hint: Use `jj -h` for a list of available commands.
Run `jj config set --user ui.default-command log` to disable this message.
@  uluzqzkr multya77@gmail.com 2025-05-07 23:24:10 20dc5641
│  (empty) (no description set)
○  qmwymuwt multya77@gmail.com 2025-05-07 23:11:26 24b41aa5
│  (empty) oh yes
○  xqotnkml multya77@gmail.com 2025-05-07 23:05:22 0a765773
│  hahaha
◆  zzzzzzzz root() 00000000
➜  hello-world jj describe -m "print goodbye as well as hello"
Working copy  (@) now at: uluzqzkr f741aef8 (empty) print goodbye as well as hello
Parent commit (@-)      : qmwymuwt 24b41aa5 (empty) oh yes
➜  hello-world jj new                                         
Working copy  (@) now at: ptltnzsu 869633c3 (empty) (no description set)
Parent commit (@-)      : uluzqzkr f741aef8 (empty) print goodbye as well as hello

如此先创建一个有目标的更改,然后再创建一个空白的目标更改

这里修改代码

jj st 
Working copy changes:
M src/main.rs
Working copy  (@) : ptltnzsu 4ff064e1 (no description set)
Parent commit (@-): uluzqzkr f741aef8 (empty) print goodbye as well as hello

现在情况比之前更奇特:当前变更含有内容,而其父级却是空的!让我们修正这一点。需要将 ” 暂存区 ” 的变更迁移至提交(变更)中,可通过 jj squash 实现:

➜  hello-world jj squash 
Working copy  (@) now at: kvvvpkly 425b7f79 (empty) (no description set)
Parent commit (@-)      : uluzqzkr d0a0878f print goodbye as well as hello

大量变更在此! @ 现在已清空且无描述,父级也不再为空。所有变更现已存在于 ywnkulko 中。

我们所做的类似于 git commit -a --amend 的操作。但如果想要更聚焦的修改呢?比如只想添加特定文件如 git add <file> && git commit --amend ,可以将其作为参数传入。由于之前只有一个文件,之前的命令实际上等同于

$ jj squash src/main.rs

但我们也能实现类似 git add -p && git commit --amend 的效果,只将文件的部分改动加入提交。这绝对会让你大开眼界。

输入

jj squash -i

会进入一个 TUI

空格选中,f 展开,F 递归展开

可以 vim 操作,可以鼠标操作,方向键操作

c 确认更改

我不打算保留任何内容,因为这些只是我为了展示 TUI 而添加的无意义内容。完全不想保留它们,所以直接丢弃即可。我们可以通过 jj abandon 清除 @ 中的内容。

edit

The workflow goes like this: 该工作流的具体步骤如下:

  1. We create a new change to work on our feature. 我们创建一个新变更来开发功能。
  2. If we end up doing exactly what we wanted to do, we’re done. 如果最终实现与预期完全一致,工作即告完成。
  3. If we realize we want to break this up into smaller changes, we do it by making a new change before the current one, swapping to it, and making that change. 若意识到需要将当前变更拆分为更小的部分,我们会在当前变更前新建变更,切换至该变更并进行修改。
  4. We then go back to the main change. 随后我们返回主变更。

让我们创建一个撤销前序功能的特性:仅回退到 Hello, World! 。

现在,之前的工作流将 @ 留在了空变更状态。但若采用本工作流, @ 通常会位于现有变更上。因此实际应用时,我们首先需要 new 一个新的,然后改 describe

这里因为本来就是新的了,所以直接

jj describe -m "only print hello world"

然后我们修改好文件。这个时候顺利的话就完成了这个目标,接下来继续 new 就行了。但有时候,在处理某件事时,我们会意识到还需要另一个不同的变更,可能还依赖于当前这个。举个例子,假设我们正在撤销这个“再见”功能,却突然想把打印逻辑重构为一个独立函数,因为这在实践中是个糟糕的主意,正好适合用来做示例练习。

我们想要做的是在当前变更之前新增一个变更。

➜  hello-world jj new -B @ -m "add more comments"
Rebased 1 descendant commits
Working copy  (@) now at: qnvornwq d6afa175 (empty) add more comments
Parent commit (@-)      : uluzqzkr d0a0878f print goodbye as well as hello
Added 0 files, modified 1 files, removed 0 files
➜  hello-world jj                                         
○  zmlmtmml multya77@gmail.com 2025-05-07 23:57:14 a5ad6740
│  only print hello world
@  qnvornwq multya77@gmail.com 2025-05-07 23:57:14 d6afa175
│  (empty) add more comments
○  uluzqzkr multya77@gmail.com 2025-05-07 23:30:38 d0a0878f
│  print goodbye as well as hello
○  qmwymuwt multya77@gmail.com 2025-05-07 23:11:26 24b41aa5
│  (empty) oh yes
○  xqotnkml multya77@gmail.com 2025-05-07 23:05:22 0a765773
│  hahaha
◆  zzzzzzzz root() 00000000

这里自动 rebase 了

修改一点代码,之后看起来是这样的:

➜  hello-world jj st                                      
Rebased 1 descendant commits onto updated working copy
Working copy changes:
M src/main.rs
Working copy  (@) : qnvornwq 1229c027 add more comments
Parent commit (@-): uluzqzkr d0a0878f print goodbye as well as hello

又一次 rebase。由于我们修改了变更内容,所有依赖于它的变更都必须重新变基。不过别担心,这种情况总是会发生,无一例外。所以在这个阶段你不会因此卡住。

现在我们需要返回到之前的地方:

➜  hello-world jj   
○  zmlmtmml multya77@gmail.com 2025-05-08 00:01:05 3f30420b
│  only print hello world
@  qnvornwq multya77@gmail.com 2025-05-08 00:01:05 1229c027
│  add more comments
○  uluzqzkr multya77@gmail.com 2025-05-07 23:30:38 d0a0878f
│  print goodbye as well as hello
○  qmwymuwt multya77@gmail.com 2025-05-07 23:11:26 24b41aa5
│  (empty) oh yes
○  xqotnkml multya77@gmail.com 2025-05-07 23:05:22 0a765773
│  hahaha
◆  zzzzzzzz root() 00000000
➜  hello-world jj edit zm
Working copy  (@) now at: zmlmtmml 3f30420b only print hello world
Parent commit (@-)      : qnvornwq 1229c027 add more comments
Added 0 files, modified 1 files, removed 0 files

也可以用另一种方案:

$ jj next --edit
Working copy now at: ootnlvpt e13b2585 only print hello world
Parent commit      : nmptruqn 90a2e97f refactor printing
Added 0 files, modified 1 files, removed 0 files

jj next 会将 @ (工作副本的变更)移动到其当前位置的子节点。 --edit 标志表示我们现在要编辑该变更,而如果省略此标志,则其行为更像 jj new 的一个变体,即基于该变更创建一个新的变更。