Files
pkgmgr/src/pkgmgr/actions/mirror/git_remote.py
T
Kevin Veen-Birkenbach 997c265cfb
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
refactor(git): introduce GitRunError hierarchy, surface non-repo errors, and improve verification queries
* Replace legacy GitError usage with a clearer exception hierarchy:

  * GitBaseError as the common root for all git-related failures
  * GitRunError for subprocess execution failures
  * GitQueryError for read-only query failures
  * GitCommandError for state-changing command failures
  * GitNotRepositoryError to explicitly signal “not a git repository” situations
* Update git runner to detect “not a git repository” stderr and raise GitNotRepositoryError with rich context (cwd, command, stderr)
* Refactor repository verification to use dedicated query helpers instead of ad-hoc subprocess calls:

  * get_remote_head_commit (ls-remote) for pull mode
  * get_head_commit for local mode
  * get_latest_signing_key (%GK) for signature verification
* Add strict vs best-effort behavior in verify_repository:

  * Best-effort collection for reporting (does not block when no verification config exists)
  * Strict retrieval and explicit error messages when verification is configured
  * Clear failure cases when commit/signing key cannot be determined
* Add new unit tests covering:

  * get_latest_signing_key output stripping and error wrapping
  * get_remote_head_commit parsing, empty output, and error wrapping
  * verify_repository success/failure scenarios and “do not swallow GitNotRepositoryError”
* Adjust imports and exception handling across actions/commands/queries to align with GitRunError-based handling while keeping GitNotRepositoryError uncaught for debugging clarity

https://chatgpt.com/share/6943173c-508c-800f-8879-af75d131c79b
2025-12-17 21:48:03 +01:00

167 lines
4.9 KiB
Python

from __future__ import annotations
import os
from typing import Optional, Set
from pkgmgr.core.git.errors import GitRunError
from pkgmgr.core.git.commands import (
GitAddRemoteError,
GitAddRemotePushUrlError,
GitSetRemoteUrlError,
add_remote,
add_remote_push_url,
set_remote_url,
)
from pkgmgr.core.git.queries import get_remote_push_urls, list_remotes
from .types import MirrorMap, RepoMirrorContext, Repository
def _is_git_remote_url(url: str) -> bool:
"""
True only for URLs that should become git remotes / push URLs.
Accepted:
- git@host:owner/repo(.git) (SCP-like SSH)
- ssh://git@host(:port)/owner/repo(.git) (SSH URL)
- https://host/owner/repo.git (HTTPS git remote)
- http://host/owner/repo.git (rare, but possible)
Everything else (e.g. PyPI project page) stays metadata only.
"""
u = (url or "").strip()
if not u:
return False
if u.startswith("git@"):
return True
if u.startswith("ssh://"):
return True
if (u.startswith("https://") or u.startswith("http://")) and u.endswith(".git"):
return True
return False
def build_default_ssh_url(repo: Repository) -> Optional[str]:
provider = repo.get("provider")
account = repo.get("account")
name = repo.get("repository")
port = repo.get("port")
if not provider or not account or not name:
return None
if port:
return f"ssh://git@{provider}:{port}/{account}/{name}.git"
return f"git@{provider}:{account}/{name}.git"
def _git_mirrors_only(m: MirrorMap) -> MirrorMap:
return {k: v for k, v in m.items() if v and _is_git_remote_url(v)}
def determine_primary_remote_url(
repo: Repository,
ctx: RepoMirrorContext,
) -> Optional[str]:
"""
Priority order (GIT URLS ONLY):
1. origin from resolved mirrors (if it is a git URL)
2. first git URL from MIRRORS file (in file order)
3. first git URL from config mirrors (in config order)
4. default SSH URL
"""
resolved = ctx.resolved_mirrors
origin = resolved.get("origin")
if origin and _is_git_remote_url(origin):
return origin
for mirrors in (ctx.file_mirrors, ctx.config_mirrors):
for _, url in mirrors.items():
if url and _is_git_remote_url(url):
return url
return build_default_ssh_url(repo)
def has_origin_remote(repo_dir: str) -> bool:
try:
return "origin" in list_remotes(cwd=repo_dir)
except GitRunError:
return False
def _set_origin_fetch_and_push(repo_dir: str, url: str, preview: bool) -> None:
"""
Ensure origin has fetch URL and push URL set to the primary URL.
Preview is handled by the underlying git runner.
"""
set_remote_url("origin", url, cwd=repo_dir, push=False, preview=preview)
set_remote_url("origin", url, cwd=repo_dir, push=True, preview=preview)
def _ensure_additional_push_urls(
repo_dir: str,
mirrors: MirrorMap,
primary: str,
preview: bool,
) -> None:
"""
Ensure all *git* mirror URLs (except primary) are configured as additional
push URLs for origin.
Non-git URLs (like PyPI) are ignored and will never land in git config.
"""
git_only = _git_mirrors_only(mirrors)
desired: Set[str] = {u for u in git_only.values() if u and u != primary}
if not desired:
return
try:
existing = get_remote_push_urls("origin", cwd=repo_dir)
except GitRunError:
existing = set()
for url in sorted(desired - existing):
add_remote_push_url("origin", url, cwd=repo_dir, preview=preview)
def ensure_origin_remote(
repo: Repository,
ctx: RepoMirrorContext,
preview: bool,
) -> None:
repo_dir = ctx.repo_dir
if not os.path.isdir(os.path.join(repo_dir, ".git")):
print(f"[WARN] {repo_dir} is not a Git repository.")
return
primary = determine_primary_remote_url(repo, ctx)
if not primary or not _is_git_remote_url(primary):
print("[WARN] No valid git primary mirror URL could be determined.")
return
# 1) Ensure origin exists
if not has_origin_remote(repo_dir):
try:
add_remote("origin", primary, cwd=repo_dir, preview=preview)
except GitAddRemoteError as exc:
print(f"[WARN] Failed to add origin remote: {exc}")
return # without origin we cannot reliably proceed
# 2) Ensure origin fetch+push URLs are correct
try:
_set_origin_fetch_and_push(repo_dir, primary, preview)
except GitSetRemoteUrlError as exc:
print(f"[WARN] Failed to set origin URLs: {exc}")
# 3) Ensure additional push URLs for mirrors (git urls only)
try:
_ensure_additional_push_urls(repo_dir, ctx.resolved_mirrors, primary, preview)
except GitAddRemotePushUrlError as exc:
print(f"[WARN] Failed to add additional push URLs: {exc}")