我们最终没有使用jj的(微不足道的)原因
摘要
Gradle解释了为何决定不采用Jujutsu (jj)作为其主要版本控制系统:jj不遵循.gitattributes,导致在Windows上像gradlew.bat这样的文件因行尾不匹配而持续出现虚假修改。他们继续使用git worktree替代。
<p><a href="https://lobste.rs/s/27dxjg/petty_reason_we_didn_t_end_up_using_jj">评论</a></p>
查看缓存全文
缓存时间: 2026/07/06 16:06
# 我们最终没有采用 jj 的(琐碎)原因
来源:https://blog.gradle.org/the-petty-reason-we-didnt-end-up-using-jj-at-gradle
一个 `.gitattributes` 的小细节为何阻止我们采用 Jujutsu 作为兼容 Git 的版本控制系统,以及我们目前使用的替代方案。
## 目录
- [实际障碍:`gradlew.bat` 与 `.gitattributes`](https://blog.gradle.org/the-petty-reason-we-didnt-end-up-using-jj-at-gradle#the-actual-blocker-gradlewbat-and-gitattributes)
- [我们坚持使用的方案:`git worktree`](https://blog.gradle.org/the-petty-reason-we-didnt-end-up-using-jj-at-gradle#what-were-sticking-with-git-worktree)
- [我们并未放弃 `jj`](https://blog.gradle.org/the-petty-reason-we-didnt-end-up-using-jj-at-gradle#were-not-done-with-jj)
## 引言
Jujutsu(`jj`,[GitHub 仓库](https://github.com/jj-vcs/jj))作为一款兼容 Git 的版本控制系统,正逐步受到关注。当使用其 Git 后端时,`jj` 将提交和文件存储在 Git 中,并能与现有 Git 仓库协同工作。但它取消了暂存区,工作副本直接表现为一个实时更新的提交。重写历史廉价且安全:编辑或重排提交后,`jj` 会自动为其后代提交变基;冲突可被记录而不中断操作;操作日志支持撤销操作。
最近,我们的一位工程师尝试在日常工作中用 `jj` 替代 `git`,但最终未能成功。原因并非 `jj` 模型本身存在根本性问题,而是 Gradle 项目在磁盘上的组织方式涉及一个小细节。我们认为这值得记录,因为 JVM 生态系统中的许多团队很可能会遇到同样的问题。
## 实际障碍:`gradlew.bat` 与 `.gitattributes`
典型的 Gradle 仓库会包含 Gradle Wrapper,其中通常有 `gradlew.bat` 脚本。该脚本使用了标签和 `goto`,当批处理文件仅包含 LF 换行符时,`cmd.exe` 可能处理出错。因此,为了在 Windows 上可靠运行,`gradlew.bat` 必须以 CRLF 结尾。我们通过 `.gitattributes` 文件强制执行此规则——该文件告知 Git 如何处理特定文件或路径,控制换行符规范化以及文件是否被视为二进制或文本:
这是 Git 的标准行为:文件在索引中以规范化格式存储,`eol=crlf` 属性告诉 Git 在检出时在工作副本中生成 CRLF。编辑后回存时,换行符会被重新规范化。第一次提交后你便会忘记此设置。
而 `jj` 不读取 `.gitattributes`,因此无法应用针对特定文件的规则。关于支持至少 `eol` 属性的问题已长期存在([jj-vcs/jj#53](https://github.com/jj-vcs/jj/issues/53)),但在该功能落地前,`jj` 唯一的控制手段是全局设置 `working-copy.eol-conversion`。这大致相当于 Git 的 `core.autocrlf`:它会统一应用于所有文件,无法单独针对 `.bat` 文件进行处理。
在共置的仓库中,问题会立即显现:Git 将 `gradlew.bat` 存储为 LF 并在检出时转换为 CRLF,但 `jj` 不遵守该属性,因此当它快照工作副本时,会看到磁盘上的 CRLF,并记录一条相对于 Git 中 LF 对象的更改。结果是在受影响的 Gradle 项目中,`gradlew.bat` 始终存在一个伪修改。这并非我们独有的问题:有用户报告出现了六位数文件数量的伪更改,多到使其放弃了 `jj`。
`jj` 的作者在 issue 中建议了一种方法:使用非共置的工作区(`jj git init --git-repo=` 放在 Git 工作副本之外)。但正如我们的工程师指出的,这并未真正解决问题:`jj` 仍会从存储的 LF 对象中生成 `gradlew.bat`,因此磁盘上会得到 LF 版本,而 LF 的 `gradlew.bat` 无法在 Windows 上运行。你也可以通过全局设置 `working-copy.eol-conversion = input-output` 强制使用 CRLF,但这会在检出时将每个文本文件都改写为 CRLF,带来新的混乱。
确实有个修复方法:将 `gradlew.bat` 直接以 CRLF 形式提交到仓库中,并停止对其进行规范化——这样 Git 和 `jj` 在存储树和工作副本中都会认可 CRLF。无需转换,没有伪差异,且仍能在 Windows 上运行。具体来说,就是将规则从 `*.bat text eol=crlf` 改为将 `.bat` 视为未规范化(`*.bat -text`),并一次性提交 CRLF 字节。这可行,但很脆弱。这种脆弱性是修复的代价:`*.bat -text` 放弃了 `text eol=crlf` 提供的自我修复能力。之前,一个意外的 LF 会在提交时被静默重新规范化;而现在,它会被保留,导致 `gradlew.bat` 在 Windows 上悄然失效,直到有人注意到。
这只是一次性转换,偶尔的编辑通常会在审查中被发现。所以这可能是一个琐碎的原因,但却是真实存在的。唯一可行的修复方案要求每个 Gradle 项目提交 CRLF 版本,并相信没有人的编辑器会静默重写它——我们不愿意将整个生态押注于此。
## 我们坚持使用的方案:`git worktree`
`jj` 的“工作区”本可让我们在共享一个仓库的情况下拥有多个工作目录,从而避免长时间的 CI 运行或进行中的修改阻塞下一个任务。而 `git worktree` 已经提供了这一功能,且足够好用:
```bash
git worktree add ../gradle-feature-x feature-x
git worktree add ../gradle-hotfix hotfix-7.6
```
两个检出,一个对象存储,无需暂存,无需二次克隆。多年来我们一直依赖这种模式进行并行工作,并在其他地方迭代时保持一个长期存在的 `main` 检出处于活跃状态。
## 我们并未放弃 `jj`
以上并非对 `jj` 在 Gradle 项目中的最终评判。仅凭 `jj` 的操作日志就值得深入探究,而 `eol` 支持是一个可解决的问题——该 issue 中已有通过 `gix-filter` 实现的相关讨论。当该功能落地后,我们将再次尝试。在那之前,`gradlew.bat` 让我们继续使用 Git,而 `git worktree` 足以胜任工作。
如果你找到了在 Gradle 仓库中干净地运行 `jj` 而避免 CRLF 混乱的方法,欢迎与我们分享。
## 讨论
相似文章
Jujutsu 如何重新思考 Git 的工作副本和冲突模型
Jujutsu (jj) 是一个新的版本控制系统,它重新思考了 Git 的工作副本和冲突模型,在保持完全 Git 兼容性的同时,提供了更一致的工作流程。
用Jujutsu战胜Git严谨疲劳
本文介绍了一种使用Jujutsu版本控制系统的工作流程,旨在克服在Git中保持严格提交纪律的疲劳感,允许开发者先进行杂乱提交,最后再整洁地重新组织它们。
jj jj jj jj jj
一篇博客文章演示了如何通过在 jj 配置文件中创建递归别名,并利用 'util exec --' 传递参数,来修复在 jj 版本控制系统中意外输入 'jj jj' 时出现的错误。
jujutsu v0.42.0 发布
Jujutsu (jj) 版本控制系统发布了 v0.42.0。Jujutsu 是一款开源 VCS,以 Git 作为存储后端,同时提供更符合人体工程学的操作界面,其功能设计灵感来源于 Mercurial、Sapling 和 Darcs。
使用 jj 生成更好的分支名称
本文介绍了 jj 版本控制系统的一个模板别名,通过将提交描述转换为 slug 来生成更易读的分支名称,从而改善默认基于变更 ID 的命名方式的可用性。