diff --git a/spec/audit.py b/spec/audit.py index 48e9a387..7006ba2e 100755 --- a/spec/audit.py +++ b/spec/audit.py @@ -255,15 +255,16 @@ def owner_repos(owner): page += 1 -GITHUB_URL_RE = re.compile(r"^https://github\.com/([^/\s]+)/([^/\s]+?)(?:\.git)?/?$") +GITHUB_URL_RE = re.compile(r"^https://github\.com/([^/\s?#]+)/([^/\s?#]+?)(?:\.git)?/?$") def repo_identity(url): """owner/repo, lowercased, parsed from a github.com URL, or None if it does not parse. - The canonical identity two repos are compared by, since a bare repo name collides across - owners and GitHub's own full_name field is already this exact shape (case-preserved). A - trailing .git is stripped so it still resolves to the same identity full_name would. + The canonical identity two repos are compared by, since a bare repo name collides across owners. + GitHub's own full_name field is already this exact shape (case-preserved). + A trailing .git is stripped so it still resolves to the same identity full_name would. + A query string or fragment is rejected outright rather than folded into the repo name. spec/validate.py rejects a url that fails to parse here at all, kept in sync with this regex. """ if not isinstance(url, str): @@ -4212,6 +4213,11 @@ def _selftest(): (" https://github.com/owner/Repo ", "owner/repo"), ("https://github.com/owner/Repo.git", "owner/repo"), ("https://github.com/owner/Repo.github", "owner/repo.github"), + ("https://github.com/owner/Repo?tab=readme", None), + ("https://github.com/owner/Repo#readme", None), + ("https://github.com/owner/Repo.git?tab=readme", None), + ("https://github.com/owner?tab=readme/Repo", None), + ("https://github.com/owner#readme/Repo", None), ("http://github.com/owner/Repo", None), ("https://gitlab.com/owner/Repo", None), ("not a url", None), diff --git a/spec/validate.py b/spec/validate.py index e838ed2e..446f144e 100755 --- a/spec/validate.py +++ b/spec/validate.py @@ -23,8 +23,10 @@ CONSUMER_MODELS = ("push", "pull") # Parses owner/repo, lowercased, from a repo's url. # A trailing .git is stripped so it still matches GitHub's own full_name. +# A query character or a fragment character is excluded from both groups too. +# Otherwise a query string or fragment folds into the repo name instead of failing to match. # A duplicate identity here would let spec/audit.py's fleet membership check silently shadow one entry with the other. -GITHUB_URL_RE = re.compile(r"^https://github\.com/([^/\s]+)/([^/\s]+?)(?:\.git)?/?$") +GITHUB_URL_RE = re.compile(r"^https://github\.com/([^/\s?#]+)/([^/\s?#]+?)(?:\.git)?/?$") # How faithfully a carried unit is checked, per spec/fidelity-model.md, defaulting to presence. FIDELITIES = ("presence", "intent", "verbatim", "interface") # The keys an interface unit's `contract` may carry (kept in sync with files.schema.json).