From a5cad921ff5ef32d3abf958da7c05621e5de1cf0 Mon Sep 17 00:00:00 2001 From: h7x4 Date: Wed, 13 Nov 2024 16:34:30 +0100 Subject: [PATCH] home/git: add git-post-pr script --- home/programs/git/default.nix | 16 +++ home/programs/git/scripts/git-post-pr.py | 130 +++++++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 home/programs/git/scripts/git-post-pr.py diff --git a/home/programs/git/default.nix b/home/programs/git/default.nix index 61df1b9..6e8001e 100644 --- a/home/programs/git/default.nix +++ b/home/programs/git/default.nix @@ -54,6 +54,7 @@ in authors = "shortlog --summary --numbered --email"; si = "switch-interactive"; ff = "fixup-fixup"; + pp = "post-pr"; subs = "submodule update --init --recursive"; rebase-author = "rebase -i -x \"git commit --amend --reset-author -CHEAD\""; git = "!git"; @@ -284,6 +285,21 @@ in "SC2001" # (style): See if you can use ${variable//search/replace} instead. (sed invocation) ]; }) + ((pkgs.writers.writePython3Bin "git-post-pr" { + libraries = with pkgs.python3Packages; [ + tkinter + ]; + flakeIgnore = [ + "E501" # I like long lines grr + ]; + } (lib.fileContents ./scripts/git-post-pr.py)).overrideAttrs (_: { + postFixup = '' + wrapProgram $out/bin/git-post-pr \ + --prefix PATH : ${lib.makeBinPath [ + pkgs.github-cli + ]} + ''; + })) pkgs.git-absorb ]; diff --git a/home/programs/git/scripts/git-post-pr.py b/home/programs/git/scripts/git-post-pr.py new file mode 100644 index 0000000..45a48ed --- /dev/null +++ b/home/programs/git/scripts/git-post-pr.py @@ -0,0 +1,130 @@ +import argparse +import json +import subprocess +import tkinter + +# TODO: add support for gitea, and maybe other git hosting options. + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + prog="post-pr", + description="Post links to PRs", + ) + + parser.add_argument("-n", "--no-clipboard", action="store_true", help="do not copy the message to the clipboard") + + pr_id = parser.add_mutually_exclusive_group() + pr_id.add_argument("-c", "--current-branch", action="store_true", help="generate post for the PR for the current branch") + pr_id.add_argument("-l", "--latest", action="store_true", help="generate post for the latest PR for the current user") + pr_id.add_argument("pr_id", nargs="?", default=None, help="generate post for the PR with the given ID") + args = parser.parse_args() + + if not any([args.current_branch, args.latest, args.pr_id,]): + args.current_branch = True + + return args + + +def _gh(args: list[str]) -> str: + try: + return subprocess.check_output(["gh"] + args).decode("utf8") + except subprocess.CalledProcessError as e: + raise RuntimeError(f"GitHub CLI command failed: 'gh {' '.join(args)}'") from e + + +def _gh_retcode(args: list[str]) -> int: + return subprocess.run(["gh"] + args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode + + +def ensure_gh_installed(): + try: + if _gh_retcode(["--version"]) != 0: + raise RuntimeError("GitHub CLI (gh) is not installed, please install it") + except FileNotFoundError: + raise RuntimeError("GitHub CLI (gh) is not installed, please install it") + + +def ensure_gh_authenticated(): + if _gh_retcode(["auth", "status"]) != 0: + raise RuntimeError("Failed to authenticate with GitHub, please run 'gh auth login'") + + +GH_PR_JSON_FIELDS = ",".join([ + "additions", + "deletions", + "state", + "title", + "url", +]) + + +def fetch_pr_data(current_branch: bool, latest: bool, pr_id: str | None) -> dict[str, any]: + if pr_id: + pr_data = _gh(["pr", "view", pr_id, "--json", GH_PR_JSON_FIELDS]) + pr_data = json.loads(pr_data) + + elif latest: + pr_list = _gh(["pr", "list", "--author", "@me", "--limit", "1", "--json", GH_PR_JSON_FIELDS]) + pr_list = json.loads(pr_list) + + if len(pr_list) == 0: + raise RuntimeError("Failed to find PR, are you sure you have any open PRs?") + + pr_data = pr_list[0] + + elif current_branch: + pr_data = _gh(["pr", "view", "--json", GH_PR_JSON_FIELDS]) + pr_data = json.loads(pr_data) + + return pr_data + + +def format_message(pr_data: dict[str, any]) -> str: + additions = pr_data["additions"] + deletions = pr_data["deletions"] + + title = pr_data["title"] + pr_url = pr_data["url"] + pr_state = pr_data["state"] + + state_html = f"({pr_state.lower()}) " if pr_state != "OPEN" else "" + additions_html = f"+{additions}" if additions > 0 else str(additions) + deletions_html = f"-{deletions}" if deletions > 0 else str(deletions) + + return f"""{state_html}{pr_url} {title} [diff: {additions_html}/{deletions_html}]""" + + +def copy_to_clipboard(message: str): + r = tkinter.Tk() + r.withdraw() + r.clipboard_clear() + r.clipboard_append(message) + r.update() + r.destroy() + + +def main(): + args = parse_args() + + ensure_gh_installed() + ensure_gh_authenticated() + + pr_data = fetch_pr_data(args.current_branch, args.latest, args.pr_id) + message = format_message(pr_data) + + print("Message:\n") + print(f" {message}\n") + + if not args.no_clipboard: + copy_to_clipboard(message) + print("Copied to clipboard") + + +if __name__ == "__main__": + try: + main() + except Exception as e: + print(f"Error: {e}") + exit(1) +