engineering blog at https://blog.tangled.sh
at master 195 lines 7.2 kB view raw view rendered
1--- 2atroot: true 3template: 4slug: pulls 5title: the lifecycle of a pull request 6subtitle: we shipped a bunch of PR features recently; here's how we built it 7date: 2025-04-16 8image: /static/img/hidden-ref.png 9authors: 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 16draft: false 17--- 18 19We've spent the last couple of weeks building out a pull 20request system for Tangled, and today we want to lift the 21hood and show you how it works. 22 23If you're new to Tangled, [read our intro](/intro) for the 24full story! 25 26You 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 30collaborator on the repo) 31- Compare across forks 32 33Whatever you choose, at the core of every PR is the patch. 34First, you write some code. Then, you run `git diff` to 35produce a patch and make everyone's lives easier, or push to 36a branch, and we generate it ourselves by comparing against 37the target. 38 39## patch generation 40 41When you create a PR from a branch, we create a "patch" by 42calculating the difference between your branch and the 43target 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 51Your `feature` branch has advanced 2 commits since you first 52branched out, but in the meanwhile, `main` has also advanced 532 commits. Doing a trivial `git diff feature main` will 54produce 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 59We obviously do not want the second part! To only show the 60changes added by `feature`, we have to identify the 61"merge-base": the nearest common ancestor of `feature` and 62`main`. 63 64 65In this case, `A` is the nearest common ancestor, and 66subsequently, the patch calculated will contain just `X` and 67`Y`. 68 69### ref comparisons across forks 70 71The plumbing described above is easy to do across two 72branches, but what about forks? And what if they live on 73different servers altogether (as they can in Tangled!)? 74 75Here's the concept: since we already have all the necessary 76components to compare two local refs, why not simply 77"localize" the remote ref? 78 79In simpler terms, we instruct Git to fetch the target branch 80from the original repository and store it in your fork under 81a special name. This approach allows us to compare your 82changes against the most current version of the branch 83you're trying to contribute to, all while remaining within 84your 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 91We call this a "hidden tracking ref." When you create a pull 92request from a fork, we establish a refspec that tracks the 93remote branch, which we then use to generate a diff. A 94refspec is essentially a rule that tells Git how to map 95references between a remote and your local repository during 96fetch or push operations. 97 98For 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 101remote `main` into a local hidden ref using a refspec like 102this: 103 104``` 105+refs/heads/main:refs/hidden/feature-1/main 106``` 107 108Since we already have a remote (`origin`, by default) to the 109original repository (remember, we cloned it earlier), we can 110use `fetch` with this refspec to bring the remote `main` 111branch into our local hidden ref. Each pull request gets its 112own hidden ref, hence the `refs/hidden/:localRef/:remoteRef` 113format. We keep this ref updated whenever you push new 114commits to your feature branch, ensuring that comparisons -- 115and any potential merge conflicts -- are always based on the 116latest state of the target branch. 117 118And just like earlier, we produce the patch by diffing your 119feature branch with the hidden tracking ref. Also, the entire pull 120request is stored as [an atproto record][atproto-record] and updated 121each time the patch changes. 122 123[atproto-record]: https://pdsls.dev/at://did:plc:qfpnj4og54vl56wngdriaxug/sh.tangled.repo.pull/3lmwniim2i722 124 125Neat, now that we have a patch; we can move on the hard 126part: code review. 127 128 129## your patch does the rounds 130 131Tangled uses a "round-based" review format. Your initial 132submission starts "round 0". Once your submission receives 133scrutiny, you can address reviews and resubmit your patch. 134This resubmission starts "round 1". You keep whittling on 135your patch till it is good enough, and eventually merged (or 136closed 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 141rounds of reviews.</figcaption> 142</figure> 143 144Rounds are a far superior to standard branch-based 145approaches: 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 159This [post by Mitchell 160Hashimoto](https://mitchellh.com/writing/github-changesets) 161goes into further detail on what can be achieved with 162round-based reviews. 163 164## future plans 165 166To close off this post, we wanted to share some of our 167future 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 **Update 2025-08-12**: We have format-patch support! 174 175* Gerrit-style `refs/for/main`: we're still hashing out the 176 details but being able to push commits to a ref to 177 "auto-create" a PR would be super handy! 178 179* Change ID support: This will allow us to group changes 180 together and track them across multiple commits, and to 181 provide "history" for each change. This works great with [Jujutsu][jj]. 182 **Update 2025-08-12**: This has now landed: https://blog.tangled.sh/stacking 183 184Join us on [Discord](https://chat.tangled.sh) or 185`#tangled` on libera.chat (the two are bridged, so we will 186never miss a message!). We are always available to help 187setup knots, listen to feedback on features, or even 188shepherd contributions! 189 190**Update 2025-08-12**: We move fast, and we now have jujutsu support, and an 191early in-house CI: https://blog.tangled.sh/ci. You no longer need a Bluesky 192account to sign-up; head to https://tangled.sh/signup and sign up with your 193email! 194 195[jj]: https://jj-vcs.github.io/jj/latest/