Skip to content
Merged
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
68 changes: 44 additions & 24 deletions cherry_picker/cherry_picker.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,23 @@


@click.command()
@click.option('--dry-run', is_flag=True)
@click.option('--push', 'pr_remote', metavar='REMOTE',
help='git remote to use for PR branches', default='origin')
@click.argument('commit_sha1', 'The commit sha1 to be cherry-picked')
@click.argument('branches', 'The branches to backport to', nargs=-1)
def cherry_pick(commit_sha1, branches):
def cherry_pick(dry_run, pr_remote, commit_sha1, branches):
if not os.path.exists('./pyconfig.h.in'):
os.chdir('./cpython/')
upstream = get_git_upstream_remote()
username = get_forked_repo_name()
upstream = get_git_fetch_remote()
username = get_forked_repo_name(pr_remote)

if dry_run:
click.echo("Dry run requested, listing expected command sequence")


click.echo("fetching upstream ...")
run_cmd(f"git fetch {upstream}")
run_cmd(f"git fetch {upstream}", dry_run=dry_run)

if not branches:
raise ValueError("at least one branch is required")
Expand All @@ -23,65 +30,78 @@ def cherry_pick(commit_sha1, branches):
click.echo(f"Now backporting '{commit_sha1}' into '{branch}'")

# git checkout -b 61e2bc7-3.5 upstream/3.5
cherry_pick_branch = f"{commit_sha1[:7]}-{branch}"
cherry_pick_branch = f"backport-{commit_sha1[:7]}-{branch}"
cmd = f"git checkout -b {cherry_pick_branch} {upstream}/{branch}"
run_cmd(cmd)
run_cmd(cmd, dry_run=dry_run)

cmd = f"git cherry-pick -x {commit_sha1}"
if run_cmd(cmd):
cmd = f"git push origin {cherry_pick_branch}"
if not run_cmd(cmd):
click.echo(f"Failed to push to origin :(")
if run_cmd(cmd, dry_run=dry_run):
cmd = f"git push {pr_remote} {cherry_pick_branch}"
if not run_cmd(cmd, dry_run=dry_run):
click.echo(f"Failed to push to {pr_remote} :(")
else:
open_pr(username, branch, cherry_pick_branch)
open_pr(username, branch, cherry_pick_branch, dry_run=dry_run)
else:
click.echo(f"Failed to cherry-pick {commit_sha1} into {branch} :(")

cmd = "git checkout master"
run_cmd(cmd)
run_cmd(cmd, dry_run=dry_run)

cmd = f"git branch -D {cherry_pick_branch}"
if run_cmd(cmd):
click.echo(f"branch {cherry_pick_branch} has been deleted.")
if run_cmd(cmd, dry_run=dry_run):
if not dry_run:
click.echo(f"branch {cherry_pick_branch} has been deleted.")
else:
click.echo(f"branch {cherry_pick_branch} NOT deleted.")


def get_git_upstream_remote():
def get_git_fetch_remote():
"""Get the remote name to use for upstream branches
Uses "upstream" if it exists, "origin" otherwise
"""
cmd = "git remote get-url upstream"
if run_cmd(cmd):
return "upstream"
else:
try:
subprocess.check_output(cmd.split(), stderr=subprocess.DEVNULL)
except subprocess.CalledProcessError:
return "origin"
return "upstream"


def get_forked_repo_name():
def get_forked_repo_name(pr_remote):
"""
Return 'myusername' out of https://github.com/myusername/cpython
:return:
"""
cmd = "git config --get remote.origin.url"
result = subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT).decode('utf-8')
username = result[len("https://github.com/"):result.index('/cpython.git')]
cmd = f"git config --get remote.{pr_remote}.url"
raw_result = subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT)
result = raw_result.decode('utf-8')
username_end = result.index('/cpython.git')
if result.startswith("https"):
username = result[len("https://github.com/"):username_end]
else:
username = result[len("git@github.com:"):username_end]
return username


def run_cmd(cmd):
def run_cmd(cmd, *, dry_run=False):
if dry_run:
click.echo(f" dry-run: {cmd}")
return True
try:
subprocess.check_output(cmd.split())
except subprocess.CalledProcessError:
return False
return True


def open_pr(forked_repo, base_branch, cherry_pick_branch):
def open_pr(forked_repo, base_branch, cherry_pick_branch, *, dry_run=False):
"""
construct the url for pull request and open it in the web browser
"""
url = f"https://github.com/python/cpython/compare/{base_branch}...{forked_repo}:{cherry_pick_branch}?expand=1"
if dry_run:
click.echo(f" dry-run: Create new PR: {url}")
return
webbrowser.open_new_tab(url)


Expand Down
46 changes: 37 additions & 9 deletions cherry_picker/readme.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Usage::

python -m cherry_picker <commit_sha1> <branches>
python -m cherry_picker [--push REMOTE] [--dry-run] <commit_sha1> <branches>



Expand Down Expand Up @@ -34,6 +34,14 @@ Requires Python 3.6.
(venv) $ git remote add upstream https://github.com/python/cpython.git
(venv) $ cd ../

The cherry picking script assumes that if an `upstream` remote is defined, then
it should be used as the source of upstream changes and as the base for
cherry-pick branches. Otherwise, `origin` is used for that purpose.

By default, the PR branches used to submit pull requests back to the main
repository are pushed to `origin`. If this is incorrect, then the correct
remote will need be specified using the ``--push`` option (e.g.
``--push pr`` to use a remote named ``pr``).

Cherry-picking :snake: :cherries: :pick:
==============
Expand Down Expand Up @@ -62,20 +70,41 @@ What this will do:

(venv) $ git fetch upstream

(venv) $ git checkout -b 6de2b78-3.5 upstream/3.5
(venv) $ git checkout -b backport-6de2b78-3.5 upstream/3.5
(venv) $ git cherry-pick -x 6de2b7817f-some-commit-sha1-d064
(venv) $ git push origin 6de2b78-3.5
(venv) $ git push origin backport-6de2b78-3.5
(venv) $ git checkout master
(venv) $ git branch -D 6de2b78-3.5
(venv) $ git branch -D backport-6de2b78-3.5

(venv) $ git checkout -b 6de2b78-3.6 upstream/3.6
(venv) $ git checkout -b backport-6de2b78-3.6 upstream/3.6
(venv) $ git cherry-pick -x 6de2b7817f-some-commit-sha1-d064
(venv) $ git push origin 6de2b78-3.6
(venv) $ git push origin backport-6de2b78-3.6
(venv) $ git checkout master
(venv) $ git branch -D 6de2b78-3.6
(venv) $ git branch -D backport-6de2b78-3.6

In case of merge conflicts or errors, then... the script will fail :stuck_out_tongue:

Passing the `--dry-run` option will cause the script to print out all the
steps it would execute without actually executing any of them. For example::

$ python -m cherry_picker --dry-run --push pr 1e32a1be4a1705e34011770026cb64ada2d340b5 3.6 3.5
Dry run requested, listing expected command sequence
fetching upstream ...
dry_run: git fetch origin
Now backporting '1e32a1be4a1705e34011770026cb64ada2d340b5' into '3.6'
dry_run: git checkout -b backport-1e32a1b-3.6 origin/3.6
dry_run: git cherry-pick -x 1e32a1be4a1705e34011770026cb64ada2d340b5
dry_run: git push pr backport-1e32a1b-3.6
dry_run: Create new PR: https://github.com/python/cpython/compare/3.6...ncoghlan:backport-1e32a1b-3.6?expand=1
dry_run: git checkout master
dry_run: git branch -D backport-1e32a1b-3.6
Now backporting '1e32a1be4a1705e34011770026cb64ada2d340b5' into '3.5'
dry_run: git checkout -b backport-1e32a1b-3.5 origin/3.5
dry_run: git cherry-pick -x 1e32a1be4a1705e34011770026cb64ada2d340b5
dry_run: git push pr backport-1e32a1b-3.5
dry_run: Create new PR: https://github.com/python/cpython/compare/3.5...ncoghlan:backport-1e32a1b-3.5?expand=1
dry_run: git checkout master
dry_run: git branch -D backport-1e32a1b-3.5

Creating Pull Requests
======================
Expand All @@ -85,8 +114,7 @@ tab that points to the pull request creation page.

The url of the pull request page looks similar to the following::

https://github.com/python/cpython/compare/3.5...<username>:6de2b78-3.5?expand=1

https://github.com/python/cpython/compare/3.5...<username>:backport-6de2b78-3.5?expand=1


1. Prefix the pull request description with the branch ``[X.Y]``, e.g.::
Expand Down