blog: Jujutsu + Radicle = ❤️

This commit is contained in:
Fintan Halpenny
2025-08-14 14:55:39 +02:00
committed by Lorenz Leutgeb
parent 38d6dd2e2c
commit 9e985935c9
2 changed files with 575 additions and 0 deletions
+574
View File
@@ -0,0 +1,574 @@
---
title: "Jujutsu + Radicle = ❤️"
subtitle: "How I use Jujutsu in tandem with Radicle"
author: "fintohaps"
authorUrl: "https://app.radicle.xyz/nodes/seed.radicle.garden/users/did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM"
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 – 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:
```ini
[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][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]. Then when have the usual upstream branch setup
for `master` for 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 piece of the puzzle in config that is an alias for easily
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 class="wide">
$ 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 class="wide">
$ 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 `r` into
the change `xn`, and I want to keep the description of `xn` and drop the
description of `r` (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 class="wide">
$ 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.
```toml
[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][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 place, 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 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 class="wide">
$ 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/seed.radicle.garden/rad:z3trNYnLWS11cJWC6BbxDs5niGo82/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 class="wide">
$ 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 update (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 is 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 -->
[Zulip]: https://radicle.zulipchat.com
[download]: https://radicle.xyz/download
[guide-user-cobs]: https://radicle.xyz/guides/user#2-collaborating-the-radicle-way
[guide-user-patch]: https://radicle.xyz/guides/user#working-with-patches
[guides]: https://radicle.xyz/guides
[rip-storage]: https://app.radicle.xyz/nodes/ash.radicle.garden/rad:z3trNYnLWS11cJWC6BbxDs5niGo82/tree/0003-storage-layout.md
[rip-storage-namespace]: https://app.radicle.xyz/nodes/ash.radicle.garden/rad:z3trNYnLWS11cJWC6BbxDs5niGo82/tree/0003-storage-layout.md#layout
+1
View File
@@ -132,6 +132,7 @@ updated, join our community on 💬 [Zulip][zulip], or <a href="{{ site.feed.pat
## Blog
- 14.08.2025 [Jujutsu + Radicle = ❤️](/2025/08/14/jj-with-radicle.html)
- 12.08.2025 [Canonical References](/2025/08/12/canonical-references.html) released. ✨
- 23.07.2025 [Using Radicle CI for Development](/2025/07/23/using-radicle-ci-for-development.html)
- 30.05.2025 [How we used Radicle with GitHub Actions](/2025/05/30/radicle-with-github-actions.html)