forked from
tangled.org/site
engineering blog at https://blog.tangled.sh
1---
2atroot: true
3template:
4slug: stacking
5title: jujutsu on tangled
6subtitle: tangled now supports jujutsu change-ids!
7date: 2025-06-02
8image: /static/img/interdiff_difference.jpeg
9authors:
10 - name: Akshay
11 email: akshay@tangled.sh
12 handle: oppi.li
13draft: false
14---
15
16Jujutsu is built around structuring your work into
17meaningful commits. Naturally, during code-review, you'd
18expect reviewers to be able to comment on individual
19commits, and also see the evolution of a commit over time,
20as reviews are addressed. We set out to natively support
21this model of code-review on Tangled.
22
23Tangled is a new social-enabled Git collaboration platform,
24[read our intro](/intro) for more about the project.
25
26For starters, I would like to contrast the two schools of
27code-review, the "diff-soup" model and the interdiff model.
28
29## the diff-soup model
30
31When you create a PR on traditional code forges (GitHub
32specifically), the UX implicitly encourages you to address
33your code review by *adding commits* on top of your original
34set of changes:
35
36- GitHub's "Apply Suggestion" button directly commits the
37 suggestion into your PR
38- GitHub only shows you the diff of all files at once by
39 default
40- It is difficult to know what changed across force pushes
41
42Consider a hypothetical PR that adds 3 commits:
43
44```
45[c] implement new feature across the board (HEAD)
46 |
47[b] introduce new feature
48 |
49[a] some small refactor
50```
51
52And when only newly added commits are easy to review, this
53is what ends up happening:
54
55```
56[f] formatting & linting (HEAD)
57 |
58[e] update name of new feature
59 |
60[d] fix bug in refactor
61 |
62[c] implement new feature across the board
63 |
64[b] introduce new feature
65 |
66[a] some small refactor
67```
68
69It is impossible to tell what addresses what at a glance,
70there is an implicit relation between each change:
71
72```
73[f] formatting & linting
74 |
75[e] update name of new feature -------------.
76 | |
77[d] fix bug in refactor -----------. |
78 | | |
79[c] implement new feature across the board |
80 | | |
81[b] introduce new feature <-----------------'
82 | |
83[a] some small refactor <----------'
84```
85
86This has the downside of clobbering the output of `git
87blame` (if there is a bug in the new feature, you will first
88land on `e`, and upon digging further, you will land on
89`b`). This becomes incredibly tricky to navigate if reviews
90go on through multiple cycles.
91
92
93## the interdiff model
94
95With jujutsu however, you have the tools at hand to
96fearlessly edit, split, squash and rework old commits (you
97can absolutely achieve this with git and interactive
98rebasing, but it is certainly not trivial).
99
100Let's try that again:
101
102```
103[c] implement new feature across the board (HEAD)
104 |
105[b] introduce new feature
106 |
107[a] some small refactor
108```
109
110To fix the bug in the refactor:
111
112```
113$ jj edit a
114Working copy (@) now at: [a] some small refactor
115
116$ # hack hack hack
117
118$ jj log -r a::
119Rebased 2 descendant commits onto updated working copy
120[c] implement new feature across the board (HEAD)
121 |
122[b] introduce new feature
123 |
124[a] some small refactor
125```
126
127Jujutsu automatically rebases the descendants without having
128to lift a finger. Brilliant! You can repeat the same
129exercise for all review comments, and effectively, your
130PR will have evolved like so:
131
132```
133 a -> b -> c initial attempt
134 | | |
135 v v v
136 a' -> b' -> c' after first cycle of reviews
137```
138
139## the catch
140
141If you use `git rebase`, you will know that it modifies
142history and therefore changes the commit SHA. How then,
143should one tell the difference between the "old" and "new"
144state of affairs?
145
146Tools like `git-range-diff` make use of a variety of
147text-based heuristics to roughly match `a` to `a'` and `b`
148to `b'` etc.
149
150Jujutsu however, works around this by assigning stable
151"change id"s to each change (which internally point to a git
152commit, if you use the git backing). If you edit a commit,
153its SHA changes, but its change-id remains the same.
154
155And this is the essence of our new stacked PRs feature!
156
157## interdiff code review on tangled
158
159To really explain how this works, let's start with a [new
160codebase](https://tangled.sh/@oppi.li/stacking-demo/):
161
162```
163$ jj git init --colocate
164
165# -- initialize codebase --
166
167$ jj log
168@ n set: introduce Set type main HEAD 1h
169```
170
171I have kicked things off by creating a new go module that
172adds a `HashSet` data structure. My first changeset
173introduces some basic set operations:
174
175```
176$ jj log
177@ so set: introduce set difference HEAD
178├ sq set: introduce set intersection
179├ mk set: introduce set union
180├ my set: introduce basic set operations
181~
182
183$ jj git push -c @
184Changes to push to origin:
185 Add bookmark push-soqmukrvport to fc06362295bd
186```
187
188When submitting a pull request, select "Submit as stacked PRs":
189
190<figure class="max-w-[550px] m-auto flex flex-col items-center justify-center">
191 <a href="static/img/submit_stacked.jpeg">
192 <img class="my-1 h-auto max-w-full" src="static/img/submit_stacked.jpeg">
193 </a>
194 <figcaption class="text-center">Submitting Stacked PRs</figcaption>
195</figure>
196
197This submits each change as an individual pull request:
198
199<figure class="max-w-[550px] m-auto flex flex-col items-center justify-center">
200 <a href="static/img/top_of_stack.jpeg">
201 <img class="my-1 h-auto max-w-full" src="static/img/top_of_stack.jpeg">
202 </a>
203 <figcaption class="text-center">The "stack" is similar to Gerrit's relation chain</figcaption>
204</figure>
205
206After a while, I receive a couple of review comments, not on
207my entire submission, but rather, on each *individual
208change*. Additionally, the reviewer is happy with my first
209change, and has gone ahead and merged that:
210
211<div class="flex justify-center items-start gap-2">
212 <figure class="w-1/3 m-0 flex flex-col items-center">
213 <a href="static/img/basic_merged.jpeg">
214 <img class="my-1 w-full h-auto cursor-pointer" src="static/img/basic_merged.jpeg" alt="The first change has been merged">
215 </a>
216 <figcaption class="text-center">The first change has been merged</figcaption>
217 </figure>
218
219 <figure class="w-1/3 m-0 flex flex-col items-center">
220 <a href="static/img/review_union.jpeg">
221 <img class="my-1 w-full h-auto cursor-pointer" src="static/img/review_union.jpeg" alt="A review on the set union implementation">
222 </a>
223 <figcaption class="text-center">A review on the set union implementation</figcaption>
224 </figure>
225
226 <figure class="w-1/3 m-0 flex flex-col items-center">
227 <a href="static/img/review_difference.jpeg">
228 <img class="my-1 w-full h-auto cursor-pointer" src="static/img/review_difference.jpeg" alt="A review on the set difference implementation">
229 </a>
230 <figcaption class="text-center">A review on the set difference implementation</figcaption>
231 </figure>
232</div>
233
234Let us address the first review:
235
236> can you use the new `maps.Copy` api here?
237
238```
239$ jj log
240@ so set: introduce set difference push-soqmukrvport
241├ sq set: introduce set intersection
242├ mk set: introduce set union
243├ my set: introduce basic set operations
244~
245
246# let's edit the implementation of `Union`
247$ jj edit mk
248
249# hack, hack, hack
250
251$ jj log
252Rebased 2 descendant commits onto updated working copy
253├ so set: introduce set difference push-soqmukrvport*
254├ sq set: introduce set intersection
255@ mk set: introduce set union
256├ my set: introduce basic set operations
257~
258```
259
260Next, let us address the bug:
261
262> there is a logic bug here, the condition should be negated.
263
264```
265# let's edit the implementation of `Difference`
266$ jj edit so
267
268# hack, hack, hack
269```
270
271We are done addressing reviews:
272```
273$ jj git push
274Changes to push to origin:
275 Move sideways bookmark push-soqmukrvport from fc06362295bd to dfe2750f6d40
276```
277
278Upon resubmitting the PR for review, Tangled is able to
279accurately trace the commit across rewrites, using jujutsu
280change-ids, and map it to the corresponding PR:
281
282<div class="flex justify-center items-start gap-2">
283 <figure class="w-1/2 m-0 flex flex-col items-center">
284 <a href="static/img/round_2_union.jpeg">
285 <img class="my-1 w-full h-auto" src="static/img/round_2_union.jpeg" alt="PR #2 advances to the next round">
286 </a>
287 <figcaption class="text-center">PR #2 advances to the next round</figcaption>
288 </figure>
289
290 <figure class="w-1/2 m-0 flex flex-col items-center">
291 <a href="static/img/round_2_difference.jpeg">
292 <img class="my-1 w-full h-auto" src="static/img/round_2_difference.jpeg" alt="PR #4 advances to the next round">
293 </a>
294 <figcaption class="text-center">PR #4 advances to the next round</figcaption>
295 </figure>
296</div>
297
298Of note here are a few things:
299
300- The initial submission is still visible under `round #0`
301- By resubmitting, the round has simply advanced to `round
302 #1`
303- There is a helpful "interdiff" button to look at the
304 difference between the two submissions
305
306The individual diffs are still available, but most
307importantly, the reviewer can view the *evolution* of a
308change by hitting the interdiff button:
309
310<div class="flex justify-center items-start gap-2">
311 <figure class="w-1/2 m-0 flex flex-col items-center">
312 <a href="static/img/diff_1_difference.jpeg">
313 <img class="my-1 w-full h-auto" src="static/img/diff_1_difference.jpeg" alt="Diff from round #0">
314 </a>
315 <figcaption class="text-center">Diff from round #0</figcaption>
316 </figure>
317
318 <figure class="w-1/2 m-0 flex flex-col items-center">
319 <a href="static/img/diff_2_difference.jpeg">
320 <img class="my-1 w-full h-auto" src="static/img/diff_2_difference.jpeg" alt="Diff from round #1">
321 </a>
322 <figcaption class="text-center">Diff from round #1</figcaption>
323 </figure>
324</div>
325
326<figure class="max-w-[550px] m-auto flex flex-col items-center justify-center">
327 <a href="static/img/interdiff_difference.jpeg">
328 <img class="my-1 w-full h-auto" src="static/img/interdiff_difference.jpeg" alt="Interdiff between round #0 and #1">
329 </a>
330 <figcaption class="text-center">Interdiff between round #1 and #0</figcaption>
331</figure>
332
333Indeed, the logic bug has been addressed!
334
335## start stacking today
336
337If you are a jujutsu user, you can enable this flag on more
338recent versions of jujutsu:
339
340```
341λ jj --version
342jj 0.29.0-8c7ca30074767257d75e3842581b61e764d022cf
343
344# -- in your config.toml file --
345[git]
346write-change-id-header = true
347```
348
349This feature writes `change-id` headers directly into the
350git commit object, and is visible to code forges upon push,
351and allows you to stack your PRs on Tangled.