Roughly a year ago at the first ever Local First Conference, a friend and
previous colleague – Alex Good – told me about this tool called
jj (Jujutsu). We did the usual thing and I sat down beside him as he
explained it to me. My brain did the usual thing and took in some of the
information but not enough of it, and so I didn’t touch jj for quite some time
after that – but what’s good enough for Alex Good is good enough for me.
After that, I feel like I saw a post about jj once every couple of months on
Hacker News – confirmation bias anyone? It was a constant talking point during
Git Merge 2024, and now it’s a third Git tool that uses the concept of change
identifiers, so it’s a talking point on the Git mailing list.
So, fast-forward a year or so, and I’ve been using jj for quite some time
while contributing to and maintaining the [heartwood repository][heartwood] –
the home of the Radicle protocol – as well as some others. Did I have to
convince my whole team that jj should be used by all of us and we all switch
to this new workflow? No. The first piece of “magic” of jj is that it is
essentially a version control system that has a transparent layer on top of Git
itself. A change in jj will always point to a Git commit. The beauty of its
implementation is that the underlying commit can change as much as it wants,
while the jj change remains the same. This unlocks a lot of nice flows for
managing changes using jj.
So, you must be wondering by now, “How do I blend Radicle with jj?” Well,
let’s dance between the three worlds of jj, Git, and Radicle, to see how they
have melted together to form a beautiful (almost) branch-less workflow.
Radicle and Git
I won’t spend too much time here, but if you don’t know by now, Radicle works on top of Git to allow people to use this ubiquitous tool, while we benefit from its storage and protocol. When you start a Radicle repository, it’s essentially a Git repository where we use some special references and extension points of Git to cryptographically secure your commits, and store all your [social, collaborative artifacts][guide-user-cobs]. If you haven’t yet, go [download] Radicle and try it yourself using our [guides].
Note that if you’re already familiar with jj this might not be that
interesting for you, and you can skip to User Config.
My .git/config
As a maintainer of a few repositories using Radicle, I naturally need to push to
and fetch from the repository in Radicle [storage][rip-storage]. This means
that I’ll need a remote – this is set up for you when you run rad init or rad clone. This looks like:
[remote "rad"] url = rad://z371PVmDHdjJucejRoRYJcDEvD5pp fetch = +refs/heads/*:refs/remotes/rad/* fetch = +refs/tags/*:refs/remotes/rad/tags/* pushurl = rad://z371PVmDHdjJucejRoRYJcDEvD5pp/z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM [branch "master"] remote = rad merge = refs/heads/master
The rad:// URL tells git which remote helper to use
by trying to find git-remote-rad. This will handle fetching/pushing from/to
the repository identified by z371PVmDHdjJucejRoRYJcDEvD5pp. The string
z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM is my Node ID, and
identifies my machine
which makes sure that when I push, my references get stored under that
[namespace][rip-storage-namespace]. Finally, we have the usual upstream branch
setup, for master, and the rad remote – you may be familiar with this Git
config entry when you have your origin set up for another Git forge.
There’s one last puzzle piece in the configuration — an alias that simplifies creating a Radicle [patch][guide-user-patch].
[alias]
patch = push rad HEAD:refs/patches
When you push to the special reference refs/patches, the remote helper will
catch this and create a new patch for you, and in this case it will use whatever
HEAD is for the head of the patch. Note that it will use whatever rad/master
is as the base of the patch – that is to say, whatever commits are between
rad/master and HEAD (including HEAD) are the commits being proposed by the
patch. So, whenever I’m ready to make a patch, I use git patch and my
$EDITOR pops open to make my well-crafted message describing what changes I’m
making.
git fetch rad and git push rad
This is going to be brief. All I do with git now is git fetch rad (or my
peer’s remotes) to fetch any new work in Radicle storage. For pushing I will use
git push rad to create or update patches (coming up), update my version of
master, and, on the rare occasion, push a branch. That’s it! No more commit,
no more rebase, no more merge – ok I still use git log – but that’s pretty
much it. So how did I ditch all of these commands? Let’s take a look jj.
Jujutsu and Git
Let’s see how I’m using jj by visiting several of its commands and seeing how
I can use them in different scenarios.
jj new
It’s only natural to start off with jj new. This command creates a new change
in jj, as well as creating a new, empty commit for that change. Whenever I’m
going to make a new change that’s based on the master branch, I run:
$ jj new master@rad Working copy (@) now at: qxuvyurn 8e711a87 (empty) (no description set) Parent commit (@-) : xslqmmsl 62cdaf6d master@rad | deployment: Vercel → Cloudflare Workers Added 0 files, modified 0 files, removed 1 files
You’ll notice that jj spits out a Change ID and a Commit ID. You may also
notice that a prefix is highlighted – this is the unique prefix for the change
and the commit at this time! Which means that I can use qx or 8e to refer to
this particular change or commit without any ambiguity; an amazing UX, if you
ask me.
At this point, I might know what I’m going to be working on so I use jj describe to give this change a message.
$ jj describe -m "blog: Radicle and JJ" Working copy (@) now at: qxuvyurn 408133a5 (empty) blog: Radicle and JJ Parent commit (@-) : xslqmmsl 62cdaf6d master@rad | deployment: Vercel → Cloudflare Workers
I’ve now changed the description so that it no longer says (no description yet), and it now reads blog: Radicle and JJ.
So let’s see what we have here:
$ jj show qx Commit ID: 408133a5e54c80d2398be0c78cccabbd6063902d Change ID: qxuvyurnqsvupzlpzsvzzpqlmlqvoxwq Author : Fintan Halpenny <fintan.halpenny@radicle.xyz> (2025-06-10 07:52:34) Committer: Fintan Halpenny <fintan.halpenny@radicle.xyz> (2025-06-10 07:52:34) blog: Radicle and JJ
We can see that it looks similar to a Git commit, which we can also inspect using:
$ git show 408133a5e54c80d2398be0c78cccabbd6063902d
commit 408133a5e54c80d2398be0c78cccabbd6063902d
Author: Fintan Halpenny <fintan.halpenny@radicle.xyz>
Date: 2025-06-10 07:52:34 +0200
blog: Radicle and JJ
This leaves us in a position to do our usual changes within our working copy of the Git repository.
At any point where I’m looking to separate changes, I can use jj new again,
specifying any change to make a new change after the given change:
$ jj new qx Working copy (@) now at: wmsmovxx c50301c1 (empty) (no description set) Parent commit (@-) : qxuvyurn 408133a5 blog: Radicle and JJ
$ jj describe -m "blog: Radicle an JJ - add body" Working copy (@) now at: wmsmovxx a3d195ad (empty) blog: Radicle and JJ – add body Parent commit (@-) : qxuvyurn 408133a5 blog: Radicle and JJ
If I ever think I’m about to make some changes before the change I’m on, then I
can use the -B option:
$ jj new -B @ Rebased 1 descendant commits Working copy (@) now at: zvrmpyox f0635336 (empty) (no description set) Parent commit (@-) : xslqmmsl 62cdaf6d master@rad | deployment: Vercel → Cloudflare Workers Added 0 files, modified 0 files, removed 1 files
jj edit
At any point in time, I can also decide to go back to an old change and edit it, specifying the change that I want to edit:
$ jj edit qx Working copy (@) now at: qxuvyurn 408133a5 blog: Radicle and JJ Parent commit (@-) : xslqmmsl 62cdaf6d master@rad | deployment: Vercel → Cloudflare Workers
You can now forget about all those fixup! commits you were making to add
changes into previous commits. No longer are you at the mercy of making a commit
that is ahead of some other changes and you need to reorder it using git rebase. You taste that? It tastes like victory…
jj squash
Ok, so you’ve made some changes that are not related to the current change? This
happens, or at least it does to me – I’m not perfect, (un)fortunately. I can use
the power of jj new, whether after or before the current change, and combine
it with jj squash:
$ jj squash -u --from w --to qx Rebased 1 descendant commits Working copy (@) now at: qxuvyurn 1e2b0ccc (empty) blog: Radicle and JJ Parent commit (@-) : xslqmmsl 62cdaf6d master@rad | deployment: Vercel → Cloudflare Workers
This says that I’m squashing the changes from the change identified by w into
the change qx, and I want to keep the description of qx and drop the
description of w (the -u option).
For extra points, jj even includes the beautiful -i option for choosing
which changes you’re taking from the source change – via a TUI. I cannot
begin to describe how useful this is for moving around file changes and keeping
my history clean and linear.
jj rebase
The final piece of the puzzle, at least for my workflow, is jj rebase. I can
move around changes and put them on top of a destination change:
$ jj rebase -d qx -r sm Working copy (@) now at: smvvuqzo 420180e8 blog: relevant blog material Parent commit (@-) : qxuvyurn 1e2b0ccc blog: Radicle and JJ Added 1 files, modified 0 files, removed 0 files
This rebases the change sm onto the change qx. In fact, the -r can take a
set of changes (see revsets) and graft them all on top of the
destination.
My .jj/config
The final part I’ll touch on is my jj config, which can be split into the user
and repo config. Thanks to Bruno, who wrote a lot of this on Zulip, and I
cribbed it from him.
User Config
Here is my user config, and we’ll discuss a couple of the entries, and I’ll leave the rest as homework.
[aliases]
dlog = ["log", "-r"]
l = ["log", "-r", "(trunk()..@):: | (trunk()..@)-"]
fresh = ["new", "trunk()"]
tug = [
"bookmark",
"move",
"--from",
"closest_bookmark(@)",
"--to",
"closest_pushable(@)",
]
[revset-aliases]
"closest_bookmark(to)" = "heads(::to & bookmarks())"
"closest_pushable(to)" = "heads(::to & mutable() & ~description(exact:\"\") & (~empty() | merges()))"
"desc(x)" = "description(x)"
"pending()" = ".. ~ ::tags() ~ ::remote_bookmarks() ~ @ ~ private()"
"private()" = "description(glob:'wip:*') | \
description(glob:'private:*') | \
description(glob:'WIP:*') | \
description(glob:'PRIVATE:*') | \
conflicts() | \
(empty() ~ merges()) | \
description('substring-i:\"DO NOT MAIL\"')"
fresh: this allows me to have an alias forjj new master@radand usejj fresh.tug: this allows me to tug the closest bookmark to a change that can be pushed – we’ll see an example of this later.
Repository Config
And here is my repository config, which we’ll discuss a bit more in detail.
[revset-aliases]
"trunk()" = "master@rad"
"immutable_heads()" = "present(trunk()) | \
tags() | \
( \
untracked_remote_bookmarks() ~ \
untracked_remote_bookmarks(remote='rad') ~ \
untracked_remote_bookmarks(regex:'^patch(es)/',remote='rad') \
)"
[git]
write-change-id-header = true
We want to change the trunk() alias from its default in jj so that it points
to master@rad, the default branch in this particular Radicle repository. The
trunk() revset is used in a few places, for example, we saw it above in
fresh, but it is also in the next revset alias.
Some changes in jj will be marked as immutable. jj
will prevent you from changing certain changes if they are marked as immutable,
and its default value for this can be very restrictive, so instead we change it
here. First we mark changes that are present in trunk() or tags() as
immutable. Then we have untracked remote bookmarks with the set difference
operator ~. What we are not marking as immutable are bookmarks that are in
rad or that patch/patches. That is, if the changes are ours or from
patches, then they’re safe to edit. You might think, “Why are patches safe?”
Well, let’s finally get into Radicle and Jujutsu.
Radicle and Jujutsu
So here we are, a lot of build up to get to the point where I can describe how I can avoid using branches as much as possible.
Contributing Patches
We will first dive into contributing a new patch using Radicle. As described in
Jujutsu and Git, I can start making a set of changes using
jj new, editing them just how I like using jj edit, and ordering them just
the way I want with jj rebase and jj squash. During this whole time, I’m in
that, initially scary, detached HEAD state. Here it comes,
we’re going to make a patch!
Creating a New Patch
git patch
That’s it. Well, the $EDITOR opens and I write a title and a body describing
my wonderful changes, and when I’m done, the remote helper will create the patch
and announce it to the network.
✓ Patch e5f0a5a5adaa33c3b931235967e4930ece9bb617 opened ✓ Synced with 8 node(s) To rad://z3cyotNHuasWowQ2h4yF9c3tFFdvc/z6MkvZwzK64f3GuDcAs6dEcje89ddfHkBjS1v9Dkh7aCGq3C * [new reference] HEAD -> refs/patches
Updating a Patch
Let’s be honest though, my wonderful changes are rarely wonderful from the get-go. They need some polishing, and my peers always have great suggestions that I should integrate into the patch.
From here, I can find the patch using rad patch:
$ rad patch ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ ● ID Title Author Reviews Head + - Updated │ ├───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ ● 18a71ad radicle-cli: Warn when using old names of nodes self (you) - - - - - 552f4af +146 -3 4 days ago │ │ ● ed450c9 node, profile, ssh: Make key location configurable self (you) - - - - - d2f7b89 +376 -74 1 month ago │ │ ● 12bc851 node, cli: Refactor test environment self (you) - - - - - d059957 +826 -1214 1 month ago │ │ ● 3219ef8 Remove predefined bootstrap nodes istankovic z6MkmiJ…mkTV5sS - - - - - 7322e3a +138 -108 2 days ago │ │ ● 058586b Suggest the git configured default branch during init stemporus z6MkqLa…jr8xo5K - - - - - 6a1147f +16 -8 2 weeks ago │ │ ● 1015e51 build: Rewrite tagging script fintohaps z6Mkire…SQZ3voM - - - - - 149de0b +24 -12 3 weeks ago │ │ ● e85ff9a node: clean up `UploadError` fintohaps z6Mkire…SQZ3voM - - - - - b408e44 +15 -13 3 weeks ago │ │ ● c54883e Canonical References fintohaps z6Mkire…SQZ3voM - - - - - 34014a6 +4642 -1575 1 month ago │ │ ● e500399 radicle: improve inline comments fintohaps z6Mkire…SQZ3voM - - - - - e7cab63 +924 -244 1 month ago │ │ ● 6080c3c Add issue instructions yorgos-laptop z6MkrnX…CPFSFS3 - - - - - 1877285 +32 -15 1 month ago │ │ ● 40a8d72 radicle: introduce COB stream fintohaps z6Mkire…SQZ3voM - - - - - ec00acb +1178 -9 4 months ago │ │ ● 8ab3f9c Add document on how to implement a new COB type liw z6MkgEM…1b2w2FV - - - - - 5a3b095 +314 -0 1 year ago │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
Let’s say I received feedback on my Canonical References patch, I can use its
ID, the shortened version above, to inspect it:
$ rad patch show c54883e ╭───────────────────────────────────────────────────────────────────────────────────────╮ │ Title Canonical References │ │ Patch c54883e5ffb1f8a99f432e3ac79c0b728cd0dab3 │ │ Author fintohaps z6Mkire…SQZ3voM │ │ Head 34014a67b0ddc859d95e17ffc71c1ae61aff5758 │ │ Branches patch/c54883e, sync-goal │ │ Commits ahead 6, behind 49 │ │ Status open │ │ │ │ See RIP-0004[^0] for the specification. │ │ │ │ This patch is an implementation of RIP-0004. It implements the rules mechanism │ │ within the `rules` module. This is interplays with the existing `canonical` │ │ mechanisms, already defined (but slightly refactored). │ │ │ │ The `rules` are then used in pushing and fetching references. A test is added to │ │ illustrate the canonical references in action via tags. │ │ │ │ There were some incidental changes that were made to ensure the tags use case is │ │ easy for users. The first change was to add a tags refspec to remotes in order │ │ to easily fetch tags from peers -- as well ensuring those tags do not pollute │ │ the `refs/tags` namespace in the working copy. │ │ │ │ This had a knock on change where there was a bug `libgit2` that didn't allow for │ │ deleting `multivar` entries, which the new remote setup fell under. This was │ │ fixed and so we update to `git2-0.19`. │ │ │ │ As well this, the `rad id update` command would error if the payload identifier │ │ was not the project identifier. This would stop adding new payloads to extend │ │ the identity -- which was needed for introducing canonical references. │ │ │ │ [^0]: │ │ https://app.radicle.xyz/nodes/iris.radicle.xyz/rad:z3trNYnLWS11cJWC6BbxDs5niGo82/ │ │ patches/1d1ce874f7c39ecdcd8c318bbae46ffd02fe1ea8?tab=changes │ ├───────────────────────────────────────────────────────────────────────────────────────┤ │ 34014a6 radicle: refactor rule matching │ │ 0e0b77e radicle: add canonical refs to identity │ │ bbe019c radicle: canonical reference rules │ │ b3ad6f2 radicle: refactor Canonical │ │ 04277b4 radicle: store threshold in Canonical │ │ 312c6a4 meta: relax radicle-git dependencies │ ├───────────────────────────────────────────────────────────────────────────────────────┤ │ ● opened by fintohaps z6Mkire…SQZ3voM (3e97837) 10 months ago │ │ ↑ updated to c1a2cc5787f44c0a835c1deae375be04c399dd7e (58e932c) 9 months ago │ │ ↑ updated to c55494efc2e780cd6c91a1f90efdae8a3eb1c7ef (1b07774) 8 months ago │ │ ↑ updated to 583e6b3366c36cc7e67910c29a66750397a60484 (fdd5277) 7 months ago │ │ ↑ updated to d54ddef216909bdd3e54e33e4f82c45df79c00d3 (f24f9d8) 7 months ago │ │ ↑ updated to ac48ae6e75d4eaa13daed657eed24dfeabb9be94 (7d8e461) 7 months ago │ │ ↑ updated to 2b31e460db7451376dc3e346ee02b5fd597fa5c6 (040cfb7) 7 months ago │ │ ↑ updated to e1c360a1311a0a215bed6eb42e4b0c8c5c44e611 (f0dec88) 6 months ago │ │ ↑ updated to 492cfbafd31e4bac85ee73af519ddc6254b47f82 (f9cb27f) 4 months ago │ │ ↑ updated to fbdf18d7683bdac7a76149777eed5cf9bbbf5bd5 (2a64755) 4 months ago │ │ ↑ updated to 4baf32afd65f2c4b374d8f21fed6877aa804a003 (0cecae6) 4 months ago │ │ └─ ⋄ reviewed by self (you) 1 month ago │ │ ↑ updated to d2ebc70caca54a8ba508d72862c1e1c79d718129 (4515d45) 1 month ago │ │ ↑ updated to 13e9ba641c624db26b6bfe85870daf064f90e9ab (045e465) 1 month ago │ │ ↑ updated to 47495c408ccf5eec49b61c7bdb339e5f2d695a30 (a6be355) 1 month ago │ │ ↑ updated to e3bdb65d3adb94360dd3449744792f6ecb1f451f (8d08215) 1 month ago │ │ └─ ⋄ reviewed by erikli z6MkgFq…FGAnBGz 1 month ago │ │ ↑ updated to 9f779028704b4c022cbe25c0e4a9bb46dc8463ba (49fcea7) 1 month ago │ │ ↑ updated to 86ebfcaaf986edba5e77ede1be4d3c4ce33bd27c (2df7cd9) 1 month ago │ │ ↑ updated to fa9bdff35d76903f72cf24f1cccca812ae26e98c (34014a6) 1 month ago │ ╰───────────────────────────────────────────────────────────────────────────────────────╯
You can see here how non-perfect my changes are, I’m being vulnerable here.
I can now grab the value Head in the above table, and use it in jj, by
running jj new 34014a67b0ddc859d95e17ffc71c1ae61aff5758. This will drop me
onto a new change after 34014a67b0ddc859d95e17ffc71c1ae61aff5758, and then I
can use jj log -r ::@ to see all the previous changes.
Again, I use the wonderful jj edit command, or perhaps I make new changes that
I then jj squash into the relevant changes – it all depends on the scope of
the change!
Once I’m done, I push HEAD to another special refspec, using
the patch’s full identifier:
git push rad HEAD:patches/c54883e5ffb1f8a99f432e3ac79c0b728cd0dab3 -f
We use -f if we are editing the changes since this will change the underlying
commits and git will reject this. Once again, this will open my $EDITOR and
I will add a message about the changes that were made in this update.
This creates a new “revision” for the patch, preserving the older revisions. So essentially, patches in Radicle are append-only. This makes it safe for us to make edits to changes, marking them as mutable – the Git history will be preserved!
Maintaining Patches
From the maintaining perspective, the flow starts off similar to updating, where
I would look up the patch that I want to merge. If I made the patch, things are
a bit easier because the Git objects are easily accessible and I can do jj new
using the commit. If I attempt to do this with a patch that came from another
contributor, then I may run into this issue:
$ jj new 7322e3ac61669ba6dbde16bb0f7d30edf1ee85ce Error: Revision `7322e3ac61669ba6dbde16bb0f7d30edf1ee85ce` doesn't exist
The way to do this instead, is to use the remote syntax and the special
patches reference:
$ jj new patches/3219ef871dd44c7ef51693f4aeba4c2c5c0c5c7b@rad Working copy (@) now at: ooxzsqoy eb9e0803 (empty) (no description set) Parent commit (@-) : swpyssrk 7322e3ac patches/3219ef871dd44c7ef51693f4aeba4c2c5c0c5c7b patches/3219ef871dd44c7ef51693f4aeba4c2c5c0c5c7b@rad | node, cli: remove predefined bootstrap nodes
At this point, I can also look at what commits are in the patch via rad patch show, or by using jj log -r ::@. If they’re already on top master@rad, then
to merge the patch I can simply git push rad master – and the remote helper
marks the patch as merged if the canonical reference of master is updated (a
topic for another time).
If the patch isn’t on top of master@rad then I can rebase the changes using
jj rebase -d master@rad -r <base>::<head> to get the series of changes on top
of our latest. It’s then necessary to push a new revision to the patch so that
the patch can know it is being merged with the new commits – remember that I
rebased, so this changes the underlying commits.
We should update our master bookmark, and this is where the tug alias comes
in. When I run jj tug, it figures out that master is the closest bookmark
and pulls it up to the latest change that can be pushed. I can then push to
update the patch:
git push rad master:patches/3219ef871dd44c7ef51693f4aeba4c2c5c0c5c7b -f
Here I’m using master instead of HEAD – this gets around a little issue I’ve
been seeing for patches that I do not own, where the remote helper rejects the
push because it cannot resolve HEAD (a mystery left for another day).
Once the patch has been rebased, I can do the usual git push rad master to
update the canonical reference and have the patch marked as merged.
Conclusion
And our adventure ends here. We dived into how Radicle works with Git, how
Jujutsu works Git, and how I use Jujutsu to have a branch-less flow in Radicle.
This has been a dream to work with. This type of tooling feels like it enables
me a lot more when managing my changes and keeping a clean history. I was able
to do this with git rebase, but it felt like it got in the way more than it
enabled me – and I haven’t even touched on how conflicts are
easy in Jujutsu!
There is plenty of room for improvements here, some things on my list are:
- Keeping track of Jujutsu change IDs in Radicle data, which is already being looked at!
- Not needing to use
rad patch showto get metadata for managing patches, and perhaps even bookmarking patch identifiers automatically.
Come help in discussion on our [Zulip], and enjoy being Radicle 🌱👾
[heartwood]: {{ “rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5” | explore }} [Zulip]: https://radicle.zulipchat.com [download]: /download [guide-user-cobs]: /guides/user#2-collaborating-the-radicle-way [guide-user-patch]: /guides/user#working-with-patches [guides]: /guides [rip-storage]: {{ “rad:z3trNYnLWS11cJWC6BbxDs5niGo82/tree/0003-storage-layout.md” | explore }} [rip-storage-namespace]: {{ “rad:z3trNYnLWS11cJWC6BbxDs5niGo82/tree/0003-storage-layout.md#layout” | explore }}
---
title: "Jujutsu + Radicle = ❤️"
subtitle: "How I use Jujutsu in tandem with Radicle"
author: fintan
layout: blog
---
Roughly a year ago at the first ever [Local First Conference], a friend and
previous colleague – [Alex Good] – told me about this tool called
`jj` ([Jujutsu][jj]). We did the usual thing and I sat down beside him as he
explained it to me. My brain did the usual thing and took in some of the
information but not enough of it, and so I didn't touch `jj` for quite some time
after that – but what's good enough for Alex Good is good enough for me.
After that, I feel like I saw a post about `jj` once every couple of months on
Hacker News – confirmation bias anyone? It was a constant talking point during
Git Merge 2024, and now it's a third Git tool that uses the concept of change
identifiers, so it's a talking point on the [Git mailing list][git-list-change-id-topic].
So, fast-forward a year or so, and I've been using `jj` for quite some time
while contributing to and maintaining the [heartwood repository][heartwood] –
the home of the Radicle protocol – as well as some others. Did I have to
convince my whole team that `jj` should be used by all of us and we all switch
to this new workflow? No. The first piece of "magic" of `jj` is that it is
essentially a version control system that has a transparent layer on top of Git
itself. A change in `jj` will always point to a Git commit. The beauty of its
implementation is that the underlying commit can change as much as it wants,
while the `jj` change remains the same. This unlocks a lot of nice flows for
managing changes using `jj`.
So, you must be wondering by now, "How do I blend Radicle with `jj`?" Well,
let's dance between the three worlds of `jj`, Git, and Radicle, to see how they
have melted together to form a beautiful _(almost)_ branch-less workflow.
### Radicle and Git
I won't spend too much time here, but if you don't know by now, Radicle works on
top of Git to allow people to use this ubiquitous tool, while we benefit from
its storage and protocol. When you start a Radicle repository, it's essentially
a Git repository where we use some special references and extension points of
Git to cryptographically secure your commits, and store all your
[social, collaborative artifacts][guide-user-cobs]. If you haven't yet, go
[download] Radicle and try it yourself using our [guides].
Note that if you're already familiar with `jj` this might not be that
interesting for you, and you can skip to [User Config](#user-config).
#### My `.git/config`
As a maintainer of a few repositories using Radicle, I naturally need to push to
and fetch from the repository in Radicle [storage][rip-storage]. This means
that I'll need a remote – this is set up for you when you run `rad init` or `rad
clone`. This looks like:
<pre class="wide">
[remote "rad"]
url = rad://z371PVmDHdjJucejRoRYJcDEvD5pp
fetch = +refs/heads/*:refs/remotes/rad/*
fetch = +refs/tags/*:refs/remotes/rad/tags/*
pushurl = rad://z371PVmDHdjJucejRoRYJcDEvD5pp/z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM
[branch "master"]
remote = rad
merge = refs/heads/master
</pre>
The `rad://` URL tells `git` which [remote helper][git-remote-helpers] to use
by trying to find `git-remote-rad`. This will handle fetching/pushing from/to
the repository identified by `z371PVmDHdjJucejRoRYJcDEvD5pp`. The string
`z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM` is my Node ID, and
identifies my machine
which makes sure that when I push, my references get stored under that
[namespace][rip-storage-namespace]. Finally, we have the usual upstream branch
setup, for `master`, and the `rad` remote – you may be familiar with this Git
config entry when you have your `origin` set up for another Git forge.
There's one last puzzle piece in the configuration — an alias that simplifies
creating a Radicle [patch][guide-user-patch].
```ini
[alias]
patch = push rad HEAD:refs/patches
```
When you push to the special reference `refs/patches`, the remote helper will
catch this and create a new patch for you, and in this case it will use whatever
`HEAD` is for the head of the patch. Note that it will use whatever `rad/master`
is as the base of the patch – that is to say, whatever commits are between
`rad/master` and `HEAD` (including `HEAD`) are the commits being proposed by the
patch. So, whenever I'm ready to make a patch, I use `git patch` and my
`$EDITOR` pops open to make my well-crafted message describing what changes I'm
making.
#### `git fetch rad` and `git push rad`
This is going to be brief. All I do with `git` now is `git fetch rad` (or my
peer's remotes) to fetch any new work in Radicle storage. For pushing I will use
`git push rad` to create or update patches (coming up), update my version of
`master`, and, on the rare occasion, push a branch. That's it! No more `commit`,
no more `rebase`, no more `merge` – ok I still use `git log` – but that's pretty
much it. So how did I ditch all of these commands? Let's take a look `jj`.
### Jujutsu and Git
Let's see how I'm using `jj` by visiting several of its commands and seeing how
I can use them in different scenarios.
#### `jj new`
It's only natural to start off with `jj new`. This command creates a new change
in `jj`, as well as creating a new, empty commit for that change. Whenever I'm
going to make a new change that's based on the `master` branch, I run:
<pre class="wide">
$ jj new master@rad
Working copy (@) now at: <span style="font-weight:bold;"></span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:purple;">qx</span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:dimgray;">uvyurn</span><span style="font-weight:bold;"> </span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:blue;">8e</span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:dimgray;">711a87</span><span style="font-weight:bold;"> </span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:green;">(empty)</span><span style="font-weight:bold;"> </span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:green;">(no description set)</span>
Parent commit (@-) : <span style="font-weight:bold;"></span><span style="font-weight:bold;color:purple;">xsl</span><span style="filter: contrast(70%) brightness(190%);color:dimgray;">qmmsl</span> <span style="font-weight:bold;"></span><span style="font-weight:bold;color:blue;">62</span><span style="filter: contrast(70%) brightness(190%);color:dimgray;">cdaf6d</span> <span style="color:purple;">master@rad</span><span style="filter: contrast(70%) brightness(190%);color:dimgray;"> | </span>deployment: Vercel → Cloudflare Workers
Added 0 files, modified 0 files, removed 1 files
</pre>
You'll notice that `jj` spits out a Change ID and a Commit ID. You may also
notice that a prefix is highlighted – this is the unique prefix for the change
and the commit at this time! Which means that I can use `qx` or `8e` to refer to
this particular change or commit without any ambiguity; an amazing UX, if you
ask me.
At this point, I might know what I'm going to be working on so I use `jj
describe` to give this change a message.
<pre class="wide">
$ jj describe -m <span style="color:green;">"blog: Radicle and JJ"</span>
Working copy (@) now at: <span style="font-weight:bold;"></span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:purple;">qx</span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:dimgray;">uvyurn</span><span style="font-weight:bold;"> </span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:blue;">40</span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:dimgray;">8133a5</span><span style="font-weight:bold;"> </span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:green;">(empty)</span><span style="font-weight:bold;"> blog: Radicle and JJ</span>
Parent commit (@-) : <span style="font-weight:bold;"></span><span style="font-weight:bold;color:purple;">xsl</span><span style="filter: contrast(70%) brightness(190%);color:dimgray;">qmmsl</span> <span style="font-weight:bold;"></span><span style="font-weight:bold;color:blue;">62</span><span style="filter: contrast(70%) brightness(190%);color:dimgray;">cdaf6d</span> <span style="color:purple;">master@rad</span><span style="filter: contrast(70%) brightness(190%);color:dimgray;"> | </span>deployment: Vercel → Cloudflare Workers
</pre>
I've now changed the description so that it no longer says `(no description
yet)`, and it now reads `blog: Radicle and JJ`.
So let's see what we have here:
<pre class="wide">
$ jj show qx
Commit ID: <span style="color:blue;">408133a5e54c80d2398be0c78cccabbd6063902d</span>
Change ID: <span style="color:purple;">qxuvyurnqsvupzlpzsvzzpqlmlqvoxwq</span>
Author : <span style="color:olive;">Fintan Halpenny</span> <<span style="color:olive;">fintan.halpenny@radicle.xyz</span>> (<span style="color:teal;">2025-06-10 07:52:34</span>)
Committer: <span style="color:olive;">Fintan Halpenny</span> <<span style="color:olive;">fintan.halpenny@radicle.xyz</span>> (<span style="color:teal;">2025-06-10 07:52:34</span>)
blog: Radicle and JJ
</pre>
We can see that it looks similar to a Git commit, which we can also inspect
using:
<pre>
$ git show 408133a5e54c80d2398be0c78cccabbd6063902d
<span style="color:olive;">commit 408133a5e54c80d2398be0c78cccabbd6063902d</span>
Author: Fintan Halpenny <fintan.halpenny@radicle.xyz>
Date: 2025-06-10 07:52:34 +0200
blog: Radicle and JJ
</pre>
This leaves us in a position to do our usual changes within our working copy of
the Git repository.
At any point where I'm looking to separate changes, I can use `jj new` again,
specifying any change to make a new change after the given change:
<pre>
$ jj new qx
Working copy (@) now at: <span style="font-weight:bold;"></span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:purple;">w</span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:dimgray;">msmovxx</span><span style="font-weight:bold;"> </span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:blue;">c5</span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:dimgray;">0301c1</span><span style="font-weight:bold;"> </span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:green;">(empty)</span><span style="font-weight:bold;"> </span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:green;">(no description set)</span>
Parent commit (@-) : <span style="font-weight:bold;"></span><span style="font-weight:bold;color:purple;">qx</span><span style="filter: contrast(70%) brightness(190%);color:dimgray;">uvyurn</span> <span style="font-weight:bold;"></span><span style="font-weight:bold;color:blue;">40</span><span style="filter: contrast(70%) brightness(190%);color:dimgray;">8133a5</span> blog: Radicle and JJ
</pre>
<pre class="wide">
$ jj describe -m <span style="color:green;">"blog: Radicle an JJ - add body"</span>
Working copy (@) now at: <span style="font-weight:bold;"></span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:purple;">w</span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:dimgray;">msmovxx</span><span style="font-weight:bold;"> </span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:blue;">a3</span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:dimgray;">d195ad</span><span style="font-weight:bold;"> </span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:green;">(empty)</span><span style="font-weight:bold;"> blog: Radicle and JJ – add body</span>
Parent commit (@-) : <span style="font-weight:bold;"></span><span style="font-weight:bold;color:purple;">qx</span><span style="filter: contrast(70%) brightness(190%);color:dimgray;">uvyurn</span> <span style="font-weight:bold;"></span><span style="font-weight:bold;color:blue;">40</span><span style="filter: contrast(70%) brightness(190%);color:dimgray;">8133a5</span> blog: Radicle and JJ
</pre>
If I ever think I'm about to make some changes before the change I'm on, then I
can use the `-B` option:
<pre class="wide">
$ jj new -B @
Rebased 1 descendant commits
Working copy (@) now at: <span style="font-weight:bold;"></span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:purple;">zv</span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:dimgray;">rmpyox</span><span style="font-weight:bold;"> </span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:blue;">f0</span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:dimgray;">635336</span><span style="font-weight:bold;"> </span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:green;">(empty)</span><span style="font-weight:bold;"> </span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:green;">(no description set)</span>
Parent commit (@-) : <span style="font-weight:bold;"></span><span style="font-weight:bold;color:purple;">xsl</span><span style="filter: contrast(70%) brightness(190%);color:dimgray;">qmmsl</span> <span style="font-weight:bold;"></span><span style="font-weight:bold;color:blue;">62</span><span style="filter: contrast(70%) brightness(190%);color:dimgray;">cdaf6d</span> <span style="color:purple;">master@rad</span><span style="filter: contrast(70%) brightness(190%);color:dimgray;"> | </span>deployment: Vercel → Cloudflare Workers
Added 0 files, modified 0 files, removed 1 files
</pre>
#### `jj edit`
At any point in time, I can also decide to go back to an old change and edit it,
specifying the change that I want to edit:
<pre class="wide">
$ jj edit qx
Working copy (@) now at: <span style="font-weight:bold;"></span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:purple;">qx</span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:dimgray;">uvyurn</span><span style="font-weight:bold;"> </span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:blue;">40</span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:dimgray;">8133a5</span><span style="font-weight:bold;"> blog: Radicle and JJ</span>
Parent commit (@-) : <span style="font-weight:bold;"></span><span style="font-weight:bold;color:purple;">xsl</span><span style="filter: contrast(70%) brightness(190%);color:dimgray;">qmmsl</span> <span style="font-weight:bold;"></span><span style="font-weight:bold;color:blue;">62</span><span style="filter: contrast(70%) brightness(190%);color:dimgray;">cdaf6d</span> <span style="color:purple;">master@rad</span><span style="filter: contrast(70%) brightness(190%);color:dimgray;"> | </span>deployment: Vercel → Cloudflare Workers
</pre>
You can now forget about all those `fixup!` commits you were making to add
changes into previous commits. No longer are you at the mercy of making a commit
that is ahead of some other changes and you need to reorder it using `git
rebase`. You taste that? It tastes like victory...
#### `jj squash`
Ok, so you've made some changes that are not related to the current change? This
happens, or at least it does to me – I'm not perfect, (un)fortunately. I can use
the power of `jj new`, whether after or before the current change, and combine
it with `jj squash`:
<pre class="wide">
$ jj squash -u --from w --to qx
Rebased 1 descendant commits
Working copy (@) now at: <span style="font-weight:bold;"></span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:purple;">qx</span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:dimgray;">uvyurn</span><span style="font-weight:bold;"> </span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:blue;">1e</span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:dimgray;">2b0ccc</span><span style="font-weight:bold;"> </span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:green;">(empty)</span><span style="font-weight:bold;"> blog: Radicle and JJ</span>
Parent commit (@-) : <span style="font-weight:bold;"></span><span style="font-weight:bold;color:purple;">xsl</span><span style="filter: contrast(70%) brightness(190%);color:dimgray;">qmmsl</span> <span style="font-weight:bold;"></span><span style="font-weight:bold;color:blue;">62</span><span style="filter: contrast(70%) brightness(190%);color:dimgray;">cdaf6d</span> <span style="color:purple;">master@rad</span><span style="filter: contrast(70%) brightness(190%);color:dimgray;"> | </span>deployment: Vercel → Cloudflare Workers
</pre>
This says that I'm squashing the changes from the change identified by `w` into
the change `qx`, and I want to keep the description of `qx` and drop the
description of `w` (the `-u` option).
For extra points, `jj` even includes the beautiful `-i` option for _choosing_
which changes you're taking from the source change – via a TUI. I cannot
begin to describe how useful this is for moving around file changes and keeping
my history clean and linear.
#### `jj rebase`
The final piece of the puzzle, at least for my workflow, is `jj rebase`. I can
move around changes and put them on top of a destination change:
<pre>
$ jj rebase -d qx -r sm
Working copy (@) now at: <span style="font-weight:bold;"></span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:purple;">sm</span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:dimgray;">vvuqzo</span><span style="font-weight:bold;"> </span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:blue;">42</span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:dimgray;">0180e8</span> blog: relevant blog material
Parent commit (@-) : <span style="font-weight:bold;"></span><span style="font-weight:bold;color:purple;">qx</span><span style="filter: contrast(70%) brightness(190%);color:dimgray;">uvyurn</span> <span style="font-weight:bold;"></span><span style="font-weight:bold;color:blue;">1e</span><span style="filter: contrast(70%) brightness(190%);color:dimgray;">2b0ccc</span> blog: Radicle and JJ
Added 1 files, modified 0 files, removed 0 files
</pre>
This rebases the change `sm` onto the change `qx`. In fact, the `-r` can take a
set of changes (see [revsets][jj-revsets]) and graft them all on top of the
destination.
#### My `.jj/config`
The final part I'll touch on is my `jj` config, which can be split into the user
and repo config. Thanks to Bruno, who wrote a lot of this on Zulip, and I
cribbed it from him.
##### User Config
Here is my user config, and we'll discuss a couple of the entries, and I'll
leave the rest as homework.
<pre class="wide">
[aliases]
dlog = ["log", "-r"]
l = ["log", "-r", "(trunk()..@):: | (trunk()..@)-"]
fresh = ["new", "trunk()"]
tug = [
"bookmark",
"move",
"--from",
"closest_bookmark(@)",
"--to",
"closest_pushable(@)",
]
[revset-aliases]
"closest_bookmark(to)" = "heads(::to & bookmarks())"
"closest_pushable(to)" = "heads(::to & mutable() & ~description(exact:\"\") & (~empty() | merges()))"
"desc(x)" = "description(x)"
"pending()" = ".. ~ ::tags() ~ ::remote_bookmarks() ~ @ ~ private()"
"private()" = "description(glob:'wip:*') | \
description(glob:'private:*') | \
description(glob:'WIP:*') | \
description(glob:'PRIVATE:*') | \
conflicts() | \
(empty() ~ merges()) | \
description('substring-i:\"DO NOT MAIL\"')"
</pre>
- `fresh`: this allows me to have an alias for `jj new master@rad` and use `jj
fresh`.
- `tug`: this allows me to tug the closest [bookmark][jj-bookmarks] to a change
that can be pushed – we'll see an example of this later.
##### Repository Config
And here is my repository config, which we'll discuss a bit more in detail.
```toml
[revset-aliases]
"trunk()" = "master@rad"
"immutable_heads()" = "present(trunk()) | \
tags() | \
( \
untracked_remote_bookmarks() ~ \
untracked_remote_bookmarks(remote='rad') ~ \
untracked_remote_bookmarks(regex:'^patch(es)/',remote='rad') \
)"
[git]
write-change-id-header = true
```
We want to change the `trunk()` alias from its default in `jj` so that it points
to `master@rad`, the default branch in this particular Radicle repository. The
`trunk()` revset is used in a few places, for example, we saw it above in
`fresh`, but it is also in the next revset alias.
Some changes in `jj` will be marked as [immutable][jj-immutables-heads]. `jj`
will prevent you from changing certain changes if they are marked as immutable,
and its default value for this can be very restrictive, so instead we change it
here. First we mark changes that are `present` in `trunk()` or `tags()` as
immutable. Then we have untracked remote bookmarks with the set difference
operator `~`. What we are not marking as immutable are bookmarks that are in
`rad` or that `patch`/`patches`. That is, if the changes are ours or from
patches, then they're safe to edit. You might think, "Why are patches safe?"
Well, let's finally get into Radicle and Jujutsu.
### Radicle and Jujutsu
So here we are, a lot of build up to get to the point where I can describe how I
can avoid using branches as much as possible.
#### Contributing Patches
We will first dive into contributing a new patch using Radicle. As described in
[Jujutsu and Git](#jujutsu-and-git), I can start making a set of changes using
`jj new`, editing them just how I like using `jj edit`, and ordering them just
the way I want with `jj rebase` and `jj squash`. During this whole time, I'm in
that, initially scary, [detached HEAD state][detached-head]. Here it comes,
we're going to make a patch!
##### Creating a New Patch
```
git patch
```
That's it. Well, the `$EDITOR` opens and I write a title and a body describing
my wonderful changes, and when I'm done, the remote helper will create the patch
and announce it to the network.
<pre class="wide">
<span style="color:green;">✓</span> Patch <span style="color:teal;">e5f0a5a5adaa33c3b931235967e4930ece9bb617</span> opened
<span style="color:green;">✓</span> Synced with <span style="color:green;">8</span> node(s)
To rad://z3cyotNHuasWowQ2h4yF9c3tFFdvc/z6MkvZwzK64f3GuDcAs6dEcje89ddfHkBjS1v9Dkh7aCGq3C
* [new reference] HEAD -> refs/patches
</pre>
##### Updating a Patch
Let's be honest though, my wonderful changes are rarely wonderful from the
get-go. They need some polishing, and my peers always have great suggestions
that I should integrate into the patch.
From here, I can find the patch using `rad patch`:
<pre>
$ rad patch
<span style="color:#2a2a2a;">╭</span><span style="color:#2a2a2a;">───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────</span><span style="color:#2a2a2a;">╮</span>
<span style="color:#2a2a2a;">│ </span>● <span style="font-weight:bold;">ID</span> <span style="font-weight:bold;">Title</span> <span style="font-weight:bold;">Author</span> <span style="font-weight:bold;">Reviews</span> <span style="font-weight:bold;">Head</span> <span style="font-weight:bold;">+</span> <span style="font-weight:bold;">-</span> <span style="font-weight:bold;">Updated</span> <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">├</span><span style="color:#2a2a2a;">───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────</span><span style="color:#2a2a2a;">┤</span>
<span style="color:#2a2a2a;">│ </span><span style="color:green;">●</span> <span style="color:teal;">18a71ad</span> radicle-cli: Warn when using old names of nodes <span style="color:purple;">self</span> <span style="font-style:italic;color:purple;">(you)</span> - - - - - <span style="color:blue;">552f4af</span> <span style="color:green;">+146</span> <span style="color:red;">-3</span> <span style="font-style:italic;">4 days ago</span> <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span><span style="color:green;">●</span> <span style="color:teal;">ed450c9</span> node, profile, ssh: Make key location configurable <span style="color:purple;">self</span> <span style="font-style:italic;color:purple;">(you)</span> - - - - - <span style="color:blue;">d2f7b89</span> <span style="color:green;">+376</span> <span style="color:red;">-74</span> <span style="font-style:italic;">1 month ago</span> <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span><span style="color:green;">●</span> <span style="color:teal;">12bc851</span> node, cli: Refactor test environment <span style="color:purple;">self</span> <span style="font-style:italic;color:purple;">(you)</span> - - - - - <span style="color:blue;">d059957</span> <span style="color:green;">+826</span> <span style="color:red;">-1214</span> <span style="font-style:italic;">1 month ago</span> <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span><span style="color:green;">●</span> <span style="color:teal;">3219ef8</span> Remove predefined bootstrap nodes <span style="color:purple;">istankovic</span> <span style="color:purple;">z6MkmiJ…mkTV5sS</span> - - - - - <span style="color:blue;">7322e3a</span> <span style="color:green;">+138</span> <span style="color:red;">-108</span> <span style="font-style:italic;">2 days ago</span> <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span><span style="color:green;">●</span> <span style="color:teal;">058586b</span> Suggest the git configured default branch during init <span style="color:purple;">stemporus</span> <span style="color:purple;">z6MkqLa…jr8xo5K</span> - - - - - <span style="color:blue;">6a1147f</span> <span style="color:green;">+16</span> <span style="color:red;">-8</span> <span style="font-style:italic;">2 weeks ago</span> <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span><span style="color:green;">●</span> <span style="color:teal;">1015e51</span> build: Rewrite tagging script <span style="color:purple;">fintohaps</span> <span style="color:purple;">z6Mkire…SQZ3voM</span> - - - - - <span style="color:blue;">149de0b</span> <span style="color:green;">+24</span> <span style="color:red;">-12</span> <span style="font-style:italic;">3 weeks ago</span> <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span><span style="color:green;">●</span> <span style="color:teal;">e85ff9a</span> node: clean up `UploadError` <span style="color:purple;">fintohaps</span> <span style="color:purple;">z6Mkire…SQZ3voM</span> - - - - - <span style="color:blue;">b408e44</span> <span style="color:green;">+15</span> <span style="color:red;">-13</span> <span style="font-style:italic;">3 weeks ago</span> <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span><span style="color:green;">●</span> <span style="color:teal;">c54883e</span> Canonical References <span style="color:purple;">fintohaps</span> <span style="color:purple;">z6Mkire…SQZ3voM</span> - - - - - <span style="color:blue;">34014a6</span> <span style="color:green;">+4642</span> <span style="color:red;">-1575</span> <span style="font-style:italic;">1 month ago</span> <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span><span style="color:green;">●</span> <span style="color:teal;">e500399</span> radicle: improve inline comments <span style="color:purple;">fintohaps</span> <span style="color:purple;">z6Mkire…SQZ3voM</span> - - - - - <span style="color:blue;">e7cab63</span> <span style="color:green;">+924</span> <span style="color:red;">-244</span> <span style="font-style:italic;">1 month ago</span> <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span><span style="color:green;">●</span> <span style="color:teal;">6080c3c</span> Add issue instructions <span style="color:purple;">yorgos-laptop</span> <span style="color:purple;">z6MkrnX…CPFSFS3</span> - - - - - <span style="color:blue;">1877285</span> <span style="color:green;">+32</span> <span style="color:red;">-15</span> <span style="font-style:italic;">1 month ago</span> <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span><span style="color:green;">●</span> <span style="color:teal;">40a8d72</span> radicle: introduce COB stream <span style="color:purple;">fintohaps</span> <span style="color:purple;">z6Mkire…SQZ3voM</span> - - - - - <span style="color:blue;">ec00acb</span> <span style="color:green;">+1178</span> <span style="color:red;">-9</span> <span style="font-style:italic;">4 months ago</span><span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span><span style="color:green;">●</span> <span style="color:teal;">8ab3f9c</span> Add document on how to implement a new COB type <span style="color:purple;">liw</span> <span style="color:purple;">z6MkgEM…1b2w2FV</span> - - - - - <span style="color:blue;">5a3b095</span> <span style="color:green;">+314</span> <span style="color:red;">-0</span> <span style="font-style:italic;">1 year ago</span> <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">╰</span><span style="color:#2a2a2a;">───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────</span><span style="color:#2a2a2a;">╯</span>
</pre>
Let's say I received feedback on my `Canonical References` patch, I can use its
`ID`, the shortened version above, to inspect it:
<pre class="wide">
$ rad patch show c54883e
<span style="color:#2a2a2a;">╭</span><span style="color:#2a2a2a;">───────────────────────────────────────────────────────────────────────────────────────</span><span style="color:#2a2a2a;">╮</span>
<span style="color:#2a2a2a;">│ </span><span style="color:teal;">Title</span> <span style="font-weight:bold;">Canonical References</span> <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span><span style="color:teal;">Patch</span> c54883e5ffb1f8a99f432e3ac79c0b728cd0dab3 <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span><span style="color:teal;">Author</span> <span style="color:purple;">fintohaps</span> <span style="color:purple;">z6Mkire…SQZ3voM</span> <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span><span style="color:teal;">Head</span> <span style="color:blue;">34014a67b0ddc859d95e17ffc71c1ae61aff5758</span> <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span><span style="color:teal;">Branches</span> <span style="color:olive;">patch/c54883e, sync-goal</span> <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span><span style="color:teal;">Commits</span> ahead <span style="color:green;">6</span>, behind <span style="color:red;">49</span> <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span><span style="color:teal;">Status</span> <span style="color:green;">open</span> <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span> <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span>See RIP-0004[^0] for the specification. <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span> <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span>This patch is an implementation of RIP-0004. It implements the rules mechanism <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span>within the `rules` module. This is interplays with the existing `canonical` <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span>mechanisms, already defined (but slightly refactored). <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span> <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span>The `rules` are then used in pushing and fetching references. A test is added to <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span>illustrate the canonical references in action via tags. <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span> <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span>There were some incidental changes that were made to ensure the tags use case is <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span>easy for users. The first change was to add a tags refspec to remotes in order <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span>to easily fetch tags from peers -- as well ensuring those tags do not pollute <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span>the `refs/tags` namespace in the working copy. <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span> <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span>This had a knock on change where there was a bug `libgit2` that didn't allow for <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span>deleting `multivar` entries, which the new remote setup fell under. This was <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span>fixed and so we update to `git2-0.19`. <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span> <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span>As well this, the `rad id update` command would error if the payload identifier <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span>was not the project identifier. This would stop adding new payloads to extend <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span>the identity -- which was needed for introducing canonical references. <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span> <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span>[^0]: <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span>https://app.radicle.xyz/nodes/iris.radicle.xyz/rad:z3trNYnLWS11cJWC6BbxDs5niGo82/ <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span>patches/1d1ce874f7c39ecdcd8c318bbae46ffd02fe1ea8?tab=changes <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">├</span><span style="color:#2a2a2a;">───────────────────────────────────────────────────────────────────────────────────────</span><span style="color:#2a2a2a;">┤</span>
<span style="color:#2a2a2a;">│ </span><span style="color:blue;">34014a6</span> radicle: refactor rule matching <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span><span style="color:blue;">0e0b77e</span> radicle: add canonical refs to identity <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span><span style="color:blue;">bbe019c</span> radicle: canonical reference rules <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span><span style="color:blue;">b3ad6f2</span> radicle: refactor Canonical <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span><span style="color:blue;">04277b4</span> radicle: store threshold in Canonical <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span><span style="color:blue;">312c6a4</span> meta: relax radicle-git dependencies <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">├</span><span style="color:#2a2a2a;">───────────────────────────────────────────────────────────────────────────────────────</span><span style="color:#2a2a2a;">┤</span>
<span style="color:#2a2a2a;">│ </span><span style="color:green;">●</span> opened by <span style="color:purple;">fintohaps</span> <span style="color:purple;">z6Mkire…SQZ3voM</span> <span style="color:blue;">(3e97837)</span> 10 months ago <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span><span style="color:teal;">↑</span> updated to c1a2cc5787f44c0a835c1deae375be04c399dd7e <span style="color:blue;">(58e932c)</span> 9 months ago <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span><span style="color:teal;">↑</span> updated to c55494efc2e780cd6c91a1f90efdae8a3eb1c7ef <span style="color:blue;">(1b07774)</span> 8 months ago <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span><span style="color:teal;">↑</span> updated to 583e6b3366c36cc7e67910c29a66750397a60484 <span style="color:blue;">(fdd5277)</span> 7 months ago <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span><span style="color:teal;">↑</span> updated to d54ddef216909bdd3e54e33e4f82c45df79c00d3 <span style="color:blue;">(f24f9d8)</span> 7 months ago <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span><span style="color:teal;">↑</span> updated to ac48ae6e75d4eaa13daed657eed24dfeabb9be94 <span style="color:blue;">(7d8e461)</span> 7 months ago <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span><span style="color:teal;">↑</span> updated to 2b31e460db7451376dc3e346ee02b5fd597fa5c6 <span style="color:blue;">(040cfb7)</span> 7 months ago <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span><span style="color:teal;">↑</span> updated to e1c360a1311a0a215bed6eb42e4b0c8c5c44e611 <span style="color:blue;">(f0dec88)</span> 6 months ago <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span><span style="color:teal;">↑</span> updated to 492cfbafd31e4bac85ee73af519ddc6254b47f82 <span style="color:blue;">(f9cb27f)</span> 4 months ago <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span><span style="color:teal;">↑</span> updated to fbdf18d7683bdac7a76149777eed5cf9bbbf5bd5 <span style="color:blue;">(2a64755)</span> 4 months ago <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span><span style="color:teal;">↑</span> updated to 4baf32afd65f2c4b374d8f21fed6877aa804a003 <span style="color:blue;">(0cecae6)</span> 4 months ago <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span> └─ ⋄ reviewed by <span style="color:purple;">self</span> <span style="font-style:italic;color:purple;">(you)</span> 1 month ago <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span><span style="color:teal;">↑</span> updated to d2ebc70caca54a8ba508d72862c1e1c79d718129 <span style="color:blue;">(4515d45)</span> 1 month ago <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span><span style="color:teal;">↑</span> updated to 13e9ba641c624db26b6bfe85870daf064f90e9ab <span style="color:blue;">(045e465)</span> 1 month ago <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span><span style="color:teal;">↑</span> updated to 47495c408ccf5eec49b61c7bdb339e5f2d695a30 <span style="color:blue;">(a6be355)</span> 1 month ago <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span><span style="color:teal;">↑</span> updated to e3bdb65d3adb94360dd3449744792f6ecb1f451f <span style="color:blue;">(8d08215)</span> 1 month ago <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span> └─ ⋄ reviewed by <span style="color:purple;">erikli</span> <span style="color:purple;">z6MkgFq…FGAnBGz</span> 1 month ago <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span><span style="color:teal;">↑</span> updated to 9f779028704b4c022cbe25c0e4a9bb46dc8463ba <span style="color:blue;">(49fcea7)</span> 1 month ago <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span><span style="color:teal;">↑</span> updated to 86ebfcaaf986edba5e77ede1be4d3c4ce33bd27c <span style="color:blue;">(2df7cd9)</span> 1 month ago <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">│ </span><span style="color:teal;">↑</span> updated to fa9bdff35d76903f72cf24f1cccca812ae26e98c <span style="color:blue;">(34014a6)</span> 1 month ago <span style="color:#2a2a2a;"> │</span>
<span style="color:#2a2a2a;">╰</span><span style="color:#2a2a2a;">───────────────────────────────────────────────────────────────────────────────────────</span><span style="color:#2a2a2a;">╯</span>
</pre>
You can see here how non-perfect my changes are, I'm being vulnerable here.
I can now grab the value `Head` in the above table, and use it in `jj`, by
running `jj new 34014a67b0ddc859d95e17ffc71c1ae61aff5758`. This will drop me
onto a new change after `34014a67b0ddc859d95e17ffc71c1ae61aff5758`, and then I
can use `jj log -r ::@` to see all the previous changes.
Again, I use the wonderful `jj edit` command, or perhaps I make new changes that
I then `jj squash` into the relevant changes – it all depends on the scope of
the change!
Once I'm done, I push `HEAD` to another special [refspec][git-refspec], using
the patch's full identifier:
```sh
git push rad HEAD:patches/c54883e5ffb1f8a99f432e3ac79c0b728cd0dab3 -f
```
We use `-f` if we are editing the changes since this will change the underlying
commits and `git` will reject this. Once again, this will open my `$EDITOR` and
I will add a message about the changes that were made in this update.
This creates a new "revision" for the patch, preserving the older revisions.
So essentially, patches in Radicle are append-only. This makes it safe for us to
make edits to changes, marking them as mutable – the Git history will be
preserved!
#### Maintaining Patches
From the maintaining perspective, the flow starts off similar to updating, where
I would look up the patch that I want to merge. If I made the patch, things are
a bit easier because the Git objects are easily accessible and I can do `jj new`
using the commit. If I attempt to do this with a patch that came from another
contributor, then I may run into this issue:
<pre class="wide">
$ jj new 7322e3ac61669ba6dbde16bb0f7d30edf1ee85ce
<span style="font-weight:bold;"></span><span style="font-weight:bold;color:red;">Error: </span><span style="font-weight:bold;">Revision `7322e3ac61669ba6dbde16bb0f7d30edf1ee85ce` doesn't exist</span>
</pre>
The way to do this instead, is to use the remote syntax and the special
`patches` reference:
<pre>
$ jj new patches/3219ef871dd44c7ef51693f4aeba4c2c5c0c5c7b@rad
Working copy (@) now at: <span style="font-weight:bold;"></span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:purple;">oo</span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:dimgray;">xzsqoy</span><span style="font-weight:bold;"> </span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:blue;">eb</span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:dimgray;">9e0803</span><span style="font-weight:bold;"> </span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:green;">(empty)</span><span style="font-weight:bold;"> </span><span style="font-weight:bold;filter: contrast(70%) brightness(190%);color:green;">(no description set)</span>
Parent commit (@-) : <span style="font-weight:bold;"></span><span style="font-weight:bold;color:purple;">s</span><span style="filter: contrast(70%) brightness(190%);color:dimgray;">wpyssrk</span> <span style="font-weight:bold;"></span><span style="font-weight:bold;color:blue;">73</span><span style="filter: contrast(70%) brightness(190%);color:dimgray;">22e3ac</span> <span style="color:purple;">patches/3219ef871dd44c7ef51693f4aeba4c2c5c0c5c7b patches/3219ef871dd44c7ef51693f4aeba4c2c5c0c5c7b@rad</span><span style="filter: contrast(70%) brightness(190%);color:dimgray;"> | </span>node, cli: remove predefined bootstrap nodes
</pre>
At this point, I can also look at what commits are in the patch via `rad patch
show`, or by using `jj log -r ::@`. If they're already on top `master@rad`, then
to merge the patch I can simply `git push rad master` – and the remote helper
marks the patch as merged if the canonical reference of `master` is updated (a
topic for another time).
If the patch isn't on top of `master@rad` then I can rebase the changes using
`jj rebase -d master@rad -r <base>::<head>` to get the series of changes on top
of our latest. It's then necessary to push a new revision to the patch so that
the patch can know it is being merged with the new commits – remember that I
rebased, so this changes the underlying commits.
We should update our `master` bookmark, and this is where the `tug` alias comes
in. When I run `jj tug`, it figures out that `master` is the closest bookmark
and pulls it up to the latest change that can be pushed. I can then push to
update the patch:
```sh
git push rad master:patches/3219ef871dd44c7ef51693f4aeba4c2c5c0c5c7b -f
```
Here I'm using `master` instead of `HEAD` – this gets around a little issue I've
been seeing for patches that I do not own, where the remote helper rejects the
push because it cannot resolve `HEAD` (a mystery left for another day).
Once the patch has been rebased, I can do the usual `git push rad master` to
update the canonical reference and have the patch marked as merged.
## Conclusion
And our adventure ends here. We dived into how Radicle works with Git, how
Jujutsu works Git, and how I use Jujutsu to have a branch-less flow in Radicle.
This has been a dream to work with. This type of tooling feels like it enables
me a lot more when managing my changes and keeping a clean history. I was *able*
to do this with `git rebase`, but it felt like it got in the way more than it
enabled me – and I haven't even touched on how [conflicts][jj-conflicts] are
easy in Jujutsu!
There is plenty of room for improvements here, some things on my list are:
- Keeping track of Jujutsu change IDs in Radicle data, which is already being
looked at!
- Not needing to use `rad patch show` to get metadata for managing patches, and
perhaps even bookmarking patch identifiers automatically.
Come help in discussion on our [Zulip], and enjoy being Radicle 🌱👾
<!-- Other people and events. --->
[Alex Good]: https://patternist.xyz
[Local First Conference]: https://www.localfirstconf.com/
[detached-head]: https://wizardzines.com/comics/detached-head-state/
<!-- Jujutsu -->
[jj]: https://jj-vcs.github.io/jj/v0.30.0/
[jj-bookmarks]: https://jj-vcs.github.io/jj/v0.30.0/bookmarks
[jj-conflicts]: https://jj-vcs.github.io/jj/v0.30.0/conflicts
[jj-immutables-heads]: https://jj-vcs.github.io/jj/v0.30.0/config/#set-of-immutable-commits
[jj-revsets]: https://jj-vcs.github.io/jj/v0.30.0/revsets
<!-- Git -->
[git-list-change-id-topic]: https://lore.kernel.org/git/CAESOdVAspxUJKGAA58i0tvks4ZOfoGf1Aa5gPr0FXzdcywqUUw@mail.gmail.com/
[git-refspec]: https://git-scm.com/book/en/v2/Git-Internals-The-Refspec
[git-remote-helpers]: https://git-scm.com/docs/gitremote-helpers
<!-- Radicle -->
[heartwood]: {{ "rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5" | explore }}
[Zulip]: https://radicle.zulipchat.com
[download]: /download
[guide-user-cobs]: /guides/user#2-collaborating-the-radicle-way
[guide-user-patch]: /guides/user#working-with-patches
[guides]: /guides
[rip-storage]: {{ "rad:z3trNYnLWS11cJWC6BbxDs5niGo82/tree/0003-storage-layout.md" | explore }}
[rip-storage-namespace]: {{ "rad:z3trNYnLWS11cJWC6BbxDs5niGo82/tree/0003-storage-layout.md#layout" | explore }}