Skip to content

Commit 0582f18

Browse files
joaopalmeironrjdalalclaude
authored
feat: add support for Codeberg repos (#62)
* feat: add support for Codeberg repos * fix: update missing banner * refactor: explicit codeberg branch + exhaustive host guard Replace the catch-all `else` with `else if (host === "codeberg.org")` and add a `never`-typed exhaustive default so adding a new host to the `Host` union without a parser branch becomes a compile error. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: support codeberg /raw and /media URLs + ssh test coverage - Extend codeberg parser to recognize /raw/<branch|tag|commit>/<ref>/path and /media/<branch|tag|commit>/<ref>/path as direct-file (blob) links; /src/... continues to map to tree. Behavior change: pasting a Codeberg raw URL now fetches just the file instead of falling through to a full repository clone. - Add dryRun coverage for the new raw/media URLs and for the previously untested git@codeberg.org: SSH prefix (repo + src path). - Drop the "codeberg" package.json keyword. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Neeraj Dalal <admin@nrjdalal.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 3808f33 commit 0582f18

4 files changed

Lines changed: 119 additions & 11 deletions

File tree

README.md

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
📦 `Zero dependencies` / `Un/packed (~67/25kb)` / `Faster and more features` yet drop-in replacement for `degit`
1313

14-
> #### Just `copy-and-paste` any GitHub, GitLab or Bitbucket URL - no editing required (shorthands work too) - to clone individual files, folders, branches, commits, raw content or even entire repositories without the `.git` directory.
14+
> #### Just `copy-and-paste` any GitHub, GitLab, Bitbucket or Codeberg URL - no editing required (shorthands work too) - to clone individual files, folders, branches, commits, raw content or even entire repositories without the `.git` directory.
1515
1616
Unlike other tools that force you to tweak URLs or follow strict formats to clone files, folders, branches or commits GitPick works seamlessly with any URL.
1717

@@ -73,6 +73,9 @@ npx gitpick https://gitlab.com/owner/repo/-/tree/main/path/to/folder
7373
# clone from Bitbucket
7474
npx gitpick https://bitbucket.org/owner/repo
7575
npx gitpick https://bitbucket.org/owner/repo/src/main/path/to/folder
76+
# clone from Codeberg
77+
npx gitpick https://codeberg.org/owner/repo
78+
npx gitpick https://codeberg.org/owner/repo/src/branch/main/path/to/folder
7679
# dry run (preview without cloning)
7780
npx gitpick owner/repo --dry-run
7881
npx gitpick owner/repo -i --dry-run
@@ -82,7 +85,7 @@ npx gitpick owner/repo -i --dry-run
8285

8386
## ✨ Features
8487

85-
- 🔍 Clone individual files or folders from GitHub, GitLab and Bitbucket
88+
- 🔍 Clone individual files or folders from GitHub, GitLab, Bitbucket and Codeberg
8689
- 🧠 Use shorthands `TanStack/router` or full URL's `https://github.com/TanStack/router`
8790
- ⚙️ Auto-detects branches and target directory (if not specified) like `git clone`
8891
- **🔥 Interactive mode** - browse and cherry-pick files/folders with `-i` | `--interactive`
@@ -122,6 +125,7 @@ npx gitpick <url/shorthand> --dry-run # preview without cloning
122125
npx gitpick https://<token>@github.com/owner/repo # private repository
123126
npx gitpick https://gitlab.com/owner/repo # GitLab
124127
npx gitpick https://bitbucket.org/owner/repo # Bitbucket
128+
npx gitpick https://codeberg.org/owner/repo # Codeberg
125129
```
126130

127131
<img width="720" alt="Image" src="https://github.com/user-attachments/assets/ddbc41b4-bfc6-4287-bb85-eb949d723591" />
@@ -156,22 +160,24 @@ npx gitpick owner/repo -i
156160
npx gitpick owner/repo -i -b canary
157161
npx gitpick https://github.com/owner/repo -i
158162
npx gitpick https://gitlab.com/owner/repo -i
163+
npx gitpick https://codeberg.org/owner/repo -i
159164
```
160165

161166
<img width="720" alt="Interactive Mode" src="https://github.com/user-attachments/assets/9d6f4db7-ed84-4783-b815-0267719b3a52" />
162167

163-
Navigate with arrow keys, select with space, expand/collapse with enter, `.` to select all, `c` to confirm. Works with GitHub, GitLab, Bitbucket, public and private repos.
168+
Navigate with arrow keys, select with space, expand/collapse with enter, `.` to select all, `c` to confirm. Works with GitHub, GitLab, Bitbucket, Codeberg, public and private repos.
164169

165170
---
166171

167172
## 🔐 Private Repos
168173

169-
Use a personal access token with read-only contents permission. Works with GitHub, GitLab and Bitbucket:
174+
Use a personal access token with read-only contents permission. Works with GitHub, GitLab, Bitbucket and Codeberg:
170175

171176
```sh
172177
npx gitpick https://<token>@github.com/owner/repo
173178
npx gitpick https://<token>@gitlab.com/owner/repo
174179
npx gitpick https://<token>@bitbucket.org/owner/repo
180+
npx gitpick https://<token>@codeberg.org/owner/repo
175181
```
176182

177183
Or use environment variables (recommended for CI):
@@ -180,6 +186,7 @@ Or use environment variables (recommended for CI):
180186
export GITHUB_TOKEN=ghp_xxxx # or GH_TOKEN
181187
export GITLAB_TOKEN=glpat-xxxx
182188
export BITBUCKET_TOKEN=xxxx
189+
export CODEBERG_TOKEN=xxxx
183190

184191
npx gitpick owner/private-repo # token is picked up automatically
185192
```
@@ -223,6 +230,9 @@ Create a `.gitpick.json` or `.gitpick.jsonc` in your project to pick multiple fi
223230
// Bitbucket
224231
"https://bitbucket.org/owner/repo",
225232
"https://bitbucket.org/owner/repo/src/main/path/to/folder",
233+
// Codeberg
234+
"https://codeberg.org/owner/repo",
235+
"https://codeberg.org/owner/repo/src/branch/main/path/to/folder",
226236
]
227237
```
228238

bin/index.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { name, version } from "~/package.json"
1919
const terminalLink = (text: string, url: string) => `\x1b]8;;${url}\x07${text}\x1b]8;;\x07`
2020

2121
const helpMessage = `
22-
With ${bold(`${terminalLink("GitPick", "https://github.com/nrjdalal/gitpick")}`)} clone specific directories or files from GitHub, GitLab and Bitbucket!
22+
With ${bold(`${terminalLink("GitPick", "https://github.com/nrjdalal/gitpick")}`)} clone specific directories or files from GitHub, GitLab, Bitbucket and Codeberg!
2323
2424
$ gitpick ${yellow("<url>")} ${green("[target]")} ${cyan("[options]")}
2525
@@ -28,7 +28,7 @@ ${bold("Hint:")}
2828
GitPick fallbacks to the default behavior of \`git clone\`
2929
3030
${bold("Arguments:")}
31-
${yellow("url")} GitHub/GitLab/Bitbucket URL with path to file/folder/repository
31+
${yellow("url")} GitHub/GitLab/Bitbucket/Codeberg URL with path to file/folder/repository
3232
${green("target")} Directory to clone into (optional)
3333
3434
${bold("Options:")}
@@ -54,7 +54,8 @@ ${bold("Examples:")}
5454
$ gitpick <url> --dry-run
5555
$ gitpick https://gitlab.com/owner/repo
5656
$ gitpick https://bitbucket.org/owner/repo
57-
57+
$ gitpick https://codeberg.org/owner/repo
58+
5859
🚀 More awesome tools at ${cyan("https://github.com/nrjdalal")}`
5960

6061
const displayPath = (targetPath: string) => {
@@ -391,7 +392,7 @@ const main = async () => {
391392

392393
if (!silent) {
393394
console.log(
394-
`\nWith ${bold(`${terminalLink("GitPick", "https://github.com/nrjdalal/gitpick")}`)} clone specific files, folders, branches,\ncommits and much more from GitHub, GitLab and Bitbucket!`,
395+
`\nWith ${bold(`${terminalLink("GitPick", "https://github.com/nrjdalal/gitpick")}`)} clone specific files, folders, branches,\ncommits and much more from GitHub, GitLab, Bitbucket and Codeberg!`,
395396
)
396397
}
397398

bin/utils/transform-url.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { getDefaultBranch } from "@/utils/get-default-branch"
22

3-
type Host = "github.com" | "gitlab.com" | "bitbucket.org"
3+
type Host = "github.com" | "gitlab.com" | "bitbucket.org" | "codeberg.org"
44

55
const PREFIXES: { prefix: string; host: Host }[] = [
66
{ prefix: "git@github.com:", host: "github.com" },
@@ -10,6 +10,8 @@ const PREFIXES: { prefix: string; host: Host }[] = [
1010
{ prefix: "https://gitlab.com/", host: "gitlab.com" },
1111
{ prefix: "git@bitbucket.org:", host: "bitbucket.org" },
1212
{ prefix: "https://bitbucket.org/", host: "bitbucket.org" },
13+
{ prefix: "git@codeberg.org:", host: "codeberg.org" },
14+
{ prefix: "https://codeberg.org/", host: "codeberg.org" },
1315
]
1416

1517
export async function configFromUrl(
@@ -22,7 +24,7 @@ export async function configFromUrl(
2224
target?: string | null
2325
},
2426
) {
25-
const tokenRegex = /^https:\/\/([^@]+)@(github\.com|gitlab\.com|bitbucket\.org)/
27+
const tokenRegex = /^https:\/\/([^@]+)@(github\.com|gitlab\.com|bitbucket\.org|codeberg\.org)/
2628
const tokenMatch = url.match(tokenRegex)
2729

2830
let token = ""
@@ -34,6 +36,7 @@ export async function configFromUrl(
3436
"github.com": process.env.GITHUB_TOKEN || process.env.GH_TOKEN || "",
3537
"gitlab.com": process.env.GITLAB_TOKEN || "",
3638
"bitbucket.org": process.env.BITBUCKET_TOKEN || "",
39+
"codeberg.org": process.env.CODEBERG_TOKEN || "",
3740
}
3841
// Detect host early to pick the right env var
3942
for (const { prefix, host: h } of PREFIXES) {
@@ -108,7 +111,7 @@ export async function configFromUrl(
108111
resolvedBranch = branch || (await getDefaultBranch(repoUrl))
109112
resolvedPath = ""
110113
}
111-
} else {
114+
} else if (host === "bitbucket.org") {
112115
// bitbucket.org — uses /src/branch/path for both files and dirs
113116
if (split[2] === "src") {
114117
type = "tree"
@@ -119,6 +122,24 @@ export async function configFromUrl(
119122
resolvedBranch = branch || (await getDefaultBranch(repoUrl))
120123
resolvedPath = ""
121124
}
125+
} else if (host === "codeberg.org") {
126+
// codeberg.org — /src/<branch|tag|commit>/<ref>/path for files+dirs,
127+
// /raw/... and /media/... for direct file (blob) links
128+
if (
129+
["src", "raw", "media"].includes(split[2]) &&
130+
["branch", "tag", "commit"].includes(split[3])
131+
) {
132+
type = split[2] === "src" ? "tree" : "blob"
133+
resolvedBranch = branch || split[4]
134+
resolvedPath = split.slice(5).join("/")
135+
} else {
136+
type = "repository"
137+
resolvedBranch = branch || (await getDefaultBranch(repoUrl))
138+
resolvedPath = ""
139+
}
140+
} else {
141+
const _exhaustive: never = host
142+
throw new Error(`Unsupported host: ${_exhaustive}`)
122143
}
123144

124145
const resolvedTarget = target

tests/cli.test.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,82 @@ describe("dry-run — URL parsing without cloning", () => {
490490
),
491491
30000,
492492
)
493+
494+
// codeberg
495+
it(
496+
"codeberg repo",
497+
() =>
498+
dryRun(
499+
["https://codeberg.org/Codeberg/avatars", "-b", "main"],
500+
"Codeberg/avatars repository:main > avatars",
501+
),
502+
30000,
503+
)
504+
it(
505+
"codeberg src/branch path",
506+
() =>
507+
dryRun(
508+
["https://codeberg.org/Codeberg/avatars/src/branch/main/example"],
509+
"Codeberg/avatars tree:main example > example",
510+
),
511+
30000,
512+
)
513+
it(
514+
"codeberg src/tag path",
515+
() =>
516+
dryRun(
517+
["https://codeberg.org/Codeberg/avatars/src/tag/v1.0.0/example"],
518+
"Codeberg/avatars tree:v1.0.0 example > example",
519+
),
520+
30000,
521+
)
522+
it(
523+
"codeberg src/commit path",
524+
() =>
525+
dryRun(
526+
[
527+
"https://codeberg.org/Codeberg/avatars/src/commit/c86887927797ce57a7e4666494903a4e9b1e901c/example",
528+
],
529+
"Codeberg/avatars tree:c86887927797ce57a7e4666494903a4e9b1e901c example > example",
530+
),
531+
30000,
532+
)
533+
it(
534+
"codeberg raw/branch file",
535+
() =>
536+
dryRun(
537+
["https://codeberg.org/Codeberg/avatars/raw/branch/main/README.md"],
538+
"Codeberg/avatars blob:main README.md > ./README.md",
539+
),
540+
30000,
541+
)
542+
it(
543+
"codeberg media/branch file",
544+
() =>
545+
dryRun(
546+
["https://codeberg.org/Codeberg/avatars/media/branch/main/README.md"],
547+
"Codeberg/avatars blob:main README.md > ./README.md",
548+
),
549+
30000,
550+
)
551+
it(
552+
"codeberg git@ repo",
553+
() =>
554+
dryRun(
555+
["git@codeberg.org:Codeberg/avatars", "-b", "main"],
556+
"Codeberg/avatars repository:main > avatars",
557+
),
558+
30000,
559+
)
560+
it(
561+
"codeberg git@ src path",
562+
() =>
563+
dryRun(
564+
["git@codeberg.org:Codeberg/avatars/src/branch/main/example"],
565+
"Codeberg/avatars tree:main example > example",
566+
),
567+
30000,
568+
)
493569
})
494570

495571
// =====================================================================

0 commit comments

Comments
 (0)