Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/composables/useRepositoryUrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ export function useRepositoryUrl(
}

let url = normalizeGitUrl(repo.url)
if (!url) {
return null
}

// append `repository.directory` for monorepo packages
if (repo.directory) {
Expand Down
37 changes: 10 additions & 27 deletions shared/utils/git-providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,33 +293,16 @@ const providers: ProviderConfig[] = [
* Handles: git+https://, git://, git@host:path, ssh://git@host/path
*/
export function normalizeGitUrl(input: string): string | null {
const raw = input.trim()
if (!raw) return null

const normalized = raw.replace(/^git\+/, '')

// Handle ssh:// and git:// URLs by converting to https://
if (/^(?:ssh|git):\/\//i.test(normalized)) {
try {
const url = new URL(normalized)
const path = url.pathname.replace(/^\/*/, '')
return `https://${url.hostname}/${path}`
} catch {
// Fall through to SCP handling
}
}

if (!/^https?:\/\//i.test(normalized)) {
// Handle SCP-style URLs: git@host:path
const scp = normalized.match(/^(?:git@)?([^:/]+):(.+)$/i)
if (scp?.[1] && scp?.[2]) {
const host = scp[1]
const path = scp[2].replace(/^\/*/, '')
return `https://${host}/${path}`
}
}

return normalized
const url = input
.trim()
.replace(/^git\+/, '')
.replace(/\.git$/, '')
.replace(/(^|\/)[^/]+?@/, '$1') // remove "user@" from "ssh://user@host.com:..."
.replace(/(\.[^./]+?):/, '$1/') // change ".com:" to ".com/" from "ssh://user@host.com:..."
.replace(/^git:\/\//, 'https://')
.replace(/^ssh:\/\//, 'https://')
if (!url) return null
return url.includes('://') ? url : `https://${url}`
}

export function parseRepoUrl(input: string): RepoRef | null {
Expand Down
77 changes: 76 additions & 1 deletion test/unit/shared/utils/git-providers.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,80 @@
import { describe, expect, it } from 'vitest'
import { parseRepositoryInfo, type RepositoryInfo } from '#shared/utils/git-providers'
import {
normalizeGitUrl,
parseRepositoryInfo,
type RepositoryInfo,
} from '#shared/utils/git-providers'

describe('normalizeGitUrl', () => {
it('should return null for empty input', () => {
expect.soft(normalizeGitUrl('')).toBeNull()
})

it('should leave plain HTTPS URLs unchanged', () => {
expect
.soft(normalizeGitUrl('https://github.com/user/repo'))
.toBe('https://github.com/user/repo')
})

it('should remove git+ prefix', () => {
expect
.soft(normalizeGitUrl('git+https://github.com/user/repo'))
.toBe('https://github.com/user/repo')
expect
.soft(normalizeGitUrl('git+https://github.com/user/repo.git'))
.toBe('https://github.com/user/repo')
expect
.soft(normalizeGitUrl('git+ssh://git@github.com/user/repo.git'))
.toBe('https://github.com/user/repo')
})

it('should remove .git suffix', () => {
expect
.soft(normalizeGitUrl('https://github.com/user/repo.git'))
.toBe('https://github.com/user/repo')
expect
.soft(normalizeGitUrl('https://gitlab.com/user/repo.git'))
.toBe('https://gitlab.com/user/repo')
expect
.soft(normalizeGitUrl('https://bitbucket.org/user/repo.git'))
.toBe('https://bitbucket.org/user/repo')
})

it('should convert git:// protocol to https://', () => {
expect.soft(normalizeGitUrl('git://github.com/user/repo')).toBe('https://github.com/user/repo')
expect
.soft(normalizeGitUrl('git://github.com/user/repo.git'))
.toBe('https://github.com/user/repo')
})

it('should convert ssh:// protocol to https://', () => {
expect
.soft(normalizeGitUrl('ssh://git@github.com/user/repo'))
.toBe('https://github.com/user/repo')
expect
.soft(normalizeGitUrl('ssh://git@github.com/user/repo.git'))
.toBe('https://github.com/user/repo')
})

it('should convert SSH format to https://', () => {
expect.soft(normalizeGitUrl('git@github.com:user/repo')).toBe('https://github.com/user/repo')
expect
.soft(normalizeGitUrl('git@github.com:user/repo.git'))
.toBe('https://github.com/user/repo')
expect
.soft(normalizeGitUrl('git@github.com/user/repo:123'))
.toBe('https://github.com/user/repo:123')
})

it('should handle combined permutations', () => {
expect
.soft(normalizeGitUrl('git+git://github.com/user/repo.git'))
.toBe('https://github.com/user/repo')
expect
.soft(normalizeGitUrl('git+ssh://git@gitlab.com/user/repo.git'))
.toBe('https://gitlab.com/user/repo')
})
})

describe('parseRepositoryInfo', () => {
it('returns undefined for undefined input', () => {
Expand Down
Loading