Compare commits

..

2 Commits

Author SHA1 Message Date
Kevin Veen-Birkenbach
c5843ccd30 Release version 1.8.3
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / lint-shell (push) Has been cancelled
Mark stable commit / lint-python (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
2025-12-16 19:49:51 +01:00
Kevin Veen-Birkenbach
3cb7852cb4 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
2025-12-16 19:49:09 +01:00
9 changed files with 69 additions and 18 deletions

View File

@@ -1,3 +1,8 @@
## [1.8.3] - 2025-12-16
* MIRRORS now supports plain URL entries, ensuring metadata-only sources like PyPI are recorded without ever being added to the Git configuration.
## [1.8.2] - 2025-12-16 ## [1.8.2] - 2025-12-16
* * ***pkgmgr tools code*** is more robust and predictable: it now fails early with clear errors if VS Code is not installed or a repository is not yet identified. * * ***pkgmgr tools code*** is more robust and predictable: it now fails early with clear errors if VS Code is not installed or a repository is not yet identified.

View File

@@ -32,7 +32,7 @@
rec { rec {
pkgmgr = pyPkgs.buildPythonApplication { pkgmgr = pyPkgs.buildPythonApplication {
pname = "package-manager"; pname = "package-manager";
version = "1.8.2"; version = "1.8.3";
# Use the git repo as source # Use the git repo as source
src = ./.; src = ./.;

View File

@@ -1,7 +1,7 @@
# Maintainer: Kevin Veen-Birkenbach <info@veen.world> # Maintainer: Kevin Veen-Birkenbach <info@veen.world>
pkgname=package-manager pkgname=package-manager
pkgver=1.8.2 pkgver=1.8.3
pkgrel=1 pkgrel=1
pkgdesc="Local-flake wrapper for Kevin's package-manager (Nix-based)." pkgdesc="Local-flake wrapper for Kevin's package-manager (Nix-based)."
arch=('any') arch=('any')

View File

@@ -1,3 +1,9 @@
package-manager (1.8.3-1) unstable; urgency=medium
* MIRRORS now supports plain URL entries, ensuring metadata-only sources like PyPI are recorded without ever being added to the Git configuration.
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 16 Dec 2025 19:49:51 +0100
package-manager (1.8.2-1) unstable; urgency=medium package-manager (1.8.2-1) unstable; urgency=medium
* * ***pkgmgr tools code*** is more robust and predictable: it now fails early with clear errors if VS Code is not installed or a repository is not yet identified. * * ***pkgmgr tools code*** is more robust and predictable: it now fails early with clear errors if VS Code is not installed or a repository is not yet identified.

View File

@@ -1,5 +1,5 @@
Name: package-manager Name: package-manager
Version: 1.8.2 Version: 1.8.3
Release: 1%{?dist} Release: 1%{?dist}
Summary: Wrapper that runs Kevin's package-manager via Nix flake Summary: Wrapper that runs Kevin's package-manager via Nix flake
@@ -74,6 +74,9 @@ echo ">>> package-manager removed. Nix itself was not removed."
/usr/lib/package-manager/ /usr/lib/package-manager/
%changelog %changelog
* Tue Dec 16 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 1.8.3-1
- MIRRORS now supports plain URL entries, ensuring metadata-only sources like PyPI are recorded without ever being added to the Git configuration.
* Tue Dec 16 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 1.8.2-1 * Tue Dec 16 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 1.8.2-1
- * ***pkgmgr tools code*** is more robust and predictable: it now fails early with clear errors if VS Code is not installed or a repository is not yet identified. - * ***pkgmgr tools code*** is more robust and predictable: it now fails early with clear errors if VS Code is not installed or a repository is not yet identified.

View File

@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "kpmx" name = "kpmx"
version = "1.8.2" version = "1.8.3"
description = "Kevin's package-manager tool (pkgmgr)" description = "Kevin's package-manager tool (pkgmgr)"
readme = "README.md" readme = "README.md"
requires-python = ">=3.9" requires-python = ">=3.9"

View File

@@ -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}")

View File

@@ -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],

View File

@@ -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",
) )