Cached at:
07/06/26, 04:06 PM
# The (Petty) Reason We Didn't End Up Using jj
Source: [https://blog.gradle.org/the-petty-reason-we-didnt-end-up-using-jj-at-gradle](https://blog.gradle.org/the-petty-reason-we-didnt-end-up-using-jj-at-gradle)
Why a small \.gitattributes detail kept us from adopting Jujutsu as our Git\-compatible VCS, and what we use instead\.
## Table of Contents
- [The actual blocker:`gradlew\.bat`and`\.gitattributes`](https://blog.gradle.org/the-petty-reason-we-didnt-end-up-using-jj-at-gradle#the-actual-blocker-gradlewbat-and-gitattributes)
- [What we’re sticking with:`git worktree`](https://blog.gradle.org/the-petty-reason-we-didnt-end-up-using-jj-at-gradle#what-were-sticking-with-git-worktree)
- [We’re not done with`jj`](https://blog.gradle.org/the-petty-reason-we-didnt-end-up-using-jj-at-gradle#were-not-done-with-jj)
## Introduction
[Jujutsu](https://github.com/jj-vcs/jj)\(`jj`\) has been picking up steam as a Git\-compatible version control system\.

When used with its Git backend,`jj`stores commits and files in Git and works with existing Git repositories, but it does away with the staging area so your working copy is represented as a real commit that updates as you go\. Rewriting history is cheap and safe: edit or reorder a commit and`jj`rebases its descendants for you; conflicts can be recorded instead of halting the operation; and operations can be undone from the operation log\.
Recently, one of our engineers set out to replace`git`with`jj`in their day\-to\-day flow\.
Unfortunately, it didn’t work out\. Not because of anything fundamental about`jj`’s model, but because of a small detail of how Gradle projects are shaped on disk\. This is worth writing down, because we suspect a lot of teams in the JVM ecosystem will hit the same wall\.
## The actual blocker:`gradlew\.bat`and`\.gitattributes`[\#](https://blog.gradle.org/the-petty-reason-we-didnt-end-up-using-jj-at-gradle#the-actual-blocker-gradlewbat-and-gitattributes)
A typical Gradle repository ships the Gradle Wrapper, which usually includes`gradlew\.bat`\. That script uses labels and`goto`, which`cmd\.exe`can mishandle when a batch file has LF\-only line endings, so`gradlew\.bat`has to be checked out with CRLF to run reliably on Windows\. We enforce this with`\.gitattributes`, a file that tells Git how to handle specific files or paths, controlling things like line\-ending normalization and whether files are treated as binary or text:
This is standard Git behavior: the file is stored normalized in the index, and the`eol=crlf`attribute tells Git to materialize CRLF in the working copy on checkout\. Edits are normalized back on the way in\. You forget about it after the first commit\.
`jj`doesn’t read`\.gitattributes`, so it can’t apply a per\-file rule\.
There’s a long\-standing[issue \(jj\-vcs/jj\#53\)](https://github.com/jj-vcs/jj/issues/53)tracking support for at least the`eol`attribute, but until it lands`jj`’s only lever is the global`working\-copy\.eol\-conversion`setting\. That’s the rough equivalent of Git’s`core\.autocrlf`: it applies to every file at once, with no way to single out`\.bat`\.
In a colocated repo, the problem shows up immediately: Git stores`gradlew\.bat`as LF and checks it out as CRLF, but`jj`doesn’t honor the attribute, so when it snapshots the working copy it sees the CRLF on disk and records a change against Git’s LF blob\. The result is a persistent phantom modification on`gradlew\.bat`in affected Gradle projects\.
And it’s not just us: one[user](https://github.com/jj-vcs/jj/issues/53#issuecomment-2832492103)reports six\-figure file counts of phantom changes, enough that they gave up on`jj`over it\.

`jj`’s author suggests one route on the issue: a non\-colocated workspace \(`jj git init \-\-git\-repo=<path\>`outside the Git working copy\)\. But as our engineer pointed out, that doesn’t actually solve it:`jj`still materializes`gradlew\.bat`from the stored LF blob, so it lands on disk as LF, and an LF`gradlew\.bat`won’t run on Windows\. You could force CRLF with the global`working\-copy\.eol\-conversion = input\-output`, but that rewrites every text file to CRLF on checkout, which is its own mess\.
There*is*a fix: commit`gradlew\.bat`with CRLF directly into the repository and stop normalizing it, so Git and`jj`agree on CRLF in both the stored tree and the working copy\. No conversion, no phantom diff, and it still runs on Windows\. Concretely that means flipping the rule from`\*\.bat text eol=crlf`to treating`\.bat`as non\-normalized \(`\*\.bat \-text`\) and committing the CRLF bytes once\. It works, but it’s fragile\. That fragility is the price of the fix:`\*\.bat \-text`gives up the self\-healing that`text eol=crlf`provided\. Before, a stray LF was silently re\-normalized on commit\. Now it sticks, and`gradlew\.bat`quietly breaks on Windows until someone notices\. It’s a one\-time conversion, and a stray edit would usually get caught in review\.
So it may be a petty reason, but it’s a real one\. The one fix that works asks every Gradle project to commit CRLF and trust that no one’s editor quietly rewrites it, and we’re not comfortable betting the ecosystem on that\.
## What we’re sticking with:`git worktree`[\#](https://blog.gradle.org/the-petty-reason-we-didnt-end-up-using-jj-at-gradle#what-were-sticking-with-git-worktree)
`jj`’s “workspaces” would have given us multiple working directories sharing one repo, so a long CI run or an in\-progress change doesn’t block the next task\. We already have that in`git worktree`, and it’s good enough:
```
git worktree add ../gradle-feature-x feature-x
git worktree add ../gradle-hotfix hotfix-7.6
```
Two checkouts, one object store, no stashing, no second clone\. We’ve leaned on this pattern for years for parallel work, and for keeping a long\-lived`main`checkout warm while iterating elsewhere\.
## We’re not done with`jj`[\#](https://blog.gradle.org/the-petty-reason-we-didnt-end-up-using-jj-at-gradle#were-not-done-with-jj)
None of this is a verdict on`jj`for Gradle projects\.`jj`’s operation log alone is worth a deeper look, and`eol`support is a tractable problem: there’s already discussion on the issue about doing it via`gix\-filter`\. When that lands, we’ll try again\. Until then,`gradlew\.bat`keeps us on Git, and`git worktree`does the job\.
If you’ve found a clean way to run`jj`against a Gradle repo without the CRLF churn, we’d love to hear about it\.
## Discuss