forked from
npmx.dev/npmx.dev
[READ-ONLY]
a fast, modern browser for the npm registry
1import { describe, expect, it } from 'vitest'
2import { parseRepositoryInfo, type RepositoryInfo } from '#shared/utils/git-providers'
3
4describe('parseRepositoryInfo', () => {
5 it('returns undefined for undefined input', () => {
6 expect(parseRepositoryInfo(undefined)).toBeUndefined()
7 })
8
9 it('parses GitHub URL from object with git+ prefix', () => {
10 const result = parseRepositoryInfo({
11 type: 'git',
12 url: 'git+https://github.com/vercel/ai.git',
13 })
14 expect(result).toMatchObject({
15 provider: 'github',
16 owner: 'vercel',
17 repo: 'ai',
18 rawBaseUrl: 'https://raw.githubusercontent.com/vercel/ai/HEAD',
19 directory: undefined,
20 })
21 })
22
23 it('parses GitHub URL with directory (monorepo)', () => {
24 const result = parseRepositoryInfo({
25 type: 'git',
26 url: 'git+https://github.com/withastro/astro.git',
27 directory: 'packages/astro',
28 })
29 expect(result).toMatchObject({
30 provider: 'github',
31 owner: 'withastro',
32 repo: 'astro',
33 rawBaseUrl: 'https://raw.githubusercontent.com/withastro/astro/HEAD',
34 directory: 'packages/astro',
35 })
36 })
37
38 it('parses shorthand GitHub string', () => {
39 const result = parseRepositoryInfo('github:nuxt/nuxt')
40 // This shorthand format is not supported
41 expect(result).toBeUndefined()
42 })
43
44 it('parses HTTPS GitHub URL without .git suffix', () => {
45 const result = parseRepositoryInfo({
46 url: 'https://github.com/nuxt/nuxt',
47 })
48 expect(result).toMatchObject({
49 provider: 'github',
50 owner: 'nuxt',
51 repo: 'nuxt',
52 rawBaseUrl: 'https://raw.githubusercontent.com/nuxt/nuxt/HEAD',
53 })
54 })
55
56 it('parses string URL directly', () => {
57 const result = parseRepositoryInfo('https://github.com/owner/repo.git')
58 expect(result).toMatchObject({
59 provider: 'github',
60 owner: 'owner',
61 repo: 'repo',
62 rawBaseUrl: 'https://raw.githubusercontent.com/owner/repo/HEAD',
63 })
64 })
65
66 it('removes trailing slash from directory', () => {
67 const result = parseRepositoryInfo({
68 url: 'git+https://github.com/org/repo.git',
69 directory: 'packages/foo/',
70 })
71 expect(result?.directory).toBe('packages/foo')
72 })
73
74 it('returns undefined for empty URL', () => {
75 const result = parseRepositoryInfo({ url: '' })
76 expect(result).toBeUndefined()
77 })
78
79 // Multi-provider tests
80 describe('GitLab support', () => {
81 it('parses GitLab URL', () => {
82 const result = parseRepositoryInfo({
83 url: 'https://gitlab.com/owner/repo.git',
84 })
85 expect(result).toMatchObject({
86 provider: 'gitlab',
87 owner: 'owner',
88 repo: 'repo',
89 host: 'gitlab.com',
90 rawBaseUrl: 'https://gitlab.com/owner/repo/-/raw/HEAD',
91 })
92 })
93
94 it('parses GitLab URL with nested groups', () => {
95 const result = parseRepositoryInfo({
96 url: 'git+https://gitlab.com/hyper-expanse/open-source/semantic-release-gitlab.git',
97 })
98 expect(result).toMatchObject({
99 provider: 'gitlab',
100 owner: 'hyper-expanse/open-source',
101 repo: 'semantic-release-gitlab',
102 host: 'gitlab.com',
103 })
104 })
105
106 it('parses self-hosted GitLab (GNOME)', () => {
107 const result = parseRepositoryInfo({
108 url: 'https://gitlab.gnome.org/ewlsh/packages.gi.ts.git',
109 })
110 expect(result).toMatchObject({
111 provider: 'gitlab',
112 host: 'gitlab.gnome.org',
113 })
114 })
115 })
116
117 describe('Codeberg support', () => {
118 it('parses Codeberg URL', () => {
119 const result = parseRepositoryInfo({
120 url: 'https://codeberg.org/jgarber/CashCash',
121 })
122 expect(result).toMatchObject({
123 provider: 'codeberg',
124 owner: 'jgarber',
125 repo: 'CashCash',
126 })
127 })
128 })
129
130 describe('Bitbucket support', () => {
131 it('parses Bitbucket URL', () => {
132 const result = parseRepositoryInfo({
133 url: 'git+https://bitbucket.org/atlassian/atlassian-frontend-mirror.git',
134 })
135 expect(result).toMatchObject({
136 provider: 'bitbucket',
137 owner: 'atlassian',
138 repo: 'atlassian-frontend-mirror',
139 })
140 })
141 })
142
143 describe('Gitee support', () => {
144 it('parses Gitee URL', () => {
145 const result = parseRepositoryInfo({
146 url: 'git+https://gitee.com/oschina/mcp-gitee.git',
147 })
148 expect(result).toMatchObject({
149 provider: 'gitee',
150 owner: 'oschina',
151 repo: 'mcp-gitee',
152 })
153 })
154 })
155
156 describe('Sourcehut support', () => {
157 it('parses Sourcehut URL', () => {
158 const result = parseRepositoryInfo({
159 url: 'https://git.sr.ht/~ayoayco/astro-resume.git',
160 })
161 expect(result).toMatchObject({
162 provider: 'sourcehut',
163 owner: '~ayoayco',
164 repo: 'astro-resume',
165 })
166 })
167 })
168
169 describe('Tangled support', () => {
170 it('parses Tangled URL with tangled.org domain', () => {
171 const result = parseRepositoryInfo({
172 url: 'https://tangled.org/nonbinary.computer/weaver',
173 })
174 expect(result).toMatchObject({
175 provider: 'tangled',
176 owner: 'nonbinary.computer',
177 repo: 'weaver',
178 rawBaseUrl: 'https://tangled.sh/nonbinary.computer/weaver/raw/branch/main',
179 })
180 })
181
182 it('parses Tangled URL with tangled.sh domain', () => {
183 const result = parseRepositoryInfo({
184 url: 'https://tangled.sh/pds.ls/pdsls',
185 })
186 expect(result).toMatchObject({
187 provider: 'tangled',
188 owner: 'pds.ls',
189 repo: 'pdsls',
190 rawBaseUrl: 'https://tangled.sh/pds.ls/pdsls/raw/branch/main',
191 })
192 })
193
194 it('parses Tangled URL with .git suffix', () => {
195 const result = parseRepositoryInfo({
196 type: 'git',
197 url: 'https://tangled.org/owner/repo.git',
198 })
199 expect(result).toMatchObject({
200 provider: 'tangled',
201 owner: 'owner',
202 repo: 'repo',
203 })
204 })
205
206 it('parses Tangled URL with directory (monorepo)', () => {
207 const result = parseRepositoryInfo({
208 url: 'https://tangled.org/tangled.org/core',
209 directory: 'packages/web',
210 })
211 expect(result).toMatchObject({
212 provider: 'tangled',
213 owner: 'tangled.org',
214 repo: 'core',
215 directory: 'packages/web',
216 })
217 })
218 })
219
220 describe('Radicle support', () => {
221 it('parses Radicle URL from app.radicle.at', () => {
222 const result = parseRepositoryInfo({
223 url: 'https://app.radicle.at/nodes/seed.radicle.at/rad:z3nP4yT1PE3m1PxLEzr173sZtJVnT',
224 })
225 expect(result).toMatchObject({
226 provider: 'radicle',
227 owner: '',
228 repo: 'rad:z3nP4yT1PE3m1PxLEzr173sZtJVnT',
229 host: 'app.radicle.at',
230 })
231 })
232
233 it('parses Radicle URL from seed.radicle.at', () => {
234 const result = parseRepositoryInfo({
235 url: 'https://seed.radicle.at/rad:z3nP4yT1PE3m1PxLEzr173sZtJVnT',
236 })
237 expect(result).toMatchObject({
238 provider: 'radicle',
239 owner: '',
240 repo: 'rad:z3nP4yT1PE3m1PxLEzr173sZtJVnT',
241 host: 'seed.radicle.at',
242 })
243 })
244 })
245
246 describe('Forgejo support', () => {
247 it('parses Forgejo URL from forgejo subdomain', () => {
248 const result = parseRepositoryInfo({
249 url: 'https://forgejo.example.com/owner/repo',
250 })
251 expect(result).toMatchObject({
252 provider: 'forgejo',
253 owner: 'owner',
254 repo: 'repo',
255 host: 'forgejo.example.com',
256 })
257 })
258
259 it('parses Forgejo URL from next.forgejo.org', () => {
260 const result = parseRepositoryInfo({
261 url: 'https://next.forgejo.org/forgejo/forgejo',
262 })
263 expect(result).toMatchObject({
264 provider: 'forgejo',
265 owner: 'forgejo',
266 repo: 'forgejo',
267 host: 'next.forgejo.org',
268 })
269 })
270
271 it('parses Forgejo URL with .git suffix', () => {
272 const result = parseRepositoryInfo({
273 url: 'git+ssh://git@forgejo.myserver.com/user/project.git',
274 })
275 expect(result).toMatchObject({
276 provider: 'forgejo',
277 owner: 'user',
278 repo: 'project',
279 host: 'forgejo.myserver.com',
280 })
281 })
282 })
283
284 describe('blobBaseUrl generation', () => {
285 it('generates correct blobBaseUrl for GitHub', () => {
286 const result = parseRepositoryInfo({
287 url: 'https://github.com/vercel/ai.git',
288 })
289 expect(result).toMatchObject({
290 rawBaseUrl: 'https://raw.githubusercontent.com/vercel/ai/HEAD',
291 blobBaseUrl: 'https://github.com/vercel/ai/blob/HEAD',
292 })
293 })
294
295 it('generates correct blobBaseUrl for GitLab', () => {
296 const result = parseRepositoryInfo({
297 url: 'https://gitlab.com/owner/repo.git',
298 })
299 expect(result).toMatchObject({
300 rawBaseUrl: 'https://gitlab.com/owner/repo/-/raw/HEAD',
301 blobBaseUrl: 'https://gitlab.com/owner/repo/-/blob/HEAD',
302 })
303 })
304
305 it('generates correct blobBaseUrl for self-hosted GitLab', () => {
306 const result = parseRepositoryInfo({
307 url: 'https://gitlab.gnome.org/ewlsh/packages.gi.ts.git',
308 })
309 expect(result).toMatchObject({
310 rawBaseUrl: 'https://gitlab.gnome.org/ewlsh/packages.gi.ts/-/raw/HEAD',
311 blobBaseUrl: 'https://gitlab.gnome.org/ewlsh/packages.gi.ts/-/blob/HEAD',
312 })
313 })
314
315 it('generates correct blobBaseUrl for Bitbucket', () => {
316 const result = parseRepositoryInfo({
317 url: 'https://bitbucket.org/atlassian/atlassian-frontend-mirror.git',
318 })
319 expect(result).toMatchObject({
320 rawBaseUrl: 'https://bitbucket.org/atlassian/atlassian-frontend-mirror/raw/HEAD',
321 blobBaseUrl: 'https://bitbucket.org/atlassian/atlassian-frontend-mirror/src/HEAD',
322 })
323 })
324
325 it('generates correct blobBaseUrl for Codeberg', () => {
326 const result = parseRepositoryInfo({
327 url: 'https://codeberg.org/jgarber/CashCash',
328 })
329 expect(result).toMatchObject({
330 rawBaseUrl: 'https://codeberg.org/jgarber/CashCash/raw/branch/main',
331 blobBaseUrl: 'https://codeberg.org/jgarber/CashCash/src/branch/main',
332 })
333 })
334
335 it('generates correct blobBaseUrl for Gitee', () => {
336 const result = parseRepositoryInfo({
337 url: 'https://gitee.com/oschina/mcp-gitee.git',
338 })
339 expect(result).toMatchObject({
340 rawBaseUrl: 'https://gitee.com/oschina/mcp-gitee/raw/master',
341 blobBaseUrl: 'https://gitee.com/oschina/mcp-gitee/blob/master',
342 })
343 })
344
345 it('generates correct blobBaseUrl for Sourcehut', () => {
346 const result = parseRepositoryInfo({
347 url: 'https://git.sr.ht/~ayoayco/astro-resume.git',
348 })
349 expect(result).toMatchObject({
350 rawBaseUrl: 'https://git.sr.ht/~ayoayco/astro-resume/blob/HEAD',
351 blobBaseUrl: 'https://git.sr.ht/~ayoayco/astro-resume/tree/HEAD/item',
352 })
353 })
354
355 it('generates correct blobBaseUrl for Tangled', () => {
356 const result = parseRepositoryInfo({
357 url: 'https://tangled.sh/pds.ls/pdsls',
358 })
359 expect(result).toMatchObject({
360 rawBaseUrl: 'https://tangled.sh/pds.ls/pdsls/raw/branch/main',
361 blobBaseUrl: 'https://tangled.sh/pds.ls/pdsls/src/branch/main',
362 })
363 })
364
365 it('generates correct blobBaseUrl for Radicle', () => {
366 const result = parseRepositoryInfo({
367 url: 'https://app.radicle.at/nodes/seed.radicle.at/rad:z3nP4yT1PE3m1PxLEzr173sZtJVnT',
368 })
369 expect(result).toMatchObject({
370 rawBaseUrl:
371 'https://seed.radicle.at/api/v1/projects/rad:z3nP4yT1PE3m1PxLEzr173sZtJVnT/blob/HEAD',
372 blobBaseUrl:
373 'https://app.radicle.at/nodes/seed.radicle.at/rad:z3nP4yT1PE3m1PxLEzr173sZtJVnT/tree/HEAD',
374 })
375 })
376
377 it('generates correct blobBaseUrl for Forgejo', () => {
378 const result = parseRepositoryInfo({
379 url: 'https://next.forgejo.org/forgejo/forgejo',
380 })
381 expect(result).toMatchObject({
382 rawBaseUrl: 'https://next.forgejo.org/forgejo/forgejo/raw/branch/main',
383 blobBaseUrl: 'https://next.forgejo.org/forgejo/forgejo/src/branch/main',
384 })
385 })
386 })
387})
388
389describe('RepositoryInfo type', () => {
390 it('includes blobBaseUrl in RepositoryInfo', () => {
391 const result = parseRepositoryInfo({
392 url: 'https://github.com/test/repo',
393 }) as RepositoryInfo
394 expect(result).toHaveProperty('blobBaseUrl')
395 expect(typeof result.blobBaseUrl).toBe('string')
396 })
397})