using-jj

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

jj Workflow

jj 工作流

Philosophy

理念

  1. Commits are cheap, descriptions are mandatory. The working copy is always a commit. Never leave it as "(no description set)".
  2. Experiment freely, the oplog is your safety net. Every mutation is recorded.
    jj undo
    and
    jj op restore
    make anything reversible.
  3. Conflicts are state, not emergencies. jj stores conflicts in commits as structured data. Rebase succeeds even with conflicts. Resolve when ready.
  4. Change IDs are your handle on work. Commit hashes change on rewrite; change IDs don't. Use change IDs to refer to work across rebases and squashes.
  5. Bookmarks exist for GitHub, not for you. Work with anonymous changes. Add bookmarks only when you need to push.
  6. Keep the stack shallow. Squash early. Don't let history grow 10 commits deep before curating.
  7. Use
    absorb
    over manual squash routing.
    When fixing across a stack, let jj figure out where each hunk belongs.
  8. Colocated = invisible to the team. Teammates see standard git. They don't know you use jj.
  1. 提交成本低,描述是必须项。工作副本始终是一个提交。绝不要让它处于"(no description set)"状态。
  2. 自由实验,oplog是你的安全网。每一次变更操作都会被记录。
    jj undo
    jj op restore
    可撤销任何操作。
  3. 冲突是状态,而非紧急事件。jj将冲突作为结构化数据存储在提交中。即使存在冲突,变基也能成功。可在方便时再解决冲突。
  4. 变更ID是你处理工作的标识。提交哈希会在重写时改变,但变更ID不会。跨变基和压缩操作引用工作时,请使用变更ID。
  5. 书签是为GitHub准备的,而非你自己。使用匿名变更进行工作。仅当需要推送时再添加书签。
  6. 保持提交栈简洁。尽早压缩提交。不要在整理前让提交历史增长到10个提交的深度。
  7. 使用
    absorb
    而非手动压缩路由
    。当在提交栈中进行修复时,让jj自动判断每个代码块所属的提交。
  8. 本地操作对团队不可见。你的同事看到的是标准git操作,他们不会知道你在使用jj。

CRITICAL: AI-Specific Rules

关键:AI专属规则

Always use
-m
flag
to prevent jj from opening an editor:
bash
undefined
始终使用
-m
参数
,避免jj打开编辑器:
bash
undefined

WRONG - opens editor, blocks AI

错误示例 - 会打开编辑器,阻塞AI

jj new jj describe jj commit jj squash
jj new jj describe jj commit jj squash

CORRECT - non-interactive

正确示例 - 非交互式

jj new -m "message" jj describe -m "message" jj commit -m "message" jj squash -m "message"

**Never use these interactive commands** (no non-interactive mode):

- `jj split` / `jj split -i`
- `jj squash -i`
- `jj diffedit`
jj new -m "message" jj describe -m "message" jj commit -m "message" jj squash -m "message"

**绝不要使用这些交互式命令**(无非交互模式):

- `jj split` / `jj split -i`
- `jj squash -i`
- `jj diffedit`

Core Concepts

核心概念

Working Copy = Commit

工作副本 = 提交

There is no staging area. Every file edit is automatically tracked in
@
(the current change). No
git add
needed.
  • @
    = your current change (working copy commit)
  • @-
    = parent of current change
  • @--
    = grandparent
暂存区不存在。所有文件编辑都会自动被
@
(当前变更)追踪。无需执行
git add
  • @
    = 当前变更(工作副本提交)
  • @-
    = 当前变更的父提交
  • @--
    = 当前变更的祖父提交

Change IDs vs Commit IDs

变更ID vs 提交ID

Every change has two identifiers:
  • Change ID (e.g.,
    kpqxywon
    ) — stable across rewrites. Use this to refer to work.
  • Commit ID (e.g.,
    a1b2c3d4
    ) — changes when content is rewritten.
When you squash, rebase, or amend, the change ID stays the same. This means you can bookmark a change ID mentally and it always resolves, unlike git commit hashes.
每个变更都有两个标识符:
  • 变更ID(例如:
    kpqxywon
    )—— 重写后保持稳定。使用它来引用工作。
  • 提交ID(例如:
    a1b2c3d4
    )—— 内容重写时会改变。
当你压缩、变基或修订提交时,变更ID保持不变。这意味着你可以在脑海中标记一个变更ID,它始终能被正确解析,不像git提交哈希。

Accessing Previous Versions (
xyz/n
syntax)

访问历史版本(
xyz/n
语法)

Every rewrite of a change is recorded. Access previous versions with
<change-id>/n
:
  • xyz/0
    — latest (current) version (same as
    xyz
    )
  • xyz/1
    — previous version
  • xyz/2
    — two versions ago
This is useful for restoring a change to its earlier state:
bash
jj restore --from xyz/1 --to xyz    # Revert xyz to its previous contents
jj diff --from xyz/1 --to xyz      # See what changed between versions
每个变更的重写记录都会被保存。使用
<change-id>/n
访问历史版本:
  • xyz/0
    —— 最新(当前)版本(与
    xyz
    相同)
  • xyz/1
    —— 上一个版本
  • xyz/2
    —— 前两个版本
这可用于将变更恢复到早期状态:
bash
jj restore --from xyz/1 --to xyz    # 将xyz恢复到上一个版本的内容
jj diff --from xyz/1 --to xyz      # 查看版本间的变更

Conflicts Are Just State

冲突只是一种状态

When a rebase produces conflicts, jj records the conflict in the commit and succeeds. No "rebase in progress" blocking state. No
--continue
ceremony.
  • Descendants of conflicted commits work normally
  • Resolve conflicts whenever convenient — check out the commit, fix files, done
  • jj log
    marks conflicted commits so you can spot them
当变基产生冲突时,jj会将冲突作为结构化数据存储在提交中,变基仍能成功。不存在“变基进行中”的阻塞状态,也无需执行
--continue
操作。
  • 冲突提交的后代可正常工作
  • 可在方便时解决冲突 —— 切换到该提交,修复文件即可
  • jj log
    会标记冲突提交,方便你识别

Workflows

工作流

The Squash Workflow (Recommended)

压缩工作流(推荐)

bash
jj describe -m "feat: what I'm building"   # State intent on current change
jj new -m "wip"                             # New empty change on top
bash
jj describe -m "feat: 我正在开发的功能"   # 在当前变更上声明意图
jj new -m "wip"                             # 在当前变更之上创建新的空变更

... make changes ...

... 进行代码修改 ...

jj squash -m "feat: done" # Squash into parent
undefined
jj squash -m "feat: 功能完成" # 合并到父提交
undefined

The Commit Workflow (Simpler)

提交工作流(更简单)

bash
undefined
bash
undefined

... make changes ...

... 进行代码修改 ...

jj commit -m "feat: what I did" # Describe + create new change in one step
jj commit -m "feat: 我完成的工作" # 一步完成描述并创建新变更

... keep working ...

... 继续工作 ...


`jj commit` is equivalent to `jj describe -m "..." && jj new`.

`jj commit`等价于`jj describe -m "..." && jj new`。

The Edit Workflow (Mid-Stack Fixes)

编辑工作流(提交栈中间的修复)

Need to fix something in an older change? No stash/rebase-i dance:
bash
jj edit <change-id>        # Switch working copy to that change
需要在较早的变更中修复问题?无需执行暂存/变基交互操作:
bash
jj edit <change-id>        # 将工作副本切换到该变更

... make your fix ...

... 进行修复 ...

jj new -m "back to work" # Return to tip (descendants auto-rebased)

All descendants of the edited change are automatically rebased.
jj new -m "回到工作" # 返回提交栈顶端(后代会自动变基)

被编辑变更的所有后代都会自动变基。

Parallel Experiments

并行实验

bash
jj new main -m "approach A"           # Branch from main
jj new main -m "approach B"           # Another branch from main (not from A)
jj diff --from <A-id> --to <B-id>    # Compare approaches
jj edit <winner-id>                   # Continue with the winner
jj abandon <loser-id>                 # Discard the loser
bash
jj new main -m "方案A"           # 从main分支创建新变更
jj new main -m "方案B"           # 从main分支创建另一个新变更(不是从A分支)
jj diff --from <A-id> --to <B-id>    # 对比两种方案
jj edit <winner-id>                   # 继续使用胜出的方案
jj abandon <loser-id>                 # 丢弃失败的方案

Absorb: Smart Squash Routing

Absorb:智能压缩路由

When you have a stack of changes and make fixes in
@
,
jj absorb
automatically distributes each hunk to the ancestor where those lines were last modified.
bash
undefined
当你有一个提交栈,并且在
@
中进行修复时,
jj absorb
会自动将每个代码块分配到最后修改这些代码行的祖先提交中。
bash
undefined

You're at the top of a 3-commit stack, fixing bugs across all of them

你处于3个提交的栈顶,正在修复所有提交中的bug

jj absorb # Each fix goes to the right commit automatically

Use `jj absorb` when fixing across a stack. Use `jj squash` when you know exactly where changes should go.
jj absorb # 每个修复会自动分配到对应的提交

当在提交栈中跨提交修复时,使用`jj absorb`。当你明确知道变更应归属的提交时,使用`jj squash`。

Bookmarks & Pushing

书签与推送

Bookmarks are jj's equivalent of git branches, but they don't auto-advance. You must move them explicitly.
书签是jj中对应git分支的功能,但它们不会自动推进。你必须显式移动它们。

Push to main

推送到main分支

bash
jj bookmark set master -r @-        # Point bookmark at your commit (not empty @)
jj git push
bash
jj bookmark set master -r @-        # 将书签指向你的提交(不是空的@)
jj git push

Feature branches

功能分支

bash
undefined
bash
undefined

Create and push

创建并推送

jj bookmark create feature-x -r @- jj git push
jj bookmark create feature-x -r @- jj git push

Update after more work

后续工作后更新

jj bookmark set feature-x -r @- jj git push
undefined
jj bookmark set feature-x -r @- jj git push
undefined

Addressing PR feedback

处理PR反馈

bash
jj new feature-x- -m "address review feedback"
bash
jj new feature-x- -m "处理评审反馈"

... make changes ...

... 进行修改 ...

jj squash -m "feat: updated per review" jj bookmark set feature-x -r @- jj git push
undefined
jj squash -m "feat: 根据评审更新内容" jj bookmark set feature-x -r @- jj git push
undefined

Revsets

Revsets(版本集)

Revsets are a functional language for selecting commits. Beyond
@
and
@-
:
ExpressionMeaning
@
Current working copy
@-
Parent
@--
Grandparent
x+
Children of x
x::
All descendants of x
::x
All ancestors of x
trunk()
The trunk/main commit
bookmarks()
All bookmarked commits
empty()
Empty commits
divergent()
Divergent changes
remote_tags()
Remote tags
diff_lines("text")
Commits with matching diff
description("text")
Filter by description
author("name")
Filter by author
Useful examples:
bash
jj log -r 'trunk()..@'              # Everything between main and here
jj log -r '::@ & ~::trunk()'         # My branch only
jj log -r 'author("trevor")'         # My commits
Revsets是一种用于选择提交的函数式语言。除了
@
@-
之外:
表达式含义
@
当前工作副本
@-
父提交
@--
祖父提交
x+
x的子提交
x::
x的所有后代提交
::x
x的所有祖先提交
trunk()
主干/main提交
bookmarks()
所有带书签的提交
empty()
空提交
divergent()
分歧变更
remote_tags()
远程标签
diff_lines("text")
包含匹配差异的提交
description("text")
根据描述筛选提交
author("name")
根据作者筛选提交
实用示例:
bash
jj log -r 'trunk()..@'              # 查看main分支到当前提交之间的所有内容
jj log -r '::@ & ~::trunk()'         # 仅查看我的分支内容
jj log -r 'author("trevor")'         # 查看我的提交

Syncing with Remote

与远程仓库同步

bash
jj git fetch                         # Pull from remote
jj rebase -d master@origin           # Rebase onto updated main
bash
jj git fetch                         # 从远程拉取
jj rebase -d master@origin           # 变基到更新后的main分支

Temporarily Disabling Immutable Commits

临时禁用不可变提交

When you need to rewrite a commit protected by
immutable_heads()
(e.g., squashing into a remote bookmark):
bash
undefined
当你需要重写被
immutable_heads()
保护的提交时(例如,合并到远程书签):
bash
undefined

Disable protection (quote the key — parentheses are invalid TOML bare keys)

禁用保护(对键名加引号——括号在TOML裸键中无效)

jj config set --repo 'revset-aliases."immutable_heads()"' 'none()'
jj config set --repo 'revset-aliases."immutable_heads()"' 'none()'

Do your rewrite

执行重写操作

jj squash -m "updated message"
jj squash -m "更新后的提交信息"

ALWAYS restore protection immediately after

完成后立即恢复保护

jj config set --repo 'revset-aliases."immutable_heads()"' 'builtin_immutable_heads() | remote_bookmarks()'

**Important:** The `NAME` argument requires shell quoting around the TOML key because `immutable_heads()` contains parentheses. Use single quotes around the full dotted key with inner double quotes: `'revset-aliases."immutable_heads()"'`.

**CRITICAL: Always ask the user before disabling immutable protection.** Rewriting remote bookmarks means force-pushing, which rewrites shared history. Confirm with the user before proceeding — never silently disable immutability.
jj config set --repo 'revset-aliases."immutable_heads()"' 'builtin_immutable_heads() | remote_bookmarks()'

**重要提示**:`NAME`参数需要对TOML键名进行shell转义,因为`immutable_heads()`包含括号。请使用单引号包裹整个带点的键名,内部用双引号:`'revset-aliases."immutable_heads()"'`。

**关键注意事项**:在禁用不可变保护前,务必先询问用户。重写远程书签意味着强制推送,这会修改共享的提交历史。在执行前请与用户确认——绝不要静默禁用不可变保护。

Recovery

恢复操作

The operation log records every mutation. Nothing is ever truly lost.
bash
jj op log                            # See all operations
jj undo                              # Undo last operation
jj op restore <id>                   # Jump to any past state
jj evolog                            # See how current change evolved
jj evolog -r <change-id>             # See how any change evolved
操作日志记录了每一次变更操作。没有任何内容会真正丢失。
bash
jj op log                            # 查看所有操作
jj undo                              # 撤销上一次操作
jj op restore <id>                   # 跳转到任意历史状态
jj evolog                            # 查看当前变更的演进历史
jj evolog -r <change-id>             # 查看任意变更的演进历史

Recommended Config

推荐配置

User config lives at
~/.config/jj/config.toml
:
toml
[remotes.origin]
auto-track-bookmarks = "*"

[revset-aliases]
用户配置文件位于
~/.config/jj/config.toml
toml
[remotes.origin]
auto-track-bookmarks = "*"

[revset-aliases]

Prevent rewriting pushed commits

防止重写已推送的提交

'immutable_heads()' = 'builtin_immutable_heads() | remote_bookmarks()'
'immutable_heads()' = 'builtin_immutable_heads() | remote_bookmarks()'

Shorthand for trunk

主干的简写

'trunk()' = 'master@origin'
undefined
'trunk()' = 'master@origin'
undefined

Bail Out

紧急退出

bash
rm -rf .jj    # Delete jj state, keep git unchanged
bash
rm -rf .jj    # 删除jj状态,保留git内容不变