Jujutsu (jj) 使用方法笔记

概述

Jujutsu(简称jj)是一个用Rust编写的版本控制系统,由Google资助开发,旨在成为更简单、性能更高、更易于使用的Git替代品。它于2019年作为个人爱好项目创建,具有创新的设计理念。

核心创新

1. 工作副本即提交(Working-copy-as-a-commit)

  • 消除Git的暂存区(index)概念
  • 工作目录直接映射为可编辑的提交
  • 修改文件后无需git add,通过jj new即可创建新提交
  • 简化日常操作流程

2. 自动重基与变更追踪

  • 修改历史提交后,依赖该提交的后续变更会自动rebase(如jj edit
  • 避免Git中手动rebase的繁琐
  • 操作日志完整记录所有变更
  • 支持任意步骤回滚(jj undo

3. 多后端支持

  • 默认使用Git仓库作为存储后端,可无缝衔接现有Git项目
  • 同时支持自研存储引擎
  • 未来计划扩展云存储支持(如Google内部系统)

局限性

目前还缺乏Git高级特性如子模块、LFS、签名提交和hooks,所以企业级应用存在缺口。

安装与初始化

安装

根据官方文档针对不同平台安装:https://jj-vcs.github.io/jj/latest/install-and-setup/

克隆现有Git仓库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 克隆GitHub仓库(注意使用"git clone"而不是"clone")
jj git clone https://github.com/octocat/Hello-World
Fetching into new repo in "/tmp/tmp.O1DWMiaKd4/Hello-World"
remote: Enumerating objects: 13, done.
remote: Total 13 (delta 0), reused 0 (delta 0), pack-reused 13 (from 1)
bookmark: master@origin [new] untracked
bookmark: octocat-patch-1@origin [new] untracked
bookmark: test@origin [new] untracked
Setting the revset alias `trunk()` to `master@origin`
Working copy (@) now at: kntqzsqt d7439b06 (empty) (no description set)
Parent commit (@-) : orrkosyo 7fd1a60b master | (empty) Merge pull request #6 from Spaceghost/patch-1
Added 1 files, modified 0 files, removed 0 files

# 进入克隆的目录
cd Hello-World

# 查看状态
jj st

初始化新仓库

1
2
3
4
5
6
7
8
9
# 与Git共用仓库(推荐)
jj git init --colocate

# 纯jj仓库
jj init

# 配置用户信息
jj config set --user user.name "Adam"
jj config set --user user.email "adam@example.com"

注意:纯jj仓库无法被原生Git打开,只有colocate模式才能与Git混合使用。

理解克隆后的状态

克隆后,jj st会显示类似这样的信息:

1
2
3
4
$ jj st
The working copy has no changes.
Working copy (@) : kntqzsqt d7439b06 (empty) (no description set)
Parent commit (@-): orrkosyo 7fd1a60b master | (empty) Merge pull request #6 from Spaceghost/patch-1

这里引入了两个重要概念:

  • 变更ID(Change ID):如kntqzsqt,是Jujutsu独有的稳定标识符
  • 提交ID(Commit ID):如d7439b06,类似Git的commit hash
  • 工作副本(@):当前所在的提交,实际上是一个可编辑的提交

基本命令映射表

目的 Git命令 jj命令(等效或更优)
查看状态 git status jj st(或jj status
查看日志 git log --oneline --graph jj log(自动图形化)
提交 git commit -am "msg" jj commit -m "msg"
暂存 git add -p 不需要:jj new自动把工作区作为「新变更集」
创建分支 git checkout -b feat jj new main -m "feat"(产生新的变更集,可理解为「匿名分支」)
切换 git switch feat jj edit <id>jj new <id>
拉取 git pull --rebase jj git fetch && jj rebase -d 'main@origin'
推送 git push origin HEAD jj git push -c <id>(第一次)或 jj git push --change <id>
修改最近一次提交 git commit --amend 直接在工作区继续编辑,然后 jj squash
交互式rebase git rebase -i jj rebase -i
stash git stash 不需要:工作区永远干净,所有修改都在「草稿变更集」

核心概念

变更集(Change)

  • 变更集(change) = Git中的一次commit,但可随意改写,直到显式push
  • 每个变更集有一个稳定的变更ID(如kntqzsqt)和一个变化的提交ID(如d7439b06
  • 变更ID在重写历史时保持不变,提交ID会随着内容变化而改变

工作副本即提交

  • 工作区永远clean:你始终处于某个变更集上
  • 工作副本实际上就是一个可编辑的提交(用@表示)
  • 修改文件后,工作副本提交会自动被下一个jj命令更新

标识符系统

  • 变更ID:短哈希(如kntqzsqt),在重写历史时保持稳定
  • 提交ID:长哈希(如d7439b06),类似Git的commit hash
  • 特殊标识符
    • @:当前工作副本
    • @-:工作副本的父提交
    • root():根提交(虚拟提交00000000

分支和书签

  • jj new 创建新的「匿名分支」
  • jj branch 给变更集贴标签(类似Git branch)
  • 所有历史都可重写,且不会丢失旧版本(自动保留不可见的「废弃变更集」)
  • 远程协作通过jj git push/fetch,底层仍是Git协议

创建和管理变更

创建第一个变更

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 1. 首先描述变更(添加提交消息)
jj describe
# 在编辑器中输入描述,如"Say goodbye",保存并关闭

# 输出示例:
# Working copy (@) now at: kntqzsqt e427edcf (empty) Say goodbye
# Parent commit (@-) : orrkosyo 7fd1a60b master | (empty) Merge pull request #6 from Spaceghost/patch-1

# 2. 修改文件(例如编辑README)
echo "Goodbye World" > README

# 3. 查看状态
jj st
# 输出:
# Working copy changes:
# M README
# Working copy (@) : kntqzsqt e427edcf Say goodbye
# Parent commit (@-): orrkosyo 7fd1a60b master | (empty) Merge pull request #6 from Spaceghost/patch-1

# 4. 提交变更
jj commit
# 输出:
# Working copy (@) now at: mpqrykyp aef4df99 (empty) (no description set)
# Parent commit (@-) : kntqzsqt 5d39e19d Say goodbye

查看历史

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 查看日志(默认显示本地提交和部分远程提交作为上下文)
jj log

# 输出示例:
# @ mpqrykyp martinvonz@google.com 2023-02-12 15:00:22 aef4df99
# │ (empty) (no description set)
# ○ kntqzsqt martinvonz@google.com 2023-02-12 14:56:59 5d39e19d
# │ Say goodbye
# ◆ orrkosyo octocat@nowhere.com 2012-03-06 15:06:50 master 7fd1a60b
# │ (empty) Merge pull request #6 from Spaceghost/patch-1
# ~

# 使用revset表达式查看特定提交
jj log -r '@ | root() | bookmarks()'

# 查看所有提交
jj log -r :: # 或 jj log -r 'all()'

Revset表达式基础

Revset是Jujutsu强大的提交选择语言:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 基本标识符
@ # 当前工作副本
@- # 工作副本的父提交
root() # 根提交
bookmarks() # 所有书签指向的提交

# 操作符
foo | bar # 并集
foo & bar # 交集
foo ~ bar # 差集
foo- # foo的父提交
foo+ # foo的子提交
::foo # foo的所有祖先
foo:: # foo的所有后代
foo::bar # foo到bar的DAG范围
foo..bar # foo到bar的范围(类似Git)

冲突解决

创建冲突场景

让我们创建一个冲突场景来学习如何解决:

1
2
3
4
5
6
7
8
9
10
11
12
# 创建一系列提交
jj new master -m A; echo a > file1
# Working copy (@) now at: nuvyytnq 00a2aeed (empty) A

jj new -m B1; echo b1 > file1
# Working copy (@) now at: ovknlmro 967d9f9f (empty) B1

jj new -m B2; echo b2 > file1
# Working copy (@) now at: puqltutt 8ebeaffa (empty) B2

jj new -m C; echo c > file2
# Working copy (@) now at: qzvqqupx 62a3c6d3 (empty) C

重基导致冲突

现在将B2直接重基到A上,这会产生冲突:

1
2
3
4
5
6
7
8
9
10
11
12
# 将B2重基到A上(替换为实际的变更ID)
jj rebase -s puqltutt -d nuvyytnq

# 输出:
# Rebased 2 commits to destination
# Working copy (@) now at: qzvqqupx 1978b534 (conflict) C
# Parent commit (@-) : puqltutt f7fb5943 (conflict) B2
# Warning: There are unresolved conflicts at these paths:
# file1 2-sided conflict
# New conflicts appeared in 2 commits:
# qzvqqupx 1978b534 (conflict) C
# puqltutt f7fb5943 (conflict) B2

解决冲突的步骤

  1. 在冲突提交上创建新提交
1
2
jj new puqltutt  # 替换为B2的实际变更ID
# Working copy (@) now at: zxoosnnp c7068d1c (conflict) (empty) (no description set)
  1. 查看冲突内容
1
2
3
4
5
6
7
8
9
cat file1
# 输出:
# <<<<<<< Conflict 1 of 1
# %%%%%%% Changes from base to side #1
# -b1
# +a
# +++++++ Contents of side #2
# b2
# >>>>>>> Conflict 1 of 1 ends
  1. 手动解决冲突
1
echo resolved > file1
  1. 检查解决状态
1
2
3
4
5
6
7
jj st
# 输出:
# Working copy changes:
# M file1
# Working copy (@) : zxoosnnp c2a31a06 (no description set)
# Parent commit (@-): puqltutt f7fb5943 (conflict) B2
# Hint: Conflict in parent commit has been resolved in working copy
  1. 将解决结果合并到冲突提交
1
2
3
4
5
6
jj squash
# 输出:
# Rebased 1 descendant commits
# Working copy (@) now at: ntxxqymr e3c279cc (empty) (no description set)
# Parent commit (@-) : puqltutt 2c7a658e B2
# Existing conflicts were resolved or abandoned from 2 commits.

冲突解决的关键点

  • 冲突不会阻止rebase完成,Jujutsu会继续重基后续提交
  • 使用jj new <conflicted_commit>在冲突提交上创建新的工作副本
  • 解决冲突后使用jj squash将解决方案合并到冲突提交中
  • 后续提交会自动重基到解决后的提交上

操作日志和撤销

查看操作日志

Jujutsu记录所有对仓库的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
jj op log

# 输出示例:
# @ d3b77addea49 martinvonz@vonz.svl.corp.google.com 3 minutes ago, lasted 3 milliseconds
# │ squash commits into f7fb5943a6b9460eb106dba2fac5cac1625c6f7a
# │ args: jj squash
# ○ 6fc1873c1180 martinvonz@vonz.svl.corp.google.com 3 minutes ago, lasted 1 milliseconds
# │ snapshot working copy
# │ args: jj st
# ○ ed91f7bcc1fb martinvonz@vonz.svl.corp.google.com 6 minutes ago, lasted 1 milliseconds
# │ new empty commit
# │ args: jj new puqltutt
# ○ 367400773f87 martinvonz@vonz.svl.corp.google.com 12 minutes ago, lasted 3 milliseconds
# │ rebase commit daa6ffd5a09a8a7d09a65796194e69b7ed0a566d and descendants
# │ args: jj rebase -s puqltutt -d nuvyytnq

撤销操作

使用jj undo撤销最后一个操作:

1
2
3
4
5
6
jj undo

# 输出:
# Reverted operation: d3b77addea49 (2025-05-12 00:27:27) squash commits into f7fb5943a6b9460eb106dba2fac5cac1625c6f7a
# Working copy (@) now at: zxoosnnp 63874fe6 (no description set)
# Parent commit (@-) : puqltutt f7fb5943 (conflict) B2

恢复到特定操作状态

查看特定操作时的仓库状态:

1
2
3
4
5
# 查看特定操作后的日志(使用实际的操作哈希)
jj log --at-op=367400773f87

# 恢复到特定操作
jj op restore <operation_id>

内容变更在提交间移动

交互式Squash

使用jj squash -i选择性地将部分变更移动到父提交:

1
2
3
4
5
6
7
8
9
10
# 创建测试场景
jj new master -m abc; printf 'a\nb\nc\n' > file
jj new -m ABC; printf 'A\nB\nc\n' > file
jj new -m ABCD; printf 'A\nB\nC\nD\n' > file

# 假设我们想把"C"大写移到ABC提交中
jj squash -i # 在ABCD提交上运行

# 在diff编辑器中选择要移动的变更(这里选择"C"行的变更)
# 按'c'确认,'q'退出

DiffEdit

使用jj diffedit直接编辑提交的变更,而不需要检出:

1
2
3
4
5
# 编辑父提交的变更
jj diffedit -r @-

# 在diff编辑器中选择要修改的内容
# 修改完成后,后续提交会自动重基

拆分提交

使用jj split将一个大提交拆分成多个:

1
2
3
4
5
# 假设当前提交有多个不相关的变更
jj split

# 在diff编辑器中选择要拆分到第一个提交的变更
# 剩余的变更会留在第二个提交中

日常最佳实践

1
2
3
4
5
6
7
8
9
10
11
12
# 1. 基于主干创建功能
jj new main -m "wip: add login"

# 2. 开发、迭代
vim foo.rust
jj commit -m "parse jwt"

# 3. 需要改更早的提交?直接rebase -i
jj rebase -i 'main'

# 4. 整理完,推到远端
jj git push -c @-

高频技巧

  • 快速拆分大提交jj split(交互式选择文件或hunk拆成两个变更集)
  • 快速squashjj squash -r <child> -r <parent>jj squash --into <parent>
  • 撤销任何操作jj op log → 找到误操作的op → jj op restore <id>(时间机器)

常见坑与排查

现象 原因 解决方法
无法push 远端有更新 jj git fetch && jj rebase -d 'main@origin'
误删变更集 历史仍在 jj log -r 'visible_heads()' 找回,或 jj op restore
Windows路径过长 jj内部使用长哈希 设置 core.longpaths=true(Git配置)
GUI工具不支持 只认.git 使用colocate模式即可

学习资源

速查清单(贴墙用)

基础操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 克隆仓库
jj git clone <url>

# 新功能
jj new main -m "xxx"

# 描述变更
jj describe

# 迭代
jj commit -m "..."

# 查看状态
jj st

# 查看日志
jj log
jj log -r '@ | root() | bookmarks()' # 使用revset

历史重写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 整理历史
jj rebase -i 'main'
jj rebase -s <source> -d <destination>

# 合并修改
jj squash
jj squash -i # 交互式
jj squash --into <parent>

# 拆分提交
jj split

# 编辑提交变更
jj diffedit -r @-

冲突解决

1
2
3
4
5
6
7
8
# 在冲突提交上创建新提交
jj new <conflicted_commit>

# 解决冲突后合并
jj squash

# 查看冲突
cat <conflicted_file>

撤销和恢复

1
2
3
4
5
6
7
8
9
10
11
# 撤销最后一个操作
jj undo

# 查看操作日志
jj op log

# 恢复到特定操作
jj op restore <operation_id>

# 查看特定操作时的状态
jj log --at-op=<operation_id>

远程协作

1
2
3
4
5
6
7
8
9
10
# 拉取
jj git fetch
jj git fetch && jj rebase -d 'main@origin'

# 推送
jj git push -c @-
jj git push --change <change_id>

# 放弃追踪文件
jj file untrack <file>

Revset表达式

1
2
3
4
5
6
@           # 当前工作副本
@- # 工作副本的父提交
root() # 根提交
bookmarks() # 所有书签
foo::bar # foo到bar的DAG范围
foo..bar # foo到bar的范围

总结

Jujutsu通过创新的架构设计解决了Git的许多痛点:

  1. 消除暂存区概念,简化工作流程
  2. 自动重基机制,减少手动操作
  3. 工作区永远干净,避免状态混乱
  4. 强大的撤销和回滚能力
  5. 与Git兼容,平滑迁移

对于Git资深用户,Jujutsu是"即插即用"的效率工具,特别适合需要频繁修改历史和重整提交的开发工作流。