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

  1. Derive names with extractRepoName(url) (or sanitizeRepoName) before calling getCloneDir
  2. Restrict inputs to [A-Za-z0-9._-] and strip/reject everything else
  3. 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

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


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)