***feat(mirror): add remote repository visibility support***
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
* Add mirror visibility subcommand and provision --public flag * Implement core visibility API with provider support (GitHub, Gitea) * Extend provider interface and EnsureStatus * Add unit, integration and e2e tests for visibility handling https://chatgpt.com/share/6946a44e-4f48-800f-8124-9c0b9b2b6b04
This commit is contained in:
@@ -14,6 +14,7 @@ from .list_cmd import list_mirrors
|
||||
from .diff_cmd import diff_mirrors
|
||||
from .merge_cmd import merge_mirrors
|
||||
from .setup_cmd import setup_mirrors
|
||||
from .visibility_cmd import set_mirror_visibility
|
||||
|
||||
__all__ = [
|
||||
"Repository",
|
||||
@@ -22,4 +23,5 @@ __all__ = [
|
||||
"diff_mirrors",
|
||||
"merge_mirrors",
|
||||
"setup_mirrors",
|
||||
"set_mirror_visibility",
|
||||
]
|
||||
|
||||
@@ -3,11 +3,14 @@ from __future__ import annotations
|
||||
from typing import List
|
||||
|
||||
from pkgmgr.core.git.queries import probe_remote_reachable_detail
|
||||
from pkgmgr.core.remote_provisioning import ProviderHint, RepoSpec, set_repo_visibility
|
||||
from pkgmgr.core.remote_provisioning.visibility import VisibilityOptions
|
||||
|
||||
from .context import build_context
|
||||
from .git_remote import determine_primary_remote_url, ensure_origin_remote
|
||||
from .remote_provision import ensure_remote_repository_for_url
|
||||
from .types import Repository
|
||||
from .url_utils import normalize_provider_host, parse_repo_from_git_url
|
||||
|
||||
|
||||
def _is_git_remote_url(url: str) -> bool:
|
||||
@@ -25,6 +28,45 @@ def _is_git_remote_url(url: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _provider_hint_from_host(host: str) -> str | None:
|
||||
h = (host or "").lower()
|
||||
if h == "github.com":
|
||||
return "github"
|
||||
return "gitea" if h else None
|
||||
|
||||
|
||||
def _apply_visibility_for_url(
|
||||
*,
|
||||
url: str,
|
||||
private: bool,
|
||||
description: str,
|
||||
preview: bool,
|
||||
) -> None:
|
||||
host_raw, owner, name = parse_repo_from_git_url(url)
|
||||
host = normalize_provider_host(host_raw)
|
||||
|
||||
if not host or not owner or not name:
|
||||
print(f"[WARN] Could not parse repo from URL: {url}")
|
||||
return
|
||||
|
||||
spec = RepoSpec(
|
||||
host=host,
|
||||
owner=owner,
|
||||
name=name,
|
||||
private=private,
|
||||
description=description,
|
||||
)
|
||||
|
||||
provider_kind = _provider_hint_from_host(host)
|
||||
res = set_repo_visibility(
|
||||
spec,
|
||||
private=private,
|
||||
provider_hint=ProviderHint(kind=provider_kind),
|
||||
options=VisibilityOptions(preview=preview),
|
||||
)
|
||||
print(f"[REMOTE VISIBILITY] {res.status.upper()}: {res.message}")
|
||||
|
||||
|
||||
def _print_probe_result(name: str | None, url: str, *, cwd: str) -> None:
|
||||
"""
|
||||
Print probe result for a git remote URL, including a short failure reason.
|
||||
@@ -67,6 +109,7 @@ def _setup_remote_mirrors_for_repo(
|
||||
all_repos: List[Repository],
|
||||
preview: bool,
|
||||
ensure_remote: bool,
|
||||
ensure_visibility: str | None,
|
||||
) -> None:
|
||||
ctx = build_context(repo, repositories_base_dir, all_repos)
|
||||
|
||||
@@ -79,6 +122,22 @@ def _setup_remote_mirrors_for_repo(
|
||||
k: v for k, v in ctx.resolved_mirrors.items() if _is_git_remote_url(v)
|
||||
}
|
||||
|
||||
def _desired_private_default() -> bool:
|
||||
# default behavior: repo['private'] (or True)
|
||||
if ensure_visibility == "public":
|
||||
return False
|
||||
if ensure_visibility == "private":
|
||||
return True
|
||||
return bool(repo.get("private", True))
|
||||
|
||||
def _should_enforce_visibility() -> bool:
|
||||
return ensure_visibility in ("public", "private")
|
||||
|
||||
def _visibility_private_value() -> bool:
|
||||
return ensure_visibility == "private"
|
||||
|
||||
description = str(repo.get("description", ""))
|
||||
|
||||
# If there are no git mirrors, fall back to primary (git) URL.
|
||||
if not git_mirrors:
|
||||
primary = determine_primary_remote_url(repo, ctx)
|
||||
@@ -91,10 +150,18 @@ def _setup_remote_mirrors_for_repo(
|
||||
print(f"[REMOTE ENSURE] ensuring primary: {primary}")
|
||||
ensure_remote_repository_for_url(
|
||||
url=primary,
|
||||
private_default=bool(repo.get("private", True)),
|
||||
description=str(repo.get("description", "")),
|
||||
private_default=_desired_private_default(),
|
||||
description=description,
|
||||
preview=preview,
|
||||
)
|
||||
# IMPORTANT: enforce visibility only if requested
|
||||
if _should_enforce_visibility():
|
||||
_apply_visibility_for_url(
|
||||
url=primary,
|
||||
private=_visibility_private_value(),
|
||||
description=description,
|
||||
preview=preview,
|
||||
)
|
||||
print()
|
||||
|
||||
_print_probe_result(None, primary, cwd=ctx.repo_dir)
|
||||
@@ -107,10 +174,17 @@ def _setup_remote_mirrors_for_repo(
|
||||
print(f"[REMOTE ENSURE] ensuring mirror {name!r}: {url}")
|
||||
ensure_remote_repository_for_url(
|
||||
url=url,
|
||||
private_default=bool(repo.get("private", True)),
|
||||
description=str(repo.get("description", "")),
|
||||
private_default=_desired_private_default(),
|
||||
description=description,
|
||||
preview=preview,
|
||||
)
|
||||
if _should_enforce_visibility():
|
||||
_apply_visibility_for_url(
|
||||
url=url,
|
||||
private=_visibility_private_value(),
|
||||
description=description,
|
||||
preview=preview,
|
||||
)
|
||||
print()
|
||||
|
||||
# Probe ALL git mirrors
|
||||
@@ -128,6 +202,7 @@ def setup_mirrors(
|
||||
local: bool = True,
|
||||
remote: bool = True,
|
||||
ensure_remote: bool = False,
|
||||
ensure_visibility: str | None = None,
|
||||
) -> None:
|
||||
for repo in selected_repos:
|
||||
if local:
|
||||
@@ -145,4 +220,5 @@ def setup_mirrors(
|
||||
all_repos,
|
||||
preview,
|
||||
ensure_remote,
|
||||
ensure_visibility,
|
||||
)
|
||||
|
||||
134
src/pkgmgr/actions/mirror/visibility_cmd.py
Normal file
134
src/pkgmgr/actions/mirror/visibility_cmd.py
Normal file
@@ -0,0 +1,134 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List
|
||||
|
||||
from pkgmgr.core.remote_provisioning import ProviderHint, RepoSpec, set_repo_visibility
|
||||
from pkgmgr.core.remote_provisioning.visibility import VisibilityOptions
|
||||
|
||||
from .context import build_context
|
||||
from .git_remote import determine_primary_remote_url
|
||||
from .types import Repository
|
||||
from .url_utils import normalize_provider_host, parse_repo_from_git_url
|
||||
|
||||
|
||||
def _is_git_remote_url(url: str) -> bool:
|
||||
# Keep same semantics as setup_cmd.py / git_remote.py
|
||||
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 _provider_hint_from_host(host: str) -> str | None:
|
||||
h = (host or "").lower()
|
||||
if h == "github.com":
|
||||
return "github"
|
||||
# Best-effort default for self-hosted git domains
|
||||
return "gitea" if h else None
|
||||
|
||||
|
||||
def _apply_visibility_for_url(
|
||||
*,
|
||||
url: str,
|
||||
private: bool,
|
||||
description: str,
|
||||
preview: bool,
|
||||
) -> None:
|
||||
host_raw, owner, name = parse_repo_from_git_url(url)
|
||||
host = normalize_provider_host(host_raw)
|
||||
|
||||
if not host or not owner or not name:
|
||||
print(f"[WARN] Could not parse repo from URL: {url}")
|
||||
return
|
||||
|
||||
spec = RepoSpec(
|
||||
host=host,
|
||||
owner=owner,
|
||||
name=name,
|
||||
private=private,
|
||||
description=description,
|
||||
)
|
||||
|
||||
provider_kind = _provider_hint_from_host(host)
|
||||
res = set_repo_visibility(
|
||||
spec,
|
||||
private=private,
|
||||
provider_hint=ProviderHint(kind=provider_kind),
|
||||
options=VisibilityOptions(preview=preview),
|
||||
)
|
||||
print(f"[REMOTE VISIBILITY] {res.status.upper()}: {res.message}")
|
||||
|
||||
|
||||
def set_mirror_visibility(
|
||||
selected_repos: List[Repository],
|
||||
repositories_base_dir: str,
|
||||
all_repos: List[Repository],
|
||||
*,
|
||||
visibility: str,
|
||||
preview: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
Set remote repository visibility for all git mirrors of each selected repo.
|
||||
|
||||
visibility:
|
||||
- "private"
|
||||
- "public"
|
||||
"""
|
||||
v = (visibility or "").strip().lower()
|
||||
if v not in ("private", "public"):
|
||||
raise ValueError("visibility must be 'private' or 'public'")
|
||||
|
||||
desired_private = v == "private"
|
||||
|
||||
for repo in selected_repos:
|
||||
ctx = build_context(repo, repositories_base_dir, all_repos)
|
||||
|
||||
print("------------------------------------------------------------")
|
||||
print(f"[MIRROR VISIBILITY] {ctx.identifier}")
|
||||
print(f"[MIRROR VISIBILITY] dir: {ctx.repo_dir}")
|
||||
print(f"[MIRROR VISIBILITY] target: {v}")
|
||||
print("------------------------------------------------------------")
|
||||
|
||||
git_mirrors = {
|
||||
name: url
|
||||
for name, url in ctx.resolved_mirrors.items()
|
||||
if url and _is_git_remote_url(url)
|
||||
}
|
||||
|
||||
# If there are no git mirrors, fall back to primary (git) URL.
|
||||
if not git_mirrors:
|
||||
primary = determine_primary_remote_url(repo, ctx)
|
||||
if not primary or not _is_git_remote_url(primary):
|
||||
print(
|
||||
"[INFO] No git mirrors found (and no primary git URL). Nothing to do."
|
||||
)
|
||||
print()
|
||||
continue
|
||||
|
||||
print(f"[MIRROR VISIBILITY] applying to primary: {primary}")
|
||||
_apply_visibility_for_url(
|
||||
url=primary,
|
||||
private=desired_private,
|
||||
description=str(repo.get("description", "")),
|
||||
preview=preview,
|
||||
)
|
||||
print()
|
||||
continue
|
||||
|
||||
# Apply to ALL git mirrors
|
||||
for name, url in git_mirrors.items():
|
||||
print(f"[MIRROR VISIBILITY] applying to mirror {name!r}: {url}")
|
||||
_apply_visibility_for_url(
|
||||
url=url,
|
||||
private=desired_private,
|
||||
description=str(repo.get("description", "")),
|
||||
preview=preview,
|
||||
)
|
||||
|
||||
print()
|
||||
@@ -8,6 +8,7 @@ from pkgmgr.actions.mirror import (
|
||||
diff_mirrors,
|
||||
list_mirrors,
|
||||
merge_mirrors,
|
||||
set_mirror_visibility,
|
||||
setup_mirrors,
|
||||
)
|
||||
from pkgmgr.cli.context import CLIContext
|
||||
@@ -30,6 +31,7 @@ def handle_mirror_command(
|
||||
- mirror setup
|
||||
- mirror check
|
||||
- mirror provision
|
||||
- mirror visibility
|
||||
"""
|
||||
if not selected:
|
||||
print("[INFO] No repositories selected for 'mirror' command.")
|
||||
@@ -92,6 +94,7 @@ def handle_mirror_command(
|
||||
local=True,
|
||||
remote=False,
|
||||
ensure_remote=False,
|
||||
ensure_visibility=None,
|
||||
)
|
||||
return
|
||||
|
||||
@@ -105,11 +108,14 @@ def handle_mirror_command(
|
||||
local=False,
|
||||
remote=True,
|
||||
ensure_remote=False,
|
||||
ensure_visibility=None,
|
||||
)
|
||||
return
|
||||
|
||||
if subcommand == "provision":
|
||||
preview = getattr(args, "preview", False)
|
||||
public = bool(getattr(args, "public", False))
|
||||
|
||||
setup_mirrors(
|
||||
selected_repos=selected,
|
||||
repositories_base_dir=ctx.repositories_base_dir,
|
||||
@@ -118,6 +124,23 @@ def handle_mirror_command(
|
||||
local=False,
|
||||
remote=True,
|
||||
ensure_remote=True,
|
||||
ensure_visibility="public" if public else None,
|
||||
)
|
||||
return
|
||||
|
||||
if subcommand == "visibility":
|
||||
preview = getattr(args, "preview", False)
|
||||
visibility = getattr(args, "visibility", None)
|
||||
if visibility not in ("private", "public"):
|
||||
print("[ERROR] mirror visibility expects 'private' or 'public'.")
|
||||
sys.exit(2)
|
||||
|
||||
set_mirror_visibility(
|
||||
selected_repos=selected,
|
||||
repositories_base_dir=ctx.repositories_base_dir,
|
||||
all_repos=ctx.all_repositories,
|
||||
visibility=visibility,
|
||||
preview=preview,
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# src/pkgmgr/cli/parser/mirror_cmd.py
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
@@ -12,7 +11,7 @@ from .common import add_identifier_arguments
|
||||
def add_mirror_subparsers(subparsers: argparse._SubParsersAction) -> None:
|
||||
mirror_parser = subparsers.add_parser(
|
||||
"mirror",
|
||||
help="Mirror-related utilities (list, diff, merge, setup, check, provision)",
|
||||
help="Mirror-related utilities (list, diff, merge, setup, check, provision, visibility)",
|
||||
)
|
||||
mirror_subparsers = mirror_parser.add_subparsers(
|
||||
dest="subcommand",
|
||||
@@ -68,4 +67,20 @@ def add_mirror_subparsers(subparsers: argparse._SubParsersAction) -> None:
|
||||
"provision",
|
||||
help="Provision remote repositories via provider APIs (create missing repos).",
|
||||
)
|
||||
mirror_provision.add_argument(
|
||||
"--public",
|
||||
action="store_true",
|
||||
help="After ensuring repos exist, enforce public visibility on the remote provider.",
|
||||
)
|
||||
add_identifier_arguments(mirror_provision)
|
||||
|
||||
mirror_visibility = mirror_subparsers.add_parser(
|
||||
"visibility",
|
||||
help="Set visibility (public/private) for all remote git mirrors via provider APIs.",
|
||||
)
|
||||
mirror_visibility.add_argument(
|
||||
"visibility",
|
||||
choices=["private", "public"],
|
||||
help="Target visibility for all git mirrors.",
|
||||
)
|
||||
add_identifier_arguments(mirror_visibility)
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
# src/pkgmgr/core/remote_provisioning/__init__.py
|
||||
"""Remote repository provisioning (ensure remote repo exists)."""
|
||||
|
||||
from .ensure import ensure_remote_repo
|
||||
from .registry import ProviderRegistry
|
||||
from .types import EnsureResult, ProviderHint, RepoSpec
|
||||
from .visibility import set_repo_visibility
|
||||
|
||||
__all__ = [
|
||||
"ensure_remote_repo",
|
||||
"set_repo_visibility",
|
||||
"RepoSpec",
|
||||
"EnsureResult",
|
||||
"ProviderHint",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# src/pkgmgr/core/remote_provisioning/providers/base.py
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
@@ -23,7 +22,26 @@ class RemoteProvider(ABC):
|
||||
def create_repo(self, token: str, spec: RepoSpec) -> EnsureResult:
|
||||
"""Create a repository (owner may be user or org)."""
|
||||
|
||||
@abstractmethod
|
||||
def get_repo_private(self, token: str, spec: RepoSpec) -> bool | None:
|
||||
"""
|
||||
Return current repo privacy, or None if repo not found / inaccessible.
|
||||
|
||||
IMPORTANT:
|
||||
- Must NOT create repositories.
|
||||
- Should return None on 404 (not found) or when the repo cannot be accessed.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def set_repo_private(self, token: str, spec: RepoSpec, *, private: bool) -> None:
|
||||
"""
|
||||
Update repo privacy (PATCH). Must NOT create repositories.
|
||||
|
||||
Implementations should raise HttpError on API failure.
|
||||
"""
|
||||
|
||||
def ensure_repo(self, token: str, spec: RepoSpec) -> EnsureResult:
|
||||
"""Ensure repository exists (create if missing)."""
|
||||
if self.repo_exists(token, spec):
|
||||
return EnsureResult(status="exists", message="Repository exists.")
|
||||
return self.create_repo(token, spec)
|
||||
|
||||
@@ -52,6 +52,39 @@ class GiteaProvider(RemoteProvider):
|
||||
return False
|
||||
raise
|
||||
|
||||
def get_repo_private(self, token: str, spec: RepoSpec) -> bool | None:
|
||||
base = self._api_base(spec.host)
|
||||
url = f"{base}/api/v1/repos/{spec.owner}/{spec.name}"
|
||||
try:
|
||||
resp = self._http.request_json("GET", url, headers=self._headers(token))
|
||||
except HttpError as exc:
|
||||
if exc.status == 404:
|
||||
return None
|
||||
raise
|
||||
|
||||
if not (200 <= resp.status < 300):
|
||||
return None
|
||||
data = resp.json or {}
|
||||
return bool(data.get("private", False))
|
||||
|
||||
def set_repo_private(self, token: str, spec: RepoSpec, *, private: bool) -> None:
|
||||
base = self._api_base(spec.host)
|
||||
url = f"{base}/api/v1/repos/{spec.owner}/{spec.name}"
|
||||
payload: Dict[str, Any] = {"private": bool(private)}
|
||||
|
||||
resp = self._http.request_json(
|
||||
"PATCH",
|
||||
url,
|
||||
headers=self._headers(token),
|
||||
payload=payload,
|
||||
)
|
||||
if not (200 <= resp.status < 300):
|
||||
raise HttpError(
|
||||
status=resp.status,
|
||||
message="Failed to update repository.",
|
||||
body=resp.text,
|
||||
)
|
||||
|
||||
def create_repo(self, token: str, spec: RepoSpec) -> EnsureResult:
|
||||
base = self._api_base(spec.host)
|
||||
|
||||
|
||||
@@ -54,6 +54,39 @@ class GitHubProvider(RemoteProvider):
|
||||
return False
|
||||
raise
|
||||
|
||||
def get_repo_private(self, token: str, spec: RepoSpec) -> bool | None:
|
||||
api = self._api_base(spec.host)
|
||||
url = f"{api}/repos/{spec.owner}/{spec.name}"
|
||||
try:
|
||||
resp = self._http.request_json("GET", url, headers=self._headers(token))
|
||||
except HttpError as exc:
|
||||
if exc.status == 404:
|
||||
return None
|
||||
raise
|
||||
|
||||
if not (200 <= resp.status < 300):
|
||||
return None
|
||||
data = resp.json or {}
|
||||
return bool(data.get("private", False))
|
||||
|
||||
def set_repo_private(self, token: str, spec: RepoSpec, *, private: bool) -> None:
|
||||
api = self._api_base(spec.host)
|
||||
url = f"{api}/repos/{spec.owner}/{spec.name}"
|
||||
payload: Dict[str, Any] = {"private": bool(private)}
|
||||
|
||||
resp = self._http.request_json(
|
||||
"PATCH",
|
||||
url,
|
||||
headers=self._headers(token),
|
||||
payload=payload,
|
||||
)
|
||||
if not (200 <= resp.status < 300):
|
||||
raise HttpError(
|
||||
status=resp.status,
|
||||
message="Failed to update repository.",
|
||||
body=resp.text,
|
||||
)
|
||||
|
||||
def create_repo(self, token: str, spec: RepoSpec) -> EnsureResult:
|
||||
api = self._api_base(spec.host)
|
||||
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
# src/pkgmgr/core/remote_provisioning/types.py
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Literal, Optional
|
||||
|
||||
EnsureStatus = Literal["exists", "created", "skipped", "failed"]
|
||||
EnsureStatus = Literal[
|
||||
"exists",
|
||||
"created",
|
||||
"updated",
|
||||
"noop",
|
||||
"notfound",
|
||||
"skipped",
|
||||
"failed",
|
||||
]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
||||
118
src/pkgmgr/core/remote_provisioning/visibility.py
Normal file
118
src/pkgmgr/core/remote_provisioning/visibility.py
Normal file
@@ -0,0 +1,118 @@
|
||||
# src/pkgmgr/core/remote_provisioning/visibility.py
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from pkgmgr.core.credentials.resolver import ResolutionOptions, TokenResolver
|
||||
|
||||
from .http.errors import HttpError
|
||||
from .registry import ProviderRegistry
|
||||
from .types import (
|
||||
AuthError,
|
||||
EnsureResult,
|
||||
NetworkError,
|
||||
PermissionError,
|
||||
ProviderHint,
|
||||
RepoSpec,
|
||||
UnsupportedProviderError,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class VisibilityOptions:
|
||||
"""Options controlling remote visibility updates."""
|
||||
|
||||
preview: bool = False
|
||||
interactive: bool = True
|
||||
allow_prompt: bool = True
|
||||
save_prompt_token_to_keyring: bool = True
|
||||
|
||||
|
||||
def _raise_mapped_http_error(exc: HttpError, host: str) -> None:
|
||||
"""Map HttpError into domain-specific error types."""
|
||||
if exc.status == 0:
|
||||
raise NetworkError(f"Network error while talking to {host}: {exc}") from exc
|
||||
if exc.status == 401:
|
||||
raise AuthError(f"Authentication failed for {host} (401).") from exc
|
||||
if exc.status == 403:
|
||||
raise PermissionError(f"Permission denied for {host} (403).") from exc
|
||||
|
||||
raise NetworkError(
|
||||
f"HTTP error from {host}: status={exc.status}, message={exc}, body={exc.body}"
|
||||
) from exc
|
||||
|
||||
|
||||
def set_repo_visibility(
|
||||
spec: RepoSpec,
|
||||
*,
|
||||
private: bool,
|
||||
provider_hint: Optional[ProviderHint] = None,
|
||||
options: Optional[VisibilityOptions] = None,
|
||||
registry: Optional[ProviderRegistry] = None,
|
||||
token_resolver: Optional[TokenResolver] = None,
|
||||
) -> EnsureResult:
|
||||
"""
|
||||
Set repository visibility (public/private) WITHOUT creating repositories.
|
||||
|
||||
Behavior:
|
||||
- If repo does not exist -> status=notfound
|
||||
- If already desired -> status=noop
|
||||
- If changed -> status=updated
|
||||
- Respects preview mode -> status=skipped
|
||||
- Maps HTTP errors to domain-specific errors
|
||||
"""
|
||||
opts = options or VisibilityOptions()
|
||||
reg = registry or ProviderRegistry.default()
|
||||
resolver = token_resolver or TokenResolver()
|
||||
|
||||
provider = reg.resolve(spec.host)
|
||||
if provider_hint and provider_hint.kind:
|
||||
forced = provider_hint.kind.strip().lower()
|
||||
forced_provider = next(
|
||||
(p for p in reg.providers if getattr(p, "kind", "").lower() == forced),
|
||||
None,
|
||||
)
|
||||
if forced_provider is not None:
|
||||
provider = forced_provider
|
||||
|
||||
if provider is None:
|
||||
raise UnsupportedProviderError(f"No provider matched host: {spec.host}")
|
||||
|
||||
token_opts = ResolutionOptions(
|
||||
interactive=opts.interactive,
|
||||
allow_prompt=opts.allow_prompt,
|
||||
save_prompt_token_to_keyring=opts.save_prompt_token_to_keyring,
|
||||
)
|
||||
token = resolver.get_token(
|
||||
provider_kind=getattr(provider, "kind", "unknown"),
|
||||
host=spec.host,
|
||||
owner=spec.owner,
|
||||
options=token_opts,
|
||||
)
|
||||
|
||||
if opts.preview:
|
||||
return EnsureResult(
|
||||
status="skipped",
|
||||
message="Preview mode: no remote changes performed.",
|
||||
)
|
||||
|
||||
try:
|
||||
current_private = provider.get_repo_private(token.token, spec)
|
||||
if current_private is None:
|
||||
return EnsureResult(status="notfound", message="Repository not found.")
|
||||
|
||||
if bool(current_private) == bool(private):
|
||||
return EnsureResult(
|
||||
status="noop",
|
||||
message=f"Repository already {'private' if private else 'public'}.",
|
||||
)
|
||||
|
||||
provider.set_repo_private(token.token, spec, private=private)
|
||||
return EnsureResult(
|
||||
status="updated",
|
||||
message=f"Visibility updated to {'private' if private else 'public'}.",
|
||||
)
|
||||
except HttpError as exc:
|
||||
_raise_mapped_http_error(exc, host=spec.host)
|
||||
return EnsureResult(status="failed", message="Unreachable error mapping.")
|
||||
Reference in New Issue
Block a user