#!/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 [--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()