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

wip pull requests post

anirudh.fi 1a99e4b5 08d8e672

verified
+17 -6
input.css
··· 34 34 font-display: swap; 35 35 } 36 36 37 - h1, h2, h3 { 37 + h1 { 38 38 @apply text-2xl; 39 - @apply font-display; 40 39 @apply text-black; 41 40 @apply font-bold; 42 41 } ··· 48 47 } 49 48 50 49 @layer base { 50 + html { 51 + font-size: 15px; 52 + } 53 + @supports (font-variation-settings: normal) { 54 + html { 55 + font-feature-settings: 'ss01' 1, 'kern' 1, 'liga' 1, 'cv05' 1, 'tnum' 1; 56 + } 57 + } 58 + 51 59 a { 52 - @apply no-underline text-black hover:underline hover:text-gray-800; 60 + @apply no-underline text-black hover:underline hover:text-gray-800 dark:text-white dark:hover:text-gray-300; 53 61 } 54 62 55 63 label { 56 - @apply block text-sm text-black; 64 + @apply block mb-2 text-gray-900 text-sm font-bold py-2 uppercase dark:text-gray-100; 57 65 } 58 66 input { 59 - @apply bg-white border border-black rounded-sm focus:ring-black p-2; 67 + @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; 60 68 } 61 69 textarea { 62 - @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; 71 + } 72 + details summary::-webkit-details-marker { 73 + display: none; 63 74 } 64 75 } 65 76
+140
pages/blog/fork-pulls.md
··· 1 + --- 2 + atroot: true 3 + template: 4 + slug: fork-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-15 8 + author: Anirudh Oppiliappan 9 + authorEmail: anirudh@tangled.sh 10 + authorHandle: icyphox.sh 11 + draft: true 12 + --- 13 + 14 + We're having great fun building pull requests -- so far you can create a 15 + PR on Tangled using one of three ways: 16 + 17 + - paste a diff in the UI 18 + - compare two local branches within the same repository 19 + - compare across forks 20 + 21 + [!!!reword this] 22 + 23 + We figured it would be fun to write about the engineering that went into 24 + building this, especially because Tangled is federated and Git repos 25 + can live across different servers (called "knots"). If you're new here, 26 + [read our intro](/intro) for the full story! 27 + 28 + Now, on with the show! 29 + 30 + ## your patch makes the rounds 31 + 32 + Creating a PR in Tangled starts with heading to `/pulls/new` in your 33 + target repository. Once there, you're presented with three options: 34 + 35 + - paste a patch in the UI 36 + - compare two local branches (you'll see this only if you're a 37 + collaborator on the repo) 38 + - compare across forks 39 + 40 + Whatever you choose, at the core of every PR is the patch. You either 41 + supply it and make everyone's lives easier, or we generate it ourselves 42 + by comparing branches (we'll talk more about this in a bit, it's very 43 + cool actually). We'll skip explaining the part where you click around on 44 + the UI to create a new PR -- instead, let's talk about what comes after. 45 + 46 + We call it "rounds". Each round consists of a code review, and updating 47 + the patch results in a new round. Rounds are -- obviously -- 0-indexed. 48 + Here's an example. 49 + 50 + <figure class="max-w-[450px] m-auto flex flex-col items-center justify-center"> 51 + <img class="h-auto max-w-full" src="/static/img/patch-pr-main.png"> 52 + <figcaption class="text-center">A new pull request with a couple 53 + rounds of reviews.</figcaption> 54 + </figure> 55 + 56 + [!!!write more about how this is good?] 57 + 58 + Hitting the 'View Patch' button lets you see the diff for each round. 59 + Inter-diffing -- what changed *between* two rounds -- is planned! 60 + 61 + [!!!close off this section] 62 + 63 + ## fine, we'll make a patch ourselves 64 + 65 + [!!!also write about the sh.tangled.repo.patch lexicon] 66 + 67 + 68 + <figure class="max-w-[550px] m-auto flex flex-col items-center justify-center"> 69 + <img class="h-auto max-w-full" src="/static/img/pr-flow.png"> 70 + <figcaption class="text-center">Simplified pull request flow.</figcaption> 71 + </figure> 72 + 73 + 74 + ## what's in a fork? 75 + 76 + Forks are just "clones" of another repository. They aren't your typical 77 + clones from `git clone` however, since we're operating on top of [bare 78 + repositories][bare-repo]. Hence, forks are "bare clones". You can create 79 + one yourself locally: 80 + 81 + ``` 82 + git clone --bare git@tangled.sh:tangled.sh/core 83 + ``` 84 + 85 + [bare-repo]: https://git-scm.com/book/en/v2/Git-on-the-Server-Getting-Git-on-a-Server 86 + 87 + On Tangled, forking a repo results in a new 88 + [`sh.tangled.repo`][repo-record] record in your PDS. What's interesting 89 + is the new `source` field that's an AT URI pointing to the original 90 + repository: 91 + 92 + { 93 + "knot": "test.hel.tangled.network", 94 + "name": "core", 95 + "$type": "sh.tangled.repo", 96 + "owner": "did:plc:hwevmowznbiukdf6uk5dwrrq", 97 + "source": "at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.repo/3liuighjy2h22", 98 + "addedAt": "2025-04-14T12:53:45Z" 99 + } 100 + 101 + [repo-record]: https://pdsls.dev/at://did:plc:hwevmowznbiukdf6uk5dwrrq/sh.tangled.repo/3lmrm7gu5dh22 102 + 103 + Great, we've got a fork on your knot now. You can now work on your 104 + change safely here -- but how do you now propose a pull request from 105 + your fork? And before that, what exactly is a "pull request" anyway? 106 + 107 + ### ref comparisons across forks 108 + 109 + Great, so we now understand the anatomy of a PR and how comparing 110 + branches (or more generally, refs) works. Astute readers would've 111 + realised that so far, this only works *within* the same repository -- 112 + and not across forks, which is another git repository entirely. 113 + 114 + We'll admit: we ... omitted some sneaky bits in the forks section above. 115 + The idea is simple: we already have all the bits needed to compare two 116 + local refs, so why not just "localize" the remote ref? 117 + 118 + That's where our hidden tracking refs come in. When you create a pull 119 + request from a fork, we create a refspec that tracks the remote branch, 120 + which we then use to produce a diff. A refspec is a rule that tells Git 121 + how to map references between a remote and your local repository during 122 + fetch or push. 123 + 124 + Let's say your fork has a feature branch called `feature-1`, and you're 125 + making a pull request into the `main` branch of the original repository. 126 + We fetch the remote `main` into a local hidden ref using a refspec like 127 + this: 128 + 129 + ``` 130 + +refs/heads/main:refs/hidden/feature-1/main 131 + ``` 132 + 133 + And since we already have a remote (`origin`, by default) towards the 134 + original repo (we just cloned it, rememeber?), we're able to `fetch` 135 + this refspec and bring the remote `main` to our local hidden ref. Each 136 + PR gets its own little hidden ref and hence the 137 + `refs/hidden/:localRef/:remoteRef` format. We keep this ref up to date 138 + whenever you push new commits to your feature branch, ensuring that the 139 + comparison -- and any potential merge conflicts -- are always based on 140 + the latest target branch state.
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/pr-flow.png

This is a binary file and will not be displayed.

+44 -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 - display: ["InterDisplay", "system-ui", "sans-serif", "ui-sans-serif"], 19 20 sans: ["InterVariable", "system-ui", "sans-serif", "ui-sans-serif"], 20 - mono: ["IBMPlexMono", "ui-monospace", "SFMono-Regular", "Menlo", "Monaco", "Consolas", "Liberation Mono", "Courier New", "monospace"], 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 + ], 21 32 }, 22 - maxWidth: { 23 - 'prose': '65ch', 33 + typography: { 34 + DEFAULT: { 35 + css: { 36 + maxWidth: "70ch", 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 p-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 + }, 24 56 }, 25 57 }, 26 58 }, 27 - plugins: [ 28 - require('@tailwindcss/typography'), 29 - ], 59 + plugins: [require("@tailwindcss/typography")], 30 60 };
+2 -2
templates/index.html
··· 11 11 </title> 12 12 13 13 <body class="bg-slate-100"> 14 - <div class="prose mx-auto px-1 pt-4 min-h-screen flex flex-col"> 14 + <div class="prose 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> ··· 28 28 <div> 29 29 <a class="title mb-0 text-lg" href="/{{ .Meta.slug }}.html">{{ .Meta.title }}</a> 30 30 {{ if .Meta.draft }} 31 - (<span class="draft">draft</span>) 31 + <span class="text-red-500">[draft]</span> 32 32 {{ end }} 33 33 <p class="italic mt-1 mb-0">{{ .Meta.subtitle }}</p> 34 34 </div>
+2 -1
templates/text.html
··· 26 26 </p> 27 27 28 28 {{ if eq .Meta.draft "true" }} 29 - <h1 class="title px-6 mb-0">{{ index .Meta "title" }} <span class="draft">[draft]</span></h1> 29 + <h1 class="title px-6 mb-0">{{ index .Meta "title" }} <span 30 + class="text-red-500">[draft]</span></h1> 30 31 {{ else }} 31 32 <h1 class="title px-6 mb-0">{{ index .Meta "title" }}</h1> 32 33 {{ end }}