{"record":{"id":"8ed46da6b0d11948","repo":"actualbudget/actual","slug":"invalid-repo-must-include-both-owner-and-repo-nam","errorCode":null,"errorMessage":"Invalid repo: must include both owner and repo name","messagePattern":"Invalid repo: must include both owner and repo name","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/desktop-client/src/style/customThemes.ts","lineNumber":53,"sourceCode":"\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(\n    `https://raw.githubusercontent.com/${repo}/refs/heads/main/actual.css`,\n  );\n  url.searchParams.set('v', Date.now().toString());\n\n  return fetchDirectCss(url.toString());\n}\n\n/**","sourceCodeStart":35,"sourceCodeEnd":71,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/desktop-client/src/style/customThemes.ts#L35-L71","documentation":"This is the second validation in normalizeGitHubRepo: after splitting the trimmed input on '/', both the owner segment and the repo name segment must be non-empty. Input like '/repo', 'owner/', or '//' produces empty parts and triggers 'Invalid repo: must include both owner and repo name'.","triggerScenarios":"Calling normalizeGitHubRepo with strings such as '/my-theme', 'my-theme/', 'a/b/' edge inputs, or a value beginning/ending with a slash after trim — often from copy-paste mistakes in the custom theme installer.","commonSituations":"Pasting 'https://github.com/' without the path; a leading slash from path-relative copying; trailing slashes from URL copies; whitespace-only extra segments.","solutions":["Supply both parts: 'owner/repo' with no leading or trailing slash","Sanitize input in the UI: strip leading/trailing slashes before validation","Split on '/' and filter empty segments, or use a stricter regex like /^[\\w.-]+\\/[\\w.-]+$/ before calling","Show inline field validation so users see the expected format immediately"],"exampleFix":"// before\nnormalizeGitHubRepo('/actualbudget/themes'); // throws\n// after\nconst cleaned = input.trim().replace(/^\\/+|\\/+$/g, '');\nif (!/^[\\w.-]+\\/[\\w.-]+$/.test(cleaned)) {\n  throw new Error('Enter a repo as owner/repo');\n}\nnormalizeGitHubRepo(cleaned);","handlingStrategy":"validation","validationCode":"const isValidOwnerRepo = (input: string): boolean => {\n  const [owner = '', repo = ''] = input.trim().split('/');\n  return owner.trim() !== '' && repo.trim() !== '';\n};\n// gate: if (!isValidOwnerRepo(input)) showInlineError('Both owner and repo name are required');","typeGuard":"const hasOwnerAndRepo = (parts: readonly (string | undefined)[]): parts is [string, string, ...string[]] =>\n  parts.length >= 2 && !!parts[0]?.trim() && !!parts[1]?.trim();","tryCatchPattern":"try {\n  const url = normalizeGitHubRepo(input);\n} catch (err) {\n  if (err.message.includes('must include both owner and repo name')) {\n    setFieldError('Remove leading/trailing slashes: use owner/repo');\n  } else throw err;\n}","preventionTips":["Strip leading and trailing slashes from pasted values before validating","Use a regex like /^[\\w.-]+\\/[\\w.-]+$/ as the single source of truth for repo input","Show the expected format as a placeholder: 'e.g. actualbudget/themes'","Test the installer with slash-edge inputs ('/a', 'a/', '/') to keep the guard solid"],"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"}