[READ-ONLY] a fast, modern browser for the npm registry
at main 397 lines 12 kB view raw
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})