mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-28 01:31:23 +00:00
fix: handle clone slug review gaps
This commit is contained in:
parent
45ea3b39f7
commit
a2d7e9be91
3 changed files with 6 additions and 84 deletions
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue