diff --git a/bin/dev b/bin/dev index 5fd67aa..b1dd9b3 100755 --- a/bin/dev +++ b/bin/dev @@ -1,13 +1,16 @@ #!/usr/bin/env python3 +import json import os import re +import shlex import shutil import struct import subprocess import sys COMMAND_TO_METHOD = { + "branch-from": "branch_from", "bash-completions": "bash_completions", } METHOD_TO_COMMAND = {method: command for command, method in COMMAND_TO_METHOD.items()} @@ -36,6 +39,28 @@ class DevHelper: command = f"rm -rf build && cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug" self._exec_in_root(command, replace=True) + ## + # Checks out a branch named after a Gitea issue URL. + # If the issue is from this project's repo, the branch name will be: + # issue-/ + # If the issue is from another repo (presumably upstream), the branch name will be: + # -issue-/ + def branch_from(self, *args, **kwargs): + issue_url = args[0] if args else None + if issue_url is None: + raise ValueError("Must provide a Gitea issue URL.") + + base_url, issue_repo, issue_number = self._parse_issue_url(issue_url) + issue_title = self._fetch_issue_title(base_url, issue_repo, issue_number) + branch_name = self._format_branch_name(issue_repo, issue_number, issue_title) + + exists = subprocess.run( + ["git", "-C", self._project_root(), "rev-parse", "--verify", "--quiet", f"refs/heads/{branch_name}"], + stdout=subprocess.DEVNULL, + ).returncode == 0 + flag = "" if exists else "-b " + self._exec_in_root(f"git checkout {flag}{shlex.quote(branch_name)}", replace=True) + def renderdoc(self, *args, **kwargs): qrenderdoc = self._find_qrenderdoc() self._exec_in_root(qrenderdoc, replace=True) @@ -84,6 +109,39 @@ class DevHelper: print(" ".join(completions)) # Private helpers + def _parse_issue_url(self, url): + match = re.match(r"^(https?://[^/]+)/([^/]+)/([^/]+)/issues/(\d+)", url) + if match is None: + raise ValueError(f"Not a Gitea issue URL: {url}") + return match[1], f"{match[2]}/{match[3]}", match[4] + + def _fetch_issue_title(self, base_url, repo, issue_number): + if shutil.which("tea") is None: + raise RuntimeError("tea not found on PATH (install gitea tea and run `tea login add`)") + + endpoint = f"{base_url}/api/v1/repos/{repo}/issues/{issue_number}" + print(f"Running: tea api {endpoint}") + result = subprocess.run(["tea", "api", endpoint], stdout=subprocess.PIPE, text=True) + if result.returncode != 0: + raise RuntimeError(f"failed to fetch issue title from {endpoint}") + return json.loads(result.stdout)["title"] + + def _format_branch_name(self, issue_repo, issue_number, issue_title): + formatted_title = re.sub(r"-+", "-", re.sub(r"[^a-z0-9\-]", "", re.sub(r"\s+", "-", issue_title.lower().strip()))) + if issue_repo == self._origin_repo(): + return f"issue-{issue_number}/{formatted_title}" + issue_owner = issue_repo.split("/")[0] + return f"{issue_owner}-issue-{issue_number}/{formatted_title}" + + def _origin_repo(self): + result = subprocess.run( + ["git", "-C", self._project_root(), "remote", "get-url", "origin"], + stdout=subprocess.PIPE, + text=True, + ) + match = re.search(r"[:/]([^/:]+/[^/]+?)(?:\.git)?$", result.stdout.strip()) + return match[1] if match else None + def _find_glslc(self): glslc = shutil.which("glslc") if glslc is None: