From Zero to GitHub: Starting A New jj (Jujutsu) Repo
I started dabbling with jj (git alternative) recently by going through
Steve Klabnik's jujutsu
tutorial ↗. This is a great
place to start (along with many other jj resources
here ↗).
Then today as I'm wrapping up a migration script for my Kit.com
account ↗, I decide it is
worth putting up on GitHub. I then further decide that I should try my hand at
using jj to go from zero (no repo) to pushing it all up to GitHub.
I'm not that far through Steve's tutorial, so there are a lot of gaps in my knowledge. The whole process was not as straightforward as I thought it would be, so I figured it would help me and others to document the steps and missteps that I took to get the job done.
Since I ultimately want a git-backed repo that I can push up to GitHub, I need to start with:
❯ jj git init
This creates both .jj and .git directories.
quick note: I use the word "commit" in a lot of places throughout this post and
most of those should probably be "change" in jj-parlance. Hard habit to
break.
Now I'll take a look at my starting status where I can see I have a couple files in the working copy on top of the root empty commit.
❯ jj st
Working copy changes:
A .gitignore
A fetch_tags.rb
A kit_client.rb
A migrate.rb
Working copy (@) : vyykzxlt 9cf518a4 (no description set)
Parent commit (@-): zzzzzzzz 00000000 (empty) (no description set)
I could start with a jj describe and jj new to finalize a commit with
message. However I want to take a more granular start by separating these files
into a couple different commits.
I'm already at the edge of my jj knowledge here. My git brain wants to
stage only the .gitignore file, but my understanding of jj is that all the
files in the working copy are essentially staged.
To split them out, I need the jj split command. This splits out a separate
commit from the working copy with the specified files.
❯ jj split .gitignore
Selected changes : vyykzxlt 37bf49bf Intial commit, add `.gitignore` file
Remaining changes: osvwmtyo 4d81ca67 (no description set)
Working copy (@) now at: osvwmtyo 4d81ca67 (no description set)
Parent commit (@-) : vyykzxlt 37bf49bf Intial commit, add `.gitignore` file
This opens my editor (nvim), prompting me to describe the commit being split
out from the working copy. I type a message, save, and check the status.
❯ jj st
Working copy changes:
A fetch_tags.rb
A kit_client.rb
A migrate.rb
Working copy (@) : osvwmtyo 4d81ca67 (no description set)
Parent commit (@-): vyykzxlt 37bf49bf Intial commit, add `.gitignore` file
Great. I still have an undescribed working copy with the three remaining files
on top of the newly described commit that includes my .gitignore file.
I now want to split out another commit from the working copy that includes both
migrate.rb and kit_client.rb.
❯ jj split migrate.rb kit_client.rb
Selected changes : osvwmtyo 198e7323 Add migration scripts
Remaining changes: kkozmqrl abc0d59f (no description set)
Working copy (@) now at: kkozmqrl abc0d59f (no description set)
Parent commit (@-) : osvwmtyo 198e7323 Add migration scripts
I'll check the status again to see that one file remains.
❯ jj st
Working copy changes:
A fetch_tags.rb
Working copy (@) : kkozmqrl abc0d59f (no description set)
Parent commit (@-): osvwmtyo 198e7323 Add migration scripts
To commit the remaining file, I'll do a jj describe and then a jj new.
❯ jj describe -m "Add fetch_tag script for posterity"
Working copy (@) now at: kkozmqrl 37e8029e Add fetch_tag script for posterity
Parent commit (@-) : osvwmtyo 198e7323 Add migration scripts
❯ jj new
Working copy (@) now at: quuotzzw cb888326 (empty) (no description set)
Parent commit (@-) : kkozmqrl 37e8029e Add fetch_tag script for posterity
I can now look at all of my progress so far with the jj log command.
❯ jj log
@ quuotzzw [email protected] 2025-11-29 13:41:24 cb888326
│ (empty) (no description set)
○ kkozmqrl [email protected] 2025-11-29 13:39:29 git_head() 37e8029e
│ Add fetch_tag script for posterity
○ osvwmtyo [email protected] 2025-11-29 13:36:44 198e7323
│ Add migration scripts
○ vyykzxlt [email protected] 2025-11-29 13:21:25 37bf49bf
│ Intial commit, add `.gitignore` file
◆ zzzzzzzz root() 00000000
Now I'd like to put all of this up on GitHub. I create my new repo on
GitHub ↗ and grab the URL of
the origin so that I can tell jj (which I imagine is primarily telling git
under the hood) what the remote is.
❯ jj git remote add origin [email protected]:jbranchaud/kit-migration-script.git
I had to look up that command which led me to figuring out how to list the remotes that I have configured.
❯ jj git remote list
origin [email protected]:jbranchaud/kit-migration-script.git
Now I figured I could go ahead and push directly to GitHub at this point. But it
doesn't work and jj does its best to tell me why.
❯ jj git push
Warning: No bookmarks found in the default push revset: remote_bookmarks(remote=origin)..@
Nothing changed.
I remember reading that jj bookmarks roughly correspond to git
branches ↗, so
this error made sense. I guess I need to create a main bookmark.
❯ jj bookmark create main
Warning: Target revision is empty.
Created 1 bookmarks pointing to lyznpkpn 002a74fe main | (empty) (no description set)
❯ jj log
@ lyznpkpn [email protected] 2025-11-29 13:59:25 main 002a74fe
│ (empty) (no description set)
○ kkozmqrl [email protected] 2025-11-29 13:39:29 git_head() 37e8029e
│ Add fetch_tag script for posterity
○ osvwmtyo [email protected] 2025-11-29 13:36:44 198e7323
│ Add migration scripts
○ vyykzxlt [email protected] 2025-11-29 13:21:25 37bf49bf
│ Intial commit, add `.gitignore` file
◆ zzzzzzzz root() 00000000
Looking at the jj log, I can see main appear on that top empty working copy
commit. This struck me as a bit odd that main is pointing to an empty,
no-description commit, but I didn't think too much of it. I quickly found that
this wasn't quite going to work.
❯ jj git push
Warning: Refusing to create new remote bookmark main@origin
Hint: Use --allow-new to push new bookmark. Use --remote to specify the remote to push to.
Nothing changed.
❯ jj git push --allow-new
Error: Won't push commit 002a74fed749 since it has no description
Hint: Rejected commit: lyznpkpn 002a74fe main | (empty) (no description set)
Ok, so my main bookmark needs to be pointed at the previous commit, not this
new, empty working copy. A bit more digging and I find that I can set a bookmark
to point at a reference relative to the "head" (@) -- in this case to one
commit back with -r @-.
❯ jj bookmark set main -r @-
Error: Refusing to move bookmark backwards or sideways: main
Hint: Use --allow-backwards to allow it.
❯ jj bookmark set main -r @- --allow-backwards
Moved 1 bookmarks to kkozmqrl 37e8029e main* | Add fetch_tag script for posterity
jj continues to be helpful letting me know that I need the --allow-backwards
flag for this to work.
Checking jj log I can see the main bookmark showing up in the correct place.
❯ jj log
@ lyznpkpn [email protected] 2025-11-29 13:59:25 002a74fe
│ (empty) (no description set)
○ kkozmqrl [email protected] 2025-11-29 13:39:29 main git_head() 37e8029e
│ Add fetch_tag script for posterity
○ osvwmtyo [email protected] 2025-11-29 13:36:44 198e7323
│ Add migration scripts
○ vyykzxlt [email protected] 2025-11-29 13:21:25 37bf49bf
│ Intial commit, add `.gitignore` file
◆ zzzzzzzz root() 00000000
Let's try that push again, specifying the --bookmark as well:
❯ jj git push --bookmark main --allow-new
Changes to push to origin:
Add bookmark main to 37e8029e5924
remote: Resolving deltas: 100% (1/1), done.
And there I have it. I went from no repo to having a series of commits with
specific files in each commit pushed up to GitHub. Along the way I learned about
jj split, jj git remote, jj bookmark, moving a bookmark, and jj git push
along with various flags.
I'm still selling myself on jj. It seems like it is going to require thinking
a bit differently about things than I've learned with git. Many developers
that I trust continue to advocate for it, so I trust that it will be worth
effort.