{"record":{"id":"6dc8467c8706dc33","repo":"abhigyanpatwari/GitNexus","slug":"invalid-repository-name","errorCode":null,"errorMessage":"Invalid repository name","messagePattern":"Invalid repository name","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"gitnexus/src/server/git-clone.ts","lineNumber":126,"sourceCode":" * names on disk — shortening them there would orphan existing indexes.\n */\nconst boundedBranchSegment = (repoName: string, branch: string): string => {\n  const slug = branchSlug(branch);\n  if (`${repoName}__${slug}`.length <= MAX_PATH_COMPONENT_BYTES) return slug;\n\n  const hash = slug.slice(slug.lastIndexOf('-')); // \"-\" + 8 hex\n  const budget = MAX_PATH_COMPONENT_BYTES - repoName.length - '__'.length - hash.length;\n  // A repo name long enough to leave no budget falls through to the caller's\n  // length check, which rejects it rather than building an unusable path.\n  return `${slug.slice(0, Math.max(0, budget))}${hash}`;\n};\n\n/** Get the clone target directory for a repo name, optionally pinned to a branch. */\nexport function getCloneDir(repoName: string, branch?: string): string {\n  // Re-validate at the boundary even though extractRepoName already checked —\n  // callers may pass a repoName from another source (test fixtures, scripts).\n  if (!repoName || repoName === '.' || repoName === '..' || !REPO_NAME_PATTERN.test(repoName)) {\n    throw new Error('Invalid repository name');\n  }\n  // A branch-pinned analyze gets its OWN working tree.\n  //\n  // Sharing one checkout per repo made `branch` unusable in practice: the tree\n  // is dirty after any analyze (generated AGENTS.md / CLAUDE.md / .claude/), so\n  // a pinned request hit `cloneOrPull`'s porcelain refusal; and a later request\n  // that OMITTED `branch` would pull whatever branch the last pin left checked\n  // out and index it as the default (#3199 review). Separate directories remove\n  // both, because the two requests no longer share a tree.\n  //\n  // `branchSlug` is the same helper the per-branch index slots use, so the two\n  // layouts agree on how a ref becomes a path segment. It emits only\n  // `[a-zA-Z0-9._-]`, so the composed name still satisfies REPO_NAME_PATTERN and\n  // round-trips through this function — which is how DELETE /api/repo re-derives\n  // the directory from the registry name.\n  const dirName = branch ? `${repoName}__${boundedBranchSegment(repoName, branch)}` : repoName;\n  if (!REPO_NAME_PATTERN.test(dirName) || dirName.length > MAX_PATH_COMPONENT_BYTES) {\n    throw new Error('Invalid repository name');","sourceCodeStart":108,"sourceCodeEnd":144,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/ac9a4e9abd8fd3058c070b72c23402a4f887929a/gitnexus/src/server/git-clone.ts#L108-L144","documentation":"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.","triggerScenarios":"Directly calling getCloneDir('my repo'), getCloneDir('foo/bar'), getCloneDir('../escape'), or getCloneDir('') from a script/test/fixture instead of deriving the name via extractRepoName.","commonSituations":"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.","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"],"exampleFix":"// before\nconst dir = getCloneDir(userTypedName); // 'my repo' -> throws\n\n// after\nimport { REPO_NAME_PATTERN } from '../server/git-clone.js';\nconst safe = REPO_NAME_PATTERN.test(userTypedName) ? userTypedName : sanitizeRepoName(userTypedName);\nconst dir = getCloneDir(safe);","handlingStrategy":"type-guard","validationCode":"import { REPO_NAME_PATTERN } from './git-clone.js';\nfunction assertSafeRepoName(name) {\n  if (!REPO_NAME_PATTERN.test(name) || name === '.' || name === '..') {\n    throw new Error(`Invalid repository name: ${JSON.stringify(name)}`);\n  }\n}","typeGuard":"import { REPO_NAME_PATTERN } from './git-clone.js';\nfunction isSafeRepoName(name) {\n  return typeof name === 'string' && name.length > 0 && name !== '.' && name !== '..' && REPO_NAME_PATTERN.test(name);\n}\n// narrow before use: if (isSafeRepoName(n)) dir = getCloneDir(n);","tryCatchPattern":"try { return getCloneDir(repoName); }\ncatch (e) {\n  if (e.message === 'Invalid repository name') return getCloneDir(sanitizeRepoName(repoName)); // one repair attempt\n  throw e;\n}","preventionTips":["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"],"tags":["git-clone","path-validation","path-traversal-guard","filesystem"],"backgroundTag":"invalid-repository-name","analyzedSha":"ac9a4e9abd8fd3058c070b72c23402a4f887929a","analyzedAt":"2026-09-15T23:29:44.066Z","contentChangedAt":"2026-09-15T23:29:44.066Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}