From 70614da67eea19da6a2a704b7221462ea66c2ae9 Mon Sep 17 00:00:00 2001 From: Jon Ross-Perkins Date: Fri, 6 Feb 2026 11:53:35 -0800 Subject: [PATCH] Add jj support to new_proposal.py (#6700) Also scrutinizing how it runs from another directory, because that's what I did to test these changes. Switching to the repo root is to make it easier to just look for ".jj". Assisted-by: Google Antigravity with Gemini 3 Flash --- proposals/scripts/new_proposal.py | 119 +++++++++++++++---------- proposals/scripts/new_proposal_test.py | 8 +- 2 files changed, 77 insertions(+), 50 deletions(-) diff --git a/proposals/scripts/new_proposal.py b/proposals/scripts/new_proposal.py index b66eb749408c..a5da09497e97 100755 --- a/proposals/scripts/new_proposal.py +++ b/proposals/scripts/new_proposal.py @@ -15,6 +15,7 @@ import shlex import shutil import subprocess import sys +from pathlib import Path from typing import List, Optional _PROMPT = """This will: @@ -63,9 +64,10 @@ def _parse_args(args: Optional[List[str]] = None) -> argparse.Namespace: "'origin'.", ) parser.add_argument( - "--proposals-dir", - metavar="PROPOSALS_DIR", - help="The proposals directory, mainly for testing cross-repository. " + "--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( @@ -109,16 +111,6 @@ def _fill_template(template_path: str, title: str, pr_num: int) -> str: return content -def _get_proposals_dir(parsed_args: argparse.Namespace) -> str: - """Returns the path to the proposals directory.""" - if parsed_args.proposals_dir: - assert isinstance(parsed_args.proposals_dir, str) - return parsed_args.proposals_dir - return os.path.realpath( - os.path.join(os.path.dirname(__file__), "../../proposals") - ) - - def _run( argv: List[str], check: bool = True, get_stdout: bool = False ) -> Optional[str]: @@ -156,22 +148,28 @@ def _run_pr_create(argv: List[str]) -> int: 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") - git_bin = _find_tool("git") + if Path(".jj").is_dir(): + jj_bin = _find_tool("jj") + git_bin = None + else: + git_bin = _find_tool("git") + jj_bin = None precommit_bin = _find_tool("pre-commit") - # Ensure a good working directory. - proposals_dir = _get_proposals_dir(parsed_args) - os.chdir(proposals_dir) - - # Verify there are no uncommitted changes. - p = subprocess.run([git_bin, "diff-index", "--quiet", "HEAD", "--"]) - if p.returncode != 0: - exit("ERROR: There are uncommitted changes in your git repo.") + # 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 = "?" @@ -181,25 +179,51 @@ def main() -> None: exit("ERROR: Cancelled") # Create a proposal branch. - _run( - [git_bin, "switch", "--create", branch, parsed_args.branch_start_point] - ) - _run([git_bin, "push", "-u", parsed_args.remote, 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]) # Copy template.md to a temp file. - template_path = os.path.join(proposals_dir, "scripts/template.md") - temp_path = os.path.join(proposals_dir, "new-proposal.tmp.md") + template_path = "proposals/scripts/template.md" + temp_path = "proposals/new-proposal.tmp.md" shutil.copyfile(template_path, temp_path) - _run([git_bin, "add", temp_path]) - _run([git_bin, "commit", "-m", "Creating new proposal: %s" % title]) + 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. - _run([git_bin, "push"]) + if git_bin: + _run([git_bin, "push"]) + user = _run([git_bin, "config", "get", "user.name"], get_stdout=True) + else: + assert jj_bin # For mypy. + _run([jj_bin, "git", "push"]) + user = _run([jj_bin, "config", "get", "user.name"], 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", @@ -216,25 +240,28 @@ def main() -> None: # 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) + final_path = "proposals/p%04d.md" % pr_num content = _fill_template(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", - "--amend", - "-m", - "Filling out template with PR %d" % pr_num, - ] - ) - # Push the PR update. - _run([git_bin, "push", "--force-with-lease"]) + # Run pre-commit 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([precommit_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( + [precommit_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" diff --git a/proposals/scripts/new_proposal_test.py b/proposals/scripts/new_proposal_test.py index 114dfb812dd1..ff3e482853c6 100644 --- a/proposals/scripts/new_proposal_test.py +++ b/proposals/scripts/new_proposal_test.py @@ -35,12 +35,12 @@ class TestNewProposal(unittest.TestCase): self.assertEqual(new_proposal._calculate_branch(parsed_args), "wiz") def test_fill_template(self): + # Switch directories for the test. parsed_args = new_proposal._parse_args(["foo"]) + os.chdir(parsed_args.repo_root) + content = new_proposal._fill_template( - os.path.join( - new_proposal._get_proposals_dir(parsed_args), - "scripts/template.md", - ), + "proposals/scripts/template.md", "TITLE", 123, )