Skip to content

Commit 7ef1122

Browse files
committed
fix: url-encode repository directory segments and strip .git suffix (#2100)
1 parent 32ae83d commit 7ef1122

File tree

4 files changed

+86
-3
lines changed

4 files changed

+86
-3
lines changed

app/composables/useRepositoryUrl.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ export function useRepositoryUrl(
2020

2121
// append `repository.directory` for monorepo packages
2222
if (repo.directory) {
23-
url = joinURL(`${url}/tree/HEAD`, repo.directory)
23+
const encodedDirectory = repo.directory.split('/').map(encodeURIComponent).join('/')
24+
url = joinURL(`${url}/tree/HEAD`, encodedDirectory)
2425
}
2526

2627
return url

shared/utils/git-providers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ export function normalizeGitUrl(input: string): string | null {
296296
const raw = input.trim()
297297
if (!raw) return null
298298

299-
const normalized = raw.replace(/^git\+/, '')
299+
const normalized = raw.replace(/^git\+/, '').replace(/\.git$/i, '')
300300

301301
// Handle ssh:// and git:// URLs by converting to https://
302302
if (/^(?:ssh|git):\/\//i.test(normalized)) {
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2+
import { computed, toValue } from 'vue'
3+
import type { SlimPackumentVersion } from '#shared/types/npm-registry'
4+
import { useRepositoryUrl } from '../../../../app/composables/useRepositoryUrl'
5+
6+
function createVersion(repository?: SlimPackumentVersion['repository']): SlimPackumentVersion {
7+
return { repository } as SlimPackumentVersion
8+
}
9+
10+
beforeEach(() => {
11+
vi.stubGlobal('computed', computed)
12+
vi.stubGlobal('toValue', toValue)
13+
vi.stubGlobal('normalizeGitUrl', (url: string) => url)
14+
})
15+
16+
afterEach(() => {
17+
vi.unstubAllGlobals()
18+
})
19+
20+
describe('useRepositoryUrl', () => {
21+
it('returns null when repository url is missing', () => {
22+
const { repositoryUrl } = useRepositoryUrl(null)
23+
expect(repositoryUrl.value).toBeNull()
24+
})
25+
26+
it('returns the url as-is when there is no directory', () => {
27+
const { repositoryUrl } = useRepositoryUrl(
28+
createVersion({ url: 'https://github.com/nuxt/nuxt' }),
29+
)
30+
expect(repositoryUrl.value).toBe('https://github.com/nuxt/nuxt')
31+
})
32+
33+
it('encodes @ in scoped package directory', () => {
34+
const { repositoryUrl } = useRepositoryUrl(
35+
createVersion({
36+
url: 'https://github.com/tailwindlabs/tailwindcss',
37+
directory: 'packages/@tailwindcss-vite',
38+
}),
39+
)
40+
expect(repositoryUrl.value).toBe(
41+
'https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/%40tailwindcss-vite',
42+
)
43+
})
44+
45+
it('does not encode slashes between directory segments', () => {
46+
const { repositoryUrl } = useRepositoryUrl(
47+
createVersion({
48+
url: 'https://github.com/withastro/astro',
49+
directory: 'packages/astro',
50+
}),
51+
)
52+
expect(repositoryUrl.value).toBe('https://github.com/withastro/astro/tree/HEAD/packages/astro')
53+
})
54+
})

test/unit/shared/utils/git-providers.spec.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { describe, expect, it } from 'vitest'
2-
import { parseRepositoryInfo, type RepositoryInfo } from '#shared/utils/git-providers'
2+
import {
3+
normalizeGitUrl,
4+
parseRepositoryInfo,
5+
type RepositoryInfo,
6+
} from '#shared/utils/git-providers'
37

48
describe('parseRepositoryInfo', () => {
59
it('returns undefined for undefined input', () => {
@@ -386,6 +390,30 @@ describe('parseRepositoryInfo', () => {
386390
})
387391
})
388392

393+
describe('normalizeGitUrl', () => {
394+
it('strips .git suffix from HTTPS URLs', () => {
395+
expect(normalizeGitUrl('https://github.com/tailwindlabs/tailwindcss.git')).toBe(
396+
'https://github.com/tailwindlabs/tailwindcss',
397+
)
398+
})
399+
400+
it('strips .git suffix from git+https URLs', () => {
401+
expect(normalizeGitUrl('git+https://github.com/tailwindlabs/tailwindcss.git')).toBe(
402+
'https://github.com/tailwindlabs/tailwindcss',
403+
)
404+
})
405+
406+
it('strips .git suffix from ssh URLs', () => {
407+
expect(normalizeGitUrl('git@github.com:tailwindlabs/tailwindcss.git')).toBe(
408+
'https://github.com/tailwindlabs/tailwindcss',
409+
)
410+
})
411+
412+
it('leaves URLs without .git suffix unchanged', () => {
413+
expect(normalizeGitUrl('https://github.com/nuxt/nuxt')).toBe('https://github.com/nuxt/nuxt')
414+
})
415+
})
416+
389417
describe('RepositoryInfo type', () => {
390418
it('includes blobBaseUrl in RepositoryInfo', () => {
391419
const result = parseRepositoryInfo({

0 commit comments

Comments
 (0)