For the past few weeks, I've been using Jujutsu (jj) as my primary version control system instead of git. I'm still learning, but I think I'm past the point of no return.
jj has a bunch of cool things, but one of my favorite commands is jj fix.
In this article, I'll describe what jj fix does and why it's better than doing the same with git rebase + git amend workflow, and how it integrates with my Clojure formatter of choice, standard-clj.
jj fix in a nutshelljj fix updates files with formatting fixes. You can hook it up with a formatter of your liking, e.g. prettier or standard-clj or any tool that can take content from stdin and output it to stdout.
At Sharetribe, we started using standard-clj as the formatter for our Clojure code, and it works really well with jj fix.
Let's see how jj fix works.
Here's the problem.
Let's say you made a commit "Hello, world", where you added the following piece of code:
(ns myapp)
(defn hello []
(println "Hello, world")
)
Notice the indentation, which is broken.
After that, you made a second commit "Bye bye", where you changed the "Hello, world" to "Bye bye":
(ns myapp)
(defn hello []
(println "Bye bye!")
)
You are now ready to commit your changes and open a PR. But then you realize: You forgot to run the code formatter.
With Git, you have two options:
git rebase --interactive and fix the formatting in the commits that introduced them.The professional in you thinks that of course option 2 is the right thing to do, but since the interactive rebase with Git is kinda cumbersome, it's tempting to just be lazy and pick option 1.
The problem with the git rebase option is the following.
Let's do git rebase --interactive to edit the "Hello, world" commit
edit 36dc4f9 # Hello, world pick 0057aed # Bye bye # Rebase 0057aed onto 049568a (2 commands)
Now while rebase is ongoing and we are editing 36dc4f9, let's run standard-clj fix:
➜ git:(36dc4f9) ✗ standard-clj fix myapp.clj standard-clj fix v0.28.0 F /myapp.clj [3.47ms] 1 file formatted with Standard Clojure Style 👍 [1067.49ms]
standard-clj successfully fixed (F) /myapp.clj
Let's amend the formatting fixes and continue rebase.
➜ git:(36dc4f9) ✗ git add myapp.clj ➜ git:(36dc4f9) ✗ git commit --amend
Now let's continue the rebase:
➜ git:(b98ec2c) git rebase --continue Auto-merging myapp.clj CONFLICT (content): Merge conflict in myapp.clj error: could not apply 0057aed... Bye bye hint: Resolve all conflicts manually, mark them as resolved with hint: "git add/rm", then run "git rebase --continue". hint: You can instead skip this commit: run "git rebase --skip". hint: To abort and get back to the state before "git rebase", run "git rebase --abort". hint: Disable this message with "git config set advice.mergeConflict false" Could not apply 0057aed... # Bye bye
Oh no... as you might have expected, we have a conflict:
(ns myapp)
(defn hello []
<<<<<<< HEAD
(println "Hello, world"))
=======
(println "Bye bye!")
)
>>>>>>> 0057aed (Bye bye)
What happened? We formatted the earlier "Hello, world" commit. Then we continued with the rebase. The "Bye bye" commit touches the same line that got formatted, so while git tries to apply the "Bye bye" commit diff, it realizes that the underlying "Hello, world" commit had changed, so it doesn't know how to apply the diff.
jj fixLet's do the same with jj fix.
I've initialized a new jj repo with the same two commits. Also, I've configured jj fix to use standard-clj for formatting (we'll see soon how that is done).
Here's how my jj log looks:
➜ jj:(pwl) jj log @ pwlomtxr mikko@sharetribe.com 2026-05-19 22:58:54 122deafe │ Bye bye ○ qoxyznmy mikko@sharetribe.com 2026-05-19 22:57:33 9d8ce0ae │ Hello, world ◆ zzzzzzzz root() 00000000
We have the same two commits: "Hello, world" (qox) and Bye bye (pwl).
Now, let's run jj fix:
➜ jj:(pwl) jj fix Fixed 2 commits of 2 checked. Working copy (@) now at: pwlomtxr 8cb76981 Bye bye Parent commit (@-) : qoxyznmy 5fd8b557 Hello, world Added 0 files, modified 1 files, removed 0 files
Boom! 💥 jj fix found and fixed formatting issues in two commits, without any conflicts!
Let's confirm it actually formatted the code as expected and included the formatting fixes in correct commits. Here's the diff for "Hello, world" (qox) commit:
➜ jj:(pwl) jj diff -r qox
Added regular file myapp.clj:
1: (ns myapp)
2:
3: (defn hello []
4: (println "Hello, world"))
Yep, the "Hello, world" commit is now correctly formatted.
And let's also see the diff for "Bye bye" (pwl) commit:
➜ jj:(pwl) jj diff -r pwl Modified regular file myapp.clj: 1 1: (ns myapp) 2 2: 3 3: (defn hello [] - 4 : (println "Hello, world")) + 4: (println "Bye bye!"))
Looks good! No formatting fixes in this commit. The only change is the text from "Hello, world" to "Bye bye".
And of course, this works with any number of commits. Two commits are still easy to handle, but with 10+ commits where all of them have formatting issues, handling all this with git rebase would be very cumbersome.
jj fix?So you might wonder, how does jj fix do this without introducing conflicts as git did?
Here's what jj fix --help says about this (emphasis mine):
The changed files in the given revisions will be updated with any fixes determined by passing their file content through any external tools the user has configured for those files. Descendants will also be updated by passing their versions of the same files through the same tools, which will ensure that the fixes are not lost. This will never result in new conflicts.
What the help text is saying is that jj fix does not care about diffs. It edits a commit and formats the file content of the changed files (the snapshots of the files, not the diffs). Then it repeats that for the descendant commits. And indeed, this never results in conflicts. Neat!
Running jj fix with standard-clj is also very fast! The reason for this is that since jj is aware of which files changed, it doesn't run the formatter for the whole codebase, just to the files that changed. Of course, if you want to run it for the whole codebase, you can use the --include-unchanged-files flag.
jj fix to use standard-clj⚠️ Make sure you're on standard-clj v0.28.0 or newer. See the note at the end of the post for why.
jj can be configured at three levels: user, repo and workspace.
The formatting tool configuration goes into the repo level. The repo configuration file is stored outside of the repository. The way to know the path is to run:
➜ jj:(ymq) jj config path --repo /Users/mikko/.config/jj/repos/3c9fd428d14ad5b1a5e0/config.toml
So that's where you should add the configuration.
jj requires that the tool used for jj fix is able to take the content in stdin and output to stdout. standard-clj fix - does exactly that. In addition to the tool command, you need to specify glob patterns. Here's an example config that you can add to your repo-specific config.toml file:
[fix.tools.standard-clj]
command = ["standard-clj", "fix", "-"]
patterns = ["glob:'src/**/*.{clj,cljs,cljc,edn}'",
"glob:'test/**/*.{clj,cljs,cljc,edn}'",
"glob:'deps.edn'"]
standard-clj --version v0.28.0 or newerWhen I first started using standard-clj with jj fix, I noticed a weird bug. Some of my files, especially the longer ones with 1000+ lines, were sometimes just cut off. And since jj fix runs the formatting for each commit, and the files were cut off in some earlier commit, you can probably imagine that this got my repository in a super broken state 😄.
Luckily, I was able to find the bug and provide the fix. This fix was released in standard-clj version v0.28.0, so make sure you use that version or newer.