Add latest commit information to the editor comments. That way we can easily adjust the PR based on the latest commit.
300 lines
8.2 KiB
Python
300 lines
8.2 KiB
Python
import argparse
|
|
import contextlib
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
# push origin HEAD:refs/for/main
|
|
# HEAD: The target branch
|
|
# origin: The target repository (not a fork!)
|
|
# HEAD: The local branch containing the changes you are proposing
|
|
TARGET_REMOTE_REPOSITORY = "origin"
|
|
DEFAULT_TARGET_BRANCH = "main"
|
|
|
|
|
|
def run_git_command(command: list) -> tuple[int, str, str]:
|
|
"""Run a git command and return exit code, stdout, and stderr."""
|
|
try:
|
|
result = subprocess.run(command, capture_output=True, text=True, check=False)
|
|
return result.returncode, result.stdout.strip(), result.stderr.strip()
|
|
except Exception as e:
|
|
return 1, "", str(e)
|
|
|
|
|
|
def get_latest_commit_info() -> tuple[str, str]:
|
|
"""Get the title and body of the latest commit."""
|
|
exit_code, commit_msg, error = run_git_command(
|
|
["git", "log", "-1", "--pretty=format:%B"]
|
|
)
|
|
|
|
if exit_code != 0:
|
|
print(f"Error getting commit info: {error}")
|
|
sys.exit(1)
|
|
|
|
lines = commit_msg.strip().split("\n")
|
|
title = lines[0].strip() if lines else ""
|
|
|
|
body_lines = []
|
|
for line in lines[1:]:
|
|
if body_lines or line.strip():
|
|
body_lines.append(line)
|
|
|
|
body = "\n".join(body_lines).strip()
|
|
|
|
return title, body
|
|
|
|
|
|
def open_editor_for_pr() -> tuple[str, str]:
|
|
"""Open editor to get PR title and description. First line is title, rest is description."""
|
|
commit_title, commit_body = get_latest_commit_info()
|
|
|
|
with tempfile.NamedTemporaryFile(
|
|
mode="w+", suffix=".txt", delete=False
|
|
) as temp_file:
|
|
temp_file.write("\n")
|
|
temp_file.write("# Please enter the PR title on the first line.\n")
|
|
temp_file.write("# Lines starting with '#' will be ignored.\n")
|
|
temp_file.write("# The first line will be used as the PR title.\n")
|
|
temp_file.write("# Everything else will be used as the PR description.\n")
|
|
temp_file.write("#\n")
|
|
temp_file.write("# Current commit information:\n")
|
|
temp_file.write("#\n")
|
|
if commit_title:
|
|
temp_file.write(f"# {commit_title}\n")
|
|
temp_file.write("#\n")
|
|
if commit_body:
|
|
for line in commit_body.split("\n"):
|
|
temp_file.write(f"# {line}\n")
|
|
temp_file.write("#\n")
|
|
temp_file.flush()
|
|
temp_file_path = temp_file.name
|
|
|
|
try:
|
|
editor = os.environ.get("EDITOR", "vim")
|
|
|
|
exit_code = subprocess.call([editor, temp_file_path])
|
|
|
|
if exit_code != 0:
|
|
print(f"Editor exited with code {exit_code}")
|
|
sys.exit(1)
|
|
|
|
with Path(temp_file_path).open() as f:
|
|
content = f.read()
|
|
|
|
lines = []
|
|
for line in content.split("\n"):
|
|
if not line.lstrip().startswith("#"):
|
|
lines.append(line)
|
|
|
|
cleaned_content = "\n".join(lines).strip()
|
|
|
|
if not cleaned_content:
|
|
print("No content provided, aborting.")
|
|
sys.exit(0)
|
|
|
|
content_lines = cleaned_content.split("\n")
|
|
title = content_lines[0].strip()
|
|
|
|
if not title:
|
|
print("No title provided, aborting.")
|
|
sys.exit(0)
|
|
|
|
description_lines = []
|
|
for line in content_lines[1:]:
|
|
if description_lines or line.strip():
|
|
description_lines.append(line)
|
|
|
|
description = "\n".join(description_lines).strip()
|
|
|
|
return title, description
|
|
|
|
finally:
|
|
with contextlib.suppress(OSError):
|
|
Path(temp_file_path).unlink()
|
|
|
|
|
|
def create_agit_push(
|
|
remote: str = "origin",
|
|
branch: str = "main",
|
|
topic: str | None = None,
|
|
title: str | None = None,
|
|
description: str | None = None,
|
|
force_push: bool = False,
|
|
local_branch: str = "HEAD",
|
|
) -> None:
|
|
if topic is None:
|
|
if title is not None:
|
|
topic = title
|
|
else:
|
|
commit_title, _ = get_latest_commit_info()
|
|
topic = commit_title
|
|
|
|
refspec = f"{local_branch}:refs/for/{branch}"
|
|
push_cmd = ["git", "push", remote, refspec]
|
|
|
|
push_cmd.extend(["-o", f"topic={topic}"])
|
|
|
|
if title:
|
|
push_cmd.extend(["-o", f"title={title}"])
|
|
|
|
if description:
|
|
escaped_desc = description.rstrip("\n").replace('"', '\\"')
|
|
push_cmd.extend(["-o", f"description={escaped_desc}"])
|
|
|
|
if force_push:
|
|
push_cmd.extend(["-o", "force-push"])
|
|
|
|
if description:
|
|
print(
|
|
f" Description: {description[:50]}..."
|
|
if len(description) > 50
|
|
else f" Description: {description}"
|
|
)
|
|
print()
|
|
|
|
exit_code, stdout, stderr = run_git_command(push_cmd)
|
|
|
|
if stdout:
|
|
print(stdout)
|
|
if stderr:
|
|
print(stderr, file=sys.stderr)
|
|
|
|
if exit_code != 0:
|
|
print("\nPush failed!")
|
|
sys.exit(exit_code)
|
|
else:
|
|
print("\nPush successful!")
|
|
|
|
|
|
def cmd_create(args: argparse.Namespace) -> None:
|
|
"""Handle the create subcommand."""
|
|
title = args.title
|
|
description = args.description
|
|
|
|
if not args.auto and (title is None or description is None):
|
|
editor_title, editor_description = open_editor_for_pr()
|
|
if title is None:
|
|
title = editor_title
|
|
if description is None:
|
|
description = editor_description
|
|
|
|
create_agit_push(
|
|
remote=args.remote,
|
|
branch=args.branch,
|
|
topic=args.topic,
|
|
title=title,
|
|
description=description,
|
|
force_push=args.force,
|
|
local_branch=args.local_branch,
|
|
)
|
|
|
|
|
|
def create_parser() -> argparse.ArgumentParser:
|
|
parser = argparse.ArgumentParser(
|
|
prog="agit",
|
|
description="AGit utility for creating and pulling PRs",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog=f"""
|
|
The defaults that are assumed are:
|
|
TARGET_REMOTE_REPOSITORY = {TARGET_REMOTE_REPOSITORY}
|
|
DEFAULT_TARGET_BRANCH = {DEFAULT_TARGET_BRANCH}
|
|
|
|
Examples:
|
|
$ agit create
|
|
Opens editor to compose PR title and description (first line is title, rest is body).
|
|
|
|
$ agit create --auto
|
|
Creates PR using latest commit message automatically (old behavior).
|
|
|
|
$ agit create --topic "my-feature"
|
|
Set a custom topic.
|
|
|
|
$ agit create --force
|
|
Force push to a certain topic
|
|
""",
|
|
)
|
|
|
|
subparsers = parser.add_subparsers(dest="subcommand", help="Commands")
|
|
|
|
create_parser = subparsers.add_parser(
|
|
"create",
|
|
aliases=["c"],
|
|
help="Create an AGit PR",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog="""
|
|
Examples:
|
|
$ agit create
|
|
Opens editor to compose PR title and description (first line is title, rest is body).
|
|
|
|
$ agit create --auto
|
|
Creates PR using latest commit message automatically (old behavior).
|
|
|
|
$ agit create --topic "my-feature"
|
|
Set a custom topic.
|
|
|
|
$ agit create --force
|
|
Force push to a certain topic
|
|
""",
|
|
)
|
|
|
|
create_parser.add_argument(
|
|
"-r",
|
|
"--remote",
|
|
default=TARGET_REMOTE_REPOSITORY,
|
|
help=f"Git remote to push to (default: {TARGET_REMOTE_REPOSITORY})",
|
|
)
|
|
|
|
create_parser.add_argument(
|
|
"-b",
|
|
"--branch",
|
|
default=DEFAULT_TARGET_BRANCH,
|
|
help=f"Target branch for the PR (default: {DEFAULT_TARGET_BRANCH})",
|
|
)
|
|
|
|
create_parser.add_argument(
|
|
"-l",
|
|
"--local-branch",
|
|
default="HEAD",
|
|
help="Local branch to push (default: HEAD)",
|
|
)
|
|
|
|
create_parser.add_argument(
|
|
"-t", "--topic", help="Set PR topic (default: last commit title)"
|
|
)
|
|
|
|
create_parser.add_argument(
|
|
"--title", help="Set the PR title (default: last commit title)"
|
|
)
|
|
|
|
create_parser.add_argument(
|
|
"--description", help="Override the PR description (default: commit body)"
|
|
)
|
|
|
|
create_parser.add_argument(
|
|
"-f", "--force", action="store_true", help="Force push the changes"
|
|
)
|
|
|
|
create_parser.add_argument(
|
|
"--auto",
|
|
action="store_true",
|
|
help="Skip editor and use commit message automatically",
|
|
)
|
|
|
|
create_parser.set_defaults(func=cmd_create)
|
|
return parser
|
|
|
|
|
|
def main() -> None:
|
|
parser = create_parser()
|
|
args = parser.parse_args()
|
|
if args.subcommand is None:
|
|
parser.print_help()
|
|
sys.exit(0)
|
|
args.func(args)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|