feat(create): scaffold repositories via templates with preview and mirror setup
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 / linter-shell (push) Has been cancelled
Mark stable commit / linter-python (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled

https://chatgpt.com/share/693f5bdb-1780-800f-a772-0ecf399627fc
This commit is contained in:
Kevin Veen-Birkenbach
2025-12-15 01:52:38 +01:00
parent 9e3ce34626
commit 2c15a4016b
14 changed files with 605 additions and 125 deletions

View File

@@ -49,6 +49,7 @@
# Runtime dependencies (matches [project.dependencies] in pyproject.toml)
propagatedBuildInputs = [
pyPkgs.pyyaml
pyPkgs.jinja2
pyPkgs.pip
];
@@ -78,6 +79,7 @@
pythonWithDeps = python.withPackages (ps: [
ps.pip
ps.pyyaml
ps.jinja2
]);
in
{

View File

@@ -21,6 +21,7 @@ authors = [
dependencies = [
"PyYAML>=6.0",
"tomli; python_version < \"3.11\"",
"jinja2>=3.1"
]
[project.urls]

View File

@@ -1,143 +1,257 @@
from __future__ import annotations
import os
import re
import subprocess
from dataclasses import dataclass
from typing import Any, Dict, Optional, Tuple
from urllib.parse import urlparse
import yaml
from pkgmgr.actions.mirror.io import write_mirrors_file
from pkgmgr.actions.mirror.setup_cmd import setup_mirrors
from pkgmgr.actions.repository.scaffold import render_default_templates
from pkgmgr.core.command.alias import generate_alias
from pkgmgr.core.config.save import save_user_config
def create_repo(identifier, config_merged, user_config_path, bin_dir, remote=False, preview=False):
"""
Creates a new repository by performing the following steps:
Repository = Dict[str, Any]
1. Parses the identifier (provider:port/account/repository) and adds a new entry to the user config
if it is not already present. The provider part is split into provider and port (if provided).
2. Creates the local repository directory and initializes a Git repository.
3. If --remote is set, checks for an existing "origin" remote (removing it if found),
adds the remote using a URL built from provider, port, account, and repository,
creates an initial commit (e.g. with a README.md), and pushes to the remote.
The push is attempted on both "main" and "master" branches.
"""
parts = identifier.split("/")
_NAME_RE = re.compile(r"^[a-z0-9_-]+$")
@dataclass(frozen=True)
class RepoParts:
host: str
port: Optional[str]
owner: str
name: str
def _run(cmd: str, cwd: str, preview: bool) -> None:
if preview:
print(f"[Preview] Would run in {cwd}: {cmd}")
return
subprocess.run(cmd, cwd=cwd, shell=True, check=True)
def _git_get(key: str) -> str:
try:
out = subprocess.run(
f"git config --get {key}",
shell=True,
check=False,
capture_output=True,
text=True,
)
return (out.stdout or "").strip()
except Exception:
return ""
def _split_host_port(host_with_port: str) -> Tuple[str, Optional[str]]:
if ":" in host_with_port:
host, port = host_with_port.split(":", 1)
return host, port or None
return host_with_port, None
def _strip_git_suffix(name: str) -> str:
return name[:-4] if name.endswith(".git") else name
def _parse_git_url(url: str) -> RepoParts:
if url.startswith("git@") and "://" not in url:
left, right = url.split(":", 1)
host = left.split("@", 1)[1]
path = right.lstrip("/")
owner, name = path.split("/", 1)
return RepoParts(host=host, port=None, owner=owner, name=_strip_git_suffix(name))
parsed = urlparse(url)
host = (parsed.hostname or "").strip()
port = str(parsed.port) if parsed.port else None
path = (parsed.path or "").strip("/")
if not host or not path or "/" not in path:
raise ValueError(f"Could not parse git URL: {url}")
owner, name = path.split("/", 1)
return RepoParts(host=host, port=port, owner=owner, name=_strip_git_suffix(name))
def _parse_identifier(identifier: str) -> RepoParts:
ident = identifier.strip()
if "://" in ident or ident.startswith("git@"):
return _parse_git_url(ident)
parts = ident.split("/")
if len(parts) != 3:
print("Identifier must be in the format 'provider:port/account/repository' (port is optional).")
raise ValueError("Identifier must be URL or 'provider(:port)/owner/repo'.")
host_with_port, owner, name = parts
host, port = _split_host_port(host_with_port)
return RepoParts(host=host, port=port, owner=owner, name=name)
def _ensure_valid_repo_name(name: str) -> None:
if not name or not _NAME_RE.fullmatch(name):
raise ValueError("Repository name must match: lowercase a-z, 0-9, '_' and '-'.")
def _repo_homepage(host: str, owner: str, name: str) -> str:
return f"https://{host}/{owner}/{name}"
def _build_default_primary_url(parts: RepoParts) -> str:
if parts.port:
return f"ssh://git@{parts.host}:{parts.port}/{parts.owner}/{parts.name}.git"
return f"git@{parts.host}:{parts.owner}/{parts.name}.git"
def _write_default_mirrors(repo_dir: str, primary: str, name: str, preview: bool) -> None:
mirrors = {"origin": primary, "pypi": f"https://pypi.org/project/{name}/"}
write_mirrors_file(repo_dir, mirrors, preview=preview)
def _git_init_and_initial_commit(repo_dir: str, preview: bool) -> None:
_run("git init", cwd=repo_dir, preview=preview)
_run("git add -A", cwd=repo_dir, preview=preview)
if preview:
print(f'[Preview] Would run in {repo_dir}: git commit -m "Initial commit"')
return
provider_with_port, account, repository = parts
# Split provider and port if a colon is present.
if ":" in provider_with_port:
provider_name, port = provider_with_port.split(":", 1)
else:
provider_name = provider_with_port
port = None
subprocess.run('git commit -m "Initial commit"', cwd=repo_dir, shell=True, check=False)
# Check if the repository is already present in the merged config (including port)
exists = False
for repo in config_merged.get("repositories", []):
if (repo.get("provider") == provider_name and
repo.get("account") == account and
repo.get("repository") == repository):
exists = True
print(f"Repository {identifier} already exists in the configuration.")
break
def _git_push_main_or_master(repo_dir: str, preview: bool) -> None:
_run("git branch -M main", cwd=repo_dir, preview=preview)
try:
_run("git push -u origin main", cwd=repo_dir, preview=preview)
return
except subprocess.CalledProcessError:
pass
try:
_run("git branch -M master", cwd=repo_dir, preview=preview)
_run("git push -u origin master", cwd=repo_dir, preview=preview)
except subprocess.CalledProcessError as exc:
print(f"[WARN] Push failed: {exc}")
def create_repo(
identifier: str,
config_merged: Dict[str, Any],
user_config_path: str,
bin_dir: str,
*,
remote: bool = False,
preview: bool = False,
) -> None:
parts = _parse_identifier(identifier)
_ensure_valid_repo_name(parts.name)
directories = config_merged.get("directories") or {}
base_dir = os.path.expanduser(str(directories.get("repositories", "~/Repositories")))
repo_dir = os.path.join(base_dir, parts.host, parts.owner, parts.name)
author_name = _git_get("user.name") or "Unknown Author"
author_email = _git_get("user.email") or "unknown@example.invalid"
homepage = _repo_homepage(parts.host, parts.owner, parts.name)
primary_url = _build_default_primary_url(parts)
repositories = config_merged.get("repositories") or []
exists = any(
(
r.get("provider") == parts.host
and r.get("account") == parts.owner
and r.get("repository") == parts.name
)
for r in repositories
)
if not exists:
# Create a new entry with an automatically generated alias.
new_entry = {
"provider": provider_name,
"port": port,
"account": account,
"repository": repository,
"alias": generate_alias({"repository": repository, "provider": provider_name, "account": account}, bin_dir, existing_aliases=set()),
"verified": {} # No initial verification info
new_entry: Repository = {
"provider": parts.host,
"port": parts.port,
"account": parts.owner,
"repository": parts.name,
"homepage": homepage,
"alias": generate_alias(
{"repository": parts.name, "provider": parts.host, "account": parts.owner},
bin_dir,
existing_aliases=set(),
),
"verified": {},
}
# Load or initialize the user configuration.
if os.path.exists(user_config_path):
with open(user_config_path, "r") as f:
with open(user_config_path, "r", encoding="utf-8") as f:
user_config = yaml.safe_load(f) or {}
else:
user_config = {"repositories": []}
user_config.setdefault("repositories", [])
user_config["repositories"].append(new_entry)
save_user_config(user_config, user_config_path)
print(f"Repository {identifier} added to the configuration.")
# Also update the merged configuration object.
config_merged.setdefault("repositories", []).append(new_entry)
# Create the local repository directory based on the configured base directory.
base_dir = os.path.expanduser(config_merged["directories"]["repositories"])
repo_dir = os.path.join(base_dir, provider_name, account, repository)
if not os.path.exists(repo_dir):
os.makedirs(repo_dir, exist_ok=True)
print(f"Local repository directory created: {repo_dir}")
else:
print(f"Local repository directory already exists: {repo_dir}")
# Initialize a Git repository if not already initialized.
if not os.path.exists(os.path.join(repo_dir, ".git")):
cmd_init = "git init"
if preview:
print(f"[Preview] Would execute: '{cmd_init}' in {repo_dir}")
print(f"[Preview] Would save user config: {user_config_path}")
else:
subprocess.run(cmd_init, cwd=repo_dir, shell=True, check=True)
print(f"Git repository initialized in {repo_dir}.")
save_user_config(user_config, user_config_path)
config_merged.setdefault("repositories", []).append(new_entry)
repo = new_entry
print(f"[INFO] Added repository to configuration: {parts.host}/{parts.owner}/{parts.name}")
else:
print("Git repository is already initialized.")
repo = next(
r
for r in repositories
if (
r.get("provider") == parts.host
and r.get("account") == parts.owner
and r.get("repository") == parts.name
)
)
print(f"[INFO] Repository already in configuration: {parts.host}/{parts.owner}/{parts.name}")
if preview:
print(f"[Preview] Would ensure directory exists: {repo_dir}")
else:
os.makedirs(repo_dir, exist_ok=True)
tpl_context = {
"provider": parts.host,
"port": parts.port,
"account": parts.owner,
"repository": parts.name,
"homepage": homepage,
"author_name": author_name,
"author_email": author_email,
"license_text": f"All rights reserved by {author_name}",
"primary_remote": primary_url,
}
render_default_templates(repo_dir, context=tpl_context, preview=preview)
_git_init_and_initial_commit(repo_dir, preview=preview)
_write_default_mirrors(repo_dir, primary=primary_url, name=parts.name, preview=preview)
repo.setdefault("mirrors", {})
repo["mirrors"].setdefault("origin", primary_url)
repo["mirrors"].setdefault("pypi", f"https://pypi.org/project/{parts.name}/")
setup_mirrors(
selected_repos=[repo],
repositories_base_dir=base_dir,
all_repos=config_merged.get("repositories", []),
preview=preview,
local=True,
remote=True,
ensure_remote=bool(remote),
)
if remote:
# Create a README.md if it does not exist to have content for an initial commit.
readme_path = os.path.join(repo_dir, "README.md")
if not os.path.exists(readme_path):
if preview:
print(f"[Preview] Would create README.md in {repo_dir}.")
else:
with open(readme_path, "w") as f:
f.write(f"# {repository}\n")
subprocess.run("git add README.md", cwd=repo_dir, shell=True, check=True)
subprocess.run('git commit -m "Initial commit"', cwd=repo_dir, shell=True, check=True)
print("README.md created and initial commit made.")
# Build the remote URL.
if provider_name.lower() == "github.com":
remote_url = f"git@{provider_name}:{account}/{repository}.git"
else:
if port:
remote_url = f"ssh://git@{provider_name}:{port}/{account}/{repository}.git"
else:
remote_url = f"ssh://git@{provider_name}/{account}/{repository}.git"
# Check if the remote "origin" already exists.
cmd_list = "git remote"
if preview:
print(f"[Preview] Would check for existing remotes in {repo_dir}")
remote_exists = False # Assume no remote in preview mode.
else:
result = subprocess.run(cmd_list, cwd=repo_dir, shell=True, capture_output=True, text=True, check=True)
remote_list = result.stdout.strip().split()
remote_exists = "origin" in remote_list
if remote_exists:
# Remove the existing remote "origin".
cmd_remove = "git remote remove origin"
if preview:
print(f"[Preview] Would execute: '{cmd_remove}' in {repo_dir}")
else:
subprocess.run(cmd_remove, cwd=repo_dir, shell=True, check=True)
print("Existing remote 'origin' removed.")
# Now add the new remote.
cmd_remote = f"git remote add origin {remote_url}"
if preview:
print(f"[Preview] Would execute: '{cmd_remote}' in {repo_dir}")
else:
try:
subprocess.run(cmd_remote, cwd=repo_dir, shell=True, check=True)
print(f"Remote 'origin' added: {remote_url}")
except subprocess.CalledProcessError:
print(f"Failed to add remote using URL: {remote_url}.")
# Push the initial commit to the remote repository
cmd_push = "git push -u origin master"
if preview:
print(f"[Preview] Would execute: '{cmd_push}' in {repo_dir}")
else:
subprocess.run(cmd_push, cwd=repo_dir, shell=True, check=True)
print("Initial push to the remote repository completed.")
_git_push_main_or_master(repo_dir, preview=preview)

View File

@@ -0,0 +1,105 @@
from __future__ import annotations
import os
import subprocess
from pathlib import Path
from typing import Any, Dict, Optional
try:
from jinja2 import Environment, FileSystemLoader, StrictUndefined
except Exception as exc: # pragma: no cover
Environment = None # type: ignore[assignment]
FileSystemLoader = None # type: ignore[assignment]
StrictUndefined = None # type: ignore[assignment]
_JINJA_IMPORT_ERROR = exc
else:
_JINJA_IMPORT_ERROR = None
def _repo_root_from_here(anchor: Optional[Path] = None) -> str:
"""
Prefer git root (robust in editable installs / different layouts).
Fallback to a conservative relative parent lookup.
"""
here = (anchor or Path(__file__)).resolve().parent
try:
r = subprocess.run(
["git", "rev-parse", "--show-toplevel"],
cwd=str(here),
check=False,
capture_output=True,
text=True,
)
if r.returncode == 0:
top = (r.stdout or "").strip()
if top:
return top
except Exception:
pass
# Fallback: src/pkgmgr/actions/repository/scaffold.py -> <repo root> = parents[5]
p = (anchor or Path(__file__)).resolve()
if len(p.parents) < 6:
raise RuntimeError(f"Unexpected path depth for: {p}")
return str(p.parents[5])
def _templates_dir() -> str:
return os.path.join(_repo_root_from_here(), "templates", "default")
def render_default_templates(
repo_dir: str,
*,
context: Dict[str, Any],
preview: bool,
) -> None:
"""
Render templates/default/*.j2 into repo_dir.
Keeps create.py clean: create.py calls this function only.
"""
tpl_dir = _templates_dir()
if not os.path.isdir(tpl_dir):
raise RuntimeError(f"Templates directory not found: {tpl_dir}")
# Preview mode: do not require Jinja2 at all. We only print planned outputs.
if preview:
for root, _, files in os.walk(tpl_dir):
for fn in files:
if not fn.endswith(".j2"):
continue
abs_src = os.path.join(root, fn)
rel_src = os.path.relpath(abs_src, tpl_dir)
rel_out = rel_src[:-3]
print(f"[Preview] Would render template: {rel_src} -> {rel_out}")
return
if Environment is None or FileSystemLoader is None or StrictUndefined is None:
raise RuntimeError(
"Jinja2 is required for repo templates but is not available. "
f"Import error: {_JINJA_IMPORT_ERROR}"
)
env = Environment(
loader=FileSystemLoader(tpl_dir),
undefined=StrictUndefined,
autoescape=False,
keep_trailing_newline=True,
)
for root, _, files in os.walk(tpl_dir):
for fn in files:
if not fn.endswith(".j2"):
continue
abs_src = os.path.join(root, fn)
rel_src = os.path.relpath(abs_src, tpl_dir)
rel_out = rel_src[:-3]
abs_out = os.path.join(repo_dir, rel_out)
os.makedirs(os.path.dirname(abs_out), exist_ok=True)
template = env.get_template(rel_src)
rendered = template.render(**context)
with open(abs_out, "w", encoding="utf-8") as f:
f.write(rendered)

View File

@@ -4,18 +4,18 @@ import argparse
from pkgmgr.cli.proxy import register_proxy_commands
from .common import SortedSubParsersAction
from .install_update import add_install_update_subparsers
from .config_cmd import add_config_subparsers
from .navigation_cmd import add_navigation_subparsers
from .branch_cmd import add_branch_subparsers
from .release_cmd import add_release_subparser
from .publish_cmd import add_publish_subparser
from .version_cmd import add_version_subparser
from .changelog_cmd import add_changelog_subparser
from .common import SortedSubParsersAction
from .config_cmd import add_config_subparsers
from .install_update import add_install_update_subparsers
from .list_cmd import add_list_subparser
from .make_cmd import add_make_subparsers
from .mirror_cmd import add_mirror_subparsers
from .navigation_cmd import add_navigation_subparsers
from .publish_cmd import add_publish_subparser
from .release_cmd import add_release_subparser
from .version_cmd import add_version_subparser
def create_parser(description_text: str) -> argparse.ArgumentParser:
@@ -23,12 +23,34 @@ def create_parser(description_text: str) -> argparse.ArgumentParser:
description=description_text,
formatter_class=argparse.RawTextHelpFormatter,
)
subparsers = parser.add_subparsers(
dest="command",
help="Subcommands",
action=SortedSubParsersAction,
)
# create
p_create = subparsers.add_parser(
"create",
help="Create a new repository (scaffold + config).",
)
p_create.add_argument(
"identifiers",
nargs="+",
help="Repository identifier(s): URL or 'provider(:port)/owner/repo'.",
)
p_create.add_argument(
"--remote",
action="store_true",
help="Also push an initial commit to the remote (main/master).",
)
p_create.add_argument(
"--preview",
action="store_true",
help="Print actions without writing files or executing commands.",
)
add_install_update_subparsers(subparsers)
add_config_subparsers(subparsers)
add_navigation_subparsers(subparsers)

View File

@@ -0,0 +1,5 @@
.venv/
dist/
build/
__pycache__/
*.pyc

View File

@@ -0,0 +1 @@
{{ license_text }}

View File

@@ -0,0 +1,6 @@
# {{ repository }}
Homepage: {{ homepage }}
## Author
{{ author_name }} <{{ author_email }}>

View File

@@ -0,0 +1,11 @@
{
description = "{{ repository }}";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
outputs = { self, nixpkgs }:
let system = "x86_64-linux"; pkgs = import nixpkgs { inherit system; };
in {
devShells.${system}.default = pkgs.mkShell {
packages = with pkgs; [ python312 python312Packages.pytest python312Packages.ruff ];
};
};
}

View File

@@ -0,0 +1,21 @@
[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "{{ repository }}"
version = "0.1.0"
description = ""
readme = "README.md"
requires-python = ">=3.10"
authors = [{ name = "{{ author_name }}", email = "{{ author_email }}" }]
license = { text = "{{ license_text }}" }
urls = { Homepage = "{{ homepage }}" }
dependencies = []
[tool.setuptools]
package-dir = {"" = "src"}
[tool.setuptools.packages.find]
where = ["src"]

View File

@@ -0,0 +1,42 @@
from __future__ import annotations
import io
import unittest
from contextlib import redirect_stdout
from unittest.mock import patch
from pkgmgr.actions.repository.create import create_repo
class TestE2ECreateRepoPreviewOutput(unittest.TestCase):
def test_create_repo_preview_prints_expected_steps(self) -> None:
cfg = {"directories": {"repositories": "/tmp/Repositories"}, "repositories": []}
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.subprocess.run"),
):
create_repo(
"github.com/acme/repo",
cfg,
"/tmp/user.yml",
"/tmp/bin",
remote=False,
preview=True,
)
s = out.getvalue()
self.assertIn("[Preview] Would save user config:", s)
self.assertIn("[Preview] Would ensure directory exists:", s)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,53 @@
from __future__ import annotations
import importlib
import io
import unittest
from contextlib import redirect_stdout
from types import SimpleNamespace
from unittest.mock import patch
class TestIntegrationReposCreatePreview(unittest.TestCase):
def test_repos_create_preview_wires_create_repo(self) -> None:
# Import lazily to avoid hard-failing if the CLI module/function name differs.
try:
repos_mod = importlib.import_module("pkgmgr.cli.commands.repos")
except Exception as exc:
self.skipTest(f"CLI module not available: {exc}")
handle = getattr(repos_mod, "handle_repos_command", None)
if handle is None:
self.skipTest("handle_repos_command not found in pkgmgr.cli.commands.repos")
ctx = SimpleNamespace(
repositories_base_dir="/tmp/Repositories",
binaries_dir="/tmp/bin",
all_repositories=[],
config_merged={"directories": {"repositories": "/tmp/Repositories"}, "repositories": []},
user_config_path="/tmp/user.yml",
)
args = SimpleNamespace(
command="create",
identifiers=["github.com/acme/repo"],
remote=False,
preview=True,
)
out = io.StringIO()
with (
redirect_stdout(out),
patch("pkgmgr.cli.commands.repos.create_repo") as create_repo,
):
handle(args, ctx, selected=[])
create_repo.assert_called_once()
called = create_repo.call_args.kwargs
self.assertEqual(called["remote"], False)
self.assertEqual(called["preview"], True)
self.assertEqual(create_repo.call_args.args[0], "github.com/acme/repo")
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,62 @@
from __future__ import annotations
import unittest
from pkgmgr.actions.repository.create import (
RepoParts,
_parse_identifier,
_parse_git_url,
_strip_git_suffix,
_split_host_port,
)
class TestRepositoryCreateParsing(unittest.TestCase):
def test_strip_git_suffix(self) -> None:
self.assertEqual(_strip_git_suffix("repo.git"), "repo")
self.assertEqual(_strip_git_suffix("repo"), "repo")
def test_split_host_port(self) -> None:
self.assertEqual(_split_host_port("example.com"), ("example.com", None))
self.assertEqual(_split_host_port("example.com:2222"), ("example.com", "2222"))
self.assertEqual(_split_host_port("example.com:"), ("example.com", None))
def test_parse_identifier_plain(self) -> None:
parts = _parse_identifier("github.com/owner/repo")
self.assertIsInstance(parts, RepoParts)
self.assertEqual(parts.host, "github.com")
self.assertEqual(parts.port, None)
self.assertEqual(parts.owner, "owner")
self.assertEqual(parts.name, "repo")
def test_parse_identifier_with_port(self) -> None:
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")
self.assertEqual(parts.name, "repo")
def test_parse_git_url_scp_style(self) -> None:
parts = _parse_git_url("git@github.com:owner/repo.git")
self.assertEqual(parts.host, "github.com")
self.assertEqual(parts.port, None)
self.assertEqual(parts.owner, "owner")
self.assertEqual(parts.name, "repo")
def test_parse_git_url_https(self) -> None:
parts = _parse_git_url("https://github.com/owner/repo.git")
self.assertEqual(parts.host, "github.com")
self.assertEqual(parts.port, None)
self.assertEqual(parts.owner, "owner")
self.assertEqual(parts.name, "repo")
def test_parse_git_url_ssh_with_port(self) -> None:
parts = _parse_git_url("ssh://git@gitea.example.com:2222/org/repo.git")
self.assertEqual(parts.host, "gitea.example.com")
self.assertEqual(parts.port, "2222")
self.assertEqual(parts.owner, "org")
self.assertEqual(parts.name, "repo")
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,35 @@
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()