Jujutsu 如何重新思考 Git 的工作副本和冲突模型
摘要
Jujutsu (jj) 是一个新的版本控制系统,它重新思考了 Git 的工作副本和冲突模型,在保持完全 Git 兼容性的同时,提供了更一致的工作流程。
<p><a href="https://lobste.rs/s/vivtdh/how_jujutsu_rethinks_git_s_working_copy">评论</a></p>
查看缓存全文
缓存时间: 2026/07/01 12:00
# Jujutsu:你不知道你需要的 Git 升级
来源:https://www.git-tower.com/blog/jujutsu
Git 深深嵌入我们的工作方式,以至于它的粗糙边缘开始让人觉得是理所当然的。暂存区、对重写历史的恐惧、切换分支前的储藏舞、让一切停摆的合并冲突。Jujutsu (https://github.com/jj-vcs/jj)——或者叫 jj ——是一个版本控制系统,它认真审视了这些粗糙边缘,并问道:非得这样吗?
答案,事实证明,是否定的。jj 不是激进的重新发明——而是一次严谨的重新设计。它保留了 Git 的精华(分布式模型、提交图、与 GitHub/GitLab 完全兼容),并用更连贯的东西取代了那些一直很尴尬的部分。而且,随着我们的工作流程变得越来越苛刻——特别是 AI 代理现在与我们一同编写和管理代码——那个更干净的基础开始变得非常重要。
本文面向已经了解 Git 并想知道 jj 实际提供了什么的开发者。不是一张命令速查表——而是真正审视 jj 的模型在哪里带来回报。把它看作你的白带。
## 入门:jj 层叠在 Git 之上 (https://www.git-tower.com/blog/jujutsu#getting-started-jj-layers-on-top-of-git)https://www.git-tower.com/blog/jujutsu#getting-started-jj-layers-on-top-of-git
第一件要知道的事:jj 不是一个替代生态系统。它是一个独立的 CLI 工具,使用 Git 仓库作为其存储后端。你的历史存储在 Git 对象中。你的远程仓库是 Git 远程仓库。你使用普通 Git 的同事不受影响。
你可以通过 Homebrew(macOS)、Cargo(如果你在 Rust 生态系统中)安装 jj,或者直接从 jj 发布页 (https://github.com/jj-vcs/jj/releases) 下载二进制文件。在 macOS 上:
```
brew install jj
```
在现有仓库中采用它只需一个命令:
```
jj git init --colocate
```
这会在你现有的 `.git` 文件夹旁边添加一个 `.jj` 文件夹。就是这样。`.jj` 文件夹保存 jj 自己的元数据——它的操作日志、工作副本状态和仓库级配置——但实际内容仍留在 `.git` 中。关键是,jj 通过在文件夹本身内部写入 `.gitignore` 来使 `.jj` 不影响 Git,而不是碰你项目的根 `.gitignore`,所以你的忽略文件保持干净,你的队友永远不会看到什么。
从这一点开始,你可以与 Git 命令并行或替代使用 jj 命令。两者在同一个仓库上工作。
在现有 Git 仓库中采用 jj:一个 colocate 命令在 `.git` 旁边添加 `.jj`,保持你的工作树干净,并给你第一个 jj 日志。
### 日志:你进入 jj 的第一个窗口 (https://www.git-tower.com/blog/jujutsu#the-log-your-first-window-into-jj)https://www.git-tower.com/blog/jujutsu#the-log-your-first-window-into-jj
在深入了解工作流程之前,值得看看 jj 如何呈现你的历史记录。直接运行 `jj log` 会得到类似这样的东西:
```
@ mykqwroo [email protected] 2026-06-08 15:07:41 my-feature a1b2c3d4
│ 添加登录验证
○ kkzrwqpo [email protected] 2026-06-07 09:14:22 main b2c3d4e5
│ 更新 README
○ nnpqvstu [email protected] 2026-06-06 16:33:01 c3d4e5f6
│ 初始提交
~
```
`@` 标记你当前的工作副本提交。无需任何标志——jj 的默认日志模板已经比 `git log --oneline --graph` 更易读。你立即看到相关上下文。
注意那些短字母字符串,如 `mykqwroo`。那些是 **变更 ID**——jj 自己的提交标识符,完全独立于 Git 的 SHA 哈希。它们非常重要,我们稍后会回来讨论它们。
## 新的心智模型 (https://www.git-tower.com/blog/jujutsu#a-new-mental-model)https://www.git-tower.com/blog/jujutsu#a-new-mental-model
jj 保留了 Git 的所有结构——提交图、分布式模型、远程仓库——而是重新思考了两个基础:你的工作副本实际上*是什么*,以及如何标识提交。几乎所有下游感觉不同的东西都可以追溯到这两个想法。
### 工作副本总是一个提交 (https://www.git-tower.com/blog/jujutsu#the-working-copy-is-always-a-commit)https://www.git-tower.com/blog/jujutsu#the-working-copy-is-always-a-commit
在 Git 中,工作目录、暂存区(索引)和提交之间存在三层区别。你在工作目录中编辑文件,通过 `git add` 选择性地添加到索引,然后提交暂存的内容。
jj 将这一点合并了。没有暂存区。你的工作副本始终自动地是一个提交。当你编辑文件时,jj 会持续快照它们。你永远不会处于未提交状态——你始终在一个提交上,只是可能还没有描述。
这意味着没有 `jj add`。你只需编辑文件,jj 就会跟踪更改。当你准备好描述你做了什么时:
```
jj describe -m "添加登录验证"
```
或者,如果你想描述当前更改并立即开始一个新的空提交——最接近 Git 的提交并继续:
```
jj commit -m "添加登录验证"
# 简写为:jj describe -m "..." && jj new
```
如果你想在没有先描述的情况下开始新工作——比如,你正在进行一个功能并想捕获一个检查点:
```
jj new
```
这将密封当前提交并在其上启动一个新的工作副本提交。如果没有提供描述,jj 会在日志中将其标记为 `(no description set)`——它仍然是一个完全被跟踪的提交,只是未命名。你进行中的工作永远不会丢失——它只是一个还没有名字的提交。
这个模型的实际影响怎么强调都不为过。暂存区一直是细微 bug 的来源(`git add -A` vs `git add .`,忘记在提交前暂存文件,什么是已暂存什么是未暂存的混淆)。在 jj 中,整个问题类别根本不存在。
没有暂存区:新创建的文件自动被跟踪到工作副本提交中——无需 jj add。`describe` 命名当前更改;`jj new` 密封它并在其上启动一个新的空提交。
### 变更 ID:能存活于重写的引用 (https://www.git-tower.com/blog/jujutsu#change-ids-references-that-survive-rewrites)https://www.git-tower.com/blog/jujutsu#change-ids-references-that-survive-rewrites
在 Git 中,每个提交都有一个从其内容和父提交派生的 SHA 哈希。修改一个提交,你会得到一个新的哈希。变基一个分支,其中的每个提交都会得到一个新的哈希。这使得脚本编写和引用提交变得脆弱——每当你重塑一份工作时,它的标识就会改变。
jj 引入了 **变更 ID**:在首次创建提交时分配的、稳定的字母标识符,并在任意次重写中保持不变。修改、变基、压缩——变更 ID 保持不变。
```
jj edit mykqwroo # 在变基前后都能工作
jj edit @- # 相对:当前提交的父提交
jj edit @-- # 祖父提交
jj edit main # 按书签名称
```
`@` 符号表示“当前工作副本提交”,带 `-` 后缀的相对导航让你无需查找标识符即可在历史中移动。
Git 哈希仍然存在——jj 在 `jj show` 中同时显示两者——但它们被视为次要的。日常工作中,你通过变更 ID、书签名称或相对位置来引用提交。
jj 可读的默认日志,然后证明变更 ID 能在重写后存活:编辑提交消息会改变其 Git 提交 ID,而变更 ID 保持不变。
## 更平静的日常工作流程 (https://www.git-tower.com/blog/jujutsu#a-calmer-everyday-workflow)https://www.git-tower.com/blog/jujutsu#a-calmer-everyday-workflow
那些基础悄然重塑了日常体验。分支、切换任务和撤销错误——这些 Git 中通常带来仪式或风险的日常时刻——都明显变得更加平静。
### 书签:没有义务的分支 (https://www.git-tower.com/blog/jujutsu#bookmarks-branches-without-the-obligation)https://www.git-tower.com/blog/jujutsu#bookmarks-branches-without-the-obligation
在 Git 中,你始终在一个分支上。HEAD 指向一个分支,该分支随着每次提交而移动,切换上下文意味着提交、储藏或丢失工作。
jj 有 **书签**——指向器,工作方式类似 Git 分支——但你不必非得在一个上面。提交自由地存在于图中,当你实际需要时,你才附加一个书签名称,通常是在你准备推送时。
工作流程会感觉熟悉,但有一个关键区别:在 Git 中你在开始工作之前声明一个分支;在 jj 中你在最后,准备推送时才命名它。中间的一切看起来大致相同:
```
# ...编辑文件...
jj commit -m "添加登录验证"
# ...编辑更多文件...
jj commit -m "修复边界情况"
jj bookmark create my-feature -r @- # 准备推送时命名分支
jj git push --bookmark my-feature
```
注意 `jj commit` 一步处理了暂存和提交——没有 `git add`。我们将书签指向 `@-`(父提交),因为在 `jj commit` 之后你的工作副本是一个位于其上的新的空提交——`@-` 是实际持有工作的最后一个提交。否则流程是一样的:一堆提交,一个命名分支,一次推送。GitHub 上的 PR 工作流程从这里开始工作方式相同。
当你的分支准备好本地合并时,你有两个选择。对于快进:
```
jj rebase -b my-feature -d main # 变基到最新 main
jj bookmark set main -r my-feature # 向前移动 main 指针
jj git push --bookmark main
```
或者对于适当的合并提交:
```
jj new main my-feature # 双亲的新提交 = 合并提交
jj bookmark set main -r @ # 将 main 移动到合并提交
```
注意 `jj new` 接受多个父提交——这就是创建合并的方式。没有专门的 `merge` 命令。
构建一个提交堆栈,一开始不声明任何分支,然后只在准备推送时才用书签命名它——然后快进 main 到它上面以进行集成。
### 无需储藏舞的上下文切换 (https://www.git-tower.com/blog/jujutsu#switching-context-without-the-stash-dance)https://www.git-tower.com/blog/jujutsu#switching-context-without-the-stash-dance
这就是“工作副本总是一个提交”模型在每日工作中最显眼地带来回报的地方。
在 Git 中,带着未提交的工作切换分支是个问题。你储藏,切换,做你的工作,切换回来,弹出储藏,希望没有冲突。既繁琐又容易出错。
在 jj 中,没有脏工作副本。一切总是已提交。如果你需要切换到其他事情:
```
jj edit kkzrwqpo # 跳转到图中的任何提交
```
你进行中的工作精确地停留在它所在的位置——一个位于图中的提交。用另一个 `jj edit` 回来。无需储藏,无需仪式。
你也可以使用 **工作区** 同时维护多个在途工作流——jj 相当于 Git worktrees:
```
jj workspace add ../hotfix-workspace
```
每个工作区都有自己的工作副本提交,完全隔离。在它们之间自由切换。没有东西会泄漏。
### 撤销一切 (https://www.git-tower.com/blog/jujutsu#undo-everything)https://www.git-tower.com/blog/jujutsu#undo-everything
如果你使用 Tower (https://www.git-tower.com/),你已经知道 ⌘ + Z(或 Windows 上的 CTRL + Z)撤销最后一次 Git 操作 (https://www.git-tower.com/features/undo) 的便利。jj 将这个想法进一步推进,将撤销构建到版本控制模型本身中。
每个 jj 操作——变基、修改、描述、还原,甚至失败的命令——都被记录在 **操作日志** 中:
```
jj op log
```
```
@ abc123def bruno@bruno-mbp 2026-06-08 10:42:05 - 2026-06-08 10:42:05
│ 变基提交
│ 参数:jj rebase -b my-feature -d main
○ def456abc bruno@bruno-mbp 2026-06-08 10:41:30 - 2026-06-08 10:41:30
│ 描述提交 mykqwroo
│ 参数:jj describe -m "添加登录验证"
○ ghi789def bruno@bruno-mbp 2026-06-08 10:40:12 - 2026-06-08 10:40:12
│ 新空提交
~
```
犯了错误?
```
jj undo # 撤销最后一次操作
jj op restore abc123 # 回到操作历史中的任何特定点
```
这与 `git reflog` 有本质不同。reflog 跟踪提交指针的移动。操作日志跟踪每个结构动作——包括变基、压缩和工作区更改——并使它们全部可逆。这是一个覆盖了任何 Git 命令都无法触及的范围的安全网。
错误地放弃了一个提交——其后代会自动变基——然后观察操作日志记录它,而 `jj undo` 直接将提交带回来。
## 毫无恐惧地重塑历史 (https://www.git-tower.com/blog/jujutsu#reshaping-history-without-fear)https://www.git-tower.com/blog/jujutsu#reshaping-history-without-fear
Git 的交互式变基功能强大但令人生畏。一个文本编辑器打开,里面是一串神秘命令,一个错误的编辑就可能破坏你的历史记录,而且如果犯了错误也没有明显的恢复方法。因此,许多开发者完全避免历史编辑。
jj 用专用的、可组合的命令取代了交互式变基仪式——每个命令都可以用 `jj undo` 撤销。
### 拆分一个提交 (https://www.git-tower.com/blog/jujutsu#splitting-a-commit)https://www.git-tower.com/blog/jujutsu#splitting-a-commit
你埋头工作,然后发现你的提交做了两件不相关的事。在 Git 中,这意味着 `git rebase -i` 和一系列小心翼翼的 `edit`、reset 和重新提交。在 jj 中:
```
jj split
```
一个交互式 hunk 选择器打开。你挑选哪些更改属于第一个提交;剩下的自动成为上面的第二个提交。这特别强大的是:你可以拆分历史中的*任何*提交,而不仅仅是最近的一个:
```
jj split -r mykqwroo
```
jj 会自动变基任何后代。
一个更改触及了两个不相关的文件。内置的 hunk 选择器为第一个提交挑选 README;剩下的成为上面的第二个提交——然后每半部分都有自己的名字。
### 压缩提交 (https://www.git-tower.com/blog/jujutsu#squashing-commits)https://www.git-tower.com/blog/jujutsu#squashing-commits
相反的操作——将一个提交折叠到其父提交中:
```
jj squash # 将工作副本压缩到父提交
jj squash -r mykqwroo # 将特定提交压缩到其父提交
jj squash --interactive # 挑选特定的 hunk 移动,留下其余部分
```
`--interactive` 标志使用与 `jj split` 相同的 hunk 选择器。你可以将提交的部分内容向上移动,而将剩余部分留在原处——这在 Git 中需要几个步骤。
### 放弃提交 (https://www.git-tower.com/blog/jujutsu#abandoning-commits)https://www.git-tower.com/blog/jujutsu#abandoning-commits
要完全丢弃一个提交:
```
jj abandon mykqwroo
```
如果放弃的提交有后代,jj 会自动将它们变基到被放弃提交的父提交上。没有任何东西被悬空。而且因为这是一个操作,`jj undo` 直接将它带回来。
### 冲突是一种状态,而非障碍 (https://www.git-tower.com/blog/jujutsu#conflicts-as-a-state-not-a-blocker)https://www.git-tower.com/blog/jujutsu#conflicts-as-a-state-not-a-blocker
这可能与 Git 模型最根本的背离。
在 Git 中,合并冲突是一个硬停止。操作暂停,你必须解决每个冲突才能继续,而在此之前,你的仓库处于悬挂状态。如果你在变基过程中有十个提交,在第三个上遇到冲突,你会被卡住直到它被解决。
jj 对冲突的处理方式不同:它们是提交可以处于的一种状态,而不是障碍。当变基或合并过程中发生冲突时,jj 将其记录在*提交内部*并继续:
```
jj rebase -b my-feature -d main
# jj 标记冲突但不停止
# 所有提交都进入图中,有些标记为冲突
```
你可以检查哪些文件有冲突,并在你准备好时解决它们:
```
jj resolve --list # 列出所有冲突文件
jj
相似文章
jujutsu v0.42.0 发布
Jujutsu (jj) 版本控制系统发布了 v0.42.0。Jujutsu 是一款开源 VCS,以 Git 作为存储后端,同时提供更符合人体工程学的操作界面,其功能设计灵感来源于 Mercurial、Sapling 和 Darcs。
用Jujutsu战胜Git严谨疲劳
本文介绍了一种使用Jujutsu版本控制系统的工作流程,旨在克服在Git中保持严格提交纪律的疲劳感,允许开发者先进行杂乱提交,最后再整洁地重新组织它们。
jj v0.43.0 发布
Jujutsu v0.43.0,一个与 Git 兼容的版本控制系统,旨在易用性和强大的历史重写功能,现已发布更新。
Evan的Jujutsu教程
面向熟悉Git的用户,关于版本控制系统Jujutsu (jj)的简明教程。
jj v0.41.0 发布
Jujutsu (jj) v0.41.0 已发布,这款实验性版本控制系统迎来了更新,旨在提升易用性和冲突处理能力。