Skip to content

Commit 66a821c

Browse files
committed
fix: strip .git suffix from normalized repository url
1 parent 8ee186a commit 66a821c

2 files changed

Lines changed: 112 additions & 2 deletions

File tree

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\/?$/, '')
300300

301301
// Handle ssh:// and git:// URLs by converting to https://
302302
if (/^(?:ssh|git):\/\//i.test(normalized)) {

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

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

49
describe('parseRepositoryInfo', () => {
510
it('returns undefined for undefined input', () => {
@@ -386,6 +391,111 @@ describe('parseRepositoryInfo', () => {
386391
})
387392
})
388393

394+
describe('normalizeGitUrl', () => {
395+
it('strips git+ prefix', () => {
396+
expect(normalizeGitUrl('git+https://github.com/owner/repo')).toBe(
397+
'https://github.com/owner/repo',
398+
)
399+
})
400+
401+
it('strips .git suffix', () => {
402+
expect(normalizeGitUrl('https://github.com/owner/repo.git')).toBe(
403+
'https://github.com/owner/repo',
404+
)
405+
})
406+
407+
it('strips .git/ suffix with trailing slash', () => {
408+
expect(normalizeGitUrl('https://github.com/owner/repo.git/')).toBe(
409+
'https://github.com/owner/repo',
410+
)
411+
})
412+
413+
it('strips both git+ prefix and .git suffix', () => {
414+
expect(normalizeGitUrl('git+https://github.com/owner/repo.git')).toBe(
415+
'https://github.com/owner/repo',
416+
)
417+
})
418+
419+
it('strips .git suffix from ssh:// URL before converting to https', () => {
420+
expect(normalizeGitUrl('ssh://git@github.com/owner/repo.git')).toBe(
421+
'https://github.com/owner/repo',
422+
)
423+
})
424+
425+
it('strips .git suffix from git:// URL before converting to https', () => {
426+
expect(normalizeGitUrl('git://github.com/owner/repo.git')).toBe('https://github.com/owner/repo')
427+
})
428+
429+
it('strips .git suffix from SCP-style URL', () => {
430+
expect(normalizeGitUrl('git@github.com:owner/repo.git')).toBe('https://github.com/owner/repo')
431+
})
432+
433+
it('strips .git/ suffix from git+ssh:// URL', () => {
434+
expect(normalizeGitUrl('git+ssh://git@gitlab.com/group/repo.git/')).toBe(
435+
'https://gitlab.com/group/repo',
436+
)
437+
})
438+
439+
it('returns null for empty string', () => {
440+
expect(normalizeGitUrl('')).toBeNull()
441+
})
442+
443+
it('returns null for whitespace-only string', () => {
444+
expect(normalizeGitUrl(' ')).toBeNull()
445+
})
446+
447+
it('preserves URL without .git suffix', () => {
448+
expect(normalizeGitUrl('https://github.com/owner/repo')).toBe('https://github.com/owner/repo')
449+
})
450+
})
451+
452+
describe('parseRepoUrl with .git suffix variations', () => {
453+
it('parses ssh:// URL with .git suffix', () => {
454+
const result = parseRepoUrl('ssh://git@github.com/owner/repo.git')
455+
expect(result).toMatchObject({
456+
provider: 'github',
457+
owner: 'owner',
458+
repo: 'repo',
459+
})
460+
})
461+
462+
it('parses git:// URL with .git suffix', () => {
463+
const result = parseRepoUrl('git://github.com/owner/repo.git')
464+
expect(result).toMatchObject({
465+
provider: 'github',
466+
owner: 'owner',
467+
repo: 'repo',
468+
})
469+
})
470+
471+
it('parses SCP-style URL with .git suffix', () => {
472+
const result = parseRepoUrl('git@github.com:owner/repo.git')
473+
expect(result).toMatchObject({
474+
provider: 'github',
475+
owner: 'owner',
476+
repo: 'repo',
477+
})
478+
})
479+
480+
it('parses URL with .git/ trailing slash', () => {
481+
const result = parseRepoUrl('https://github.com/owner/repo.git/')
482+
expect(result).toMatchObject({
483+
provider: 'github',
484+
owner: 'owner',
485+
repo: 'repo',
486+
})
487+
})
488+
489+
it('parses GitLab SCP-style URL with .git suffix', () => {
490+
const result = parseRepoUrl('git@gitlab.com:group/subgroup/repo.git')
491+
expect(result).toMatchObject({
492+
provider: 'gitlab',
493+
owner: 'group/subgroup',
494+
repo: 'repo',
495+
})
496+
})
497+
})
498+
389499
describe('RepositoryInfo type', () => {
390500
it('includes blobBaseUrl in RepositoryInfo', () => {
391501
const result = parseRepositoryInfo({

0 commit comments

Comments
 (0)