feat(mirror,create): make MIRRORS single source of truth and exclude PyPI from git config
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

- Treat MIRRORS as the only authority for mirror URLs
- Filter non-git URLs (e.g. PyPI) from git remotes and push URLs
- Prefer SSH git URLs when determining primary origin
- Ensure mirror probing only targets valid git remotes
- Refactor repository create into service-based architecture
- Write PyPI metadata exclusively to MIRRORS, never to git config
- Add integration test verifying PyPI is not written into .git/config
- Update preview and unit tests to match new create flow

https://chatgpt.com/share/69415c61-1c5c-800f-86dd-0405edec25db
This commit is contained in:
Kevin Veen-Birkenbach
2025-12-16 14:19:19 +01:00
parent 374f4ed745
commit 8583fdf172
19 changed files with 792 additions and 418 deletions

View File

@@ -15,17 +15,47 @@ class TestCreateRepoPreviewOutput(unittest.TestCase):
out = io.StringIO()
with (
redirect_stdout(out),
patch("pkgmgr.actions.repository.create.os.path.exists", return_value=False),
patch("pkgmgr.actions.repository.create.generate_alias", return_value="repo"),
patch("pkgmgr.actions.repository.create.save_user_config"),
patch("pkgmgr.actions.repository.create.os.makedirs"),
patch("pkgmgr.actions.repository.create.render_default_templates"),
patch("pkgmgr.actions.repository.create.write_mirrors_file"),
patch("pkgmgr.actions.repository.create.setup_mirrors"),
patch("pkgmgr.actions.repository.create.get_config_value", return_value=None),
patch("pkgmgr.actions.repository.create.init"),
patch("pkgmgr.actions.repository.create.add_all"),
patch("pkgmgr.actions.repository.create.commit"),
patch(
"pkgmgr.actions.repository.create.config_writer.generate_alias",
return_value="repo",
),
patch(
"pkgmgr.actions.repository.create.config_writer.save_user_config",
),
patch(
"pkgmgr.actions.repository.create.config_writer.os.path.exists",
return_value=False,
),
patch(
"pkgmgr.actions.repository.create.service.os.makedirs",
),
patch(
"pkgmgr.actions.repository.create.templates.TemplateRenderer._resolve_templates_dir",
return_value="/tpl",
),
patch(
"pkgmgr.actions.repository.create.templates.os.walk",
return_value=[("/tpl", [], ["README.md.j2"])],
),
patch(
"pkgmgr.actions.repository.create.git_bootstrap.init",
),
patch(
"pkgmgr.actions.repository.create.git_bootstrap.add_all",
),
patch(
"pkgmgr.actions.repository.create.git_bootstrap.commit",
),
patch(
"pkgmgr.actions.repository.create.mirrors.write_mirrors_file",
),
patch(
"pkgmgr.actions.repository.create.mirrors.setup_mirrors",
),
patch(
"pkgmgr.actions.repository.create.service.get_config_value",
return_value=None,
),
):
create_repo(
"github.com/acme/repo",
@@ -37,7 +67,7 @@ class TestCreateRepoPreviewOutput(unittest.TestCase):
)
s = out.getvalue()
self.assertIn("[Preview] Would save user config:", s)
self.assertIn("[Preview] Would add repository to config:", s)
self.assertIn("[Preview] Would ensure directory exists:", s)

View File

@@ -0,0 +1,115 @@
# tests/integration/test_repos_create_pypi_not_in_git_config.py
from __future__ import annotations
import os
import subprocess
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
from pkgmgr.actions.repository.create import create_repo
class TestCreateRepoPypiNotInGitConfig(unittest.TestCase):
def test_create_repo_writes_pypi_to_mirrors_but_not_git_config(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
tmp_path = Path(tmp)
# Repositories base dir used by create flow
repos_base = tmp_path / "Repositories"
user_cfg = tmp_path / "user.yml"
bin_dir = tmp_path / "bin"
bin_dir.mkdir(parents=True, exist_ok=True)
cfg = {
"directories": {"repositories": str(repos_base)},
"repositories": [],
}
# Provide a minimal templates directory so TemplateRenderer can run
tpl_dir = tmp_path / "tpl"
tpl_dir.mkdir(parents=True, exist_ok=True)
(tpl_dir / "README.md.j2").write_text(
"# {{ repository }}\n", encoding="utf-8"
)
# Expected repo dir for identifier github.com/acme/repo
repo_dir = repos_base / "github.com" / "acme" / "repo"
with (
# Avoid any real network calls during mirror "remote probing"
patch(
"pkgmgr.actions.mirror.setup_cmd.probe_remote_reachable",
return_value=True,
),
# Force templates to come from our temp directory
patch(
"pkgmgr.actions.repository.create.templates.TemplateRenderer._resolve_templates_dir",
return_value=str(tpl_dir),
),
# Make git commit deterministic without depending on global git config
patch.dict(
os.environ,
{
"GIT_AUTHOR_NAME": "Test Author",
"GIT_AUTHOR_EMAIL": "author@example.invalid",
"GIT_COMMITTER_NAME": "Test Author",
"GIT_COMMITTER_EMAIL": "author@example.invalid",
},
clear=False,
),
):
create_repo(
"github.com/acme/repo",
cfg,
str(user_cfg),
str(bin_dir),
remote=False,
preview=False,
)
# --- Assertions: MIRRORS file ---
mirrors_file = repo_dir / "MIRRORS"
self.assertTrue(mirrors_file.exists(), "MIRRORS file was not created")
mirrors_content = mirrors_file.read_text(encoding="utf-8")
self.assertIn(
"pypi https://pypi.org/project/repo/",
mirrors_content,
"PyPI mirror entry must exist in MIRRORS",
)
self.assertIn(
"origin git@github.com:acme/repo.git",
mirrors_content,
"origin SSH URL must exist in MIRRORS",
)
# --- Assertions: git config must NOT contain PyPI ---
git_config = repo_dir / ".git" / "config"
self.assertTrue(git_config.exists(), ".git/config was not created")
git_config_content = git_config.read_text(encoding="utf-8")
self.assertNotIn(
"pypi.org/project",
git_config_content,
"PyPI must never be written into git config",
)
# --- Assertions: origin remote exists and points to SSH ---
remotes = subprocess.check_output(
["git", "-C", str(repo_dir), "remote"],
text=True,
).splitlines()
self.assertIn("origin", remotes, "origin remote was not created")
remote_v = subprocess.check_output(
["git", "-C", str(repo_dir), "remote", "-v"],
text=True,
)
self.assertIn("git@github.com:acme/repo.git", remote_v)
if __name__ == "__main__":
unittest.main()

View File

@@ -2,9 +2,9 @@ from __future__ import annotations
import unittest
from pkgmgr.actions.repository.create import (
RepoParts,
_parse_identifier,
from pkgmgr.actions.repository.create.model import RepoParts
from pkgmgr.actions.repository.create.parser import (
parse_identifier,
_parse_git_url,
_strip_git_suffix,
_split_host_port,
@@ -22,7 +22,7 @@ class TestRepositoryCreateParsing(unittest.TestCase):
self.assertEqual(_split_host_port("example.com:"), ("example.com", None))
def test_parse_identifier_plain(self) -> None:
parts = _parse_identifier("github.com/owner/repo")
parts = parse_identifier("github.com/owner/repo")
self.assertIsInstance(parts, RepoParts)
self.assertEqual(parts.host, "github.com")
self.assertEqual(parts.port, None)
@@ -30,7 +30,7 @@ class TestRepositoryCreateParsing(unittest.TestCase):
self.assertEqual(parts.name, "repo")
def test_parse_identifier_with_port(self) -> None:
parts = _parse_identifier("gitea.example.com:2222/org/repo")
parts = parse_identifier("gitea.example.com:2222/org/repo")
self.assertEqual(parts.host, "gitea.example.com")
self.assertEqual(parts.port, "2222")
self.assertEqual(parts.owner, "org")

View File

@@ -0,0 +1,43 @@
from __future__ import annotations
import unittest
from unittest.mock import patch
from pkgmgr.actions.repository.create.templates import TemplateRenderer
class TestTemplateRendererPreview(unittest.TestCase):
def test_render_preview_does_not_write(self) -> None:
# Ensure TemplateRenderer does not try to resolve real repo root.
with (
patch(
"pkgmgr.actions.repository.create.templates.TemplateRenderer._resolve_templates_dir",
return_value="/tpl",
),
patch(
"pkgmgr.actions.repository.create.templates.os.walk",
return_value=[("/tpl", [], ["README.md.j2"])],
),
patch(
"pkgmgr.actions.repository.create.templates.os.path.relpath",
return_value="README.md.j2",
),
patch("pkgmgr.actions.repository.create.templates.os.makedirs") as mk,
patch("pkgmgr.actions.repository.create.templates.open", create=True) as op,
patch("pkgmgr.actions.repository.create.templates.Environment") as env_cls,
):
renderer = TemplateRenderer()
renderer.render(
repo_dir="/repo",
context={"repository": "x"},
preview=True,
)
mk.assert_not_called()
op.assert_not_called()
env_cls.assert_not_called()
if __name__ == "__main__":
unittest.main()

View File

@@ -1,35 +0,0 @@
from __future__ import annotations
import unittest
from unittest.mock import patch
from pkgmgr.actions.repository.scaffold import render_default_templates
class TestScaffoldRenderPreview(unittest.TestCase):
def test_render_preview_does_not_write(self) -> None:
with (
patch("pkgmgr.actions.repository.scaffold._templates_dir", return_value="/tpl"),
patch("pkgmgr.actions.repository.scaffold.os.path.isdir", return_value=True),
patch("pkgmgr.actions.repository.scaffold.os.walk", return_value=[("/tpl", [], ["README.md.j2"])]),
patch("pkgmgr.actions.repository.scaffold.os.path.relpath", return_value="README.md.j2"),
patch("pkgmgr.actions.repository.scaffold.os.makedirs") as mk,
patch("pkgmgr.actions.repository.scaffold.open", create=True) as op,
patch("pkgmgr.actions.repository.scaffold.Environment") as env_cls,
):
env = env_cls.return_value
env.get_template.return_value.render.return_value = "X"
render_default_templates(
"/repo",
context={"repository": "x"},
preview=True,
)
mk.assert_not_called()
op.assert_not_called()
env.get_template.assert_not_called()
if __name__ == "__main__":
unittest.main()