#!/usr/bin/env python3 """Create a Gitea pull request. Parameterized over title/body/head/base and the target Gitea instance. Two instances are known out of the box: dadeschools -> gitea.dadeschools.net / Contractor / Timesheet prgs -> gitea.prgs.cc / Scaled-Tech-Consulting / Timesheet Auth is pulled from the macOS keychain via `git credential fill` for the chosen host -- no tokens on the command line. Examples: create_pr.py --remote dadeschools \\ --title "Open generated PDF after creation (#7)" \\ --head feat/7-open-pdf-2234ddf5 --base feat/4-5-24-validations \\ --body "Closes #7" create_pr.py --remote prgs --title "Fix X" --head fix/x --body-file body.md # override any field of a known remote, or point at an arbitrary repo: create_pr.py --host gitea.example.com --org Foo --repo Bar \\ --title "..." --head topic """ import sys import json import base64 import argparse import subprocess import urllib.request import urllib.error REMOTES = { "dadeschools": {"host": "gitea.dadeschools.net", "org": "Contractor", "repo": "Timesheet"}, "prgs": {"host": "gitea.prgs.cc", "org": "Scaled-Tech-Consulting", "repo": "Timesheet"}, } def get_credentials(host): """Return (user, password) for `host` via `git credential fill`.""" p = subprocess.Popen( ["git", "credential", "fill"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True, ) out, _ = p.communicate(f"protocol=https\nhost={host}\n\n") user = password = "" for line in out.splitlines(): # split(maxsplit=1): tokens/passwords can themselves contain '='. if line.startswith("username="): user = line.split("=", 1)[1] elif line.startswith("password="): password = line.split("=", 1)[1] return user, password def main(argv=None): parser = argparse.ArgumentParser(description="Create a Gitea pull request.") parser.add_argument("--remote", choices=sorted(REMOTES), default="dadeschools", help="Known Gitea instance (default: dadeschools).") parser.add_argument("--host", help="Override the Gitea host.") parser.add_argument("--org", help="Override the owner/org.") parser.add_argument("--repo", help="Override the repository.") parser.add_argument("--title", required=True, help="PR title.") parser.add_argument("--head", required=True, help="Source branch.") parser.add_argument("--base", default="main", help="Target branch (default: main).") parser.add_argument("--body", default="", help="PR body text.") parser.add_argument("--body-file", help="Read PR body from this file ('-' for stdin).") args = parser.parse_args(argv) profile = REMOTES[args.remote] host = args.host or profile["host"] org = args.org or profile["org"] repo = args.repo or profile["repo"] body = args.body if args.body_file: if args.body_file == "-": body = sys.stdin.read() else: with open(args.body_file, "r", encoding="utf-8") as fh: body = fh.read() user, password = get_credentials(host) if not user or not password: print(f"Could not get credentials for {host} " f"(no keychain entry? try a manual `git credential fill`).", file=sys.stderr) return 1 url = f"https://{host}/api/v1/repos/{org}/{repo}/pulls" payload = {"title": args.title, "body": body, "head": args.head, "base": args.base} req = urllib.request.Request( url, data=json.dumps(payload).encode("utf-8"), headers={"Content-Type": "application/json"}, ) auth_b64 = base64.b64encode(f"{user}:{password}".encode("utf-8")).decode("utf-8") req.add_header("Authorization", f"Basic {auth_b64}") try: with urllib.request.urlopen(req) as response: data = json.load(response) print(f"PR #{data.get('number')}: {data.get('html_url')}") return 0 except urllib.error.HTTPError as e: print(f"Error {e.code}: {e.read().decode()}", file=sys.stderr) return 1 if __name__ == "__main__": sys.exit(main())