blob: aab1903748d7a7ba4c4e974d405dec3d07e0ee5a [file]
"""Subcommand to create a release branch from a merged PR commit."""
from dev.release.gh import GitHub
from dev.release.git import Git
from dev.release.release_issue import (
RELEASE_TITLE_RE,
parse_checklist_state,
update_task_in_body,
)
from dev.release.utils import REPO_URL
class CreateReleaseBranch:
"""Class to create a release branch from a merged PR commit."""
def __init__(self, args, git: Git, gh: GitHub):
self.args = args
self.git = git
self.gh = gh
def run(self) -> int:
"""Executes the create-release-branch subcommand."""
args = self.args
print(f"Evaluating branch creation for tracking issue #{args.issue}...")
body = self.gh.get_issue_body(args.issue)
state = parse_checklist_state(body)
if (
state["prepare_release"].status != "done"
or not state["prepare_release"].commit
):
print(
"Error: Prepare Release task is not marked 'done' with a valid"
" commit SHA."
)
return 1
if state["create_branch"].checked:
print("Release branch has already been created and checked. Skipping.")
return 0
# Extract version from issue title
issue_title = self.gh.get_issue_title(args.issue)
version_match = RELEASE_TITLE_RE.search(issue_title)
if not version_match:
print(f"Error: Could not parse version from issue title: {issue_title}")
return 1
version = version_match.group(1)
branch_version = ".".join(version.split(".")[:2])
branch_name = f"release/{branch_version}"
commit_sha = state["prepare_release"].commit
print(f"Cutting branch {branch_name} from commit {commit_sha}...")
# Create and push branch without affecting local checkout
self.git.fetch(args.remote)
if self.git.remote_branch_exists(args.remote, branch_name):
remote_ref = f"{args.remote}/{branch_name}"
remote_sha = self.git.get_commit_sha(remote_ref)
if remote_sha == commit_sha:
print(
f"Branch {branch_name} already exists on {args.remote} and"
f" points to {commit_sha}. Skipping push."
)
elif self.git.is_ancestor(remote_ref, commit_sha):
print(
f"Branch {branch_name} exists on {args.remote} but can be"
f" fast-forwarded to {commit_sha}. Pushing..."
)
ref_spec = f"{commit_sha}:refs/heads/{branch_name}"
self.git.push(args.remote, ref_spec)
else:
print(
f"Error: Branch {branch_name} already exists on"
f" {args.remote} at {remote_sha[:8]}, which is not an"
f" ancestor of {commit_sha[:8]}. Cannot fast-forward."
)
return 1
else:
print(f"Branch {branch_name} does not exist on {args.remote}. Pushing...")
ref_spec = f"{commit_sha}:refs/heads/{branch_name}"
self.git.push(args.remote, ref_spec)
print(
f"Successfully pushed branch {branch_name} pointing to"
f" {commit_sha} to {args.remote}"
)
# Update tracking issue checklist
print("Updating tracking issue checklist...")
branch_url = f"{REPO_URL}/tree/{branch_name}"
metadata = {
"status": "done",
"branch_url": branch_url,
"commit": commit_sha[:8],
}
updated_body = update_task_in_body(
body, "Create Release branch", checked=True, metadata=metadata
)
self.gh.update_issue_body(args.issue, updated_body)
print("Create Release branch task marked complete successfully!")
return 0
@classmethod
def add_parser(cls, subparsers):
"""Adds parser for create-release-branch subcommand."""
parser = subparsers.add_parser(
"create-release-branch",
help="Create the release branch pointing to the merged PR commit.",
)
parser.add_argument(
"--issue",
type=int,
required=True,
help="The tracking issue number (required).",
)
parser.add_argument(
"--remote",
type=str,
required=True,
help="The git remote to create the branch on (required).",
)
parser.set_defaults(command=cls.run_from_args)
@classmethod
def run_from_args(cls, args):
"""Instantiates and runs the command from parsed args."""
git = Git(".")
gh = GitHub()
return cls(args, git, gh).run()