···1515draft: true
1616---
17171818-We've spent the last few weeks building out a pull request system for Tangled,
1919-and today we want to lift the hood and show you how it works. What makes our
2020-implementation particularly interesting is that Tangled is federated --
2121-repositories can exist across different servers (which we call "knots"). This
2222-distributed nature creates unique engineering challenges that we had to solve.
1818+We've spent the last couple of weeks building out a pull
1919+request system for Tangled, and today we want to lift the
2020+hood and show you how it works.
23212424-If you're new to Tangled and wondering what this knot business is all about,
2525-[read our intro](/intro) for the full story!
2626-2727-Now, on with the show!
2828-2929-## your patch makes the rounds
2222+If you're new to Tangled, [read our intro](/intro) for the
2323+full story!
30243131-Creating a PR in Tangled starts with heading to `/pulls/new` in your
3232-target repository. Once there, you're presented with three options:
2525+You have three options to contribute to a repository:
33263434-- Paste a patch
2727+- Paste a patch on the web UI
3528- Compare two local branches (you'll see this only if you're a
3629collaborator on the repo)
3730- Compare across forks
38313939-Whatever you choose, at the core of every PR is the patch. You either
4040-supply it and make everyone's lives easier, or we generate it ourselves
4141-by comparing branches (we'll talk more about this in a bit, it's very
4242-cool actually). We'll skip explaining the part where you click around on
4343-the UI to create a new PR -- instead, let's talk about what comes after.
3232+Whatever you choose, at the core of every PR is the patch.
3333+First, you write some code. Then, you run `git diff` to
3434+produce a patch and make everyone's lives easier, or push to
3535+a branch, and we generate it ourselves by comparing against
3636+the target.
44374545-We call it "rounds". Each round consists of a code review: your patch recieves
4646-scrutiny, and updating the patch in response, results in a new round. Rounds are
4747-obviously 0-indexed. Here's an example.
3838+## patch generation
3939+4040+When you create a PR from a branch, we create a "patch" by
4141+calculating the difference between your branch and the
4242+target branch. Consider this scenario:
48434949-<figure class="max-w-[450px] m-auto flex flex-col items-center justify-center">
5050- <img class="h-auto max-w-full" src="/static/img/patch-pr-main.png">
5151- <figcaption class="text-center">A new pull request with a couple
5252-rounds of reviews. Thanks Jay!</figcaption>
4444+<figure class="max-w-[550px] m-auto flex flex-col items-center justify-center">
4545+ <img class="h-auto max-w-full" src="/static/img/merge-base.png">
4646+ <figcaption class="text-center">Merge base caption here! [!!!change this!]</figcaption>
5347</figure>
54485555-Rounds are a far superior to standard branch-based
5656-approaches:
4949+Your `feature` branch has advanced 2 commits since you first
5050+branched out, but in the meanwhile, `main` has also advanced
5151+2 commits. Doing a trivial `git diff feature main` will
5252+produce a confusing patch:
57535858-- Submissions are immutable: how many times have your
5959- reviews gone out-of-date because the author pushed commits
6060- _during_ your review?
6161-- Reviews are attached to submissions: at a glance, it is
6262- easy to tell which comment applies to which "version" of the
6363- pull-request
6464-- The author can choose when to resubmit! They can commit as
6565- much as they want, but a new round begins when they choose
6666- to hit "resubmit"
6767-- It is possible to "interdiff" and observe changes made
6868- across submissions (this is coming very soon to Tangled!)
5454+- the patch will apply the changes from X and Y
5555+- the patch will **revert** the changes from B and C
69567070-This [post by Mitchell
7171-Hashimoto](https://mitchellh.com/writing/github-changesets) goes into further
7272-detail on what can be achieved with round-based reviews.
5757+We obviously do not want the second part! To only show the
5858+changes added by `feature`, we have to identify the
5959+"merge-base": the nearest common ancestor of `feature` and
6060+`main`.
73617474-## fine, we'll make a patch ourselves
75627676-Remember our patch from earlier? Yeah, let's get into how comparing branches works.
6363+In this case, `A` is the nearest common ancestor, and
6464+subsequently, the patch calculated will contain just `X` and
6565+`Y`.
77667878-[you gotta talk about]
7979-- merge and merge check?
8080-- merge base thing
8181-- sh.tangled.repo.patch lexicon
8282-- nice segue into the fork section
6767+### ref comparisons across forks
83686969+The plumbing described above is easy to do across two
7070+branches, but what about forks? and what if they live on
7171+different servers altogether (as they can in tangled!)?
84728585-<figure class="max-w-[550px] m-auto flex flex-col items-center justify-center">
8686- <img class="h-auto max-w-full" src="/static/img/merge-base.png">
8787- <figcaption class="text-center">Merge base caption here! [!!!change this!]</figcaption>
8888-</figure>
7373+Here's the concept: since we already have all the necessary
7474+components to compare two local refs, why not simply
7575+"localize" the remote ref?
89767777+In simpler terms, we instruct Git to fetch the target branch
7878+from the original repository and store it in your fork under
7979+a special name. This approach allows us to compare your
8080+changes against the most current version of the branch
8181+you're trying to contribute to, all while remaining within
8282+your fork.
90839191-[!!!do we want this? use it to explain the patch merge/check process maybe]
9284<figure class="max-w-[550px] m-auto flex flex-col items-center justify-center">
9393- <img class="h-auto max-w-full" src="/static/img/pr-flow.png">
9494- <figcaption class="text-center">Simplified pull request flow.</figcaption>
8585+ <img class="h-auto max-w-full" src="/static/img/hidden-ref.png">
8686+ <figcaption class="text-center">Hidden tracking ref.</figcaption>
9587</figure>
96888989+We call this a "hidden tracking ref." When you create a pull
9090+request from a fork, we establish a refspec that tracks the
9191+remote branch, which we then use to generate a diff. A
9292+refspec is essentially a rule that tells Git how to map
9393+references between a remote and your local repository during
9494+fetch or push operations.
97959898-## quick detour: what's in a fork?
9999-100100-Forks are just "clones" of another repository. They aren't your typical
101101-clones from `git clone` however, since we're operating on top of [bare
102102-repositories][bare-repo]. Hence, forks are "bare clones". You can create
103103-one yourself locally:
9696+For example, if your fork has a feature branch called
9797+`feature-1`, and you want to make a pull request to the
9898+`main` branch of the original repository, we fetch the
9999+remote `main` into a local hidden ref using a refspec like
100100+this:
104101105102```
106106-git clone --bare git@tangled.sh:tangled.sh/core
103103++refs/heads/main:refs/hidden/feature-1/main
107104```
108105109109-[bare-repo]: https://git-scm.com/book/en/v2/Git-on-the-Server-Getting-Git-on-a-Server
106106+Since we already have a remote (`origin`, by default) to the
107107+original repository (remember, we cloned it earlier), we can
108108+use `fetch` with this refspec to bring the remote `main`
109109+branch into our local hidden ref. Each pull request gets its
110110+own hidden ref, hence the `refs/hidden/:localRef/:remoteRef`
111111+format. We keep this ref updated whenever you push new
112112+commits to your feature branch, ensuring that comparisons --
113113+and any potential merge conflicts -- are always based on the
114114+latest state of the target branch.
110115111111-On Tangled, forking a repo results in a new
112112-[`sh.tangled.repo`][repo-record] record in your PDS. What's interesting
113113-is the new `source` field that's an AT URI pointing to the original
114114-repository:
116116+And just like earlier, we produce the patch by diffing your
117117+feature branch with the hidden tracking ref and do the whole
118118+atproto record thing.
115119116116- {
117117- "knot": "test.hel.tangled.network",
118118- "name": "core",
119119- "$type": "sh.tangled.repo",
120120- "owner": "did:plc:hwevmowznbiukdf6uk5dwrrq",
121121- "source": "at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.repo/3liuighjy2h22",
122122- "addedAt": "2025-04-14T12:53:45Z"
123123- }
120120+Neat, now that we have a patch; we can move on the hard
121121+part: code review.
124122125125-[repo-record]: https://pdsls.dev/at://did:plc:hwevmowznbiukdf6uk5dwrrq/sh.tangled.repo/3lmrm7gu5dh22
126123127127-Great, we've got a fork on your knot now. You can now work on your change safely
128128-here -- but let's get back to how we generate a patch across forks.
124124+## your patch does the rounds
129125130130-### ref comparisons across forks
126126+Tangled uses a "round-based" review format. Your initial
127127+submission starts "round 0". Once your submission receives
128128+scrutiny, you can address reviews and resubmit your patch.
129129+This resubmission starts "round 1". You keep whittling on
130130+your patch till it is good enough, and eventually merged (or
131131+closed if you are unlucky).
131132132132-We'll admit: we ... skipped some sneaky bits about forks earlier. Here's the
133133-concept: since we already have all the necessary components to compare two local
134134-refs, why not simply "localize" the remote ref?
135135-136136-In simpler terms, we instruct Git to fetch the target branch from the original
137137-repository and store it in your fork under a special name. This approach allows
138138-us to compare your changes against the most current version of the branch you're
139139-trying to contribute to, all while remaining within your fork.
140140-141141-<figure class="max-w-[550px] m-auto flex flex-col items-center justify-center">
142142- <img class="h-auto max-w-full" src="/static/img/hidden-ref.png">
143143- <figcaption class="text-center">Hidden tracking ref.</figcaption>
133133+<figure class="max-w-[450px] m-auto flex flex-col items-center justify-center">
134134+ <img class="h-auto max-w-full" src="/static/img/patch-pr-main.png">
135135+ <figcaption class="text-center">A new pull request with a couple
136136+rounds of reviews. Thanks Jay!</figcaption>
144137</figure>
145138146146-We call this a "hidden tracking ref." When you create a pull request from a
147147-fork, we establish a refspec that tracks the remote branch, which we then use to
148148-generate a diff. A refspec is essentially a rule that tells Git how to map
149149-references between a remote and your local repository during fetch or push
150150-operations.
139139+Rounds are a far superior to standard branch-based
140140+approaches:
151141152152-For example, if your fork has a feature branch called `feature-1`, and you want
153153-to make a pull request to the `main` branch of the original repository, we fetch
154154-the remote `main` into a local hidden ref using a refspec like this:
142142+- Submissions are immutable: how many times have your
143143+ reviews gone out-of-date because the author pushed commits
144144+ _during_ your review?
145145+- Reviews are attached to submissions: at a glance, it is
146146+ easy to tell which comment applies to which "version" of
147147+ the pull-request
148148+- The author can choose when to resubmit! They can commit as
149149+ much as they want to their branch, but a new round begins
150150+ when they choose to hit "resubmit"
151151+- It is possible to "interdiff" and observe changes made
152152+ across submissions (this is coming very soon to Tangled!)
155153156156-```
157157-+refs/heads/main:refs/hidden/feature-1/main
158158-```
159159-160160-Since we already have a remote (`origin`, by default) to the original repository
161161-(remember, we cloned it earlier), we can use `fetch` with this refspec to bring
162162-the remote `main` branch into our local hidden ref. Each pull request gets its
163163-own hidden ref, hence the `refs/hidden/:localRef/:remoteRef` format. We keep
164164-this ref updated whenever you push new commits to your feature branch, ensuring
165165-that comparisons -- and any potential merge conflicts -- are always based on the
166166-latest state of the target branch.
167167-168168-And just like earlier, we produce the patch by diffing your feature branch with
169169-the hidden tracking ref and do the whole atproto record thing.
154154+This [post by Mitchell
155155+Hashimoto](https://mitchellh.com/writing/github-changesets)
156156+goes into further detail on what can be achieved with
157157+round-based reviews.
170158171159## future plans
172160173173-To close off this post, we wanted to share some of our future plans for pull requests:
161161+To close off this post, we wanted to share some of our
162162+future plans for pull requests:
174163175175-* `format-patch` support: both for pasting in the UI and internally. This allows
176176-us to show commits in the PR page, and offer different merge strategies to
177177-choose from (squash, rebase, ...).
164164+* `format-patch` support: both for pasting in the UI and
165165+ internally. This allows us to show commits in the PR page,
166166+ and offer different merge strategies to choose from
167167+ (squash, rebase, ...).
178168179179-* Gerrit-style `refs/for/main`: we're still hashing out the details but being
180180-able to push commits to a ref to "auto-create" a PR would be super handy!
169169+* Gerrit-style `refs/for/main`: we're still hashing out the
170170+ details but being able to push commits to a ref to
171171+ "auto-create" a PR would be super handy!
181172182182-* Change ID support: This will allow us to group changes together and track them
183183-across multiple commits, and to provide "history" for each change.
173173+* Change ID support: This will allow us to group changes
174174+ together and track them across multiple commits, and to
175175+ provide "history" for each change. This works great with
176176+ `jujutsu`.