abhigyanpatwari/GitNexus · error
Invalid repository name
Error message
Invalid repository name
What it means
getCloneDir re-validates its repoName argument at the boundary — callers may pass names from test fixtures, scripts, or other sources that never went through extractRepoName. Any name failing REPO_NAME_PATTERN ^[a-zA-Z0-9._-]+$, or '.'/'..'/empty, throws before path.join(CLONE_ROOT, repoName), so the result can never escape the clone root.
Solutions
- Derive names with extractRepoName(url) (or sanitizeRepoName) before calling getCloneDir
- Restrict inputs to [A-Za-z0-9._-] and strip/reject everything else
- Import the exported REPO_NAME_PATTERN for shared validation
Example fix
// before
const dir = getCloneDir(userTypedName); // 'my repo' -> throws
// after
import { REPO_NAME_PATTERN } from '../server/git-clone.js';
const safe = REPO_NAME_PATTERN.test(userTypedName) ? userTypedName : sanitizeRepoName(userTypedName);
const dir = getCloneDir(safe); Defensive patterns
Strategy: type-guard
Validate before calling
import { REPO_NAME_PATTERN } from './git-clone.js';
function assertSafeRepoName(name) {
if (!REPO_NAME_PATTERN.test(name) || name === '.' || name === '..') {
throw new Error(`Invalid repository name: ${JSON.stringify(name)}`);
}
} Type guard
import { REPO_NAME_PATTERN } from './git-clone.js';
function isSafeRepoName(name) {
return typeof name === 'string' && name.length > 0 && name !== '.' && name !== '..' && REPO_NAME_PATTERN.test(name);
}
// narrow before use: if (isSafeRepoName(n)) dir = getCloneDir(n); Try / catch
try { return getCloneDir(repoName); }
catch (e) {
if (e.message === 'Invalid repository name') return getCloneDir(sanitizeRepoName(repoName)); // one repair attempt
throw e;
} Prevention
- Never pass raw user strings to getCloneDir — derive via extractRepoName/sanitizeRepoName
- Reuse the exported REPO_NAME_PATTERN for shared validation instead of re-implementing it
- In tests, generate fixture names from the safe charset
When it happens
Trigger: Directly calling getCloneDir('my repo'), getCloneDir('foo/bar'), getCloneDir('../escape'), or getCloneDir('') from a script/test/fixture instead of deriving the name via extractRepoName.
Common situations: Test fixtures inventing repo names with spaces or slashes; scripts passing user-typed display names straight through; refactors that route names around the validated extractor.
Related errors
- Clone target must be a subdirectory of
- Could not extract a valid repository name from URL
- Analyzer build changed while its identity was being computed
- Analyzer identity directory changed while it was read
- Analyzer identity input changed while it was being read
AI-assisted analysis of abhigyanpatwari/GitNexus@ac9a4e9abd (2026-09-15).
Data as JSON: /api/errors/6dc8467c8706dc33.
Report an issue: GitHub.
Appendix: source
Thrown at gitnexus/src/server/git-clone.ts:126
* names on disk — shortening them there would orphan existing indexes.
*/
const boundedBranchSegment = (repoName: string, branch: string): string => {
const slug = branchSlug(branch);
if (`${repoName}__${slug}`.length <= MAX_PATH_COMPONENT_BYTES) return slug;
const hash = slug.slice(slug.lastIndexOf('-')); // "-" + 8 hex
const budget = MAX_PATH_COMPONENT_BYTES - repoName.length - '__'.length - hash.length;
// A repo name long enough to leave no budget falls through to the caller's
// length check, which rejects it rather than building an unusable path.
return `${slug.slice(0, Math.max(0, budget))}${hash}`;
};
/** Get the clone target directory for a repo name, optionally pinned to a branch. */
export function getCloneDir(repoName: string, branch?: string): string {
// Re-validate at the boundary even though extractRepoName already checked —
// callers may pass a repoName from another source (test fixtures, scripts).
if (!repoName || repoName === '.' || repoName === '..' || !REPO_NAME_PATTERN.test(repoName)) {
throw new Error('Invalid repository name');
}
// A branch-pinned analyze gets its OWN working tree.
//
// Sharing one checkout per repo made `branch` unusable in practice: the tree
// is dirty after any analyze (generated AGENTS.md / CLAUDE.md / .claude/), so
// a pinned request hit `cloneOrPull`'s porcelain refusal; and a later request
// that OMITTED `branch` would pull whatever branch the last pin left checked
// out and index it as the default (#3199 review). Separate directories remove
// both, because the two requests no longer share a tree.
//
// `branchSlug` is the same helper the per-branch index slots use, so the two
// layouts agree on how a ref becomes a path segment. It emits only
// `[a-zA-Z0-9._-]`, so the composed name still satisfies REPO_NAME_PATTERN and
// round-trips through this function — which is how DELETE /api/repo re-derives
// the directory from the registry name.
const dirName = branch ? `${repoName}__${boundedBranchSegment(repoName, branch)}` : repoName;
if (!REPO_NAME_PATTERN.test(dirName) || dirName.length > MAX_PATH_COMPONENT_BYTES) {
throw new Error('Invalid repository name');View on GitHub (pinned to ac9a4e9abd)