/
githubmirror
/
carbon-lang
Обзор
Документация
Войти
/
githubmirror
/
carbon-lang
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
trunk
proposals/scripts/new_proposal.py
291 строка
8 KB
David Blaikie
new_proposal.py fix for rumdl missing anchor from dangling ToC TODO entry (#7540)
21 июл 2026, 02:01
Не верифицирован
21 июл 2026, 02:01
973e499
Код
Авторство
О чём код?
#!/usr/bin/env -S uv run --script # /// script # requires-python = ">=3.12" # /// """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 argparse import os import re import shlex import shutil import subprocess import sys from pathlib import Path from typing import List, Optional # Add repo root to sys.path to allow importing from proposals.scripts sys.path.append(str(Path(__file__).resolve().parents[2])) from proposals.scripts.utils import slugify # noqa: E402 _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) """ _LINK_TEMPLATE = """Proposal links (add links as proposal evolves): - Evolution links: - [Proposal PR](https://github.com/carbon-language/carbon-lang/pull/%s) - `[RFC topic](TODO)` - `[Decision topic](TODO)` - `[Decision PR](TODO)` - `[Announcement](TODO)` - Related links (optional): - `[Idea topic](TODO)` - `[TODO](TODO)` """ def _parse_args(args: Optional[List[str]] = None) -> argparse.Namespace: """Parses command-line arguments and flags.""" parser = argparse.ArgumentParser( description="Generates a branch and PR for a new proposal with the " "specified title." ) parser.add_argument( "title", metavar="TITLE", help="The title of the proposal.", ) parser.add_argument( "--branch", metavar="BRANCH", help="The name of the branch. Automatically generated from the title " "by default.", ) parser.add_argument( "--remote", metavar="REMOTE", default="origin", help="The git remote name where the branch will be pushed. Defaults to " "'origin'.", ) parser.add_argument( "--repo_root", metavar="REPO_ROOT", default=Path(__file__).parents[2], help="The repository root, mainly for testing cross-repository. " "Automatically found by default.", ) parser.add_argument( "--branch-start-point", metavar="BRANCH_START_POINT", default="trunk", type=str, help="The starting point for the new branch.", ) return parser.parse_args(args=args) def _calculate_branch(parsed_args: argparse.Namespace) -> str: """Returns the branch name.""" if parsed_args.branch: assert isinstance(parsed_args.branch, str) return parsed_args.branch return "proposal-%s" % slugify(parsed_args.title) def _find_tool(tool: str) -> str: """Checks if a tool is present.""" tool_path = shutil.which(tool) if not tool_path: exit("ERROR: Missing the '%s' command-line tool." % tool) return tool_path def _fill_template(template_path: str, title: str, pr_num: int) -> str: """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"\n- \[TODO: .*?\n", r"\n", content) content = re.sub(r"\n## TODO(?:.|\n)*?(\n## )", r"\1", content) return content def _run( argv: List[str], check: bool = True, get_stdout: bool = False ) -> Optional[str]: """Runs a command.""" cmd = " ".join([shlex.quote(x) for x in argv]) print("\n+ RUNNING: %s" % cmd, file=sys.stderr) stdout_pipe = None if get_stdout: stdout_pipe = subprocess.PIPE p = subprocess.Popen(argv, stdout=stdout_pipe) stdout, _ = p.communicate() if get_stdout: out = stdout.decode("utf-8") print(out, end="") if check and p.returncode != 0: exit("ERROR: Command failed: %s" % cmd) if get_stdout: return out return None def _run_pr_create(argv: List[str]) -> int: """Runs a command and returns the PR#.""" out = _run(argv, get_stdout=True) assert out is not None match = re.search( r"^https://github.com/[^/]+/[^/]+/pull/(\d+)$", out, re.MULTILINE ) if not match: exit("ERROR: Failed to find PR# in output.") return int(match.group(1)) def main() -> None: parsed_args = _parse_args() # Switch to repo root early for consistent execution. os.chdir(parsed_args.repo_root) title = parsed_args.title branch = _calculate_branch(parsed_args) # Verify tools are available. gh_bin = _find_tool("gh") if Path(".jj").is_dir(): jj_bin = _find_tool("jj") git_bin = None else: git_bin = _find_tool("git") jj_bin = None prek_bin = _find_tool("prek") # Verify there are no uncommitted changes (jj has no equivalent). if git_bin: p = subprocess.run([git_bin, "diff-index", "--quiet", "HEAD", "--"]) if p.returncode != 0: exit("ERROR: There are uncommitted changes in your git repo.") # Prompt before proceeding. response = "?" while response not in ("y", "n", ""): response = input(_PROMPT % (branch, title)).lower() if response == "n": exit("ERROR: Cancelled") # Create a proposal branch. if git_bin: _run( [ git_bin, "switch", "--create", branch, parsed_args.branch_start_point, ] ) _run([git_bin, "push", "-u", parsed_args.remote, branch]) else: assert jj_bin # For mypy. _run([jj_bin, "new", parsed_args.branch_start_point]) _run([jj_bin, "bookmark", "create", branch]) _run( [ jj_bin, "bookmark", "track", branch, "--remote", parsed_args.remote, ] ) # Copy template.md to a temp file. template_path = "proposals/scripts/template.md" temp_path = "proposals/new-proposal.tmp.md" shutil.copyfile(template_path, temp_path) initial_desc = "Creating new proposal: %s" % title if git_bin: _run([git_bin, "add", temp_path]) _run([git_bin, "commit", "-m", initial_desc]) else: assert jj_bin # For mypy. _run([jj_bin, "describe", "-m", initial_desc]) # Create a PR with WIP+proposal labels. if git_bin: _run([git_bin, "push"]) else: assert jj_bin # For mypy. _run([jj_bin, "git", "push"]) user = _run([gh_bin, "api", "user", "-q", ".login"], get_stdout=True) assert user # For mypy. user = user.strip() pr_num = _run_pr_create( [ gh_bin, "pr", "create", "--head", "%s:%s" % (user, branch), "--draft", "--label", "proposal", "--label", "proposal draft", "--repo", "carbon-language/carbon-lang", "--title", title, "--body", "TODO: add summary and links here", ] ) # Remove the temp file, create p######-title.md, and fill in PR information. os.remove(temp_path) final_path = "proposals/p%06d-%s.md" % (pr_num, slugify(title)) content = _fill_template(template_path, title, pr_num) with open(final_path, "w") as final_file: final_file.write(content) # Run prek for a ToC update, then push the PR update. final_desc = "Filling out template with PR %d" % pr_num if git_bin: _run([git_bin, "add", temp_path, final_path]) _run([prek_bin, "run"], check=False) _run([git_bin, "commit", "--amend", "-m", final_desc]) _run([git_bin, "push", "--force-with-lease"]) else: assert jj_bin # For mypy. _run( [prek_bin, "run", "--files", "$(jj diff --name-only)"], check=False, ) _run([jj_bin, "describe", "-m", final_desc]) _run([jj_bin, "git", "push"]) print( "\nCreated PR %d for %s. Make changes to:\n %s" % (pr_num, title, final_path) ) if __name__ == "__main__": main()