关于 jj-workflow
面向使用 jj 的开发者,介绍变更描述、分支、变基、冲突处理和恢复等工作流。指南还为 AI 辅助操作规定非交互式命令,并说明共置模式可让团队成员继续只看到 Git。
主要功能
- 01用非交互式命令描述变更
- 02改写前后使用稳定的变更 ID
- 03将冲突作为提交状态管理
- 04创建并比较并行实验
- 05用 jj absorb 分配修复内容
- 06通过操作日志恢复旧状态
适用场景
- 01维护较浅的变更栈
- 02修复旧变更并自动变基后续变更
- 03从同一基点尝试不同实现
- 04撤销操作或恢复仓库旧状态
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ trevors/ dot-claude --skill jj-workflow安装后在 Claude Code 中输入/jj-workflow确认已加载。
附带文件REFERENCE.md
详细介绍
jj 工作流
理念
- 提交很轻松,描述必不可少。 工作副本始终是一个提交。绝不要让它处于“(no description set)”状态。
- 尽情尝试,操作日志会为你兜底。 每次变更都会被记录。
jj undo和jj op restore可以撤销任何操作。 - 冲突是一种状态,不是紧急情况。 jj 会将冲突作为结构化数据存储在提交中。即使存在冲突,变基也能成功。准备好后再解决。
- 变更 ID 是你跟踪工作的凭据。 重写时提交哈希会变化,变更 ID 不会。变基和压缩提交期间,用变更 ID 引用工作。
- 书签是为 GitHub 准备的,不是为你准备的。 使用匿名变更进行工作。只有在需要推送时才添加书签。
- 保持提交栈浅。 尽早压缩提交。别让历史记录在整理前增长到 10 个提交那么深。
- 用
absorb代替手动路由压缩提交。 跨提交栈修复时,让 jj 判断每个代码块应归属何处。 - 共置仓库 = 团队成员无从察觉。 队友看到的是标准 git。他们不知道你在用 jj。
重要:AI 专属规则
始终使用 -m 标志,避免 jj 打开编辑器:
# WRONG - opens editor, blocks AI
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"
绝不要使用这些交互式命令(没有非交互模式):
jj split/jj split -ijj squash -ijj diffedit
核心概念
工作副本 = 提交
这里没有暂存区。每次文件编辑都会自动记录在 @(当前变更)中。不需要执行 git add。
@= 当前变更(工作副本提交)@-= 当前变更的父提交@--= 祖父提交
变更 ID 与提交 ID
每个变更都有两个标识符:
- 变更 ID(例如,
kpqxywon)——重写后保持不变。用它来引用工作。 - 提交 ID(例如,
a1b2c3d4)——内容重写时会变化。
压缩、变基或修订时,变更 ID 会保持不变。这意味着你可以在心里记住一个变更 ID,它始终都能解析到对应变更;git 提交哈希则不同。
访问先前版本(xyz/n 语法)
每次重写变更都会被记录。使用 <change-id>/n 访问先前版本:
xyz/0— 最新(当前)版本(与xyz相同)xyz/1— 上一个版本xyz/2— 两个版本之前
这在需要将变更恢复到较早状态时很有用:
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
冲突只是一种状态
变基产生冲突时,jj 会将冲突记录在提交中并成功完成操作。不会出现阻塞状态“rebase in progress”,也不需要执行 --continue。
- 冲突提交的后代可以正常工作
- 方便时再解决冲突——检出该提交、修复文件即可
jj log会标记冲突提交,方便你发现它们
工作流
压缩工作流(推荐)
jj describe -m "feat: what I'm building" # State intent on current change
jj new -m "wip" # New empty change on top
# ... make changes ...
jj squash -m "feat: done" # Squash into parent
提交工作流(更简单)
# ... make changes ...
jj commit -m "feat: what I did" # Describe + create new change in one step
# ... keep working ...
jj commit 等同于 jj describe -m "..." && jj new。
编辑工作流(修复提交栈中间的内容)
需要修复较早变更中的内容?不必经历 stash/rebase-i 的繁琐操作:
jj edit <change-id> # Switch working copy to that change
# ... make your fix ...
jj new -m "back to work" # Return to tip (descendants auto-rebased)
编辑后的变更的所有后代都会自动变基。
并行试验
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
Absorb:智能路由压缩
当你有一组提交,并在 @ 中进行修复时,jj absorb 会自动将每个代码块分发到最后修改过对应行的祖先提交中。
# You're at the top of a 3-commit stack, fixing bugs across all of them
jj absorb # Each fix goes to the right commit automatically
跨提交栈修复时使用 jj absorb。如果明确知道变更应该放到哪里,则使用 jj squash。
书签与推送
书签相当于 jj 中的 git 分支,但不会自动前移。你必须显式移动它们。
推送到 main
jj bookmark set master -r @- # Point bookmark at your commit (not empty @)
jj git push
功能分支
# Create and push
jj bookmark create feature-x -r @-
jj git push
# Update after more work
jj bookmark set feature-x -r @-
jj git push
处理 PR 反馈
jj new feature-x- -m "address review feedback"
# ... make changes ...
jj squash -m "feat: updated per review"
jj bookmark set feature-x -r @-
jj git push
Revsets
Revsets 是一种用于选择提交的函数式语言。除了 @ 和 @- 之外:
| 表达式 | 含义 |
|---|---|
@ | 当前工作副本 |
@- | 父提交 |
@-- | 祖父提交 |
x+ | x 的子提交 |
x:: | x 的所有后代 |
::x | x 的所有祖先 |
trunk() | trunk/main 提交 |
bookmarks() | 所有带书签的提交 |
empty() | 空提交 |
divergent() | 分叉的更改 |
remote_tags() | 远程标签 |
diff_lines("text") | 差异匹配的提交 |
description("text") | 按描述筛选 |
author("name") | 按作者筛选 |
实用示例:
jj log -r 'trunk()..@' # Everything between main and here
jj log -r '::@ & ~::trunk()' # My branch only
jj log -r 'author("trevor")' # My commits
与远程同步
jj git fetch # Pull from remote
jj rebase -d master@origin # Rebase onto updated main
临时禁用不可变提交
当你需要重写受 immutable_heads() 保护的提交时(例如,将提交压入远程书签):
# Disable protection (quote the key — parentheses are invalid TOML bare keys)
jj config set --repo 'revset-aliases."immutable_heads()"' 'none()'
# Do your rewrite
jj squash -m "updated message"
# ALWAYS restore protection immediately after
jj config set --repo 'revset-aliases."immutable_heads()"' 'builtin_immutable_heads() | remote_bookmarks()'
重要: NAME 参数需要对 TOML 键使用 shell 引号,因为 immutable_heads() 包含括号。对完整的点分隔键使用单引号,内部使用双引号:'revset-aliases."immutable_heads()"'。
关键:禁用不可变保护前务必征得用户同意。 重写远程书签意味着强制推送,这会改写共享历史。继续操作前请向用户确认——绝不能在未告知的情况下禁用不可变保护。
恢复
操作日志会记录每次变更。任何内容都不会真正丢失。
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
推荐配置
用户配置位于 ~/.config/jj/config.toml:
[remotes.origin]
auto-track-bookmarks = "*"
[revset-aliases]
# Prevent rewriting pushed commits
'immutable_heads()' = 'builtin_immutable_heads() | remote_bookmarks()'
# Shorthand for trunk
'trunk()' = 'master@origin'
退出
rm -rf .jj # Delete jj state, keep git unchanged