+17
-6
input.css
+17
-6
input.css
···
34
34
font-display: swap;
35
35
}
36
36
37
-
h1, h2, h3 {
37
+
h1 {
38
38
@apply text-2xl;
39
-
@apply font-display;
40
39
@apply text-black;
41
40
@apply font-bold;
42
41
}
···
48
47
}
49
48
50
49
@layer base {
50
+
html {
51
+
font-size: 15px;
52
+
}
53
+
@supports (font-variation-settings: normal) {
54
+
html {
55
+
font-feature-settings: 'ss01' 1, 'kern' 1, 'liga' 1, 'cv05' 1, 'tnum' 1;
56
+
}
57
+
}
58
+
51
59
a {
52
-
@apply no-underline text-black hover:underline hover:text-gray-800;
60
+
@apply no-underline text-black hover:underline hover:text-gray-800 dark:text-white dark:hover:text-gray-300;
53
61
}
54
62
55
63
label {
56
-
@apply block text-sm text-black;
64
+
@apply block mb-2 text-gray-900 text-sm font-bold py-2 uppercase dark:text-gray-100;
57
65
}
58
66
input {
59
-
@apply bg-white border border-black rounded-sm focus:ring-black p-2;
67
+
@apply bg-white border border-gray-400 rounded-sm focus:ring-black p-3 dark:bg-gray-800 dark:border-gray-600 dark:text-white dark:focus:ring-gray-400;
60
68
}
61
69
textarea {
62
-
@apply bg-white border border-black rounded-sm focus:ring-black p-2;
70
+
@apply bg-white border border-gray-400 rounded-sm focus:ring-black p-3 dark:bg-gray-800 dark:border-gray-600 dark:text-white dark:focus:ring-gray-400;
71
+
}
72
+
details summary::-webkit-details-marker {
73
+
display: none;
63
74
}
64
75
}
65
76
+140
pages/blog/fork-pulls.md
+140
pages/blog/fork-pulls.md
···
1
+
---
2
+
atroot: true
3
+
template:
4
+
slug: fork-pulls
5
+
title: the lifecycle of a pull request
6
+
subtitle: We shipped a bunch of PR features recently; here's how we built it
7
+
date: 2025-04-15
8
+
author: Anirudh Oppiliappan
9
+
authorEmail: anirudh@tangled.sh
10
+
authorHandle: icyphox.sh
11
+
draft: true
12
+
---
13
+
14
+
We're having great fun building pull requests -- so far you can create a
15
+
PR on Tangled using one of three ways:
16
+
17
+
- paste a diff in the UI
18
+
- compare two local branches within the same repository
19
+
- compare across forks
20
+
21
+
[!!!reword this]
22
+
23
+
We figured it would be fun to write about the engineering that went into
24
+
building this, especially because Tangled is federated and Git repos
25
+
can live across different servers (called "knots"). If you're new here,
26
+
[read our intro](/intro) for the full story!
27
+
28
+
Now, on with the show!
29
+
30
+
## your patch makes the rounds
31
+
32
+
Creating a PR in Tangled starts with heading to `/pulls/new` in your
33
+
target repository. Once there, you're presented with three options:
34
+
35
+
- paste a patch in the UI
36
+
- compare two local branches (you'll see this only if you're a
37
+
collaborator on the repo)
38
+
- compare across forks
39
+
40
+
Whatever you choose, at the core of every PR is the patch. You either
41
+
supply it and make everyone's lives easier, or we generate it ourselves
42
+
by comparing branches (we'll talk more about this in a bit, it's very
43
+
cool actually). We'll skip explaining the part where you click around on
44
+
the UI to create a new PR -- instead, let's talk about what comes after.
45
+
46
+
We call it "rounds". Each round consists of a code review, and updating
47
+
the patch results in a new round. Rounds are -- obviously -- 0-indexed.
48
+
Here's an example.
49
+
50
+
<figure class="max-w-[450px] m-auto flex flex-col items-center justify-center">
51
+
<img class="h-auto max-w-full" src="/static/img/patch-pr-main.png">
52
+
<figcaption class="text-center">A new pull request with a couple
53
+
rounds of reviews.</figcaption>
54
+
</figure>
55
+
56
+
[!!!write more about how this is good?]
57
+
58
+
Hitting the 'View Patch' button lets you see the diff for each round.
59
+
Inter-diffing -- what changed *between* two rounds -- is planned!
60
+
61
+
[!!!close off this section]
62
+
63
+
## fine, we'll make a patch ourselves
64
+
65
+
[!!!also write about the sh.tangled.repo.patch lexicon]
66
+
67
+
68
+
<figure class="max-w-[550px] m-auto flex flex-col items-center justify-center">
69
+
<img class="h-auto max-w-full" src="/static/img/pr-flow.png">
70
+
<figcaption class="text-center">Simplified pull request flow.</figcaption>
71
+
</figure>
72
+
73
+
74
+
## what's in a fork?
75
+
76
+
Forks are just "clones" of another repository. They aren't your typical
77
+
clones from `git clone` however, since we're operating on top of [bare
78
+
repositories][bare-repo]. Hence, forks are "bare clones". You can create
79
+
one yourself locally:
80
+
81
+
```
82
+
git clone --bare git@tangled.sh:tangled.sh/core
83
+
```
84
+
85
+
[bare-repo]: https://git-scm.com/book/en/v2/Git-on-the-Server-Getting-Git-on-a-Server
86
+
87
+
On Tangled, forking a repo results in a new
88
+
[`sh.tangled.repo`][repo-record] record in your PDS. What's interesting
89
+
is the new `source` field that's an AT URI pointing to the original
90
+
repository:
91
+
92
+
{
93
+
"knot": "test.hel.tangled.network",
94
+
"name": "core",
95
+
"$type": "sh.tangled.repo",
96
+
"owner": "did:plc:hwevmowznbiukdf6uk5dwrrq",
97
+
"source": "at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.repo/3liuighjy2h22",
98
+
"addedAt": "2025-04-14T12:53:45Z"
99
+
}
100
+
101
+
[repo-record]: https://pdsls.dev/at://did:plc:hwevmowznbiukdf6uk5dwrrq/sh.tangled.repo/3lmrm7gu5dh22
102
+
103
+
Great, we've got a fork on your knot now. You can now work on your
104
+
change safely here -- but how do you now propose a pull request from
105
+
your fork? And before that, what exactly is a "pull request" anyway?
106
+
107
+
### ref comparisons across forks
108
+
109
+
Great, so we now understand the anatomy of a PR and how comparing
110
+
branches (or more generally, refs) works. Astute readers would've
111
+
realised that so far, this only works *within* the same repository --
112
+
and not across forks, which is another git repository entirely.
113
+
114
+
We'll admit: we ... omitted some sneaky bits in the forks section above.
115
+
The idea is simple: we already have all the bits needed to compare two
116
+
local refs, so why not just "localize" the remote ref?
117
+
118
+
That's where our hidden tracking refs come in. When you create a pull
119
+
request from a fork, we create a refspec that tracks the remote branch,
120
+
which we then use to produce a diff. A refspec is a rule that tells Git
121
+
how to map references between a remote and your local repository during
122
+
fetch or push.
123
+
124
+
Let's say your fork has a feature branch called `feature-1`, and you're
125
+
making a pull request into the `main` branch of the original repository.
126
+
We fetch the remote `main` into a local hidden ref using a refspec like
127
+
this:
128
+
129
+
```
130
+
+refs/heads/main:refs/hidden/feature-1/main
131
+
```
132
+
133
+
And since we already have a remote (`origin`, by default) towards the
134
+
original repo (we just cloned it, rememeber?), we're able to `fetch`
135
+
this refspec and bring the remote `main` to our local hidden ref. Each
136
+
PR gets its own little hidden ref and hence the
137
+
`refs/hidden/:localRef/:remoteRef` format. We keep this ref up to date
138
+
whenever you push new commits to your feature branch, ensuring that the
139
+
comparison -- and any potential merge conflicts -- are always based on
140
+
the latest target branch state.
static/img/patch-pr-diff.png
static/img/patch-pr-diff.png
This is a binary file and will not be displayed.
static/img/patch-pr-main.png
static/img/patch-pr-main.png
This is a binary file and will not be displayed.
static/img/pr-flow.png
static/img/pr-flow.png
This is a binary file and will not be displayed.
+44
-14
tailwind.config.js
+44
-14
tailwind.config.js
···
1
-
/** @type {import('tailwindcss').Config} */
1
+
const colors = require("tailwindcss/colors");
2
+
2
3
module.exports = {
3
-
content: ["./templates/**/*.html"],
4
+
content: ["./templates/**/*.html", "./pages/**/*.md"],
5
+
darkMode: "media",
4
6
theme: {
5
7
container: {
6
8
padding: "2rem",
7
9
center: true,
8
10
screens: {
9
-
sm: "540px",
10
-
md: "640px",
11
-
lg: "768px",
12
-
xl: "900px",
13
-
"2xl": "1024px"
11
+
sm: "500px",
12
+
md: "600px",
13
+
lg: "800px",
14
+
xl: "1000px",
15
+
"2xl": "1200px",
14
16
},
15
17
},
16
18
extend: {
17
19
fontFamily: {
18
-
display: ["InterDisplay", "system-ui", "sans-serif", "ui-sans-serif"],
19
20
sans: ["InterVariable", "system-ui", "sans-serif", "ui-sans-serif"],
20
-
mono: ["IBMPlexMono", "ui-monospace", "SFMono-Regular", "Menlo", "Monaco", "Consolas", "Liberation Mono", "Courier New", "monospace"],
21
+
mono: [
22
+
"IBMPlexMono",
23
+
"ui-monospace",
24
+
"SFMono-Regular",
25
+
"Menlo",
26
+
"Monaco",
27
+
"Consolas",
28
+
"Liberation Mono",
29
+
"Courier New",
30
+
"monospace",
31
+
],
21
32
},
22
-
maxWidth: {
23
-
'prose': '65ch',
33
+
typography: {
34
+
DEFAULT: {
35
+
css: {
36
+
maxWidth: "70ch",
37
+
pre: {
38
+
backgroundColor: colors.gray[100],
39
+
color: colors.black,
40
+
"@apply font-normal text-black bg-gray-100 dark:bg-gray-900 dark:text-gray-300 dark:border-gray-700 dark:border": {},
41
+
},
42
+
code: {
43
+
"@apply font-normal font-mono p-1 rounded text-black bg-gray-100 dark:bg-gray-900 dark:text-gray-300 dark:border-gray-700": {},
44
+
},
45
+
"code::before": {
46
+
content: '""',
47
+
},
48
+
"code::after": {
49
+
content: '""',
50
+
},
51
+
blockquote: {
52
+
quotes: "none",
53
+
},
54
+
},
55
+
},
24
56
},
25
57
},
26
58
},
27
-
plugins: [
28
-
require('@tailwindcss/typography'),
29
-
],
59
+
plugins: [require("@tailwindcss/typography")],
30
60
};
+2
-2
templates/index.html
+2
-2
templates/index.html
···
11
11
</title>
12
12
13
13
<body class="bg-slate-100">
14
-
<div class="prose mx-auto px-1 pt-4 min-h-screen flex flex-col">
14
+
<div class="prose mx-auto px-1 pt-4 min-h-screen flex flex-col container">
15
15
<main>
16
16
<header class="px-12">
17
17
<h1 class="mb-0">{{ index .Meta "title" }}</h1>
···
28
28
<div>
29
29
<a class="title mb-0 text-lg" href="/{{ .Meta.slug }}.html">{{ .Meta.title }}</a>
30
30
{{ if .Meta.draft }}
31
-
(<span class="draft">draft</span>)
31
+
<span class="text-red-500">[draft]</span>
32
32
{{ end }}
33
33
<p class="italic mt-1 mb-0">{{ .Meta.subtitle }}</p>
34
34
</div>
+2
-1
templates/text.html
+2
-1
templates/text.html
···
26
26
</p>
27
27
28
28
{{ if eq .Meta.draft "true" }}
29
-
<h1 class="title px-6 mb-0">{{ index .Meta "title" }} <span class="draft">[draft]</span></h1>
29
+
<h1 class="title px-6 mb-0">{{ index .Meta "title" }} <span
30
+
class="text-red-500">[draft]</span></h1>
30
31
{{ else }}
31
32
<h1 class="title px-6 mb-0">{{ index .Meta "title" }}</h1>
32
33
{{ end }}