diff --git a/shared/utils/git-providers.ts b/shared/utils/git-providers.ts index aa92dab98a..6a834be461 100644 --- a/shared/utils/git-providers.ts +++ b/shared/utils/git-providers.ts @@ -308,6 +308,29 @@ export function normalizeGitUrl(input: string): string | null { .replace(/(\.[^./]+?):/, '$1/') // change ".com:" to ".com/" from "ssh://user@host.com:..." .replace(/^git:\/\//, 'https://') .replace(/^ssh:\/\//, 'https://') + // SCP-style shorthand with a dotless host (e.g. "git@localhost:owner/repo"); hosts with a + // dot (e.g. "git@github.com:user/repo") are already handled by the replacements above + if (!url.includes('://')) { + const scpMatch = /^(?:[^@/]+@)?([^/:]+):(.+)$/.exec(url) + const host = scpMatch?.[1] + const path = scpMatch?.[2] + if (host && path) { + url = `https://${host}/${path}` + } + } + + // Bare GitHub shorthand (e.g. "repository": "owner/repo"), following npm's convention; + // host-prefixed paths (e.g. "git.sr.ht/~user/repo") are preserved as-is + if (!url.includes('://')) { + const shorthandMatch = /^([^./?#]+)\/([^?#]*)([?#].*)?$/.exec(url) + const owner = shorthandMatch?.[1] + const repo = shorthandMatch?.[2] + const suffix = shorthandMatch?.[3] ?? '' + if (owner && repo) { + return `https://github.com/${owner}/${repo}${suffix}` + } + } + if (!url) return null return url.includes('://') ? url : `https://${url}` } diff --git a/test/unit/shared/utils/git-providers.spec.ts b/test/unit/shared/utils/git-providers.spec.ts index 2d7c337522..c035dc4e65 100644 --- a/test/unit/shared/utils/git-providers.spec.ts +++ b/test/unit/shared/utils/git-providers.spec.ts @@ -96,6 +96,37 @@ describe('normalizeGitUrl', () => { .soft(normalizeGitUrl('github:user/repo.git#readme')) .toBe('https://github.com/user/repo#readme') }) + + it('should expand bare owner/repo GitHub shorthand', () => { + expect.soft(normalizeGitUrl('wevm/ox')).toBe('https://github.com/wevm/ox') + expect.soft(normalizeGitUrl('user/repo.git')).toBe('https://github.com/user/repo') + expect.soft(normalizeGitUrl(' user/repo ')).toBe('https://github.com/user/repo') + expect.soft(normalizeGitUrl('user/repo#readme')).toBe('https://github.com/user/repo#readme') + expect + .soft(normalizeGitUrl('user/repo?path=packages/core')) + .toBe('https://github.com/user/repo?path=packages/core') + expect + .soft(normalizeGitUrl('user/repo/tree/main')) + .toBe('https://github.com/user/repo/tree/main') + }) + + it('should convert scp-style shorthand with a dotless host', () => { + expect.soft(normalizeGitUrl('git@localhost:owner/repo')).toBe('https://localhost/owner/repo') + }) + + it('should not treat host-prefixed paths as owner/repo shorthand', () => { + expect.soft(normalizeGitUrl('git.sr.ht/~user/repo')).toBe('https://git.sr.ht/~user/repo') + expect.soft(normalizeGitUrl('example.com/user/repo')).toBe('https://example.com/user/repo') + }) + + it('should parse bare shorthand repository fields', () => { + const info = parseRepositoryInfo('wevm/ox') + expect.soft(info?.provider).toBe('github') + expect.soft(info?.owner).toBe('wevm') + expect.soft(info?.repo).toBe('ox') + expect.soft(info?.rawBaseUrl).toBe('https://raw.githubusercontent.com/wevm/ox/HEAD') + expect.soft(info?.blobBaseUrl).toBe('https://github.com/wevm/ox/blob/HEAD') + }) }) describe('parseRepositoryInfo', () => {