Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ options:
--auto-pr, --no-auto-pr
If auto PR is enabled, cherry-picker will
automatically open a PR through API if
GH_AUTH env var is set, or automatically open
the PR creation page in the web browser otherwise.
GH_AUTH env var is set, through the GitHub CLI
if it is logged in, or automatically open the PR
creation page in the web browser otherwise.
--config-path CONFIG-PATH
Path to config file, .cherry_picker.toml from
project root by default. You can prepend a colon-
Expand Down
68 changes: 61 additions & 7 deletions cherry_picker/cherry_picker.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import functools
import os
import re
import shutil
import subprocess
import sys
import tomllib
Expand Down Expand Up @@ -448,6 +449,15 @@ def push_to_remote(self, base_branch, head_branch, commit_message=""):
except GitHubException:
set_state(WORKFLOW_STATES.PR_CREATING_FAILED)
raise
elif self.gh_cli_available():
set_state(WORKFLOW_STATES.PR_CREATING)
try:
self.create_pr_with_gh_cli(
base_branch, head_branch, commit_message=commit_message
)
except GitHubException:
set_state(WORKFLOW_STATES.PR_CREATING_FAILED)
raise
else:
set_state(WORKFLOW_STATES.PR_OPENING)
self.open_pr(self.get_pr_url(base_branch, head_branch))
Expand All @@ -463,10 +473,7 @@ def create_gh_pr(self, base_branch, head_branch, *, commit_message, gh_auth):
"Authorization": f"token {gh_auth}",
"X-GitHub-Api-Version": "2022-11-28",
}
title, body = normalize_commit_message(commit_message)
if not self.prefix_commit:
title = remove_commit_prefix(title)
title = f"[{base_branch}] {title}"
title, body = self.get_pr_title_and_body(base_branch, commit_message)
data = {
"title": title,
"body": body,
Expand All @@ -493,6 +500,52 @@ def create_gh_pr(self, base_branch, head_branch, *, commit_message, gh_auth):
print(f"Backport PR created at {response_data['html_url']}")
self.pr_number = response_data["number"]

def get_pr_title_and_body(
self, base_branch: str, commit_message: str
) -> tuple[str, str]:
title, body = normalize_commit_message(commit_message)
if not self.prefix_commit:
title = remove_commit_prefix(title)
title = f"[{base_branch}] {title}"
return title, body

def gh_cli_available(self) -> bool:
if shutil.which("gh") is None:
return False
cmd = ["gh", "auth", "status", "--hostname", "github.com"]
return subprocess.run(cmd, capture_output=True).returncode == 0

@stamina.retry(on=GitHubException, timeout=120)
def create_pr_with_gh_cli(
self, base_branch: str, head_branch: str, *, commit_message: str
) -> None:
title, body = self.get_pr_title_and_body(base_branch, commit_message)
cmd = [
"gh",
"pr",
"create",
"--repo",
f"{self.config['team']}/{self.config['repo']}",
"--base",
base_branch,
"--head",
f"{self.username}:{head_branch}",
"--title",
title,
"--body",
body,
]
if self.config["draft_pr"]:
cmd.append("--draft")
try:
output = self.run_cmd(cmd)
except subprocess.CalledProcessError as exc:
raise GitHubException(f"Creating PR with gh failed: {_decode(exc.output)}")
if output:
url = output.strip().splitlines()[-1]
print(f"Backport PR created at {url}")
self.pr_number = int(url.rsplit("/", 1)[-1])

def open_pr(self, url):
"""
open url in the web browser
Expand Down Expand Up @@ -664,7 +717,7 @@ def continue_cherry_pick(self):
self.run_cmd(cmd)

if self.push:
self.push_to_remote(base, cherry_pick_branch)
self.push_to_remote(base, cherry_pick_branch, commit_message)

if not self.is_mirror():
self.cleanup_branch(cherry_pick_branch)
Expand Down Expand Up @@ -797,8 +850,9 @@ def get_parser():
default=True,
help=(
"If auto PR is enabled, cherry-picker will automatically open a PR "
"through API if `GH_AUTH` env var is set, or automatically open the PR "
"creation page in the web browser otherwise."
"through API if `GH_AUTH` env var is set, through the GitHub CLI if it "
"is logged in, or automatically open the PR creation page in the web "
"browser otherwise."
),
)
parser.add_argument(
Expand Down
22 changes: 22 additions & 0 deletions cherry_picker/test_cherry_picker.py
Original file line number Diff line number Diff line change
Expand Up @@ -972,13 +972,35 @@ def test_push_to_remote_interactive(tmp_git_repo_dir):

with (
mock.patch.object(cherry_picker, "run_cmd"),
mock.patch.object(cherry_picker, "gh_cli_available", return_value=False),
mock.patch.object(cherry_picker, "open_pr"),
mock.patch.object(cherry_picker, "get_pr_url", return_value="https://pr_url"),
):
cherry_picker.push_to_remote("main", "backport-branch-test")
assert get_state() == WORKFLOW_STATES.PR_OPENING


@mock.patch.object(CherryPicker, "username", new_callable=mock.PropertyMock)
def test_push_to_remote_gh_cli(mock_username, tmp_git_repo_dir, monkeypatch):
monkeypatch.delenv("GH_AUTH", raising=False)
mock_username.return_value = "username"
with mock.patch("cherry_picker.cherry_picker.validate_sha", return_value=True):
cherry_picker = CherryPicker("origin", "xxx", [])

with (
mock.patch.object(cherry_picker, "gh_cli_available", return_value=True),
mock.patch.object(
cherry_picker,
"run_cmd",
return_value="https://github.com/python/cpython/pull/1347\n",
) as run_cmd,
):
cherry_picker.push_to_remote("3.14", "backport-branch-test", "Title")

assert run_cmd.call_args.args[0][:3] == ["gh", "pr", "create"]
assert cherry_picker.pr_number == 1347


def test_push_to_remote_botflow(tmp_git_repo_dir, monkeypatch):
monkeypatch.setenv("GH_AUTH", "True")
with mock.patch("cherry_picker.cherry_picker.validate_sha", return_value=True):
Expand Down
Loading