289 lines
7.7 KiB
Python
289 lines
7.7 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."""
|
|
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.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.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()
|