+115
-122
pages/blog/pulls.md
+115
-122
pages/blog/pulls.md
···
15
15
draft: true
16
16
---
17
17
18
-
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.
18
+
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.
23
21
24
-
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
22
+
If you're new to Tangled, [read our intro](/intro) for the
23
+
full story!
30
24
31
-
Creating a PR in Tangled starts with heading to `/pulls/new` in your
32
-
target repository. Once there, you're presented with three options:
25
+
You have three options to contribute to a repository:
33
26
34
-
- Paste a patch
27
+
- Paste a patch on the web UI
35
28
- Compare two local branches (you'll see this only if you're a
36
29
collaborator on the repo)
37
30
- Compare across forks
38
31
39
-
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.
32
+
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.
44
37
45
-
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.
38
+
## 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:
48
43
49
-
<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>
44
+
<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>
53
47
</figure>
54
48
55
-
Rounds are a far superior to standard branch-based
56
-
approaches:
49
+
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:
57
53
58
-
- 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!)
54
+
- the patch will apply the changes from X and Y
55
+
- the patch will **revert** the changes from B and C
69
56
70
-
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.
57
+
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`.
73
61
74
-
## fine, we'll make a patch ourselves
75
62
76
-
Remember our patch from earlier? Yeah, let's get into how comparing branches works.
63
+
In this case, `A` is the nearest common ancestor, and
64
+
subsequently, the patch calculated will contain just `X` and
65
+
`Y`.
77
66
78
-
[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
67
+
### ref comparisons across forks
83
68
69
+
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!)?
84
72
85
-
<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>
73
+
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?
89
76
77
+
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.
90
83
91
-
[!!!do we want this? use it to explain the patch merge/check process maybe]
92
84
<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>
85
+
<img class="h-auto max-w-full" src="/static/img/hidden-ref.png">
86
+
<figcaption class="text-center">Hidden tracking ref.</figcaption>
95
87
</figure>
96
88
89
+
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.
97
95
98
-
## 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:
96
+
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:
104
101
105
102
```
106
-
git clone --bare git@tangled.sh:tangled.sh/core
103
+
+refs/heads/main:refs/hidden/feature-1/main
107
104
```
108
105
109
-
[bare-repo]: https://git-scm.com/book/en/v2/Git-on-the-Server-Getting-Git-on-a-Server
106
+
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.
110
115
111
-
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:
116
+
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.
115
119
116
-
{
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
-
}
120
+
Neat, now that we have a patch; we can move on the hard
121
+
part: code review.
124
122
125
-
[repo-record]: https://pdsls.dev/at://did:plc:hwevmowznbiukdf6uk5dwrrq/sh.tangled.repo/3lmrm7gu5dh22
126
123
127
-
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.
124
+
## your patch does the rounds
129
125
130
-
### ref comparisons across forks
126
+
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).
131
132
132
-
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>
133
+
<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>
144
137
</figure>
145
138
146
-
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.
139
+
Rounds are a far superior to standard branch-based
140
+
approaches:
151
141
152
-
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:
142
+
- 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!)
155
153
156
-
```
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.
154
+
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.
170
158
171
159
## future plans
172
160
173
-
To close off this post, we wanted to share some of our future plans for pull requests:
161
+
To close off this post, we wanted to share some of our
162
+
future plans for pull requests:
174
163
175
-
* `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, ...).
164
+
* `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, ...).
178
168
179
-
* 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!
169
+
* 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!
181
172
182
-
* 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.
173
+
* 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`.