Radish alpha
r
rad:z371PVmDHdjJucejRoRYJcDEvD5pp
Radicle website including documentation and guides
Radicle
Git
radicle.xyz _posts 2025-08-14-jujutsu-with-radicle.md

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 for jj new master@rad and use jj 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 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 🌱👾

[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> &lt;<span style="color:olive;">fintan.halpenny@radicle.xyz</span>&gt; (<span style="color:teal;">2025-06-10 07:52:34</span>)
Committer: <span style="color:olive;">Fintan Halpenny</span> &lt;<span style="color:olive;">fintan.halpenny@radicle.xyz</span>&gt; (<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 &lt;fintan.halpenny@radicle.xyz&gt;
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 -&gt; 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 }}