giturlparse review
giturlparse 0.15.0 parses Git remote strings that ordinary URL parsers handle poorly, especially `git@host:owner/repo.git`. It recognizes provider-specific forms for GitHub, GitLab, Bitbucket, Assembla, and FriendCode, then exposes the host, owner, repository, protocol, nested GitLab groups, explicit port, and selected web-page path fields. The result can format the same parsed repository as SSH, HTTPS, HTTP, or git when that provider defines the protocol. Version 0.15.0 fixes nested `/blob/` and `/tree/` text being removed from file paths and branch names.
giturlparse 0.15.0 installed in 0.5 seconds as 1 dependency-free package and imported in 0.10 seconds in our sandbox, making it a cheap normalizer for recognized Git hosts. Do not use its provider guess, branch split, or `valid` flag as proof that a remote is safe, reachable, or real.
We installed it
| Install | ✓ · 0.5s | 1 package on disk · 1 MB |
| Import | ✓ | import giturlparse in 0.10s · pure Python · requires Python >=3.8 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does giturlparse install cleanly?
Yes. In a fresh container with an empty cache, pip install giturlparse finished in 0.5s, leaving 1 package and 1 MB on disk. pip-audit reported no known vulnerabilities.
What does giturlparse need to run?
Python >=3.8, and nothing compiled: it is pure Python. In our run import giturlparse succeeded in 0.10s.
giturlparse or git-url-parse: which should you use?
git-url-parse: Compare it when you want another small Git URL parser with a different provider grammar and result API. giturlparse 0.15.0 installed in 0.5 seconds as 1 dependency-free package and imported in 0.10 seconds in our sandbox, making it a cheap normalizer for recognized Git hosts.
When should you not use giturlparse?
Repository existence or access must be verified. validate() checks parsed fields only and never contacts the host.
Use it if
- An import tool accepts HTTPS and SCP-style SSH remotes and needs one host, owner, and repository record shape.
- You need to rewrite a recognized provider URL between clone protocols without calling a Git executable.
- GitLab subgroup paths must remain separate from the top-level owner and repository name.
- A pure-Python parser with 0 runtime dependencies fits better than a full Git client library.
- Repository existence or access must be verified. `validate()` checks parsed fields only and never contacts the host.
- A provider label will drive authorization. With domain checking disabled, ordered regular expressions can classify a self-hosted URL as the wrong forge.
- Static typing must cross the package boundary. Version 0.15.0 has no `py.typed` marker and its public result object is populated from regex matches at runtime.
- You need an exact branch/file split from GitHub or GitLab browsing URLs. A slash may belong to either a ref or a path, and the parser cannot ask the provider which one exists.
- Credential-bearing URLs may be logged automatically. Parsed `username`, `access_token`, `url`, and `data` values retain the secret even when a rewritten HTTPS URL omits it.
Setup reality
We installed giturlparse 0.15.0 in a fresh Python 3.12 sandbox in 0.5 seconds. The install left 1 package and 1 MB on disk, with 0 direct dependencies. import giturlparse worked in 0.10 seconds, and pip-audit reported 0 known vulnerabilities. The wheel is pure Python, declares Apache v2, and requires Python 3.8 or newer. It does not publish a py.typed marker, so a type checker cannot treat this unannotated package as a typed dependency.
No credentials, environment variables, native compiler, or configuration file are needed to parse text. The security work belongs at the call site. Keep the default check_domain=True when the provider name matters, maintain your own host allowlist, and check .valid before using a formatter. An invalid parse still returns GitUrlParsed; it lacks _platform_obj, so properties such as .url2https, .normalized, and .urls can raise rather than returning None.
Version 0.15.0 changes one narrow path rule. GitHub and GitLab parsing now slices only the leading /blob/, /tree/, /-/blob/, or /-/tree/ marker. Earlier code used replace() and could erase the same segment later in a branch or filename. The fix preserves a URL such as .../blob/main/src/blob/data.py, but it does not solve the general ambiguity between a slash-containing branch and the file path that follows it.
Rewriting is local string formatting. It may append .git, remove embedded HTTPS credentials from the formatted URL, or omit a protocol the matched provider does not define. The original object still keeps its source URL and extracted secret fields. Mutating .owner or .name recalculates .url; it does not rename a remote repository. Treat the parsed result as untrusted metadata until the host is allowed and, when existence matters, a separate provider or Git request succeeds.
Patterns
Read an SCP-style remote parse-scp-remote
from giturlparse import parse
remote = parse('git@bitbucket.org:AaronO/some-repo.git')
if not remote.valid:
raise ValueError('unsupported Git remote')
print(remote.host, remote.owner, remote.repo)`git@host:owner/repo.git` is Git's SCP-like syntax, so `urllib.parse` does not split it like a normal URL.
Reject text that does not match validate-shape
from giturlparse import validate
if not validate(candidate):
raise ValueError('expected a recognized Git remote')`validate()` requires parsed domain and repository fields; it performs 0 network requests and does not confirm access.
Turn SSH syntax into HTTPS rewrite-to-https
from giturlparse import parse
remote = parse('git@github.com:octo/widgets.git')
if not remote.valid:
raise ValueError('unsupported remote')
print(remote.url2https)The GitHub formatter returns `https://github.com/octo/widgets.git`; it does not test whether that repository exists.
Build an SSH clone form rewrite-to-ssh
from giturlparse import parse
remote = parse('https://github.com/octo/widgets')
if remote.valid:
print(remote.url2ssh)The formatter adds `.git` when the parsed repository name lacks it and emits GitHub's SCP-style SSH form.
Enumerate the available clone forms list-provider-protocols
from giturlparse import parse
remote = parse('https://github.com/octo/widgets.git')
if not remote.valid:
raise ValueError('unsupported remote')
for protocol, url in remote.urls.items():
print(protocol, url)`.urls` follows the matched provider's protocol patterns, so its keys are not guaranteed to contain all 4 formatter names.
Separate GitLab subgroups read-gitlab-groups
from giturlparse import parse
remote = parse('https://gitlab.com/acme/platform/payments/api.git')
print(remote.owner)
print(remote.groups)
print(remote.repo)This URL yields owner `acme`, 2 group entries, and repository `api`; other provider parsers normally leave groups empty.
Parse a path containing another blob segment preserve-nested-blob-path
from giturlparse import parse
page = parse(
'https://github.com/acme/widgets/blob/main/src/blob/data.py'
)
print(page.path)Version 0.15.0 preserves `main/src/blob/data.py`; earlier replacement logic deleted the inner `/blob/` segment.
Keep a nested tree segment in a branch read-tree-branch
from giturlparse import parse
page = parse(
'https://github.com/acme/widgets/tree/feature/tree/parser'
)
print(page.branch)Version 0.15.0 returns `feature/tree/parser` because only the first `/tree/` marker is removed.
Parse a custom host with an explicit warning try-self-hosted-forge
from giturlparse import parse
remote = parse(
'ssh://git@git.example.com/team/widgets.git',
check_domain=False,
)
if not remote.valid:
raise ValueError('unsupported remote')
print(remote.platform, remote.host)`check_domain=False` lets broad provider regexes compete; verify `git.example.com` against your own allowlist and do not trust the platform label.
Create a log-safe clone URL redact-embedded-token
from giturlparse import parse
remote = parse(
'https://build-user:secret@github.com/acme/widgets.git'
)
if not remote.valid:
raise ValueError('unsupported remote')
safe_url = remote.url2https
print(safe_url)The rewritten URL omits the credential, but 4 places can still expose it: the source string, `.url`, `.access_token`, and `.data`.
Format the same name under another owner change-owner-locally
from giturlparse import parse
remote = parse('https://github.com/old-team/widgets.git')
remote.owner = 'new-team'
print(remote.url)Assigning `.owner` only recalculates the in-memory URL; it sends 0 requests and does not move the repository.
Normalize fields for an allowed host build-comparison-key
from giturlparse import parse
def repo_key(url: str) -> tuple[str, str, str]:
remote = parse(url)
if not remote.valid or remote.host not in {'github.com', 'gitlab.com'}:
raise ValueError('host not allowed')
return (
remote.host.lower(),
remote.owner.lower(),
remote.repo.removesuffix('.git').lower(),
)The 2-host allowlist makes lowercasing an application choice; do not assume every self-hosted forge has the same case rules.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| git-url-parse | PyPI | Compare it when you want another small Git URL parser with a different provider grammar and result API. |
| GitPython | PyPI | Use it when the program must open repositories, read configured remotes, or run Git operations as well as parse names. |
| dulwich | PyPI | Use it when a Python Git implementation should inspect or communicate with repositories without a system Git process. |
More utils guides
lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

