diff --git a/gitnexus/src/server/api.ts b/gitnexus/src/server/api.ts index 59096e5b8..d9aa00ae6 100644 --- a/gitnexus/src/server/api.ts +++ b/gitnexus/src/server/api.ts @@ -33,7 +33,7 @@ import { mountMCPEndpoints } from './mcp-http.js'; import { fork } from 'child_process'; import { fileURLToPath, pathToFileURL } from 'url'; import { JobManager } from './analyze-job.js'; -import { extractRepoName, extractRepoSlug, getCloneDir, cloneOrPull } from './git-clone.js'; +import { extractRepoName, getCloneDir, cloneOrPull } from './git-clone.js'; const _require = createRequire(import.meta.url); const pkg = _require('../../package.json'); @@ -776,9 +776,7 @@ export const createServer = async (port: number, host: string = '127.0.0.1') => await fs.rm(storagePath, { recursive: true, force: true }).catch(() => {}); // 2. Delete the cloned repo dir if it lives under ~/.gitnexus/repos/ - const cloneDir = getCloneDir( - entry.remoteUrl ? extractRepoSlug(entry.remoteUrl) : entry.name, - ); + const cloneDir = getCloneDir(entry.name); try { const stat = await fs.stat(cloneDir); if (stat.isDirectory()) { @@ -1293,8 +1291,7 @@ export const createServer = async (port: number, host: string = '127.0.0.1') => // Clone if URL provided if (repoUrl && !repoLocalPath) { const repoName = extractRepoName(repoUrl); - const repoSlug = extractRepoSlug(repoUrl); - targetPath = getCloneDir(repoSlug); + targetPath = getCloneDir(repoName); jobManager.updateJob(job.id, { status: 'cloning', diff --git a/gitnexus/src/server/git-clone.ts b/gitnexus/src/server/git-clone.ts index b07171f1c..0f7bc2653 100644 --- a/gitnexus/src/server/git-clone.ts +++ b/gitnexus/src/server/git-clone.ts @@ -15,51 +15,10 @@ import { isIP } from 'net'; export function extractRepoName(url: string): string { const cleaned = url.replace(/\/+$/, ''); const lastSegment = cleaned.split(/[/:]/).pop() || 'unknown'; - return lastSegment.replace(/\.git$/i, ''); + return lastSegment.replace(/\.git$/, ''); } -const sanitizeClonePathSegment = (segment: string): string => { - const sanitized = segment.replace(/[^a-zA-Z0-9._-]/g, '-'); - return sanitized && sanitized !== '.' && sanitized !== '..' ? sanitized : 'unknown'; -}; - -/** - * Extract an owner-qualified clone slug from an HTTP(S) git URL. - * - * The server's clone cache must not be keyed only by the repository basename: - * different owners can legitimately publish repositories with the same name. - * Include the host and path components so those repositories do not silently - * reuse each other's cached clone directory. - */ -export function extractRepoSlug(url: string): string { - let parsed: URL; - try { - parsed = new URL(url); - } catch { - return sanitizeClonePathSegment(extractRepoName(url)); - } - - const rawSegments = parsed.pathname.split('/').filter(Boolean); - if (rawSegments.length === 0) { - return sanitizeClonePathSegment(extractRepoName(url)); - } - - rawSegments[rawSegments.length - 1] = rawSegments[rawSegments.length - 1].replace( - /\.git$/i, - '', - ); - const segments = [parsed.hostname.toLowerCase(), ...rawSegments].map((segment) => { - try { - return sanitizeClonePathSegment(decodeURIComponent(segment)); - } catch { - return sanitizeClonePathSegment(segment); - } - }); - - return segments.join('/'); -} - -/** Get the clone target directory for a repo name or owner-qualified slug. */ +/** Get the clone target directory for a repo name. */ export function getCloneDir(repoName: string): string { return path.join(os.homedir(), '.gitnexus', 'repos', repoName); } diff --git a/gitnexus/test/unit/git-clone.test.ts b/gitnexus/test/unit/git-clone.test.ts index fa4152f85..832f95641 100644 --- a/gitnexus/test/unit/git-clone.test.ts +++ b/gitnexus/test/unit/git-clone.test.ts @@ -1,10 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { - extractRepoName, - extractRepoSlug, - getCloneDir, - validateGitUrl, -} from '../../src/server/git-clone.js'; +import { extractRepoName, getCloneDir, validateGitUrl } from '../../src/server/git-clone.js'; describe('git-clone', () => { describe('extractRepoName', () => { @@ -29,23 +24,6 @@ describe('git-clone', () => { }); }); - describe('extractRepoSlug', () => { - it('keeps the owner path so same-named repositories do not collide', () => { - expect(extractRepoSlug('https://github.com/owner-a/my-repo.git')).toBe( - 'github.com/owner-a/my-repo', - ); - expect(extractRepoSlug('https://github.com/owner-b/my-repo.git')).toBe( - 'github.com/owner-b/my-repo', - ); - }); - - it('keeps nested group paths for non-GitHub hosts', () => { - expect(extractRepoSlug('https://gitlab.com/group/subgroup/repo.git')).toBe( - 'gitlab.com/group/subgroup/repo', - ); - }); - }); - describe('getCloneDir', () => { it('returns path under ~/.gitnexus/repos/', () => { const dir = getCloneDir('my-repo'); @@ -53,18 +31,6 @@ describe('git-clone', () => { expect(dir).toMatch(/repos/); expect(dir).toContain('my-repo'); }); - - it('returns distinct paths for owner-qualified clone slugs', () => { - const first = getCloneDir( - extractRepoSlug('https://github.com/owner-a/shared.git'), - ); - const second = getCloneDir( - extractRepoSlug('https://github.com/owner-b/shared.git'), - ); - expect(first).not.toBe(second); - expect(first).toContain('owner-a'); - expect(second).toContain('owner-b'); - }); }); describe('validateGitUrl', () => {