···11+---
22+atroot: true
33+template:
44+slug: fork-pulls
55+title: the lifecycle of a pull request
66+subtitle: We shipped a bunch of PR features recently; here's how we built it
77+date: 2025-04-15
88+author: Anirudh Oppiliappan
99+authorEmail: anirudh@tangled.sh
1010+authorHandle: icyphox.sh
1111+draft: true
1212+---
1313+1414+We're having great fun building pull requests -- so far you can create a
1515+PR on Tangled using one of three ways:
1616+1717+- paste a diff in the UI
1818+- compare two local branches within the same repository
1919+- compare across forks
2020+2121+[!!!reword this]
2222+2323+We figured it would be fun to write about the engineering that went into
2424+building this, especially because Tangled is federated and Git repos
2525+can live across different servers (called "knots"). If you're new here,
2626+[read our intro](/intro) for the full story!
2727+2828+Now, on with the show!
2929+3030+## your patch makes the rounds
3131+3232+Creating a PR in Tangled starts with heading to `/pulls/new` in your
3333+target repository. Once there, you're presented with three options:
3434+3535+- paste a patch in the UI
3636+- compare two local branches (you'll see this only if you're a
3737+collaborator on the repo)
3838+- compare across forks
3939+4040+Whatever you choose, at the core of every PR is the patch. You either
4141+supply it and make everyone's lives easier, or we generate it ourselves
4242+by comparing branches (we'll talk more about this in a bit, it's very
4343+cool actually). We'll skip explaining the part where you click around on
4444+the UI to create a new PR -- instead, let's talk about what comes after.
4545+4646+We call it "rounds". Each round consists of a code review, and updating
4747+the patch results in a new round. Rounds are -- obviously -- 0-indexed.
4848+Here's an example.
4949+5050+<figure class="max-w-[450px] m-auto flex flex-col items-center justify-center">
5151+ <img class="h-auto max-w-full" src="/static/img/patch-pr-main.png">
5252+ <figcaption class="text-center">A new pull request with a couple
5353+rounds of reviews.</figcaption>
5454+</figure>
5555+5656+[!!!write more about how this is good?]
5757+5858+Hitting the 'View Patch' button lets you see the diff for each round.
5959+Inter-diffing -- what changed *between* two rounds -- is planned!
6060+6161+[!!!close off this section]
6262+6363+## fine, we'll make a patch ourselves
6464+6565+[!!!also write about the sh.tangled.repo.patch lexicon]
6666+6767+6868+<figure class="max-w-[550px] m-auto flex flex-col items-center justify-center">
6969+ <img class="h-auto max-w-full" src="/static/img/pr-flow.png">
7070+ <figcaption class="text-center">Simplified pull request flow.</figcaption>
7171+</figure>
7272+7373+7474+## what's in a fork?
7575+7676+Forks are just "clones" of another repository. They aren't your typical
7777+clones from `git clone` however, since we're operating on top of [bare
7878+repositories][bare-repo]. Hence, forks are "bare clones". You can create
7979+one yourself locally:
8080+8181+```
8282+git clone --bare git@tangled.sh:tangled.sh/core
8383+```
8484+8585+[bare-repo]: https://git-scm.com/book/en/v2/Git-on-the-Server-Getting-Git-on-a-Server
8686+8787+On Tangled, forking a repo results in a new
8888+[`sh.tangled.repo`][repo-record] record in your PDS. What's interesting
8989+is the new `source` field that's an AT URI pointing to the original
9090+repository:
9191+9292+ {
9393+ "knot": "test.hel.tangled.network",
9494+ "name": "core",
9595+ "$type": "sh.tangled.repo",
9696+ "owner": "did:plc:hwevmowznbiukdf6uk5dwrrq",
9797+ "source": "at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.repo/3liuighjy2h22",
9898+ "addedAt": "2025-04-14T12:53:45Z"
9999+ }
100100+101101+[repo-record]: https://pdsls.dev/at://did:plc:hwevmowznbiukdf6uk5dwrrq/sh.tangled.repo/3lmrm7gu5dh22
102102+103103+Great, we've got a fork on your knot now. You can now work on your
104104+change safely here -- but how do you now propose a pull request from
105105+your fork? And before that, what exactly is a "pull request" anyway?
106106+107107+### ref comparisons across forks
108108+109109+Great, so we now understand the anatomy of a PR and how comparing
110110+branches (or more generally, refs) works. Astute readers would've
111111+realised that so far, this only works *within* the same repository --
112112+and not across forks, which is another git repository entirely.
113113+114114+We'll admit: we ... omitted some sneaky bits in the forks section above.
115115+The idea is simple: we already have all the bits needed to compare two
116116+local refs, so why not just "localize" the remote ref?
117117+118118+That's where our hidden tracking refs come in. When you create a pull
119119+request from a fork, we create a refspec that tracks the remote branch,
120120+which we then use to produce a diff. A refspec is a rule that tells Git
121121+how to map references between a remote and your local repository during
122122+fetch or push.
123123+124124+Let's say your fork has a feature branch called `feature-1`, and you're
125125+making a pull request into the `main` branch of the original repository.
126126+We fetch the remote `main` into a local hidden ref using a refspec like
127127+this:
128128+129129+```
130130++refs/heads/main:refs/hidden/feature-1/main
131131+```
132132+133133+And since we already have a remote (`origin`, by default) towards the
134134+original repo (we just cloned it, rememeber?), we're able to `fetch`
135135+this refspec and bring the remote `main` to our local hidden ref. Each
136136+PR gets its own little hidden ref and hence the
137137+`refs/hidden/:localRef/:remoteRef` format. We keep this ref up to date
138138+whenever you push new commits to your feature branch, ensuring that the
139139+comparison -- and any potential merge conflicts -- are always based on
140140+the latest target branch state.