From af67e7452f47a6dd444bd699e347a2f3a2d8703f Mon Sep 17 00:00:00 2001 From: Jon Meow <46229924+jonmeow@users.noreply.github.com> Date: Fri, 10 Jul 2020 13:37:46 -0700 Subject: [PATCH] Creates a convenience script for making new proposals. (#96) Co-authored-by: Richard Smith --- proposals/template.md | 2 + src/scripts/new-proposal.py | 159 ++++++++++++++++++++++++++++++++++++ 2 files changed, 161 insertions(+) create mode 100755 src/scripts/new-proposal.py diff --git a/proposals/template.md b/proposals/template.md index 0de0e64a2522..edc51d50b99b 100644 --- a/proposals/template.md +++ b/proposals/template.md @@ -23,6 +23,8 @@ SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception ## TODO: Initial proposal setup +> TIP: Run `../src/scripts/new-proposal.py "TITLE"` to do new proposal setup. + 1. Copy this template to `new.md`, and create a commit. 2. Create a GitHub pull request, to get a pull request number. - Add the `proposal` and `WIP` labels to the pull request. diff --git a/src/scripts/new-proposal.py b/src/scripts/new-proposal.py new file mode 100755 index 000000000000..d0ef228dde51 --- /dev/null +++ b/src/scripts/new-proposal.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 + +"""Prepares a new proposal file and PR.""" + +__copyright__ = """ +Part of the Carbon Language project, under the Apache License v2.0 with LLVM +Exceptions. See /LICENSE for license information. +SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +""" + +import os +import re +import shlex +import shutil +import subprocess +import sys + +_USAGE = """Usage: + ./new-proposal.py [<branch>] + +Generates a branch and PR for a new proposal with the specified title. +""" + +_PROMPT = """This will: + - Create and switch to a new branch named '%s'. + - Create a new proposal titled '%s'. + - Create a PR for the proposal. + +Continue? (Y/n) """ + + +def _FillTemplate(template_path, title, pr_num): + """Fills out template TODO fields.""" + with open(template_path) as template_file: + content = template_file.read() + content = re.sub(r"^# TODO\n", "# %s\n" % title, content) + content = re.sub( + r"(https://github.com/[^/]+/[^/]+/pull/)####", + r"\g<1>%d" % pr_num, + content, + ) + content = re.sub(r"## TODO(?:.|\n)*(## Problem)", r"\1", content) + return content + + +def _Run(argv, check=True): + """Runs a command.""" + cmd = " ".join([shlex.quote(x) for x in argv]) + print("\n+ RUNNING: %s" % cmd, file=sys.stderr) + p = subprocess.run(argv) + if check and p.returncode != 0: + sys.exit("ERROR: Command failed: %s" % cmd) + + +def _RunPRCreate(argv): + """Runs a command and returns the PR#.""" + cmd = " ".join([shlex.quote(x) for x in argv]) + print("\n+ RUNNING: %s" % cmd, file=sys.stderr) + p = subprocess.Popen(argv, stdout=subprocess.PIPE) + out, _ = p.communicate() + out = out.decode("utf-8") + print(out, end="") + if p.returncode != 0: + sys.exit("ERROR: Command failed: %s" % cmd) + match = re.search( + r"^https://github.com/[^/]+/[^/]+/pull/(\d+)$", out, re.MULTILINE + ) + if not match: + sys.exit("ERROR: Failed to find PR# in output.") + return int(match[1]) + + +if __name__ == "__main__": + # Require an argument. + if len(sys.argv) not in (2, 3): + sys.exit(_USAGE) + title = sys.argv[1] + branch = None + if len(sys.argv) == 3: + branch = sys.argv[2] + + # Verify git and gh are available. + git_bin = shutil.which("git") + if not git_bin: + sys.exit("ERROR: Missing `git` CLI.") + gh_bin = shutil.which("gh") + if not gh_bin: + sys.exit("ERROR: Missing `gh` CLI.") + precommit_bin = shutil.which("pre-commit") + if not precommit_bin: + sys.exit("ERROR: Missing `pre-commit` CLI.") + + # Ensure a good working directory. + proposals_dir = os.path.realpath( + os.path.join(os.path.dirname(__file__), "../../proposals") + ) + os.chdir(proposals_dir) + + # Verify there are no uncommitted changes. + p = subprocess.run([git_bin, "diff-index", "--quiet", "HEAD", "--"]) + if p.returncode != 0: + sys.exit("ERROR: There are uncommitted changes in your git repo.") + + # Only use the first 20 chars of the title for branch names. + if not branch: + branch = "proposal-%s" % (title.lower().replace(" ", "-")[0:20]) + + # Prompt before proceeding. + response = "?" + while response not in ("y", "n", ""): + response = input(_PROMPT % (branch, title)).lower() + if response == "n": + sys.exit("ERROR: Cancelled") + + # Create a proposal branch. + _Run([git_bin, "checkout", "-b", branch, "trunk"]) + _Run([git_bin, "push", "-u", "origin", branch]) + + # Copy template.md to a temp file. + template_path = os.path.join(proposals_dir, "template.md") + temp_path = os.path.join(proposals_dir, "new-proposal.tmp") + shutil.copyfile(template_path, temp_path) + _Run([git_bin, "add", temp_path]) + _Run([git_bin, "commit", "-m", "Creating new proposal: %s" % title]) + + # Create a PR with WIP+proposal labels. + _Run([git_bin, "push"]) + pr_num = _RunPRCreate( + [ + gh_bin, + "pr", + "create", + "--label", + "WIP,proposal", + "--title", + title, + "--body", + "", + ] + ) + + # Remove the temp file, create p####.md, and fill in PR information. + os.remove(temp_path) + final_path = os.path.join(proposals_dir, "p%04d.md" % pr_num) + content = _FillTemplate(template_path, title, pr_num) + with open(final_path, "w") as final_file: + final_file.write(content) + _Run([git_bin, "add", temp_path, final_path]) + _Run([precommit_bin, "run"], check=False) # Needs a ToC update. + _Run([git_bin, "add", final_path, os.path.join(proposals_dir, "README.md")]) + _Run([git_bin, "commit", "-m", "Filling out template with PR %d" % pr_num]) + + # Push the PR update. + _Run([git_bin, "push"]) + + print( + "\nCreated PR %d for %s. Make changes to:\n %s" + % (pr_num, title, final_path) + )