The (Petty) Reason We Didn't End Up Using jj

Lobsters Hottest News

Summary

Gradle explains why they decided not to adopt Jujutsu (jj) as their primary version control system: jj does not honor .gitattributes, causing persistent phantom modifications on files like gradlew.bat on Windows due to line-ending mismatches. They continue using git worktree instead.

<p><a href="https://lobste.rs/s/27dxjg/petty_reason_we_didn_t_end_up_using_jj">Comments</a></p>
Original Article
View Cached Full Text

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\. ![JJ Github intro](https://blog.gradle.org/images/2026/petty-jj-v-git/image2.png) 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 Github comment](https://blog.gradle.org/images/2026/petty-jj-v-git/image1.png) `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

Similar Articles

Defeating Git Rigour Fatigue with Jujutsu

Hacker News Top

This article presents a workflow using the Jujutsu version control system to overcome the fatigue of maintaining strict commit discipline in Git, allowing developers to make messy commits and then reorganize them cleanly at the end.

jj jj jj jj jj

Lobsters Hottest

A blog post demonstrates how to fix the error when accidentally typing 'jj jj' in the jj version control system by creating a recursive alias in the jj config file that passes arguments through using 'util exec --'.

jujutsu v0.42.0 released

Lobsters Hottest

Jujutsu (jj) version control system has released v0.42.0. Jujutsu is an open-source VCS that uses Git as a storage backend while providing a more ergonomic interface with features inspired by Mercurial, Sapling, and Darcs.

Better generated branch names with jj

Lobsters Hottest

The article presents a template alias for the jj version control system to generate more readable branch names by slugifying commit descriptions, improving usability over the default change ID-based names.