Skip to content

Commit 05a2c39

Browse files
committed
feat(forges): support Codeberg
Codeberg is a public instance of Forgejo. Ref #10
1 parent 4467825 commit 05a2c39

5 files changed

Lines changed: 133 additions & 5 deletions

File tree

‎DOCUMENTATION.md‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,12 @@ For GitHub, create a [personal access token][gh-token] (classic or
108108
fine-grained) — no scope is required for public repositories; grant repository
109109
read access for private ones. For Bitbucket Cloud, create a
110110
[repository, project or workspace access token][bb-token] (or an API token)
111-
with pull-request read access.
111+
with pull-request read access. For Codeberg, create an [access token][cb-token]
112+
in your account settings with repository read scope.
112113

113114
[gh-token]: https://github.com/settings/tokens
114115
[bb-token]: https://support.atlassian.com/bitbucket-cloud/docs/access-tokens/
116+
[cb-token]: https://docs.codeberg.org/advanced/access-token/
115117

116118
## Forges
117119

@@ -149,7 +151,8 @@ So, to add a new forge:
149151

150152
Supported forges:
151153
- **GitHub**: https://docs.github.com/en/rest/pulls
152-
- **Bitbucket** (Cloud): https://developer.atlassian.com/cloud/bitbucket/rest/api-group-pullrequests/.
154+
- **Bitbucket** (Cloud): https://developer.atlassian.com/cloud/bitbucket/rest/api-group-pullrequests/
155+
- **Codeberg** (Forgejo, Gitea-compatible API): https://forgejo.org/docs/latest/user/api-usage/.
153156

154157
## Internationalization
155158

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,4 @@ API rate limit and list pull requests from private repositories.
2323

2424
- GitHub
2525
- Bitbucket
26+
- Codeberg

‎forges.js‎

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,60 @@ export const bitbucket = {
130130
},
131131
}
132132

133+
// Codeberg is a public instance of Forgejo (a Gitea fork), so it speaks the
134+
// Gitea-compatible REST API served at https://codeberg.org/api/v1.
135+
// API docs: https://forgejo.org/docs/latest/user/api-usage/
136+
export const codeberg = {
137+
label: "Codeberg",
138+
hostnames: ["codeberg.org"],
139+
base_url: "https://codeberg.org/api/v1",
140+
141+
parseUrl(url) {
142+
// file view: /{owner}/{repo}/src/{branch|commit|tag}/{ref}/{filepath}
143+
const m = url.pathname.match(
144+
"^/(?<projectKey>[^/]+)/(?<repoSlug>[^/]+)/src/[^/]+/[^/]+/(?<filepath>.+)$")
145+
if (!m) return null
146+
return { ...m.groups, origin: url.origin }
147+
},
148+
149+
listPRsUrl(info) {
150+
return `${this.base_url}/repos/${info.projectKey}/${info.repoSlug}/pulls`
151+
},
152+
153+
// the list endpoint returns a JSON array of PRs
154+
pullRequests(data) {
155+
return data
156+
},
157+
158+
filesUrl(info, pr) {
159+
return `${this.base_url}/repos/${info.projectKey}/${info.repoSlug}/pulls/${this.prNumber(pr)}/files`
160+
},
161+
162+
filenames(files) {
163+
return files.map(file => file.filename)
164+
},
165+
166+
prNumber(pr) {
167+
return pr.number
168+
},
169+
170+
prWebUrl(info, prNumber) {
171+
return `${info.origin}/${info.projectKey}/${info.repoSlug}/pulls/${prNumber}`
172+
},
173+
174+
// Codeberg exposes no simple rate-limit endpoint.
175+
rateLimit: null,
176+
177+
// Codeberg accepts a personal access token via the Gitea `token` scheme.
178+
tokenStorageKey: "codebergToken",
179+
180+
authHeader(token) {
181+
return { Authorization: `token ${token}` }
182+
},
183+
}
184+
133185
// registry of forges matched by a static hostname
134-
const staticForges = [github, bitbucket]
186+
const staticForges = [github, bitbucket, codeberg]
135187

136188
// pick a forge adapter for a page hostname (null if none matches).
137189
export function forgeForHostname(hostname) {

‎manifest.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
],
5454
"host_permissions": [
5555
"*://api.github.com/*",
56-
"*://api.bitbucket.org/*"
56+
"*://api.bitbucket.org/*",
57+
"*://codeberg.org/*"
5758
]
5859
}

‎tests/forges.test.js‎

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { test } from "node:test"
22
import assert from "node:assert/strict"
33

4-
import { github, bitbucket, forgeForHostname } from "../forges.js"
4+
import { github, bitbucket, codeberg, forgeForHostname } from "../forges.js"
55

66
//
77
// forgeForHostname
@@ -14,6 +14,10 @@ test("forgeForHostname matches bitbucket.org", () => {
1414
assert.equal(forgeForHostname("bitbucket.org"), bitbucket)
1515
})
1616

17+
test("forgeForHostname matches codeberg.org", () => {
18+
assert.equal(forgeForHostname("codeberg.org"), codeberg)
19+
})
20+
1721
test("forgeForHostname matches a subdomain containing the hostname", () => {
1822
// hostnames are matched as substrings of the page hostname
1923
assert.equal(forgeForHostname("www.github.com"), github)
@@ -189,3 +193,70 @@ test("bitbucket.authHeader builds a Bearer Authorization header", () => {
189193
Authorization: "Bearer bb_secret",
190194
})
191195
})
196+
197+
//
198+
// codeberg adapter (Forgejo / Gitea-compatible API)
199+
//
200+
test("codeberg.parseUrl extracts owner, repo and filepath from a src URL", () => {
201+
const url = new URL("https://codeberg.org/forgejo/forgejo/src/branch/forgejo/README.md")
202+
assert.deepEqual(codeberg.parseUrl(url), {
203+
projectKey: "forgejo",
204+
repoSlug: "forgejo",
205+
filepath: "README.md",
206+
origin: "https://codeberg.org",
207+
})
208+
})
209+
210+
test("codeberg.parseUrl keeps nested file paths intact", () => {
211+
const url = new URL("https://codeberg.org/acme/widget/src/commit/deadbeef/dir/app.js")
212+
assert.equal(codeberg.parseUrl(url).filepath, "dir/app.js")
213+
})
214+
215+
test("codeberg.parseUrl returns null for a non-file URL", () => {
216+
assert.equal(codeberg.parseUrl(new URL("https://codeberg.org/acme/widget")), null)
217+
})
218+
219+
test("codeberg.listPRsUrl builds the pulls endpoint", () => {
220+
const info = { projectKey: "acme", repoSlug: "widget" }
221+
assert.equal(
222+
codeberg.listPRsUrl(info),
223+
"https://codeberg.org/api/v1/repos/acme/widget/pulls")
224+
})
225+
226+
test("codeberg.filesUrl builds the files endpoint for a PR", () => {
227+
const info = { projectKey: "acme", repoSlug: "widget" }
228+
assert.equal(
229+
codeberg.filesUrl(info, { number: 42 }),
230+
"https://codeberg.org/api/v1/repos/acme/widget/pulls/42/files")
231+
})
232+
233+
test("codeberg.prWebUrl builds the web URL of a PR", () => {
234+
const info = { projectKey: "acme", repoSlug: "widget", origin: "https://codeberg.org" }
235+
assert.equal(
236+
codeberg.prWebUrl(info, 42),
237+
"https://codeberg.org/acme/widget/pulls/42")
238+
})
239+
240+
test("codeberg.prNumber reads the number off a PR object", () => {
241+
assert.equal(codeberg.prNumber({ number: 7, title: "ignored" }), 7)
242+
})
243+
244+
test("codeberg.pullRequests returns the list response as-is", () => {
245+
const prs = [{ number: 1 }, { number: 2 }]
246+
assert.equal(codeberg.pullRequests(prs), prs)
247+
})
248+
249+
test("codeberg.filenames maps files to their filename", () => {
250+
const files = [{ filename: "a.js" }, { filename: "dir/b.js" }]
251+
assert.deepEqual(codeberg.filenames(files), ["a.js", "dir/b.js"])
252+
})
253+
254+
test("codeberg exposes no rate-limit endpoint", () => {
255+
assert.equal(codeberg.rateLimit, null)
256+
})
257+
258+
test("codeberg.authHeader builds a token Authorization header", () => {
259+
assert.deepEqual(codeberg.authHeader("cb_secret"), {
260+
Authorization: "token cb_secret",
261+
})
262+
})

0 commit comments

Comments
 (0)