You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
152 lines
4.7 KiB
152 lines
4.7 KiB
/** |
|
* Repository URL Parser |
|
* Handles parsing and validation of repository URLs |
|
*/ |
|
|
|
import { join } from 'path'; |
|
import { GIT_DOMAIN } from '../../config.js'; |
|
import { extractCloneUrls } from '../../utils/nostr-utils.js'; |
|
import type { NostrEvent } from '../../types/nostr.js'; |
|
|
|
export interface RepoPath { |
|
npub: string; |
|
repoName: string; |
|
fullPath: string; |
|
} |
|
|
|
/** |
|
* Check if a URL is a GRASP (Git Repository Access via Secure Protocol) URL |
|
* GRASP URLs contain npub (Nostr public key) in the path: https://host/npub.../repo.git |
|
*/ |
|
export function isGraspUrl(url: string): boolean { |
|
// GRASP URLs have npub (starts with npub1) in the path |
|
return /\/npub1[a-z0-9]+/i.test(url); |
|
} |
|
|
|
/** |
|
* Repository URL Parser |
|
* Handles parsing git domain URLs and extracting repository information |
|
*/ |
|
export class RepoUrlParser { |
|
private repoRoot: string; |
|
private domain: string; |
|
|
|
constructor(repoRoot: string = '/repos', domain: string = GIT_DOMAIN) { |
|
this.repoRoot = repoRoot; |
|
this.domain = domain; |
|
} |
|
|
|
/** |
|
* Parse git domain URL to extract npub and repo name |
|
*/ |
|
parseRepoUrl(url: string): RepoPath | null { |
|
// Match: https://{domain}/{npub}/{repo-name}.git or http://{domain}/{npub}/{repo-name}.git |
|
// Escape domain for regex (replace dots with \.) |
|
const escapedDomain = this.domain.replace(/\./g, '\\.'); |
|
const match = url.match(new RegExp(`${escapedDomain}\\/(npub[a-z0-9]+)\\/([^\\/]+)\\.git`)); |
|
if (!match) return null; |
|
|
|
const [, npub, repoName] = match; |
|
const fullPath = join(this.repoRoot, npub, `${repoName}.git`); |
|
|
|
return { npub, repoName, fullPath }; |
|
} |
|
|
|
/** |
|
* Extract clone URLs from a NIP-34 repo announcement |
|
* Uses shared utility with normalization enabled |
|
*/ |
|
extractCloneUrls(event: NostrEvent): string[] { |
|
return extractCloneUrls(event, true); |
|
} |
|
|
|
/** |
|
* Convert SSH URL to HTTPS URL if possible |
|
* e.g., git@github.com:user/repo.git -> https://github.com/user/repo.git |
|
*/ |
|
convertSshToHttps(url: string): string | null { |
|
// Check if it's an SSH URL (git@host:path or ssh://) |
|
const sshMatch = url.match(/^git@([^:]+):(.+)$/); |
|
if (sshMatch) { |
|
const [, host, path] = sshMatch; |
|
// Remove .git suffix if present, we'll add it back |
|
const cleanPath = path.replace(/\.git$/, ''); |
|
return `https://${host}/${cleanPath}.git`; |
|
} |
|
|
|
// Check for ssh:// URLs |
|
if (url.startsWith('ssh://')) { |
|
const sshUrlMatch = url.match(/^ssh:\/\/([^/]+)\/(.+)$/); |
|
if (sshUrlMatch) { |
|
const [, host, path] = sshUrlMatch; |
|
const cleanPath = path.replace(/\.git$/, ''); |
|
return `https://${host}/${cleanPath}.git`; |
|
} |
|
} |
|
|
|
return null; |
|
} |
|
|
|
/** |
|
* Filter and prepare remote URLs from clone URLs |
|
* Respects the repo owner's order in the clone list |
|
*/ |
|
prepareRemoteUrls(cloneUrls: string[]): string[] { |
|
const httpsUrls: string[] = []; |
|
const sshUrls: string[] = []; |
|
|
|
for (const url of cloneUrls) { |
|
const lowerUrl = url.toLowerCase(); |
|
|
|
// Skip localhost and our own domain |
|
if (lowerUrl.includes('localhost') || |
|
lowerUrl.includes('127.0.0.1') || |
|
url.includes(this.domain)) { |
|
continue; |
|
} |
|
|
|
// Check if it's an SSH URL |
|
if (url.startsWith('git@') || url.startsWith('ssh://')) { |
|
sshUrls.push(url); |
|
// Try to convert to HTTPS (preserve original order by appending) |
|
const httpsUrl = this.convertSshToHttps(url); |
|
if (httpsUrl) { |
|
httpsUrls.push(httpsUrl); |
|
} |
|
} else { |
|
// It's already HTTPS/HTTP - preserve original order |
|
httpsUrls.push(url); |
|
} |
|
} |
|
|
|
// Respect the repo owner's order: use HTTPS URLs in the order they appeared in clone list |
|
let remoteUrls = httpsUrls; |
|
|
|
// If no HTTPS URLs, try SSH URLs (but log a warning) |
|
if (remoteUrls.length === 0 && sshUrls.length > 0) { |
|
remoteUrls = sshUrls; |
|
} |
|
|
|
// If no external URLs, try any URL that's not our domain (preserve order) |
|
if (remoteUrls.length === 0) { |
|
remoteUrls = cloneUrls.filter(url => !url.includes(this.domain)); |
|
} |
|
|
|
// If still no remote URLs, but there are *any* clone URLs, try the first one |
|
// This handles cases where the only clone URL is our own domain, but the repo doesn't exist locally yet |
|
if (remoteUrls.length === 0 && cloneUrls.length > 0) { |
|
remoteUrls.push(cloneUrls[0]); |
|
} |
|
|
|
return remoteUrls; |
|
} |
|
|
|
/** |
|
* Parse repo path to extract repo name (helper for verification file creation) |
|
*/ |
|
parseRepoPathForName(repoPath: string): { repoName: string } | null { |
|
const match = repoPath.match(/\/([^\/]+)\.git$/); |
|
if (!match) return null; |
|
return { repoName: match[1] }; |
|
} |
|
}
|
|
|