engineering blog at https://blog.tangled.sh

Compare changes

Choose any two refs to compare.

+1
.gitignore
··· 2 2 build/ 3 3 static/tw.css 4 4 *.ttf 5 + *.woff2 5 6 !.gitignore
+1 -1
config.yaml
··· 1 1 preBuild: 2 2 - tailwindcss -i input.css -o static/tw.css 3 - title: tangled.sh engineering blog 3 + title: tangled engineering 4 4 # note the trailing slash! 5 5 url: "https://tangled.sh" 6 6 description: ""
+31 -20
flake.lock
··· 1 1 { 2 2 "nodes": { 3 - "ia-fonts-src": { 3 + "ibm-plex-mono-src": { 4 4 "flake": false, 5 5 "locked": { 6 - "lastModified": 1686932517, 7 - "narHash": "sha256-2T165nFfCzO65/PIHauJA//S+zug5nUwPcg8NUEydfc=", 8 - "owner": "iaolo", 9 - "repo": "iA-Fonts", 10 - "rev": "f32c04c3058a75d7ce28919ce70fe8800817491b", 11 - "type": "github" 6 + "lastModified": 1731402384, 7 + "narHash": "sha256-OwUmrPfEehLDz0fl2ChYLK8FQM2p0G1+EMrGsYEq+6g=", 8 + "type": "tarball", 9 + "url": "https://github.com/IBM/plex/releases/download/@ibm/plex-mono@1.1.0/ibm-plex-mono.zip" 12 10 }, 13 11 "original": { 14 - "owner": "iaolo", 15 - "repo": "iA-Fonts", 16 - "type": "github" 12 + "type": "tarball", 13 + "url": "https://github.com/IBM/plex/releases/download/@ibm/plex-mono@1.1.0/ibm-plex-mono.zip" 14 + } 15 + }, 16 + "inter-fonts-src": { 17 + "flake": false, 18 + "locked": { 19 + "lastModified": 1731680160, 20 + "narHash": "sha256-5vdKKvHAeZi6igrfpbOdhZlDX2/5+UvzlnCQV6DdqoQ=", 21 + "type": "tarball", 22 + "url": "https://github.com/rsms/inter/releases/download/v4.1/Inter-4.1.zip" 23 + }, 24 + "original": { 25 + "type": "tarball", 26 + "url": "https://github.com/rsms/inter/releases/download/v4.1/Inter-4.1.zip" 17 27 } 18 28 }, 19 29 "nixpkgs": { ··· 48 58 }, 49 59 "root": { 50 60 "inputs": { 51 - "ia-fonts-src": "ia-fonts-src", 61 + "ibm-plex-mono-src": "ibm-plex-mono-src", 62 + "inter-fonts-src": "inter-fonts-src", 52 63 "nixpkgs": "nixpkgs", 53 64 "vite": "vite" 54 65 } ··· 58 69 "nixpkgs": "nixpkgs_2" 59 70 }, 60 71 "locked": { 61 - "lastModified": 1737395755, 62 - "narHash": "sha256-p69EGNDGdCtdKcuu7y8VAC1q2thV5mpEQDs8/N7xliQ=", 63 - "owner": "icyphox", 64 - "repo": "go-vite", 65 - "rev": "589694da6b9c219b6627980ac86d1a6435d0176a", 66 - "type": "github" 72 + "lastModified": 1744745948, 73 + "narHash": "sha256-8Sda70LhLVDXChifA52xdaIyCS5Sr7UB2bN0Qe4v4eY=", 74 + "ref": "refs/heads/master", 75 + "rev": "92c9c9f24b0f0b78c4a81ab2d3490c756813fe71", 76 + "revCount": 89, 77 + "type": "git", 78 + "url": "https://tangled.sh/@icyphox.sh/vite" 67 79 }, 68 80 "original": { 69 - "owner": "icyphox", 70 - "repo": "go-vite", 71 - "type": "github" 81 + "type": "git", 82 + "url": "https://tangled.sh/@icyphox.sh/vite" 72 83 } 73 84 } 74 85 },
+15 -6
flake.nix
··· 3 3 4 4 inputs = { 5 5 nixpkgs.url = "github:nixos/nixpkgs"; 6 - vite.url = "github:icyphox/go-vite"; 7 - ia-fonts-src = { 8 - url = "github:iaolo/iA-Fonts"; 6 + vite = { 7 + url = "git+https://tangled.sh/@icyphox.sh/vite"; 8 + flake = true; 9 + }; 10 + inter-fonts-src = { 11 + url = "https://github.com/rsms/inter/releases/download/v4.1/Inter-4.1.zip"; 12 + flake = false; 13 + }; 14 + ibm-plex-mono-src = { 15 + url = "https://github.com/IBM/plex/releases/download/%40ibm%2Fplex-mono%401.1.0/ibm-plex-mono.zip"; 9 16 flake = false; 10 17 }; 11 18 }; ··· 14 21 { self 15 22 , nixpkgs 16 23 , vite 17 - , ia-fonts-src 24 + , inter-fonts-src 25 + , ibm-plex-mono-src 18 26 }: 19 27 let 20 28 supportedSystems = [ ··· 42 50 pkgs.tailwindcss 43 51 ]; 44 52 shellHook = '' 45 - cp -f ${ia-fonts-src}/"iA Writer Quattro"/Static/*.ttf static/fonts/ 46 - cp -f ${ia-fonts-src}/"iA Writer Mono"/Static/*.ttf static/fonts/ 53 + cp -f ${inter-fonts-src}/web/InterVariable*.woff2 static/fonts/ 54 + cp -f ${inter-fonts-src}/web/InterDisplay*.woff2 static/fonts/ 55 + cp -f ${ibm-plex-mono-src}/fonts/complete/woff2/IBMPlexMono-Regular.woff2 static/fonts/ 47 56 ''; 48 57 }; 49 58 }
+51 -104
input.css
··· 2 2 @tailwind components; 3 3 @tailwind utilities; 4 4 @layer base { 5 - @font-face { 6 - font-family: "iA Writer Quattro S"; 7 - src: url("/static/fonts/iAWriterQuattroS-Regular.ttf") 8 - format("truetype"); 9 - font-weight: normal; 10 - font-style: normal; 11 - font-display: swap; 12 - font-feature-settings: 13 - "calt" 1, 14 - "kern" 1; 15 - } 16 - @font-face { 17 - font-family: "iA Writer Quattro S"; 18 - src: url("/static/fonts/iAWriterQuattroS-Bold.ttf") format("truetype"); 19 - font-weight: bold; 20 - font-style: normal; 21 - font-display: swap; 22 - font-feature-settings: 23 - "calt" 1, 24 - "kern" 1; 25 - } 26 - @font-face { 27 - font-family: "iA Writer Quattro S"; 28 - src: url("/static/fonts/iAWriterQuattroS-Italic.ttf") format("truetype"); 29 - font-weight: normal; 30 - font-style: italic; 31 - font-display: swap; 32 - font-feature-settings: 33 - "calt" 1, 34 - "kern" 1; 35 - } 36 - @font-face { 37 - font-family: "iA Writer Quattro S"; 38 - src: url("/static/fonts/iAWriterQuattroS-BoldItalic.ttf") 39 - format("truetype"); 40 - font-weight: bold; 41 - font-style: italic; 42 - font-display: swap; 43 - font-feature-settings: 44 - "calt" 1, 45 - "kern" 1; 46 - } 5 + @font-face { 6 + font-family: "InterVariable"; 7 + src: url("/static/fonts/InterVariable.woff2") format("woff2"); 8 + font-weight: normal; 9 + font-style: normal; 10 + font-display: swap; 11 + } 12 + 13 + @font-face { 14 + font-family: "InterVariable"; 15 + src: url("/static/fonts/InterVariable-Italic.woff2") format("woff2"); 16 + font-weight: normal; 17 + font-style: italic; 18 + font-display: swap; 19 + } 20 + 21 + @font-face { 22 + font-family: "InterDisplay"; 23 + src: url("/static/fonts/InterDisplay-Regular.woff2") format("woff2"); 24 + font-weight: normal; 25 + font-style: normal; 26 + font-display: swap; 27 + } 47 28 48 - @font-face { 49 - font-family: "iA Writer Mono S"; 50 - src: url("/static/fonts/iAWriterMonoS-Regular.ttf") format("truetype"); 51 - font-weight: normal; 52 - font-style: normal; 53 - font-display: swap; 54 - font-feature-settings: 55 - "calt" 1, 56 - "kern" 1; 57 - } 58 - @font-face { 59 - font-family: "iA Writer Mono S"; 60 - src: url("/static/fonts/iAWriterMonoS-Bold.ttf") format("truetype"); 61 - font-weight: bold; 62 - font-style: normal; 63 - font-display: swap; 64 - font-feature-settings: 65 - "calt" 1, 66 - "kern" 1; 67 - } 68 - @font-face { 69 - font-family: "iA Writer Mono S"; 70 - src: url("/static/fonts/iAWriterMonoS-Italic.ttf") format("truetype"); 71 - font-weight: normal; 72 - font-style: italic; 73 - font-display: swap; 74 - font-feature-settings: 75 - "calt" 1, 76 - "kern" 1; 77 - } 78 - @font-face { 79 - font-family: "iA Writer Mono S"; 80 - src: url("/static/fonts/iAWriterMonoS-BoldItalic.ttf") 81 - format("truetype"); 82 - font-weight: bold; 83 - font-style: italic; 84 - font-display: swap; 85 - font-feature-settings: 86 - "calt" 1, 87 - "kern" 1; 88 - } 29 + @font-face { 30 + font-family: "IBMPlexMono"; 31 + src: url("/static/fonts/IBMPlexMono-Regular.woff2") format("woff2"); 32 + font-weight: normal; 33 + font-style: italic; 34 + font-display: swap; 35 + } 89 36 90 - @font-face { 91 - font-family: "Inter"; 92 - font-style: normal; 93 - font-weight: 400; 94 - font-display: swap; 95 - font-feature-settings: 96 - "calt" 1, 97 - "kern" 1; 98 - } 99 - h1 { 37 + h1 { 100 38 @apply text-2xl; 101 - @apply font-sans; 102 39 @apply text-black; 103 40 @apply font-bold; 104 41 } 105 42 106 43 ::selection { 107 44 @apply bg-yellow-400; 108 - @apply text-black; 109 45 @apply bg-opacity-30; 110 46 } 111 47 112 - html { 113 - letter-spacing: -0.01em; 114 - word-spacing: -0.07em; 115 - } 116 - 117 48 @layer base { 49 + html { 50 + font-size: 15px; 51 + } 52 + @supports (font-variation-settings: normal) { 53 + html { 54 + font-feature-settings: 'ss01' 1, 'kern' 1, 'liga' 1, 'cv05' 1, 'tnum' 1; 55 + } 56 + } 57 + 118 58 a { 119 - @apply no-underline text-black hover:underline hover:text-gray-800; 59 + @apply no-underline text-black hover:underline hover:text-gray-800 dark:text-white dark:hover:text-gray-300; 60 + } 61 + 62 + img { 63 + @apply dark:brightness-75 dark:opacity-90 border border-gray-200 rounded-sm dark:border-gray-700; 120 64 } 121 65 122 66 label { 123 - @apply block text-sm text-black; 67 + @apply block mb-2 text-gray-900 text-sm font-bold py-2 uppercase dark:text-gray-100; 124 68 } 125 69 input { 126 - @apply bg-white border border-black rounded-sm focus:ring-black p-2; 70 + @apply bg-white border border-gray-400 rounded-sm focus:ring-black p-3 dark:bg-gray-800 dark:border-gray-600 dark:text-white dark:focus:ring-gray-400; 127 71 } 128 72 textarea { 129 - @apply bg-white border border-black rounded-sm focus:ring-black p-2; 73 + @apply bg-white border border-gray-400 rounded-sm focus:ring-black p-3 dark:bg-gray-800 dark:border-gray-600 dark:text-white dark:focus:ring-gray-400; 74 + } 75 + details summary::-webkit-details-marker { 76 + display: none; 130 77 } 131 78 } 132 79
+9 -2
pages/blog/intro.md
··· 5 5 title: introducing tangled 6 6 subtitle: a git collaboration platform, built on atproto 7 7 date: 2025-03-02 8 + authors: 9 + - name: Anirudh 10 + email: anirudh@tangled.sh 11 + handle: icyphox.sh 8 12 --- 9 13 10 14 ··· 45 49 Collaborating on code isn't easy, and the tools and workflows we use 46 50 should feel natural and stay out of the way. Tangled's architecture 47 51 enables common workflows to work as you'd expect, all while remaining 48 - decentralized. 52 + decentralized. 49 53 50 54 We believe that atproto has greatly simplfied one of the hardest parts 51 55 of social media: having your friends on it. Today, we're rolling out 52 56 invite-only access to Tangled -- join us on IRC at `#tangled` on 53 - [libera.chat](https://libera.chat) and we'll get you set up. 57 + [libera.chat](https://libera.chat) and we'll get you set up. 58 + 59 + **Update**: Tangled is open to public, simply login at 60 + [tangled.sh/login](https://tangled.sh/login)! Have fun! 54 61 55 62 [pds]: https://atproto.com/guides/glossary#pds-personal-data-server 56 63 [appview]: https://docs.bsky.app/docs/advanced-guides/federation-architecture#app-views
+188
pages/blog/pulls.md
··· 1 + --- 2 + atroot: true 3 + template: 4 + slug: pulls 5 + title: the lifecycle of a pull request 6 + subtitle: we shipped a bunch of PR features recently; here's how we built it 7 + date: 2025-04-16 8 + image: /static/img/hidden-ref.png 9 + authors: 10 + - name: Anirudh 11 + email: anirudh@tangled.sh 12 + handle: icyphox.sh 13 + - name: Akshay 14 + email: akshay@tangled.sh 15 + handle: oppili.bsky.social 16 + draft: false 17 + --- 18 + 19 + We've spent the last couple of weeks building out a pull 20 + request system for Tangled, and today we want to lift the 21 + hood and show you how it works. 22 + 23 + If you're new to Tangled, [read our intro](/intro) for the 24 + full story! 25 + 26 + You have three options to contribute to a repository: 27 + 28 + - Paste a patch on the web UI 29 + - Compare two local branches (you'll see this only if you're a 30 + collaborator on the repo) 31 + - Compare across forks 32 + 33 + Whatever you choose, at the core of every PR is the patch. 34 + First, you write some code. Then, you run `git diff` to 35 + produce a patch and make everyone's lives easier, or push to 36 + a branch, and we generate it ourselves by comparing against 37 + the target. 38 + 39 + ## patch generation 40 + 41 + When you create a PR from a branch, we create a "patch" by 42 + calculating the difference between your branch and the 43 + target branch. Consider this scenario: 44 + 45 + <figure class="max-w-[550px] m-auto flex flex-col items-center justify-center"> 46 + <img class="h-auto max-w-full" src="/static/img/merge-base.png"> 47 + <figcaption class="text-center"><code>A</code> is the merge-base for 48 + <code>feature</code> and <code>main</code>.</figcaption> 49 + </figure> 50 + 51 + Your `feature` branch has advanced 2 commits since you first 52 + branched out, but in the meanwhile, `main` has also advanced 53 + 2 commits. Doing a trivial `git diff feature main` will 54 + produce a confusing patch: 55 + 56 + - the patch will apply the changes from `X` and `Y` 57 + - the patch will **revert** the changes from `B` and `C` 58 + 59 + We obviously do not want the second part! To only show the 60 + changes added by `feature`, we have to identify the 61 + "merge-base": the nearest common ancestor of `feature` and 62 + `main`. 63 + 64 + 65 + In this case, `A` is the nearest common ancestor, and 66 + subsequently, the patch calculated will contain just `X` and 67 + `Y`. 68 + 69 + ### ref comparisons across forks 70 + 71 + The plumbing described above is easy to do across two 72 + branches, but what about forks? And what if they live on 73 + different servers altogether (as they can in Tangled!)? 74 + 75 + Here's the concept: since we already have all the necessary 76 + components to compare two local refs, why not simply 77 + "localize" the remote ref? 78 + 79 + In simpler terms, we instruct Git to fetch the target branch 80 + from the original repository and store it in your fork under 81 + a special name. This approach allows us to compare your 82 + changes against the most current version of the branch 83 + you're trying to contribute to, all while remaining within 84 + your fork. 85 + 86 + <figure class="max-w-[550px] m-auto flex flex-col items-center justify-center"> 87 + <img class="h-auto max-w-full" src="/static/img/hidden-ref.png"> 88 + <figcaption class="text-center">Hidden tracking ref.</figcaption> 89 + </figure> 90 + 91 + We call this a "hidden tracking ref." When you create a pull 92 + request from a fork, we establish a refspec that tracks the 93 + remote branch, which we then use to generate a diff. A 94 + refspec is essentially a rule that tells Git how to map 95 + references between a remote and your local repository during 96 + fetch or push operations. 97 + 98 + For example, if your fork has a feature branch called 99 + `feature-1`, and you want to make a pull request to the 100 + `main` branch of the original repository, we fetch the 101 + remote `main` into a local hidden ref using a refspec like 102 + this: 103 + 104 + ``` 105 + +refs/heads/main:refs/hidden/feature-1/main 106 + ``` 107 + 108 + Since we already have a remote (`origin`, by default) to the 109 + original repository (remember, we cloned it earlier), we can 110 + use `fetch` with this refspec to bring the remote `main` 111 + branch into our local hidden ref. Each pull request gets its 112 + own hidden ref, hence the `refs/hidden/:localRef/:remoteRef` 113 + format. We keep this ref updated whenever you push new 114 + commits to your feature branch, ensuring that comparisons -- 115 + and any potential merge conflicts -- are always based on the 116 + latest state of the target branch. 117 + 118 + And just like earlier, we produce the patch by diffing your 119 + feature branch with the hidden tracking ref. Also, the entire pull 120 + request is stored as [an atproto record][atproto-record] and updated 121 + each time the patch changes. 122 + 123 + [atproto-record]: https://pdsls.dev/at://did:plc:qfpnj4og54vl56wngdriaxug/sh.tangled.repo.pull/3lmwniim2i722 124 + 125 + Neat, now that we have a patch; we can move on the hard 126 + part: code review. 127 + 128 + 129 + ## your patch does the rounds 130 + 131 + Tangled uses a "round-based" review format. Your initial 132 + submission starts "round 0". Once your submission receives 133 + scrutiny, you can address reviews and resubmit your patch. 134 + This resubmission starts "round 1". You keep whittling on 135 + your patch till it is good enough, and eventually merged (or 136 + closed if you are unlucky). 137 + 138 + <figure class="max-w-[700px] m-auto flex flex-col items-center justify-center"> 139 + <img class="h-auto max-w-full" src="/static/img/patch-pr-main.png"> 140 + <figcaption class="text-center">A new pull request with a couple 141 + rounds of reviews.</figcaption> 142 + </figure> 143 + 144 + Rounds are a far superior to standard branch-based 145 + approaches: 146 + 147 + - Submissions are immutable: how many times have your 148 + reviews gone out-of-date because the author pushed commits 149 + _during_ your review? 150 + - Reviews are attached to submissions: at a glance, it is 151 + easy to tell which comment applies to which "version" of 152 + the pull-request 153 + - The author can choose when to resubmit! They can commit as 154 + much as they want to their branch, but a new round begins 155 + when they choose to hit "resubmit" 156 + - It is possible to "interdiff" and observe changes made 157 + across submissions (this is coming very soon to Tangled!) 158 + 159 + This [post by Mitchell 160 + Hashimoto](https://mitchellh.com/writing/github-changesets) 161 + goes into further detail on what can be achieved with 162 + round-based reviews. 163 + 164 + ## future plans 165 + 166 + To close off this post, we wanted to share some of our 167 + future plans for pull requests: 168 + 169 + * `format-patch` support: both for pasting in the UI and 170 + internally. This allows us to show commits in the PR page, 171 + and offer different merge strategies to choose from 172 + (squash, rebase, ...). 173 + 174 + * Gerrit-style `refs/for/main`: we're still hashing out the 175 + details but being able to push commits to a ref to 176 + "auto-create" a PR would be super handy! 177 + 178 + * Change ID support: This will allow us to group changes 179 + together and track them across multiple commits, and to 180 + provide "history" for each change. This works great with [Jujutsu][jj]. 181 + 182 + Join us on [Discord](https://chat.tangled.sh) or 183 + `#tangled` on libera.chat (the two are bridged, so we will 184 + never miss a message!). We are always available to help 185 + setup knots, listen to feedback on features, or even 186 + shepherd contributions! 187 + 188 + [jj]: https://jj-vcs.github.io/jj/latest/
+364
pages/blog/stacking.md
··· 1 + --- 2 + atroot: true 3 + template: 4 + slug: stacking 5 + title: jujutsu-style code review 6 + subtitle: we now support jujutsu style "interdiff" code-review! 7 + date: 2025-06-02 8 + image: /static/img/hidden-ref.png 9 + authors: 10 + - name: Akshay 11 + email: akshay@tangled.sh 12 + handle: oppi.li 13 + draft: true 14 + --- 15 + 16 + Jujutsu is built around structuring your work into 17 + meaningful commits. Naturally, during code-review, you'd 18 + expect reviewers to be able to comment on individual 19 + commits, and also see the evolution of a commit over time, 20 + as reviews are addressed. We set out to natively support 21 + this model of code-review on Tangled. 22 + 23 + If you're new to Tangled, [read our intro](/intro) for more 24 + about the project. 25 + 26 + For starters, I would like to contrast the two schools of 27 + code-review, the "diff-soup" model and the interdiff model. 28 + 29 + ## the diff soup model 30 + 31 + When you create a PR on traditional code-forges (GitHub 32 + specifically), the UX implicitly encourages you to address 33 + your code review by *adding commits* on top of your original 34 + set 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 + 42 + Consider 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 + 52 + And when only *newly added commits* are easy to review, this 53 + is 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 + 69 + It is impossible to tell what addresses what at a glance, 70 + there 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 + 86 + This has the downside of clobbering the output of `git 87 + blame` (if there is a bug in the new feature, you will first 88 + land on `e`, and upon digging further, you will land on 89 + `b`). 90 + 91 + 92 + ## the interdiff model 93 + 94 + With jujutsu however, you have the tools at hand to 95 + fearlessly edit, split, squash and rework old commits (you 96 + could do this with git if you are familiar with fixup 97 + commits or interactive rebasing). 98 + 99 + Lets try that again: 100 + 101 + ``` 102 + [c] implement new feature across the board (HEAD) 103 + | 104 + [b] introduce new feature 105 + | 106 + [a] some small refactor 107 + ``` 108 + 109 + To fix the bug in the refactor: 110 + 111 + ``` 112 + $ jj edit a 113 + Working copy (@) now at: [a] some small refactor 114 + 115 + $ # hack hack hack 116 + 117 + $ jj log -r a:: 118 + Rebased 2 descendant commits onto updated working copy 119 + [c] implement new feature across the board (HEAD) 120 + | 121 + [b] introduce new feature 122 + | 123 + [a] some small refactor 124 + ``` 125 + 126 + This is the most important part: 127 + 128 + ``` 129 + Rebased 2 descendant commits onto updated working copy 130 + ``` 131 + 132 + Jujutsu automatically rebases the descendants without having 133 + to lift a finger. Brilliant! You can repeat the same 134 + exercise for all review comments, and effectively, your 135 + PR will have evolved like so: 136 + 137 + ``` 138 + a -> b -> c 139 + | | | 140 + v v v 141 + a' -> b' -> c' 142 + ``` 143 + 144 + ## the catch 145 + 146 + If you use `git rebase`, you will know that it modifies 147 + history and therefore changes the commit SHA. How then, 148 + should one tell the difference between the "old" and "new" 149 + state of affairs? 150 + 151 + Tools like `git-range-diff` make use of a variety of 152 + text-based heuristics to roughly match `a` to `a'` and `b` 153 + to `b'` etc. 154 + 155 + Jujutsu however, works around this by assigning stable 156 + "change id"s to each change (which internally point to a git 157 + commit, if you use the git backing). If you edit an old 158 + commit, its SHA changes, but its change-id remains the same. 159 + 160 + And this is the essence of our new stacked PRs feature! 161 + 162 + ## interdiff code review on tangled 163 + 164 + To really explain how this works, lets start with a [new 165 + codebase](https://tangled.sh/@oppi.li/stacking-demo/): 166 + 167 + ``` 168 + $ jj git init --colocate 169 + 170 + # -- initialize codebase -- 171 + 172 + $ jj log 173 + @ n set: introduce Set type main HEAD 1h 174 + ``` 175 + 176 + I have kicked things off by creating a new go module that 177 + adds a `HashSet` data structure. My first changeset 178 + introduces some basic set operations: 179 + 180 + ``` 181 + $ jj log 182 + @ so set: introduce set difference HEAD 183 + โ”œ sq set: introduce set intersection 184 + โ”œ mk set: introduce set union 185 + โ”œ my set: introduce basic set operations 186 + ~ 187 + 188 + $ jj git push -c @ 189 + Changes to push to origin: 190 + Add bookmark push-soqmukrvport to fc06362295bd 191 + ``` 192 + 193 + On the New Pull Request page, when submitting this branch 194 + for review, select "Submit as stacked PRs": 195 + 196 + <figure class="max-w-[550px] m-auto flex flex-col items-center justify-center"> 197 + <a href="static/img/submit_stacked.jpeg"> 198 + <img class="my-1 h-auto max-w-full" src="static/img/submit_stacked.jpeg"> 199 + </a> 200 + <figcaption class="text-center">Submitting Stacked PRs</figcaption> 201 + </figure> 202 + 203 + This submits each change as an individual pull request: 204 + 205 + <figure class="max-w-[550px] m-auto flex flex-col items-center justify-center"> 206 + <a href="static/img/top_of_stack.jpeg"> 207 + <img class="my-1 h-auto max-w-full" src="static/img/top_of_stack.jpeg"> 208 + </a> 209 + <figcaption class="text-center">The "stack" attachment is similar to Gerrit's relation chain</figcaption> 210 + </figure> 211 + 212 + After a while, I receive a couple of review comments, but 213 + not on my entire submission, but rather, on each *individual 214 + change*. Additionally, the reviewer is happy with my first 215 + change, and has gone ahead and merged that: 216 + 217 + <div class="flex justify-center items-start gap-2"> 218 + <figure class="w-1/3 m-0 flex flex-col items-center"> 219 + <a href="static/img/basic_merged.jpeg"> 220 + <img class="my-1 w-full h-auto cursor-pointer" src="static/img/basic_merged.jpeg" alt="The first change has been merged"> 221 + </a> 222 + <figcaption class="text-center">The first change has been merged</figcaption> 223 + </figure> 224 + 225 + <figure class="w-1/3 m-0 flex flex-col items-center"> 226 + <a href="static/img/review_union.jpeg"> 227 + <img class="my-1 w-full h-auto cursor-pointer" src="static/img/review_union.jpeg" alt="A review on the set union implementation"> 228 + </a> 229 + <figcaption class="text-center">A review on the set union implementation</figcaption> 230 + </figure> 231 + 232 + <figure class="w-1/3 m-0 flex flex-col items-center"> 233 + <a href="static/img/review_difference.jpeg"> 234 + <img class="my-1 w-full h-auto cursor-pointer" src="static/img/review_difference.jpeg" alt="A review on the set difference implementation"> 235 + </a> 236 + <figcaption class="text-center">A review on the set difference implementation</figcaption> 237 + </figure> 238 + </div> 239 + 240 + Let us address the first review: 241 + 242 + > can you use the new `maps.Copy` api here? 243 + 244 + ``` 245 + $ jj log 246 + @ so set: introduce set difference push-soqmukrvport 247 + โ”œ sq set: introduce set intersection 248 + โ”œ mk set: introduce set union 249 + โ”œ my set: introduce basic set operations 250 + ~ 251 + 252 + # lets edit the implementation of `Union` 253 + $ jj edit mk 254 + 255 + # hack, hack, hack 256 + 257 + $ jj log 258 + Rebased 2 descendant commits onto updated working copy 259 + โ”œ so set: introduce set difference push-soqmukrvport* 260 + โ”œ sq set: introduce set intersection 261 + @ mk set: introduce set union 262 + โ”œ my set: introduce basic set operations 263 + ~ 264 + ``` 265 + 266 + Next, let us address the bug: 267 + 268 + > there is a logic bug here, the condition should be negated. 269 + 270 + ``` 271 + # lets edit the implementation of `Difference` 272 + $ jj edit so 273 + 274 + # hack, hack, hack 275 + 276 + $ jj log 277 + @ so set: introduce set difference push-soqmukrvport* 278 + โ”œ sq set: introduce set intersection 279 + โ”œ mk set: introduce set union 280 + โ”œ my set: introduce basic set operations 281 + ~ 282 + ``` 283 + 284 + We are done addressing reviews, let us push our code: 285 + ``` 286 + $ jj git push 287 + Changes to push to origin: 288 + Move sideways bookmark push-soqmukrvport from fc06362295bd to dfe2750f6d40 289 + ``` 290 + 291 + Upon resubmitting the PR for review: 292 + 293 + <div class="flex justify-center items-start gap-2"> 294 + <figure class="w-1/2 m-0 flex flex-col items-center"> 295 + <a href="static/img/round_2_union.jpeg"> 296 + <img class="my-1 w-full h-auto" src="static/img/round_2_union.jpeg" alt="PR #2 advances to the next round"> 297 + </a> 298 + <figcaption class="text-center">PR #2 advances to the next round</figcaption> 299 + </figure> 300 + 301 + <figure class="w-1/2 m-0 flex flex-col items-center"> 302 + <a href="static/img/round_2_difference.jpeg"> 303 + <img class="my-1 w-full h-auto" src="static/img/round_2_difference.jpeg" alt="PR #4 advances to the next round"> 304 + </a> 305 + <figcaption class="text-center">PR #4 advances to the next round</figcaption> 306 + </figure> 307 + </div> 308 + 309 + Of note here are a few things: 310 + 311 + - The initial submission is still visible under `round #0` 312 + - By resubmitting, the round has simply advanced to `round 313 + #1` 314 + - There is a helpful "interdiff" button to look at the 315 + difference between the two submissions 316 + 317 + The individual diffs are still available, but most 318 + importantly, the reviewer can view the *evolution* of a 319 + change by hitting the interdiff button: 320 + 321 + <div class="flex justify-center items-start gap-2"> 322 + <figure class="w-1/2 m-0 flex flex-col items-center"> 323 + <a href="static/img/diff_1_difference.jpeg"> 324 + <img class="my-1 w-full h-auto" src="static/img/diff_1_difference.jpeg" alt="Diff from round #0"> 325 + </a> 326 + <figcaption class="text-center">Diff from round #0</figcaption> 327 + </figure> 328 + 329 + <figure class="w-1/2 m-0 flex flex-col items-center"> 330 + <a href="static/img/diff_2_difference.jpeg"> 331 + <img class="my-1 w-full h-auto" src="static/img/diff_2_difference.jpeg" alt="Diff from round #1"> 332 + </a> 333 + <figcaption class="text-center">Diff from round #1</figcaption> 334 + </figure> 335 + </div> 336 + 337 + <figure class="max-w-[550px] m-auto flex flex-col items-center justify-center"> 338 + <a href="static/img/interdiff_difference.jpeg"> 339 + <img class="my-1 w-full h-auto" src="static/img/interdiff_difference.jpeg" alt="Interdiff between round #0 and #1"> 340 + </a> 341 + <figcaption class="text-center">Interdiff between round #1 and #0</figcaption> 342 + </figure> 343 + 344 + Indeed, the logic bug has been addressed! 345 + 346 + ## start stacking today 347 + 348 + If you are a jujutsu user, you can enable this flag on more 349 + recent versions of jujutsu: 350 + 351 + ``` 352 + ฮป jj --version 353 + jj 0.29.0-8c7ca30074767257d75e3842581b61e764d022cf 354 + 355 + # -- in your config.toml file -- 356 + [git] 357 + write-change-id-header = true 358 + ``` 359 + 360 + This feature writes `change-id` headers directly into the 361 + git commit object, and is visible to code-forges upon push, 362 + and allows you to stack your PRs on Tangled. 363 + 364 +
static/img/basic_merged.jpeg

This is a binary file and will not be displayed.

static/img/diff_1_difference.jpeg

This is a binary file and will not be displayed.

static/img/diff_1_union.jpeg

This is a binary file and will not be displayed.

static/img/diff_2_difference.jpeg

This is a binary file and will not be displayed.

static/img/diff_2_union.jpeg

This is a binary file and will not be displayed.

static/img/hidden-ref.png

This is a binary file and will not be displayed.

static/img/interdiff_difference.jpeg

This is a binary file and will not be displayed.

static/img/interdiff_union.jpeg

This is a binary file and will not be displayed.

static/img/merge-base.png

This is a binary file and will not be displayed.

static/img/patch-pr-diff.png

This is a binary file and will not be displayed.

static/img/patch-pr-main.png

This is a binary file and will not be displayed.

static/img/review_difference.jpeg

This is a binary file and will not be displayed.

static/img/review_union.jpeg

This is a binary file and will not be displayed.

static/img/round_2_difference.jpeg

This is a binary file and will not be displayed.

static/img/round_2_union.jpeg

This is a binary file and will not be displayed.

static/img/submit_stacked.jpeg

This is a binary file and will not be displayed.

static/img/top_of_stack.jpeg

This is a binary file and will not be displayed.

+45 -14
tailwind.config.js
··· 1 - /** @type {import('tailwindcss').Config} */ 1 + const colors = require("tailwindcss/colors"); 2 + 2 3 module.exports = { 3 - content: ["./templates/**/*.html"], 4 + content: ["./templates/**/*.html", "./pages/**/*.md"], 5 + darkMode: "media", 4 6 theme: { 5 7 container: { 6 8 padding: "2rem", 7 9 center: true, 8 10 screens: { 9 - sm: "540px", 10 - md: "640px", 11 - lg: "768px", 12 - xl: "900px", 13 - "2xl": "1024px" 11 + sm: "500px", 12 + md: "600px", 13 + lg: "800px", 14 + xl: "1000px", 15 + "2xl": "1200px", 14 16 }, 15 17 }, 16 18 extend: { 17 19 fontFamily: { 18 - sans: ["iA Writer Quattro S", "Inter", "system-ui", "sans-serif", "ui-sans-serif"], 19 - mono: ["iA Writer Mono S", "ui-monospace", "SFMono-Regular", "Menlo", "Monaco", "Consolas", "Liberation Mono", "Courier New", "monospace"], 20 + sans: ["InterVariable", "system-ui", "sans-serif", "ui-sans-serif"], 21 + mono: [ 22 + "IBMPlexMono", 23 + "ui-monospace", 24 + "SFMono-Regular", 25 + "Menlo", 26 + "Monaco", 27 + "Consolas", 28 + "Liberation Mono", 29 + "Courier New", 30 + "monospace", 31 + ], 20 32 }, 21 - maxWidth: { 22 - 'prose': '65ch', 33 + typography: { 34 + DEFAULT: { 35 + css: { 36 + maxWidth: "75ch", 37 + pre: { 38 + backgroundColor: colors.gray[100], 39 + color: colors.black, 40 + "@apply font-normal text-black bg-gray-100 dark:bg-gray-900 dark:text-gray-300 dark:border-gray-700 dark:border": {}, 41 + }, 42 + code: { 43 + "@apply font-normal font-mono px-1 rounded text-black bg-gray-100 dark:bg-gray-900 dark:text-gray-300 dark:border-gray-700": {}, 44 + }, 45 + "code::before": { 46 + content: '""', 47 + }, 48 + "code::after": { 49 + content: '""', 50 + }, 51 + blockquote: { 52 + quotes: "none", 53 + }, 54 + }, 55 + }, 23 56 }, 24 57 }, 25 58 }, 26 - plugins: [ 27 - require('@tailwindcss/typography'), 28 - ], 59 + plugins: [require("@tailwindcss/typography")], 29 60 };
+10 -10
templates/index.html
··· 10 10 {{ .Meta.title }} 11 11 </title> 12 12 13 - <body class="bg-slate-100"> 14 - <div class="prose mx-auto px-1 pt-4 min-h-screen flex flex-col"> 13 + <body class="bg-slate-100 dark:bg-gray-900"> 14 + <div class="prose dark:prose-invert mx-auto px-1 pt-4 min-h-screen flex flex-col container"> 15 15 <main> 16 16 <header class="px-12"> 17 17 <h1 class="mb-0">{{ index .Meta "title" }}</h1> ··· 21 21 {{ .Body }} 22 22 23 23 <section class="py-4"> 24 - <ul class="px-6"> 24 + <ul class="px-2"> 25 25 {{ $posts := .Extra.blog }} 26 26 {{ range $posts }} 27 - <li class="mt-5 bg-white py-4 px-6 rounded drop-shadow-sm list-none"> 27 + <li class="mt-5 bg-white dark:bg-gray-800 py-4 px-6 rounded drop-shadow-sm list-none"> 28 + {{ $dateStr := .Meta.date }} 29 + {{ $date := parsedate $dateStr }} 30 + <div class="post-date py-1 mb-0 text-sm">{{ $date.Format "02 Jan, 2006" }}</div> 28 31 <div> 29 - <a class="title mb-0 text-lg" href="/{{ .Meta.slug }}.html">{{ .Meta.title }}</a> 32 + <a class="title mb-0 text-xl no-underline font-bold" href="/{{ .Meta.slug }}.html">{{ .Meta.title }}</a> 30 33 {{ if .Meta.draft }} 31 - (<span class="draft">draft</span>) 34 + <span class="text-red-500">[draft]</span> 32 35 {{ end }} 33 36 <p class="italic mt-1 mb-0">{{ .Meta.subtitle }}</p> 34 37 </div> 35 - {{ $dateStr := .Meta.date }} 36 - {{ $date := parsedate $dateStr }} 37 - <div class="post-date py-1 mb-0 text-sm">{{ $date.Format "02 Jan, 2006" }}</div> 38 38 </li> 39 39 {{ end }} 40 40 </ul> 41 41 </section> 42 42 </main> 43 - <footer class="px-6"> 43 + <footer class="px-2"> 44 44 {{ template "partials/footer.html" }} 45 45 </footer> 46 46 </div>
+7 -3
templates/partials/nav.html
··· 1 1 <nav> 2 - <div class="logo"> 3 - <a href="/"> 4 - back to all posts 2 + <div> 3 + <a href="/" class="no-underline"> 4 + โ† back to all posts 5 + </a> 6 + 7 + <a class="font-bold italic float-right no-underline hover:no-underline" href="https://tangled.sh"> 8 + tangled.sh 5 9 </a> 6 10 </div> 7 11 </nav>
+30 -6
templates/text.html
··· 1 1 <!doctype html> 2 2 <html lang=en> 3 3 <head> 4 - {{ template "partials/head.html" }} 4 + {{ template "partials/head.html" . }} 5 5 <meta name="description" content="{{ index .Meta "subtitle" }}"> 6 + <meta property="og:title" content="{{ .Meta.title }}" /> 7 + <meta property="og:description" content="{{ .Meta.subtitle }}" /> 8 + <meta property="og:url" content="https://blog.tangled.sh/{{ .Meta.slug }}" /> 9 + <meta property="og:image" content="{{ .Meta.image }}" /> 10 + <meta property="og:type" content="website" /> 6 11 </head> 7 12 <title> 8 13 {{ index .Meta "title" }} 9 14 </title> 10 - <body class="bg-slate-100"> 11 - <div class="prose mx-auto px-1 pt-4 min-h-screen flex flex-col"> 15 + <body class="bg-slate-100 dark:bg-gray-900"> 16 + <div class="prose dark:prose-invert mx-auto px-1 pt-4 min-h-screen flex flex-col"> 12 17 <main> 13 18 <div class="sidenav px-6"> 14 19 {{ template "partials/nav.html" }} ··· 19 24 {{ $dateStr := index .Meta "date" }} 20 25 {{ $date := parsedate $dateStr }} 21 26 {{ $date.Format "02 Jan, 2006" }} 27 + 28 + <span class="mx-2 select-none">&middot;</span> 29 + 30 + by 31 + {{ $authors := index .Meta "authors" }} 32 + {{ if eq (len $authors) 2 }} 33 + <a href="https://bsky.app/profile/{{ (index $authors 0).handle }}" class="no-underline">{{ (index $authors 0).name }}</a> 34 + & 35 + <a href="https://bsky.app/profile/{{ (index $authors 1).handle }}" class="no-underline"> 36 + {{ (index $authors 1).name }} 37 + </a> 38 + {{ else }} 39 + {{ range $author := $authors }} 40 + <a href="https://bsky.app/profile/{{ $author.handle }}" class="no-underline"> 41 + {{ $author.name }} 42 + </a> 43 + {{ end }} 44 + {{ end }} 22 45 </p> 23 46 24 - {{ if eq .Meta.draft "true" }} 25 - <h1 class="title px-6 mb-0">{{ index .Meta "title" }} <span class="draft">[draft]</span></h1> 47 + {{ if .Meta.draft }} 48 + <h1 class="title px-6 mb-0">{{ index .Meta "title" }} <span 49 + class="text-red-500">[draft]</span></h1> 26 50 {{ else }} 27 51 <h1 class="title px-6 mb-0">{{ index .Meta "title" }}</h1> 28 52 {{ end }} 29 53 <p class="italic px-6 mt-1 mb-0 text-lg">{{ index .Meta "subtitle" }}</p> 30 54 </header> 31 55 32 - <article class="mt-5 bg-white px-6 py-2 rounded drop-shadow-sm"> 56 + <article class="mt-5 bg-white dark:bg-gray-800 px-6 py-2 rounded drop-shadow-sm"> 33 57 {{ .Body }} 34 58 </article> 35 59 </main>