- git mv of all existing files into version-python/ (history preserved) - version-rust/: minimal cargo scaffold (edition 2024), no deps yet - root AGENTS.md documents the transition rules and artifact/schema contract - main is untouched; the python layout there stays as-is
109 lines
3.3 KiB
Python
109 lines
3.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Create or list repositories on the Gitea instance at git.dieterunterstrich.de.
|
|
|
|
Credentials are read from ~/.git-credentials (the line for that host); the
|
|
secret is never printed. Useful when you need a new repo without touching the
|
|
web UI.
|
|
|
|
Usage:
|
|
python3 tools/new_repo.py list
|
|
python3 tools/new_repo.py create <name> [--private] [--desc "..."]
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import base64
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
from urllib.request import Request, urlopen
|
|
|
|
HOST = "git.dieterunterstrich.de"
|
|
API = f"https://{HOST}/api/v1"
|
|
CREDENTIALS_FILE = Path.home() / ".git-credentials"
|
|
|
|
|
|
def load_credentials() -> tuple[str, str]:
|
|
"""Return (username, password) for the Gitea host from ~/.git-credentials."""
|
|
for line in CREDENTIALS_FILE.read_text().splitlines():
|
|
line = line.strip()
|
|
if not line.startswith("https://") or HOST not in line:
|
|
continue
|
|
# https://user:pass@host
|
|
rest = line.removeprefix("https://")
|
|
userinfo = rest.split("@", 1)[0]
|
|
user, _, password = userinfo.partition(":")
|
|
return user, password
|
|
raise SystemExit(f"No credentials for {HOST} in {CREDENTIALS_FILE}")
|
|
|
|
|
|
def api(method: str, path: str, body: dict | None = None) -> tuple[int, dict]:
|
|
"""Call the Gitea API; returns (status_code, parsed_json)."""
|
|
user, password = load_credentials()
|
|
auth = base64.b64encode(f"{user}:{password}".encode()).decode()
|
|
data = json.dumps(body).encode() if body is not None else None
|
|
req = Request(
|
|
f"{API}{path}",
|
|
data=data,
|
|
method=method,
|
|
headers={
|
|
"Authorization": f"Basic {auth}",
|
|
"Content-Type": "application/json",
|
|
},
|
|
)
|
|
try:
|
|
resp = urlopen(req, timeout=30)
|
|
return resp.status, json.loads(resp.read() or b"{}")
|
|
except Exception as exc: # noqa: BLE001
|
|
# HTTPError carries the response body, useful for API errors
|
|
code = getattr(exc, "code", None)
|
|
if code is not None:
|
|
try:
|
|
return code, json.loads(exc.read() or b"{}")
|
|
except Exception: # noqa: BLE001
|
|
return code, {"message": str(exc)}
|
|
raise
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description="Gitea repo helper")
|
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
|
|
sub.add_parser("list")
|
|
|
|
p_create = sub.add_parser("create")
|
|
p_create.add_argument("name")
|
|
p_create.add_argument("--desc", default=None)
|
|
p_create.add_argument("--public", action="store_true")
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.command == "list":
|
|
status, repos = api("GET", "/user/repos?limit=100")
|
|
print(f"HTTP {status}")
|
|
for repo in repos:
|
|
print(f" {repo['full_name']} (private={repo['private']})")
|
|
return
|
|
|
|
status, result = api(
|
|
"POST",
|
|
"/user/repos",
|
|
{
|
|
"name": args.name,
|
|
"description": args.desc or "",
|
|
"private": not args.public,
|
|
"auto_init": False,
|
|
"default_branch": "main",
|
|
},
|
|
)
|
|
if status in (201, 200):
|
|
print(f"Created: {result['full_name']} -> {result['clone_url']}")
|
|
else:
|
|
print(f"Failed (HTTP {status}): {result.get('message', result)}")
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|