{"record":{"id":"c6a80360ca4e1695","repo":"stablyai/orca","slug":"sparse-checkout-directories-must-be-repo-relative","errorCode":null,"errorMessage":"Sparse checkout directories must be repo-relative paths.","messagePattern":"Sparse checkout directories must be repo-relative paths\\.","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/ipc/sparse-checkout-directories.ts","lineNumber":14,"sourceCode":"const WINDOWS_DRIVE_PATH_PATTERN = /^[A-Za-z]:/\n\nfunction isAbsoluteSparseDirectoryPath(entry: string): boolean {\n  return entry.startsWith('/') || entry.startsWith('\\\\') || WINDOWS_DRIVE_PATH_PATTERN.test(entry)\n}\n\nexport function normalizeSparseDirectories(directories: string[]): string[] {\n  const seen = new Set<string>()\n  return directories\n    .map((entry) => entry.trim())\n    .map((entry) => {\n      // Why: absolute paths can look repo-relative after slash normalization.\n      if (isAbsoluteSparseDirectoryPath(entry)) {\n        throw new Error('Sparse checkout directories must be repo-relative paths.')\n      }\n      return entry.replace(/\\\\/g, '/').replace(/^\\/+|\\/+$/g, '')\n    })\n    .filter((entry) => entry.length > 0 && entry !== '.')\n    .filter((entry) => {\n      if (entry.split('/').includes('..')) {\n        throw new Error('Sparse checkout directories must be repo-relative paths.')\n      }\n      if (seen.has(entry)) {\n        return false\n      }\n      seen.add(entry)\n      return true\n    })\n}\n","sourceCodeStart":1,"sourceCodeEnd":30,"githubUrl":"https://github.com/stablyai/orca/blob/1136503c6a231a16dce8f921f6fadb63d181e8db/src/main/ipc/sparse-checkout-directories.ts#L1-L30","documentation":"The canonical low-level error thrown by normalizeSparseDirectories in src/main/ipc/sparse-checkout-directories.ts. It fires when an entry is detected as absolute by isAbsoluteSparseDirectoryPath (leading '/', leading '\\\\', or a Windows drive-letter pattern /^[A-Za-z]:/), or when any segment after slash-split equals '..'. The wrapper in repos.ts (1291) re-words this for the preset UI, but direct callers of normalizeSparseDirectories see this message.","triggerScenarios":"Calling normalizeSparseDirectories with entries like '/abs/path', '\\\\server\\share', 'C:\\dir', 'a/../b', or '../sibling'. Each is rejected because, after backslash-to-slash conversion and edge trimming, it would resolve outside the worktree root or be ambiguous.","commonSituations":"Code paths that accept user or config-provided sparse paths without sanitizing; cross-platform configs mixing Windows and POSIX paths; copy-paste of absolute paths from file explorers; tools that build sparse sets from filesystem walks.","solutions":["Sanitize entries: strip leading slashes/backslashes and drive prefixes, and reject '..' segments before calling normalize.","Always pass repo-relative paths; compute them via path.relative(repoRoot, absPath) when starting from absolute inputs.","Run normalizeSparseDirectories in a try/catch and report the offending entry to the user.","In config ingestion, validate with the same helper and reject the config with a precise error."],"exampleFix":"// before\nconst dirs = userInput // may contain 'C:\\src' or '../x'\nconst normalized = normalizeSparseDirectories(dirs)\n\n// after\nimport { relative } from 'path'\nconst dirs = userInput.map((p) =>\n  path.isAbsolute(p) ? relative(repoRoot, p) : p\n).filter((p) => p && !p.startsWith('..'))\nconst normalized = normalizeSparseDirectories(dirs)","handlingStrategy":"validation","validationCode":"const DRIVE_RE = /^[A-Za-z]:/\n\nfunction sanitizeSparseEntry(entry: string): string | null {\n  let t = entry.trim()\n  if (!t || t === '.') return null\n  if (t.startsWith('/') || t.startsWith('\\\\') || DRIVE_RE.test(t)) return null // absolute\n  t = t.replace(/\\\\/g, '/').replace(/^\\/+|\\/+$/g, '')\n  if (t.split('/').includes('..')) return null // parent escape\n  return t || null\n}\n\nconst clean = directories.map(sanitizeSparseEntry).filter((d): d is string => d !== null)\nif (clean.length === 0) throw new Error('No valid repo-relative directories.')\nconst normalized = normalizeSparseDirectories(clean)","typeGuard":"const DRIVE_RE = /^[A-Za-z]:/\nfunction isRepoRelativeSparsePath(entry: string): boolean {\n  const t = entry.trim()\n  if (!t || t === '.') return false\n  if (t.startsWith('/') || t.startsWith('\\\\') || DRIVE_RE.test(t)) return false\n  if (t.replace(/\\\\/g, '/').split('/').includes('..')) return false\n  return true\n}","tryCatchPattern":"try {\n  normalized = normalizeSparseDirectories(directories)\n} catch (e) {\n  if (/repo-relative/i.test((e as Error).message)) {\n    reportInvalidEntries(directories)\n  } else throw e\n}","preventionTips":["Compute sparse paths with path.relative(repoRoot, absPath) when starting from absolute inputs.","Reject absolute prefixes and '..' segments at the input boundary.","Dry-run normalizeSparseDirectories and report the offending entry."],"tags":["validation","sparse-checkout","path-safety","filesystem","low-level"],"backgroundTag":null,"analyzedSha":"1136503c6a231a16dce8f921f6fadb63d181e8db","analyzedAt":"2026-08-12T23:15:58.167Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}