engineering blog at https://blog.tangled.sh
at master 351 lines 11 kB view raw view rendered
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.