mirror of
https://github.com/brunopostle/ifcurl.git
synced 2026-07-12 10:18:14 +00:00
Fix port preservation and HTTP fallback for local Gitea
- url.py: preserve non-standard ports in host field; ifc://host:3000/... now correctly generates https://host:3000/... rather than dropping :3000 - git.py: fall back from https to http on clone/fetch failure, enabling local Gitea instances running on plain HTTP; inject tokens into http:// URLs as well as https://; extract _clone_bare() helper - tests/test_url.py: three new tests for port preservation in host, git_remote_url with non-standard HTTPS port, and SSH with non-standard port
This commit is contained in:
parent
173cf653a8
commit
778f0004e7
3 changed files with 72 additions and 12 deletions
|
|
@ -128,12 +128,35 @@ def _cache_dir_for(remote_url: str) -> Path:
|
||||||
|
|
||||||
def _auth_url(remote_url: str, token: str | None) -> str:
|
def _auth_url(remote_url: str, token: str | None) -> str:
|
||||||
"""Return *remote_url* with the token injected, or the original URL."""
|
"""Return *remote_url* with the token injected, or the original URL."""
|
||||||
if token and remote_url.startswith("https://"):
|
if token and (remote_url.startswith("https://") or remote_url.startswith("http://")):
|
||||||
from ifcurl.auth import inject_token
|
from ifcurl.auth import inject_token
|
||||||
return inject_token(remote_url, token)
|
return inject_token(remote_url, token)
|
||||||
return remote_url
|
return remote_url
|
||||||
|
|
||||||
|
|
||||||
|
def _http_fallback(url: str) -> str | None:
|
||||||
|
"""Return the http:// equivalent of an https:// URL, or None."""
|
||||||
|
if url.startswith("https://"):
|
||||||
|
return "http" + url[5:]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _clone_bare(auth_url: str, git_dir: Path, remote_url: str) -> git.Repo:
|
||||||
|
"""Clone *auth_url* as a bare repo to *git_dir*, falling back to http
|
||||||
|
when the server does not speak HTTPS (common for local dev instances).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return git.Repo.clone_from(auth_url, str(git_dir), bare=True)
|
||||||
|
except git.exc.GitCommandError as exc:
|
||||||
|
fallback = _http_fallback(auth_url)
|
||||||
|
if fallback:
|
||||||
|
try:
|
||||||
|
return git.Repo.clone_from(fallback, str(git_dir), bare=True)
|
||||||
|
except git.exc.GitCommandError:
|
||||||
|
pass # report the original error
|
||||||
|
raise ValueError(f"Failed to clone {remote_url!r}: {exc.stderr.strip()}") from exc
|
||||||
|
|
||||||
|
|
||||||
def _open_remote(remote_url: str, is_mutable: bool, token: str | None = None) -> git.Repo:
|
def _open_remote(remote_url: str, is_mutable: bool, token: str | None = None) -> git.Repo:
|
||||||
"""Return a GitPython Repo for *remote_url*, cloning it if necessary.
|
"""Return a GitPython Repo for *remote_url*, cloning it if necessary.
|
||||||
|
|
||||||
|
|
@ -141,26 +164,35 @@ def _open_remote(remote_url: str, is_mutable: bool, token: str | None = None) ->
|
||||||
For mutable refs (branches, HEAD) the remote is fetched on every call.
|
For mutable refs (branches, HEAD) the remote is fetched on every call.
|
||||||
Immutable refs (commit hashes, tags) skip the fetch.
|
Immutable refs (commit hashes, tags) skip the fetch.
|
||||||
|
|
||||||
If *token* is provided it is injected into the HTTPS URL for clone and
|
If *token* is provided it is injected into the URL for clone and fetch
|
||||||
fetch operations but is never written to the on-disk git config.
|
operations but is never written to the on-disk git config.
|
||||||
|
|
||||||
|
HTTPS URLs automatically fall back to HTTP when the server does not
|
||||||
|
speak TLS — this handles local Gitea instances running on HTTP.
|
||||||
"""
|
"""
|
||||||
cache_dir = _cache_dir_for(remote_url)
|
cache_dir = _cache_dir_for(remote_url)
|
||||||
git_dir = cache_dir / "repo.git"
|
git_dir = cache_dir / "repo.git"
|
||||||
auth = _auth_url(remote_url, token)
|
auth = _auth_url(remote_url, token)
|
||||||
|
|
||||||
if not git_dir.exists():
|
if not git_dir.exists():
|
||||||
try:
|
repo = _clone_bare(auth, git_dir, remote_url)
|
||||||
repo = git.Repo.clone_from(auth, str(git_dir), bare=True)
|
|
||||||
except git.exc.GitCommandError as exc:
|
|
||||||
raise ValueError(f"Failed to clone {remote_url!r}: {exc.stderr.strip()}") from exc
|
|
||||||
else:
|
else:
|
||||||
repo = git.Repo(str(git_dir))
|
repo = git.Repo(str(git_dir))
|
||||||
if is_mutable:
|
if is_mutable:
|
||||||
try:
|
try:
|
||||||
if auth != remote_url:
|
if auth != remote_url:
|
||||||
# Fetch from the authenticated URL directly so the token
|
# Fetch directly from the authenticated URL so the token
|
||||||
# is never stored in the repo config.
|
# is never stored in the repo config.
|
||||||
repo.git.fetch(auth, "+refs/*:refs/*")
|
fetch_url = auth
|
||||||
|
fallback = _http_fallback(auth)
|
||||||
|
try:
|
||||||
|
repo.git.fetch(fetch_url, "+refs/*:refs/*")
|
||||||
|
except git.exc.GitCommandError:
|
||||||
|
if fallback:
|
||||||
|
try:
|
||||||
|
repo.git.fetch(fallback, "+refs/*:refs/*")
|
||||||
|
except git.exc.GitCommandError:
|
||||||
|
pass
|
||||||
else:
|
else:
|
||||||
repo.git.fetch("origin")
|
repo.git.fetch("origin")
|
||||||
except git.exc.GitCommandError:
|
except git.exc.GitCommandError:
|
||||||
|
|
|
||||||
|
|
@ -58,14 +58,20 @@ class IfcUrl:
|
||||||
raise ValueError(f"Expected ifc:// scheme, got {parsed.scheme!r}")
|
raise ValueError(f"Expected ifc:// scheme, got {parsed.scheme!r}")
|
||||||
|
|
||||||
# --- Transport and authority ---
|
# --- Transport and authority ---
|
||||||
|
# parsed.hostname strips the port; we rebuild host as hostname:port
|
||||||
|
# so that git_remote_url() generates correct clone URLs for
|
||||||
|
# non-standard ports (e.g. a local Gitea on :3000 or SSH on :2222).
|
||||||
|
def _host(p) -> str:
|
||||||
|
return f"{p.hostname}:{p.port}" if p.port else (p.hostname or "")
|
||||||
|
|
||||||
if parsed.username:
|
if parsed.username:
|
||||||
transport = "ssh"
|
transport = "ssh"
|
||||||
user: str | None = parsed.username
|
user: str | None = parsed.username
|
||||||
host = parsed.hostname or ""
|
host = _host(parsed)
|
||||||
elif parsed.netloc:
|
elif parsed.netloc:
|
||||||
transport = "https"
|
transport = "https"
|
||||||
user = None
|
user = None
|
||||||
host = parsed.hostname or ""
|
host = _host(parsed)
|
||||||
else:
|
else:
|
||||||
transport = "local"
|
transport = "local"
|
||||||
user = None
|
user = None
|
||||||
|
|
@ -147,7 +153,14 @@ class IfcUrl:
|
||||||
)
|
)
|
||||||
|
|
||||||
def git_remote_url(self) -> str:
|
def git_remote_url(self) -> str:
|
||||||
"""Return the git remote URL for this IFC URL (SSH or HTTPS form)."""
|
"""Return the git remote URL for this IFC URL.
|
||||||
|
|
||||||
|
The port is preserved when non-standard. SSH uses ssh://, HTTPS
|
||||||
|
uses https://. Whether the server actually speaks HTTPS or plain
|
||||||
|
HTTP is resolved at clone time — see ifcurl.git._open_remote which
|
||||||
|
falls back from https to http when the server refuses the TLS
|
||||||
|
handshake.
|
||||||
|
"""
|
||||||
if self.transport == "local":
|
if self.transport == "local":
|
||||||
return self.repo_path
|
return self.repo_path
|
||||||
if self.transport == "ssh":
|
if self.transport == "ssh":
|
||||||
|
|
|
||||||
|
|
@ -176,6 +176,21 @@ def test_git_remote_url_local():
|
||||||
assert url.git_remote_url() == "/home/alice/project"
|
assert url.git_remote_url() == "/home/alice/project"
|
||||||
|
|
||||||
|
|
||||||
|
def test_git_remote_url_https_nonstandard_port():
|
||||||
|
url = IfcUrl.parse("ifc://localhost:3000/org/repo@heads/main?path=m.ifc")
|
||||||
|
assert url.git_remote_url() == "https://localhost:3000/org/repo"
|
||||||
|
|
||||||
|
|
||||||
|
def test_git_remote_url_ssh_nonstandard_port():
|
||||||
|
url = IfcUrl.parse("ifc://git@localhost:2222/org/repo@heads/main?path=m.ifc")
|
||||||
|
assert url.git_remote_url() == "ssh://git@localhost:2222/org/repo"
|
||||||
|
|
||||||
|
|
||||||
|
def test_host_preserves_port():
|
||||||
|
url = IfcUrl.parse("ifc://example.com:8080/org/repo@HEAD?path=m.ifc")
|
||||||
|
assert url.host == "example.com:8080"
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# is_mutable_ref()
|
# is_mutable_ref()
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue