feat(mirrors): support URL-only MIRRORS entries and keep git config clean
- Allow MIRRORS to contain plain URLs (one per line) in addition to legacy "NAME URL" - Treat strings as single URLs to avoid iterable pitfalls - Write PyPI URLs as metadata-only entries (never added to git config) - Keep MIRRORS as the single source of truth for mirror setup - Update integration test to assert URL-only MIRRORS output https://chatgpt.com/share/6941a9aa-b8b4-800f-963d-2486b34856b1
This commit is contained in:
@@ -1,8 +1,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
from collections.abc import Iterable, Mapping
|
||||||
|
from typing import Union
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
from typing import Mapping
|
|
||||||
|
|
||||||
from .types import MirrorMap, Repository
|
from .types import MirrorMap, Repository
|
||||||
|
|
||||||
@@ -32,7 +33,7 @@ def read_mirrors_file(repo_dir: str, filename: str = "MIRRORS") -> MirrorMap:
|
|||||||
"""
|
"""
|
||||||
Supports:
|
Supports:
|
||||||
NAME URL
|
NAME URL
|
||||||
URL → auto name = hostname
|
URL -> auto-generate name from hostname
|
||||||
"""
|
"""
|
||||||
path = os.path.join(repo_dir, filename)
|
path = os.path.join(repo_dir, filename)
|
||||||
mirrors: MirrorMap = {}
|
mirrors: MirrorMap = {}
|
||||||
@@ -52,7 +53,8 @@ def read_mirrors_file(repo_dir: str, filename: str = "MIRRORS") -> MirrorMap:
|
|||||||
# Case 1: "name url"
|
# Case 1: "name url"
|
||||||
if len(parts) == 2:
|
if len(parts) == 2:
|
||||||
name, url = parts
|
name, url = parts
|
||||||
# Case 2: "url" → auto-generate name
|
|
||||||
|
# Case 2: "url" -> auto name
|
||||||
elif len(parts) == 1:
|
elif len(parts) == 1:
|
||||||
url = parts[0]
|
url = parts[0]
|
||||||
parsed = urlparse(url)
|
parsed = urlparse(url)
|
||||||
@@ -67,21 +69,56 @@ def read_mirrors_file(repo_dir: str, filename: str = "MIRRORS") -> MirrorMap:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
mirrors[name] = url
|
mirrors[name] = url
|
||||||
|
|
||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
print(f"[WARN] Could not read MIRRORS file at {path}: {exc}")
|
print(f"[WARN] Could not read MIRRORS file at {path}: {exc}")
|
||||||
|
|
||||||
return mirrors
|
return mirrors
|
||||||
|
|
||||||
|
|
||||||
|
MirrorsInput = Union[Mapping[str, str], Iterable[str]]
|
||||||
|
|
||||||
|
|
||||||
def write_mirrors_file(
|
def write_mirrors_file(
|
||||||
repo_dir: str,
|
repo_dir: str,
|
||||||
mirrors: Mapping[str, str],
|
mirrors: MirrorsInput,
|
||||||
filename: str = "MIRRORS",
|
filename: str = "MIRRORS",
|
||||||
preview: bool = False,
|
preview: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""
|
||||||
|
Write MIRRORS in one of two formats:
|
||||||
|
|
||||||
|
1) Mapping[str, str] -> "NAME URL" per line (legacy / compatible)
|
||||||
|
2) Iterable[str] -> "URL" per line (new preferred)
|
||||||
|
|
||||||
|
Strings are treated as a single URL (not iterated character-by-character).
|
||||||
|
"""
|
||||||
path = os.path.join(repo_dir, filename)
|
path = os.path.join(repo_dir, filename)
|
||||||
lines = [f"{name} {url}" for name, url in sorted(mirrors.items())]
|
|
||||||
|
lines: list[str]
|
||||||
|
|
||||||
|
if isinstance(mirrors, Mapping):
|
||||||
|
items = [
|
||||||
|
(str(name), str(url))
|
||||||
|
for name, url in mirrors.items()
|
||||||
|
if url is not None and str(url).strip()
|
||||||
|
]
|
||||||
|
items.sort(key=lambda x: (x[0], x[1]))
|
||||||
|
lines = [f"{name} {url}" for name, url in items]
|
||||||
|
|
||||||
|
else:
|
||||||
|
if isinstance(mirrors, (str, bytes)):
|
||||||
|
urls = [str(mirrors).strip()]
|
||||||
|
else:
|
||||||
|
urls = [
|
||||||
|
str(url).strip()
|
||||||
|
for url in mirrors
|
||||||
|
if url is not None and str(url).strip()
|
||||||
|
]
|
||||||
|
|
||||||
|
urls = sorted(set(urls))
|
||||||
|
lines = urls
|
||||||
|
|
||||||
content = "\n".join(lines) + ("\n" if lines else "")
|
content = "\n".join(lines) + ("\n" if lines else "")
|
||||||
|
|
||||||
if preview:
|
if preview:
|
||||||
@@ -94,5 +131,6 @@ def write_mirrors_file(
|
|||||||
with open(path, "w", encoding="utf-8") as fh:
|
with open(path, "w", encoding="utf-8") as fh:
|
||||||
fh.write(content)
|
fh.write(content)
|
||||||
print(f"[INFO] Wrote MIRRORS file at {path}")
|
print(f"[INFO] Wrote MIRRORS file at {path}")
|
||||||
|
|
||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
print(f"[ERROR] Failed to write MIRRORS file at {path}: {exc}")
|
print(f"[ERROR] Failed to write MIRRORS file at {path}: {exc}")
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ class MirrorBootstrapper:
|
|||||||
"""
|
"""
|
||||||
MIRRORS is the single source of truth.
|
MIRRORS is the single source of truth.
|
||||||
|
|
||||||
We write defaults to MIRRORS and then call mirror setup which will
|
Defaults are written to MIRRORS and mirror setup derives
|
||||||
configure git remotes based on MIRRORS content (but only for git URLs).
|
git remotes exclusively from that file (git URLs only).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def write_defaults(
|
def write_defaults(
|
||||||
@@ -25,10 +25,8 @@ class MirrorBootstrapper:
|
|||||||
preview: bool,
|
preview: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
mirrors = {
|
mirrors = {
|
||||||
# preferred SSH url is supplied by CreateRepoPlanner.primary_remote
|
primary,
|
||||||
"origin": primary,
|
f"https://pypi.org/project/{name}/",
|
||||||
# metadata only: must NEVER be configured as a git remote
|
|
||||||
"pypi": f"https://pypi.org/project/{name}/",
|
|
||||||
}
|
}
|
||||||
write_mirrors_file(repo_dir, mirrors, preview=preview)
|
write_mirrors_file(repo_dir, mirrors, preview=preview)
|
||||||
|
|
||||||
@@ -41,7 +39,8 @@ class MirrorBootstrapper:
|
|||||||
preview: bool,
|
preview: bool,
|
||||||
remote: bool,
|
remote: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
# IMPORTANT: do NOT set repo["mirrors"] here.
|
# IMPORTANT:
|
||||||
|
# Do NOT set repo["mirrors"] here.
|
||||||
# MIRRORS file is the single source of truth.
|
# MIRRORS file is the single source of truth.
|
||||||
setup_mirrors(
|
setup_mirrors(
|
||||||
selected_repos=[repo],
|
selected_repos=[repo],
|
||||||
|
|||||||
@@ -75,12 +75,12 @@ class TestCreateRepoPypiNotInGitConfig(unittest.TestCase):
|
|||||||
|
|
||||||
mirrors_content = mirrors_file.read_text(encoding="utf-8")
|
mirrors_content = mirrors_file.read_text(encoding="utf-8")
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
"pypi https://pypi.org/project/repo/",
|
"https://pypi.org/project/repo/",
|
||||||
mirrors_content,
|
mirrors_content,
|
||||||
"PyPI mirror entry must exist in MIRRORS",
|
"PyPI mirror entry must exist in MIRRORS",
|
||||||
)
|
)
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
"origin git@github.com:acme/repo.git",
|
"git@github.com:acme/repo.git",
|
||||||
mirrors_content,
|
mirrors_content,
|
||||||
"origin SSH URL must exist in MIRRORS",
|
"origin SSH URL must exist in MIRRORS",
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user