{"record":{"id":"15f8d94f339147c2","repo":"datawhalechina/hello-agents","slug":"github-url-url","errorCode":null,"errorMessage":"无法解析 GitHub URL: {url}","messagePattern":"无法解析 GitHub URL: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"Co-creation-projects/Yixiang-Wu-LearningAgent/specialist/repo_analyzer.py","lineNumber":58,"sourceCode":"        从 GitHub URL 提取 owner 和 repo 名称\n\n        Args:\n            url: GitHub URL（如 https://github.com/vuejs/core）\n\n        Returns:\n            (owner, repo) 元组\n        \"\"\"\n        # 去掉 .git 后缀\n        url = url.rstrip(\".git\")\n\n        # 提取 owner 和 repo\n        parts = url.rstrip(\"/\").split(\"/\")\n        if len(parts) >= 2:\n            owner = parts[-2]\n            repo = parts[-1]\n            return owner, repo\n\n        raise ValueError(f\"无法解析 GitHub URL: {url}\")\n\n    def _fetch_repo_info(self, owner: str, repo: str) -> Dict:\n        \"\"\"\n        获取仓库基本信息\n\n        Args:\n            owner: 仓库所有者\n            repo: 仓库名称\n\n        Returns:\n            仓库信息字典\n        \"\"\"\n        url = f\"{self.GITHUB_API_BASE}/repos/{owner}/{repo}\"\n        response = requests.get(url, headers=self.headers, timeout=10)\n        response.raise_for_status()\n        return response.json()\n\n    def _fetch_readme(self, owner: str, repo: str) -> Optional[str]:","sourceCodeStart":40,"sourceCodeEnd":76,"githubUrl":"https://github.com/datawhalechina/hello-agents/blob/606a07d341a47be773fab7f4b71177f53f96b2c3/Co-creation-projects/Yixiang-Wu-LearningAgent/specialist/repo_analyzer.py#L40-L76","documentation":"ValueError raised by RepoAnalyzer._parse_github_url when the URL, after stripping '.git' and trailing slashes, does not contain at least two slash-separated tail segments. The naive split-based parser has real holes: it accepts any string whose last two '/'-segments exist (e.g. 'not a url/owner/repo' or 'https://gitlab.com/o/r'), and url.rstrip('.git') strips the character set {.g,i,t} not the suffix — so a repo ending in 'g', 'i', 't', or combinations like 'agent' loses characters.","triggerScenarios":"Passing a bare repo name 'owner/repo' still works, but 'myrepo' alone (one segment) raises; SSH form 'git@github.com:owner/repo.git' is mangled because there is no '/' before the colon-joined tail — split gives ['git@github.com:owner', 'repo'] which parses but yields owner 'git@github.com:owner'; rstrip('.git') truncating repos like 'fastapi.git' fine but 'torch' -> 'torc', 'langchain' -> 'langchain' (ends 'n' ok) yet 'kgit' -> 'kg'; URL like 'https://github.com/owner' (single segment after domain is merged) raises.","commonSituations":"LLM-generated or user-typed URLs in noncanonical shapes (SSH clone URLs, missing scheme, trailing '/tree/main'); repos whose names end in g/i/t characters; copy-paste from git clone output.","solutions":["Replace rstrip('.git') with a real suffix removal: if url.endswith('.git'): url = url[:-4]","Parse with a regex or urllib.parse that handles https://, http://, and git@github.com:owner/repo.git forms explicitly","Validate owner/repo against GitHub's charset ([A-Za-z0-9_.-]) after parsing","Raise with the original unmodified URL in the message so the user sees what was rejected"],"exampleFix":"# before\nurl = url.rstrip(\".git\")\nparts = url.rstrip(\"/\").split(\"/\")\nif len(parts) >= 2:\n    owner, repo = parts[-2], parts[-1]\n    return owner, repo\nraise ValueError(f\"无法解析 GitHub URL: {url}\")\n\n# after\nimport re\nm = re.search(\n    r\"(?:github\\.com[:/])([A-Za-z0-9_.-]+)/([A-Za-z0-9_.-]+?)(?:\\.git)?/?$\",\n    url,\n)\nif not m:\n    raise ValueError(f\"无法解析 GitHub URL: {url!r}，\"\n                     \"期望形如 https://github.com/owner/repo(.git)\")\nreturn m.group(1), m.group(2)","handlingStrategy":"validation","validationCode":"import re\n\nGH_RE = re.compile(\n    r\"(?:https?://)?(?:www\\.)?github\\.com/\"\n    r\"([A-Za-z0-9_.-]+)/([A-Za-z0-9_.-]+?)(?:\\.git)?/?$\"\n)\n\ndef parse_github_url(url: str) -> tuple[str, str] | None:\n    m = GH_RE.search(url.strip())\n    return (m.group(1), m.group(2)) if m else None","typeGuard":"from typing import Optional, Tuple\n\ndef is_github_repo_url(url: str) -> bool:\n    return isinstance(url, str) and bool(\n        re.search(r\"github\\.com[:/][A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+\", url)\n    )","tryCatchPattern":"owner_repo = parse_github_url(url)\nif owner_repo is None:\n    raise ValueError(f\"not a GitHub repo URL: {url!r}\")\n# no try/except needed — validation replaces the exception path","preventionTips":["Validate URL shape before calling the analyzer; prefer a regex covering https and git@ SSH forms","Never strip suffixes with rstrip — it removes character sets, not suffixes","Fuzz-test the parser with bare names, SSH URLs, and trailing '/tree/main' variants"],"tags":["url-parsing","github","python","validation","rstrip-bug"],"backgroundTag":null,"analyzedSha":"606a07d341a47be773fab7f4b71177f53f96b2c3","analyzedAt":"2026-08-14T22:57:27.446Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}