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
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:
@@ -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)
|
||||
|
||||
|
||||
|
||||
115
tests/integration/test_repos_create_pypi_not_in_git_config.py
Normal file
115
tests/integration/test_repos_create_pypi_not_in_git_config.py
Normal 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()
|
||||
@@ -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")
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user