{"record":{"id":"d3fcf44e065a0b83","repo":"actualbudget/actual","slug":"invalid-repo-must-be-in-owner-repo-format","errorCode":null,"errorMessage":"Invalid repo: must be in \"owner/repo\" format","messagePattern":"Invalid repo: must be in \"owner/repo\" format","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/desktop-client/src/style/customThemes.ts","lineNumber":45,"sourceCode":"export function extractRepoOwner(repo: string): string {\n  if (!repo || typeof repo !== 'string' || !repo.includes('/')) {\n    return 'Unknown';\n  }\n  const parts = repo.split('/');\n  const owner = parts[0]?.trim();\n  return owner || 'Unknown';\n}\n\n/**\n * Normalize a GitHub repo identifier to a full GitHub URL.\n * Accepts \"owner/repo\" format.\n * Returns \"https://github.com/owner/repo\".\n * @throws {Error} If repo is invalid or missing owner/repo.\n */\nexport function normalizeGitHubRepo(repo: string): string {\n  const trimmed = repo.trim();\n  if (!trimmed.includes('/')) {\n    throw new Error('Invalid repo: must be in \"owner/repo\" format');\n  }\n\n  const parts = trimmed.split('/');\n  const owner = parts[0]?.trim();\n  const repoName = parts[1]?.trim();\n\n  if (!owner || !repoName) {\n    throw new Error('Invalid repo: must include both owner and repo name');\n  }\n\n  return `https://github.com/${owner}/${repoName}`;\n}\n\n/**\n * Try fetching actual.css from main branch.\n */\nexport function fetchThemeCss(repo: string): Promise<string> {\n  const url = new URL(","sourceCodeStart":27,"sourceCodeEnd":63,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/desktop-client/src/style/customThemes.ts#L27-L63","documentation":"normalizeGitHubRepo validates a user-supplied GitHub repo string: after trimming, it must contain a '/'. If not, it throws 'Invalid repo: must be in \"owner/repo\" format'. This is the first of two validation checks that turn free-text input into a canonical https://github.com/owner/repo URL for custom theme installation.","triggerScenarios":"Calling normalizeGitHubRepo (directly or via ThemeInstaller) with a bare repo name like 'my-theme', a full URL like 'https://github.com/owner/repo' (contains slashes so passes, but 'github.com owner repo' spaced input trimmed without slash fails), or an empty/whitespace string.","commonSituations":"Users pasting just the theme name; typing the repo without the owner; leaving the field blank and submitting; input fields that strip the slash.","solutions":["Enter the repository as 'owner/repo' (e.g. 'actualbudget/themes') in the theme installer input","Trim input and reject empty strings at the UI layer before calling normalizeGitHubRepo","If accepting full URLs, strip the 'https://github.com/' prefix before validation","Improve the UI with a placeholder and inline validation to enforce the owner/repo shape"],"exampleFix":"// before\nnormalizeGitHubRepo('my-theme'); // throws\n// after\nconst input = 'my-theme';\nconst repo = input.includes('/') ? input : `actualbudget/${input}`;\nnormalizeGitHubRepo(repo); // 'https://github.com/actualbudget/my-theme'","handlingStrategy":"validation","validationCode":"const parseRepoInput = (input: string): string | null => {\n  const cleaned = input.trim()\n    .replace(/^https?:\\/\\/github\\.com\\//i, '')\n    .replace(/^\\/+|\\/+$/g, '');\n  return /^[\\w.-]+\\/[\\w.-]+$/.test(cleaned) ? cleaned : null;\n};\n// call normalizeGitHubRepo only when parseRepoInput returns non-null","typeGuard":"const isOwnerRepo = (s: string): s is `${string}/${string}` =>\n  s.includes('/') && s.trim().split('/').every(p => p.trim().length > 0);","tryCatchPattern":"try {\n  const url = normalizeGitHubRepo(userInput);\n} catch (err) {\n  if (err.message.includes('Invalid repo')) {\n    setFieldError('Repository must look like owner/repo');\n  } else throw err;\n}","preventionTips":["Validate the owner/repo shape inline in the form before submission","Accept and normalize full GitHub URLs by stripping the prefix first","Trim and strip stray slashes from pasted input","Add unit tests for edge inputs: empty, 'name', '/name', 'name/'"],"tags":["validation","input","github"],"backgroundTag":"invalid-input-format","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}