forked from
tangled.org/site
engineering blog at https://blog.tangled.sh
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/