Skip to content

(don't merge) Add --abort/--continue option to cherry_picker.py #65

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
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
161 changes: 130 additions & 31 deletions cherry_picker/cherry_picker.py
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,59 +1,144 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import click
import os
import subprocess
import webbrowser


@click.command()
@click.option('--dry-run', is_flag=True)
@click.option('--dry-run', is_flag=True,
help="Prints out the commands, but not executed.")
@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.option('--abort', 'abort', flag_value=True, default=None,
help="Abort current cherry-pick and clean up branch")
@click.option('--continue', 'abort', flag_value=False, default=None,
help="Continue cherry-pick, push, and clean up branch")
@click.argument('commit_sha1', 'The commit sha1 to be cherry-picked', nargs=1,
default = "")
@click.argument('branches', 'The branches to backport to', nargs=-1)
def cherry_pick(dry_run, pr_remote, commit_sha1, branches):
def cherry_pick(dry_run, pr_remote, abort, commit_sha1, branches):
click.echo("\U0001F40D \U0001F352 \u26CF ")
if not os.path.exists('./pyconfig.h.in'):
os.chdir('./cpython/')

upstream = get_git_fetch_remote()
username = get_forked_repo_name(pr_remote)

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

if abort is not None:
if abort:
abort_cherry_pick(dry_run=dry_run)
else:
continue_cherry_pick(username, pr_remote, dry_run=dry_run)

click.echo("fetching upstream ...")
run_cmd(f"git fetch {upstream}", dry_run=dry_run)
else:
backport_branches(commit_sha1, branches, username, upstream, pr_remote,
dry_run=dry_run)

def backport_branches(commit_sha1, branches, username, upstream, pr_remote,
*, dry_run=False):
if commit_sha1 == "":
raise ValueError("Missing the commit_sha1 argument.")
if not branches:
raise ValueError("at least one branch is required")
raise ValueError("At least one branch is required.")
else:
run_cmd(f"git fetch {upstream}", dry_run=dry_run)

for branch in get_sorted_branch(branches):
click.echo(f"Now backporting '{commit_sha1}' into '{branch}'")
for branch in get_sorted_branch(branches):
click.echo(f"Now backporting '{commit_sha1}' into '{branch}'")

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

cmd = f"git cherry-pick -x {commit_sha1}"
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} :(")
cmd = f"git cherry-pick -x {commit_sha1}"
if run_cmd(cmd, dry_run=dry_run):
push_to_remote(pr_url, pr_remote, cherry_pick_branch, dry_run=dry_run)
cleanup_branch(cherry_pick_branch, dry_run=dry_run)
else:
open_pr(username, branch, cherry_pick_branch, dry_run=dry_run)
else:
click.echo(f"Failed to cherry-pick {commit_sha1} into {branch} :(")
click.echo(f"Failed to cherry-pick {commit_sha1} into {branch} \u2639")
click.echo(" ... Stopping here. ")

click.echo("")
click.echo("To continue and resolve the conflict: ")
click.echo(" $ cd cpython")
click.echo(" $ git status # to find out which files need attention")
click.echo(" # Fix the conflict")
click.echo(" $ git status # should now say `all conflicts fixed`")
click.echo(" $ cd ..")
click.echo(" $ python -m cherry_picker --continue")

click.echo("")
click.echo("To abort the cherry-pick and cleanup: ")
click.echo(" $ python -m cherry_picker --abort")


cmd = "git checkout master"
def abort_cherry_pick(*, dry_run=False):
"""
run `git cherry-pick --abort` and then clean up the branch
"""
if run_cmd("git cherry-pick --abort", dry_run=dry_run):
cleanup_branch(get_current_branch(), dry_run=dry_run)


def continue_cherry_pick(username, pr_remote, *, dry_run=False):
"""
git push origin <current_branch>
open the PR
clean up branch

"""
cherry_pick_branch = get_current_branch()
if cherry_pick_branch != 'master':

# this has the same effect as `git cherry-pick --continue`
cmd = f"git commit -am 'Resolved.' --allow-empty"
run_cmd(cmd, dry_run=dry_run)

cmd = f"git branch -D {cherry_pick_branch}"
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.")
base_branch = get_base_branch(cherry_pick_branch)
pr_url = get_pr_url(username, base_branch, cherry_pick_branch)
push_to_remote(pr_url, pr_remote, cherry_pick_branch, dry_run=dry_run)

cleanup_branch(cherry_pick_branch, dry_run=dry_run)
else:
click.echo(u"Refuse to push to master \U0001F61B")


def get_base_branch(cherry_pick_branch):
"""
return '2.7' from 'backport-sha-2.7'
"""
return cherry_pick_branch[cherry_pick_branch.rfind('-')+1:]


def cleanup_branch(cherry_pick_branch, *, dry_run=False):
"""
git checkout master
git branch -D <branch to delete>
"""
cmd = "git checkout master"
run_cmd(cmd, dry_run=dry_run)

cmd = f"git branch -D {cherry_pick_branch}"
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 push_to_remote(pr_url, pr_remote, cherry_pick_branch, *, dry_run=False):
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} \u2639")
else:
open_pr(pr_url, dry_run=dry_run)

def get_git_fetch_remote():
"""Get the remote name to use for upstream branches
Expand All @@ -70,7 +155,6 @@ def get_git_fetch_remote():
def get_forked_repo_name(pr_remote):
"""
Return 'myusername' out of https://github.com/myusername/cpython
:return:
"""
cmd = f"git config --get remote.{pr_remote}.url"
raw_result = subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT)
Expand All @@ -94,17 +178,32 @@ def run_cmd(cmd, *, dry_run=False):
return True


def open_pr(forked_repo, base_branch, cherry_pick_branch, *, dry_run=False):
def get_pr_url(forked_repo, base_branch, cherry_pick_branch):
"""
construct the url for the pull request
"""
return f"https://github.com/python/cpython/compare/{base_branch}...{forked_repo}:{cherry_pick_branch}?expand=1"


def open_pr(url, *, dry_run=False):
"""
construct the url for pull request and open it in the web browser
open url 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)


def get_current_branch():
"""
Return the current branch
"""
cmd = "git symbolic-ref HEAD | sed 's!refs\/heads\/!!'"
output = subprocess.check_output(cmd, shell=True)
return output.strip().decode()


def get_sorted_branch(branches):
return sorted(
branches,
Expand Down
50 changes: 46 additions & 4 deletions cherry_picker/readme.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Usage::

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



Expand Down Expand Up @@ -43,16 +43,42 @@ 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:
==============

(Setup first! See prev section)

::

(venv) $ python -m cherry_picker <commit_sha1> <branches>
(venv) $ python -m cherry_picker [--dry-run] [--abort/--continue] <commit_sha1> <branches>

The commit sha1 is obtained from the merged pull request on ``master``.


Options
-------

::

-- dry-run Dry Run Mode. Prints out the commands, but not executed.
-- push REMOTE Specify the branch to push into. Default is 'origin'.


Additional options::

-- abort Abort current cherry-pick and clean up branch
-- continue Continue cherry-pick, push, and clean up branch

The commit sha1 is obtained from the merged pull request on ``master``.

Demo
----

https://asciinema.org/a/dfalzy45oq8b3c6dvakwfs6la


Example
-------

For example, to cherry-pick ``6de2b7817f-some-commit-sha1-d064`` into
``3.5`` and ``3.6``:
Expand Down Expand Up @@ -82,7 +108,22 @@ What this will do:
(venv) $ git checkout master
(venv) $ git branch -D backport-6de2b78-3.6

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

Failed to cherry-pick 554626ada769abf82a5dabe6966afa4265acb6a6 into 2.7 :frowning_face:
... Stopping here.

To continue and resolve the conflict:
$ cd cpython
$ git status # to find out which files need attention
# Fix the conflict
$ git status # should now say `all conflicts fixed`
$ cd ..
$ python -m cherry_picker --continue

To abort the cherry-pick and cleanup:
$ python -m cherry_picker --abort


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::
Expand All @@ -106,6 +147,7 @@ steps it would execute without actually executing any of them. For example::
dry_run: git checkout master
dry_run: git branch -D backport-1e32a1b-3.5


Creating Pull Requests
======================

Expand Down