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

reword entire thing

be more to the point:
- use shorter sentences
- flow properly from web ui to review

Changed files
+115 -122
pages
blog
+115 -122
pages/blog/pulls.md
··· 15 15 draft: true 16 16 --- 17 17 18 - We've spent the last few weeks building out a pull request system for Tangled, 19 - and today we want to lift the hood and show you how it works. What makes our 20 - implementation particularly interesting is that Tangled is federated -- 21 - repositories can exist across different servers (which we call "knots"). This 22 - distributed nature creates unique engineering challenges that we had to solve. 18 + We've spent the last couple of weeks building out a pull 19 + request system for Tangled, and today we want to lift the 20 + hood and show you how it works. 23 21 24 - If you're new to Tangled and wondering what this knot business is all about, 25 - [read our intro](/intro) for the full story! 26 - 27 - Now, on with the show! 28 - 29 - ## your patch makes the rounds 22 + If you're new to Tangled, [read our intro](/intro) for the 23 + full story! 30 24 31 - Creating a PR in Tangled starts with heading to `/pulls/new` in your 32 - target repository. Once there, you're presented with three options: 25 + You have three options to contribute to a repository: 33 26 34 - - Paste a patch 27 + - Paste a patch on the web UI 35 28 - Compare two local branches (you'll see this only if you're a 36 29 collaborator on the repo) 37 30 - Compare across forks 38 31 39 - Whatever you choose, at the core of every PR is the patch. You either 40 - supply it and make everyone's lives easier, or we generate it ourselves 41 - by comparing branches (we'll talk more about this in a bit, it's very 42 - cool actually). We'll skip explaining the part where you click around on 43 - the UI to create a new PR -- instead, let's talk about what comes after. 32 + Whatever you choose, at the core of every PR is the patch. 33 + First, you write some code. Then, you run `git diff` to 34 + produce a patch and make everyone's lives easier, or push to 35 + a branch, and we generate it ourselves by comparing against 36 + the target. 44 37 45 - We call it "rounds". Each round consists of a code review: your patch recieves 46 - scrutiny, and updating the patch in response, results in a new round. Rounds are 47 - obviously 0-indexed. Here's an example. 38 + ## patch generation 39 + 40 + When you create a PR from a branch, we create a "patch" by 41 + calculating the difference between your branch and the 42 + target branch. Consider this scenario: 48 43 49 - <figure class="max-w-[450px] m-auto flex flex-col items-center justify-center"> 50 - <img class="h-auto max-w-full" src="/static/img/patch-pr-main.png"> 51 - <figcaption class="text-center">A new pull request with a couple 52 - rounds of reviews. Thanks Jay!</figcaption> 44 + <figure class="max-w-[550px] m-auto flex flex-col items-center justify-center"> 45 + <img class="h-auto max-w-full" src="/static/img/merge-base.png"> 46 + <figcaption class="text-center">Merge base caption here! [!!!change this!]</figcaption> 53 47 </figure> 54 48 55 - Rounds are a far superior to standard branch-based 56 - approaches: 49 + Your `feature` branch has advanced 2 commits since you first 50 + branched out, but in the meanwhile, `main` has also advanced 51 + 2 commits. Doing a trivial `git diff feature main` will 52 + produce a confusing patch: 57 53 58 - - Submissions are immutable: how many times have your 59 - reviews gone out-of-date because the author pushed commits 60 - _during_ your review? 61 - - Reviews are attached to submissions: at a glance, it is 62 - easy to tell which comment applies to which "version" of the 63 - pull-request 64 - - The author can choose when to resubmit! They can commit as 65 - much as they want, but a new round begins when they choose 66 - to hit "resubmit" 67 - - It is possible to "interdiff" and observe changes made 68 - across submissions (this is coming very soon to Tangled!) 54 + - the patch will apply the changes from X and Y 55 + - the patch will **revert** the changes from B and C 69 56 70 - This [post by Mitchell 71 - Hashimoto](https://mitchellh.com/writing/github-changesets) goes into further 72 - detail on what can be achieved with round-based reviews. 57 + We obviously do not want the second part! To only show the 58 + changes added by `feature`, we have to identify the 59 + "merge-base": the nearest common ancestor of `feature` and 60 + `main`. 73 61 74 - ## fine, we'll make a patch ourselves 75 62 76 - Remember our patch from earlier? Yeah, let's get into how comparing branches works. 63 + In this case, `A` is the nearest common ancestor, and 64 + subsequently, the patch calculated will contain just `X` and 65 + `Y`. 77 66 78 - [you gotta talk about] 79 - - merge and merge check? 80 - - merge base thing 81 - - sh.tangled.repo.patch lexicon 82 - - nice segue into the fork section 67 + ### ref comparisons across forks 83 68 69 + The plumbing described above is easy to do across two 70 + branches, but what about forks? and what if they live on 71 + different servers altogether (as they can in tangled!)? 84 72 85 - <figure class="max-w-[550px] m-auto flex flex-col items-center justify-center"> 86 - <img class="h-auto max-w-full" src="/static/img/merge-base.png"> 87 - <figcaption class="text-center">Merge base caption here! [!!!change this!]</figcaption> 88 - </figure> 73 + Here's the concept: since we already have all the necessary 74 + components to compare two local refs, why not simply 75 + "localize" the remote ref? 89 76 77 + In simpler terms, we instruct Git to fetch the target branch 78 + from the original repository and store it in your fork under 79 + a special name. This approach allows us to compare your 80 + changes against the most current version of the branch 81 + you're trying to contribute to, all while remaining within 82 + your fork. 90 83 91 - [!!!do we want this? use it to explain the patch merge/check process maybe] 92 84 <figure class="max-w-[550px] m-auto flex flex-col items-center justify-center"> 93 - <img class="h-auto max-w-full" src="/static/img/pr-flow.png"> 94 - <figcaption class="text-center">Simplified pull request flow.</figcaption> 85 + <img class="h-auto max-w-full" src="/static/img/hidden-ref.png"> 86 + <figcaption class="text-center">Hidden tracking ref.</figcaption> 95 87 </figure> 96 88 89 + We call this a "hidden tracking ref." When you create a pull 90 + request from a fork, we establish a refspec that tracks the 91 + remote branch, which we then use to generate a diff. A 92 + refspec is essentially a rule that tells Git how to map 93 + references between a remote and your local repository during 94 + fetch or push operations. 97 95 98 - ## quick detour: what's in a fork? 99 - 100 - Forks are just "clones" of another repository. They aren't your typical 101 - clones from `git clone` however, since we're operating on top of [bare 102 - repositories][bare-repo]. Hence, forks are "bare clones". You can create 103 - one yourself locally: 96 + For example, if your fork has a feature branch called 97 + `feature-1`, and you want to make a pull request to the 98 + `main` branch of the original repository, we fetch the 99 + remote `main` into a local hidden ref using a refspec like 100 + this: 104 101 105 102 ``` 106 - git clone --bare git@tangled.sh:tangled.sh/core 103 + +refs/heads/main:refs/hidden/feature-1/main 107 104 ``` 108 105 109 - [bare-repo]: https://git-scm.com/book/en/v2/Git-on-the-Server-Getting-Git-on-a-Server 106 + Since we already have a remote (`origin`, by default) to the 107 + original repository (remember, we cloned it earlier), we can 108 + use `fetch` with this refspec to bring the remote `main` 109 + branch into our local hidden ref. Each pull request gets its 110 + own hidden ref, hence the `refs/hidden/:localRef/:remoteRef` 111 + format. We keep this ref updated whenever you push new 112 + commits to your feature branch, ensuring that comparisons -- 113 + and any potential merge conflicts -- are always based on the 114 + latest state of the target branch. 110 115 111 - On Tangled, forking a repo results in a new 112 - [`sh.tangled.repo`][repo-record] record in your PDS. What's interesting 113 - is the new `source` field that's an AT URI pointing to the original 114 - repository: 116 + And just like earlier, we produce the patch by diffing your 117 + feature branch with the hidden tracking ref and do the whole 118 + atproto record thing. 115 119 116 - { 117 - "knot": "test.hel.tangled.network", 118 - "name": "core", 119 - "$type": "sh.tangled.repo", 120 - "owner": "did:plc:hwevmowznbiukdf6uk5dwrrq", 121 - "source": "at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.repo/3liuighjy2h22", 122 - "addedAt": "2025-04-14T12:53:45Z" 123 - } 120 + Neat, now that we have a patch; we can move on the hard 121 + part: code review. 124 122 125 - [repo-record]: https://pdsls.dev/at://did:plc:hwevmowznbiukdf6uk5dwrrq/sh.tangled.repo/3lmrm7gu5dh22 126 123 127 - Great, we've got a fork on your knot now. You can now work on your change safely 128 - here -- but let's get back to how we generate a patch across forks. 124 + ## your patch does the rounds 129 125 130 - ### ref comparisons across forks 126 + Tangled uses a "round-based" review format. Your initial 127 + submission starts "round 0". Once your submission receives 128 + scrutiny, you can address reviews and resubmit your patch. 129 + This resubmission starts "round 1". You keep whittling on 130 + your patch till it is good enough, and eventually merged (or 131 + closed if you are unlucky). 131 132 132 - We'll admit: we ... skipped some sneaky bits about forks earlier. Here's the 133 - concept: since we already have all the necessary components to compare two local 134 - refs, why not simply "localize" the remote ref? 135 - 136 - In simpler terms, we instruct Git to fetch the target branch from the original 137 - repository and store it in your fork under a special name. This approach allows 138 - us to compare your changes against the most current version of the branch you're 139 - trying to contribute to, all while remaining within your fork. 140 - 141 - <figure class="max-w-[550px] m-auto flex flex-col items-center justify-center"> 142 - <img class="h-auto max-w-full" src="/static/img/hidden-ref.png"> 143 - <figcaption class="text-center">Hidden tracking ref.</figcaption> 133 + <figure class="max-w-[450px] m-auto flex flex-col items-center justify-center"> 134 + <img class="h-auto max-w-full" src="/static/img/patch-pr-main.png"> 135 + <figcaption class="text-center">A new pull request with a couple 136 + rounds of reviews. Thanks Jay!</figcaption> 144 137 </figure> 145 138 146 - We call this a "hidden tracking ref." When you create a pull request from a 147 - fork, we establish a refspec that tracks the remote branch, which we then use to 148 - generate a diff. A refspec is essentially a rule that tells Git how to map 149 - references between a remote and your local repository during fetch or push 150 - operations. 139 + Rounds are a far superior to standard branch-based 140 + approaches: 151 141 152 - For example, if your fork has a feature branch called `feature-1`, and you want 153 - to make a pull request to the `main` branch of the original repository, we fetch 154 - the remote `main` into a local hidden ref using a refspec like this: 142 + - Submissions are immutable: how many times have your 143 + reviews gone out-of-date because the author pushed commits 144 + _during_ your review? 145 + - Reviews are attached to submissions: at a glance, it is 146 + easy to tell which comment applies to which "version" of 147 + the pull-request 148 + - The author can choose when to resubmit! They can commit as 149 + much as they want to their branch, but a new round begins 150 + when they choose to hit "resubmit" 151 + - It is possible to "interdiff" and observe changes made 152 + across submissions (this is coming very soon to Tangled!) 155 153 156 - ``` 157 - +refs/heads/main:refs/hidden/feature-1/main 158 - ``` 159 - 160 - Since we already have a remote (`origin`, by default) to the original repository 161 - (remember, we cloned it earlier), we can use `fetch` with this refspec to bring 162 - the remote `main` branch into our local hidden ref. Each pull request gets its 163 - own hidden ref, hence the `refs/hidden/:localRef/:remoteRef` format. We keep 164 - this ref updated whenever you push new commits to your feature branch, ensuring 165 - that comparisons -- and any potential merge conflicts -- are always based on the 166 - latest state of the target branch. 167 - 168 - And just like earlier, we produce the patch by diffing your feature branch with 169 - the hidden tracking ref and do the whole atproto record thing. 154 + This [post by Mitchell 155 + Hashimoto](https://mitchellh.com/writing/github-changesets) 156 + goes into further detail on what can be achieved with 157 + round-based reviews. 170 158 171 159 ## future plans 172 160 173 - To close off this post, we wanted to share some of our future plans for pull requests: 161 + To close off this post, we wanted to share some of our 162 + future plans for pull requests: 174 163 175 - * `format-patch` support: both for pasting in the UI and internally. This allows 176 - us to show commits in the PR page, and offer different merge strategies to 177 - choose from (squash, rebase, ...). 164 + * `format-patch` support: both for pasting in the UI and 165 + internally. This allows us to show commits in the PR page, 166 + and offer different merge strategies to choose from 167 + (squash, rebase, ...). 178 168 179 - * Gerrit-style `refs/for/main`: we're still hashing out the details but being 180 - able to push commits to a ref to "auto-create" a PR would be super handy! 169 + * Gerrit-style `refs/for/main`: we're still hashing out the 170 + details but being able to push commits to a ref to 171 + "auto-create" a PR would be super handy! 181 172 182 - * Change ID support: This will allow us to group changes together and track them 183 - across multiple commits, and to provide "history" for each change. 173 + * Change ID support: This will allow us to group changes 174 + together and track them across multiple commits, and to 175 + provide "history" for each change. This works great with 176 + `jujutsu`.