···15draft: true
16---
1718-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.
2324-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
3031-Creating a PR in Tangled starts with heading to `/pulls/new` in your
32-target repository. Once there, you're presented with three options:
3334-- Paste a patch
35- Compare two local branches (you'll see this only if you're a
36collaborator on the repo)
37- Compare across forks
3839-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.
4445-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.
004849-<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>
53</figure>
5455-Rounds are a far superior to standard branch-based
56-approaches:
005758-- 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!)
6970-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.
07374-## fine, we'll make a patch ourselves
7576-Remember our patch from earlier? Yeah, let's get into how comparing branches works.
007778-[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
830008485-<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>
890000009091-[!!!do we want this? use it to explain the patch merge/check process maybe]
92<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>
95</figure>
960000009798-## 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:
104105```
106-git clone --bare git@tangled.sh:tangled.sh/core
107```
108109-[bare-repo]: https://git-scm.com/book/en/v2/Git-on-the-Server-Getting-Git-on-a-Server
00000000110111-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:
115116- {
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- }
124125-[repo-record]: https://pdsls.dev/at://did:plc:hwevmowznbiukdf6uk5dwrrq/sh.tangled.repo/3lmrm7gu5dh22
126127-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.
129130-### ref comparisons across forks
00000131132-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>
144</figure>
145146-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.
151152-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:
00000000155156-```
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.
170171## future plans
172173-To close off this post, we wanted to share some of our future plans for pull requests:
0174175-* `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, ...).
0178179-* 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!
0181182-* 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.
00
···15draft: true
16---
1718+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.
002122+If you're new to Tangled, [read our intro](/intro) for the
23+full story!
00002425+You have three options to contribute to a repository:
02627+- Paste a patch on the web UI
28- Compare two local branches (you'll see this only if you're a
29collaborator on the repo)
30- Compare across forks
3132+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.
3738+## 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:
4344+<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>
047</figure>
4849+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:
5354+- the patch will apply the changes from X and Y
55+- the patch will **revert** the changes from B and C
0000000005657+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`.
6106263+In this case, `A` is the nearest common ancestor, and
64+subsequently, the patch calculated will contain just `X` and
65+`Y`.
6667+### ref comparisons across forks
00006869+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!)?
7273+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?
07677+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.
83084<figure class="max-w-[550px] m-auto flex flex-col items-center justify-center">
85+ <img class="h-auto max-w-full" src="/static/img/hidden-ref.png">
86+ <figcaption class="text-center">Hidden tracking ref.</figcaption>
87</figure>
8889+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.
9596+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:
0101102```
103++refs/heads/main:refs/hidden/feature-1/main
104```
105106+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.
115116+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.
0119120+Neat, now that we have a patch; we can move on the hard
121+part: code review.
0000001220123124+## your patch does the rounds
0125126+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).
132133+<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>
00000000137</figure>
138139+Rounds are a far superior to standard branch-based
140+approaches:
000141142+- 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!)
153154+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.
0000000000158159## future plans
160161+To close off this post, we wanted to share some of our
162+future plans for pull requests:
163164+* `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, ...).
168169+* 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!
172173+* 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`.