gitea issue helper
This commit is contained in:
@@ -0,0 +1,17 @@
|
|||||||
|
# Sophia
|
||||||
|
|
||||||
|
## Dev helper (`bin/dev`)
|
||||||
|
|
||||||
|
`bin/dev` (on PATH via direnv as `dev`) wraps common project tasks. Run `dev bash-completions` to list commands.
|
||||||
|
|
||||||
|
### Creating issues
|
||||||
|
|
||||||
|
Use `dev issue` to create Gitea issues instead of calling the API or `tea` directly:
|
||||||
|
|
||||||
|
```
|
||||||
|
dev issue "<title>" [-b "<body>"] [-l <label>]... [-a <user>]... [--open]
|
||||||
|
```
|
||||||
|
|
||||||
|
- Labels are repo label names (`bug`, `enhancement`, `question`, ...).
|
||||||
|
- The Gitea API cannot add an issue to a project. After creating an issue, tell the user its URL so they can add it to the "sophia" project (issue sidebar > Projects), or pass `--open`.
|
||||||
|
- Only create issues when the user asks; issues are visible on the remote.
|
||||||
@@ -19,6 +19,7 @@ WAIT_FOR_PROCESS = "wait_for_process"
|
|||||||
|
|
||||||
class DevHelper:
|
class DevHelper:
|
||||||
SHADER_EXTENSIONS = (".vert", ".frag", ".comp", ".geom", ".tesc", ".tese")
|
SHADER_EXTENSIONS = (".vert", ".frag", ".comp", ".geom", ".tesc", ".tese")
|
||||||
|
PROJECT_NAME = "sophia"
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.last_exit_status = 0
|
self.last_exit_status = 0
|
||||||
@@ -61,6 +62,40 @@ class DevHelper:
|
|||||||
flag = "" if exists else "-b "
|
flag = "" if exists else "-b "
|
||||||
self._exec_in_root(f"git checkout {flag}{shlex.quote(branch_name)}", replace=True)
|
self._exec_in_root(f"git checkout {flag}{shlex.quote(branch_name)}", replace=True)
|
||||||
|
|
||||||
|
##
|
||||||
|
# Creates an issue on this project's Gitea repo.
|
||||||
|
# dev issue <title> [-b|--body <text>] [-l|--label <name>]... [-a|--assignee <user>]... [-o|--open]
|
||||||
|
# The body is read from stdin if --body is not given and stdin is piped.
|
||||||
|
# Gitea's API cannot add issues to a project (only the web UI can), so the
|
||||||
|
# issue URL is printed and --open opens it in the browser to do so.
|
||||||
|
def issue(self, *args, **kwargs):
|
||||||
|
title, body, labels, assignees, open_in_browser = self._parse_issue_args(args)
|
||||||
|
|
||||||
|
repo = self._origin_repo()
|
||||||
|
if repo is None:
|
||||||
|
raise RuntimeError("could not determine the Gitea repo from the origin remote")
|
||||||
|
|
||||||
|
payload = {"title": title, "body": body}
|
||||||
|
if labels:
|
||||||
|
label_ids = self._fetch_label_ids(repo)
|
||||||
|
unknown = [label for label in labels if label not in label_ids]
|
||||||
|
if unknown:
|
||||||
|
raise ValueError(f"Unknown label(s): {', '.join(unknown)} (available: {', '.join(sorted(label_ids))})")
|
||||||
|
payload["labels"] = [label_ids[label] for label in labels]
|
||||||
|
if assignees:
|
||||||
|
payload["assignees"] = assignees
|
||||||
|
|
||||||
|
issue = self._tea_api(f"repos/{repo}/issues", payload)
|
||||||
|
print(f"Created issue #{issue['number']}: {issue['title']}")
|
||||||
|
print(issue["html_url"])
|
||||||
|
print(f"Add it to the project from the issue sidebar: Projects > {self.PROJECT_NAME}")
|
||||||
|
|
||||||
|
if open_in_browser:
|
||||||
|
opener = shutil.which("xdg-open") or shutil.which("open")
|
||||||
|
if opener is None:
|
||||||
|
raise RuntimeError("xdg-open not found on PATH")
|
||||||
|
subprocess.Popen([opener, issue["html_url"]], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
|
||||||
def renderdoc(self, *args, **kwargs):
|
def renderdoc(self, *args, **kwargs):
|
||||||
qrenderdoc = self._find_qrenderdoc()
|
qrenderdoc = self._find_qrenderdoc()
|
||||||
self._exec_in_root(qrenderdoc, replace=True)
|
self._exec_in_root(qrenderdoc, replace=True)
|
||||||
@@ -126,6 +161,63 @@ class DevHelper:
|
|||||||
raise RuntimeError(f"failed to fetch issue title from {endpoint}")
|
raise RuntimeError(f"failed to fetch issue title from {endpoint}")
|
||||||
return json.loads(result.stdout)["title"]
|
return json.loads(result.stdout)["title"]
|
||||||
|
|
||||||
|
def _parse_issue_args(self, args):
|
||||||
|
title = None
|
||||||
|
body = None
|
||||||
|
labels = []
|
||||||
|
assignees = []
|
||||||
|
open_in_browser = False
|
||||||
|
|
||||||
|
args = list(args)
|
||||||
|
while args:
|
||||||
|
arg = args.pop(0)
|
||||||
|
if arg in ("-b", "--body", "-l", "--label", "-a", "--assignee"):
|
||||||
|
if not args:
|
||||||
|
raise ValueError(f"{arg} requires a value")
|
||||||
|
value = args.pop(0)
|
||||||
|
if arg in ("-b", "--body"):
|
||||||
|
body = value
|
||||||
|
elif arg in ("-l", "--label"):
|
||||||
|
labels.append(value)
|
||||||
|
else:
|
||||||
|
assignees.append(value)
|
||||||
|
elif arg in ("-o", "--open"):
|
||||||
|
open_in_browser = True
|
||||||
|
elif arg.startswith("-"):
|
||||||
|
raise ValueError(f"Unknown option: {arg}")
|
||||||
|
elif title is None:
|
||||||
|
title = arg
|
||||||
|
else:
|
||||||
|
title = f"{title} {arg}"
|
||||||
|
|
||||||
|
if not title:
|
||||||
|
raise ValueError("Must provide an issue title. Usage: dev issue <title> [-b <body>] [-l <label>]... [-a <user>]... [-o]")
|
||||||
|
if body is None:
|
||||||
|
body = "" if sys.stdin.isatty() else sys.stdin.read()
|
||||||
|
return title, body, labels, assignees, open_in_browser
|
||||||
|
|
||||||
|
def _tea_api(self, endpoint, payload=None):
|
||||||
|
if shutil.which("tea") is None:
|
||||||
|
raise RuntimeError("tea not found on PATH (install gitea tea and run `tea login add`)")
|
||||||
|
|
||||||
|
command = ["tea", "api", endpoint]
|
||||||
|
if payload is not None:
|
||||||
|
command += ["-d", "@-"]
|
||||||
|
print(f"Running: {' '.join(command)}")
|
||||||
|
result = subprocess.run(
|
||||||
|
command,
|
||||||
|
input=json.dumps(payload) if payload is not None else None,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError(f"tea api request failed: {endpoint}")
|
||||||
|
return json.loads(result.stdout)
|
||||||
|
|
||||||
|
def _fetch_label_ids(self, repo):
|
||||||
|
labels = self._tea_api(f"repos/{repo}/labels?limit=100")
|
||||||
|
return {label["name"]: label["id"] for label in labels}
|
||||||
|
|
||||||
def _format_branch_name(self, issue_repo, issue_number, issue_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())))
|
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():
|
if issue_repo == self._origin_repo():
|
||||||
|
|||||||
Reference in New Issue
Block a user