diff --git a/shared/utils/git-providers.ts b/shared/utils/git-providers.ts index e14e82f5f7..30ce16f096 100644 --- a/shared/utils/git-providers.ts +++ b/shared/utils/git-providers.ts @@ -296,7 +296,7 @@ export function normalizeGitUrl(input: string): string | null { const raw = input.trim() if (!raw) return null - const normalized = raw.replace(/^git\+/, '') + const normalized = raw.replace(/^git\+/, '').replace(/\.git\/?$/, '') // Handle ssh:// and git:// URLs by converting to https:// if (/^(?:ssh|git):\/\//i.test(normalized)) { diff --git a/test/unit/shared/utils/git-providers.spec.ts b/test/unit/shared/utils/git-providers.spec.ts index 242f8b1026..49ea00f526 100644 --- a/test/unit/shared/utils/git-providers.spec.ts +++ b/test/unit/shared/utils/git-providers.spec.ts @@ -1,5 +1,10 @@ import { describe, expect, it } from 'vitest' -import { parseRepositoryInfo, type RepositoryInfo } from '#shared/utils/git-providers' +import { + normalizeGitUrl, + parseRepoUrl, + parseRepositoryInfo, + type RepositoryInfo, +} from '#shared/utils/git-providers' describe('parseRepositoryInfo', () => { it('returns undefined for undefined input', () => { @@ -386,6 +391,111 @@ describe('parseRepositoryInfo', () => { }) }) +describe('normalizeGitUrl', () => { + it('strips git+ prefix', () => { + expect(normalizeGitUrl('git+https://github.com/owner/repo')).toBe( + 'https://github.com/owner/repo', + ) + }) + + it('strips .git suffix', () => { + expect(normalizeGitUrl('https://github.com/owner/repo.git')).toBe( + 'https://github.com/owner/repo', + ) + }) + + it('strips .git/ suffix with trailing slash', () => { + expect(normalizeGitUrl('https://github.com/owner/repo.git/')).toBe( + 'https://github.com/owner/repo', + ) + }) + + it('strips both git+ prefix and .git suffix', () => { + expect(normalizeGitUrl('git+https://github.com/owner/repo.git')).toBe( + 'https://github.com/owner/repo', + ) + }) + + it('strips .git suffix from ssh:// URL before converting to https', () => { + expect(normalizeGitUrl('ssh://git@github.com/owner/repo.git')).toBe( + 'https://github.com/owner/repo', + ) + }) + + it('strips .git suffix from git:// URL before converting to https', () => { + expect(normalizeGitUrl('git://github.com/owner/repo.git')).toBe('https://github.com/owner/repo') + }) + + it('strips .git suffix from SCP-style URL', () => { + expect(normalizeGitUrl('git@github.com:owner/repo.git')).toBe('https://github.com/owner/repo') + }) + + it('strips .git/ suffix from git+ssh:// URL', () => { + expect(normalizeGitUrl('git+ssh://git@gitlab.com/group/repo.git/')).toBe( + 'https://gitlab.com/group/repo', + ) + }) + + it('returns null for empty string', () => { + expect(normalizeGitUrl('')).toBeNull() + }) + + it('returns null for whitespace-only string', () => { + expect(normalizeGitUrl(' ')).toBeNull() + }) + + it('preserves URL without .git suffix', () => { + expect(normalizeGitUrl('https://github.com/owner/repo')).toBe('https://github.com/owner/repo') + }) +}) + +describe('parseRepoUrl with .git suffix variations', () => { + it('parses ssh:// URL with .git suffix', () => { + const result = parseRepoUrl('ssh://git@github.com/owner/repo.git') + expect(result).toMatchObject({ + provider: 'github', + owner: 'owner', + repo: 'repo', + }) + }) + + it('parses git:// URL with .git suffix', () => { + const result = parseRepoUrl('git://github.com/owner/repo.git') + expect(result).toMatchObject({ + provider: 'github', + owner: 'owner', + repo: 'repo', + }) + }) + + it('parses SCP-style URL with .git suffix', () => { + const result = parseRepoUrl('git@github.com:owner/repo.git') + expect(result).toMatchObject({ + provider: 'github', + owner: 'owner', + repo: 'repo', + }) + }) + + it('parses URL with .git/ trailing slash', () => { + const result = parseRepoUrl('https://github.com/owner/repo.git/') + expect(result).toMatchObject({ + provider: 'github', + owner: 'owner', + repo: 'repo', + }) + }) + + it('parses GitLab SCP-style URL with .git suffix', () => { + const result = parseRepoUrl('git@gitlab.com:group/subgroup/repo.git') + expect(result).toMatchObject({ + provider: 'gitlab', + owner: 'group/subgroup', + repo: 'repo', + }) + }) +}) + describe('RepositoryInfo type', () => { it('includes blobBaseUrl in RepositoryInfo', () => { const result = parseRepositoryInfo({