Skip to content

Commit fd1fbac

Browse files
committed
Add --abort/--continue option
--abort: Do `git cherry-pick --abort` Clean up branch --continue Do `git commit -am "resolved" --allow-empty` Clean up branch Closes #45
1 parent 2b1a4b9 commit fd1fbac

File tree

2 files changed

+151
-34
lines changed

2 files changed

+151
-34
lines changed

cherry_picker/cherry_picker.py

100644100755
Lines changed: 130 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,144 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
14
import click
25
import os
36
import subprocess
47
import webbrowser
58

69

710
@click.command()
8-
@click.option('--dry-run', is_flag=True)
11+
@click.option('--dry-run', is_flag=True,
12+
help="Prints out the commands, but not executed.")
913
@click.option('--push', 'pr_remote', metavar='REMOTE',
1014
help='git remote to use for PR branches', default='origin')
11-
@click.argument('commit_sha1', 'The commit sha1 to be cherry-picked')
15+
@click.option('--abort', 'abort', flag_value=True, default=None,
16+
help="")
17+
@click.option('--continue', 'abort', flag_value=False, default=None,
18+
help="")
19+
@click.argument('commit_sha1', 'The commit sha1 to be cherry-picked', nargs=1,
20+
default = "")
1221
@click.argument('branches', 'The branches to backport to', nargs=-1)
13-
def cherry_pick(dry_run, pr_remote, commit_sha1, branches):
22+
def cherry_pick(dry_run, pr_remote, abort, commit_sha1, branches):
23+
click.echo("\U0001F40D \U0001F352 \u26CF ")
1424
if not os.path.exists('./pyconfig.h.in'):
1525
os.chdir('./cpython/')
26+
1627
upstream = get_git_fetch_remote()
1728
username = get_forked_repo_name(pr_remote)
1829

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

33+
if abort is not None:
34+
if abort:
35+
abort_cherry_pick(dry_run=dry_run)
36+
else:
37+
continue_cherry_pick(username, pr_remote, dry_run=dry_run)
2238

23-
click.echo("fetching upstream ...")
24-
run_cmd(f"git fetch {upstream}", dry_run=dry_run)
39+
else:
40+
backport_branches(commit_sha1, branches, username, upstream, pr_remote,
41+
dry_run=dry_run)
2542

43+
def backport_branches(commit_sha1, branches, username, upstream, pr_remote,
44+
*, dry_run=False):
45+
if commit_sha1 == "":
46+
raise ValueError("Missing the commit_sha1 argument.")
2647
if not branches:
27-
raise ValueError("at least one branch is required")
48+
raise ValueError("At least one branch is required.")
49+
else:
50+
run_cmd(f"git fetch {upstream}", dry_run=dry_run)
2851

29-
for branch in get_sorted_branch(branches):
30-
click.echo(f"Now backporting '{commit_sha1}' into '{branch}'")
52+
for branch in get_sorted_branch(branches):
53+
click.echo(f"Now backporting '{commit_sha1}' into '{branch}'")
3154

32-
# git checkout -b 61e2bc7-3.5 upstream/3.5
33-
cherry_pick_branch = f"backport-{commit_sha1[:7]}-{branch}"
34-
cmd = f"git checkout -b {cherry_pick_branch} {upstream}/{branch}"
35-
run_cmd(cmd, dry_run=dry_run)
55+
# git checkout -b backport-61e2bc7-3.5 upstream/3.5
56+
cherry_pick_branch = f"backport-{commit_sha1[:7]}-{branch}"
57+
pr_url = get_pr_url(username, branch, cherry_pick_branch)
58+
cmd = f"git checkout -b {cherry_pick_branch} {upstream}/{branch}"
59+
run_cmd(cmd, dry_run=dry_run)
3660

37-
cmd = f"git cherry-pick -x {commit_sha1}"
38-
if run_cmd(cmd, dry_run=dry_run):
39-
cmd = f"git push {pr_remote} {cherry_pick_branch}"
40-
if not run_cmd(cmd, dry_run=dry_run):
41-
click.echo(f"Failed to push to {pr_remote} :(")
61+
cmd = f"git cherry-pick -x {commit_sha1}"
62+
if run_cmd(cmd, dry_run=dry_run):
63+
push_to_remote(pr_url, pr_remote, cherry_pick_branch, dry_run=dry_run)
64+
cleanup_branch(cherry_pick_branch, dry_run=dry_run)
4265
else:
43-
open_pr(username, branch, cherry_pick_branch, dry_run=dry_run)
44-
else:
45-
click.echo(f"Failed to cherry-pick {commit_sha1} into {branch} :(")
66+
click.echo(f"Failed to cherry-pick {commit_sha1} into {branch} \u2639")
67+
click.echo(" ... Stopping here. ")
68+
69+
click.echo("")
70+
click.echo("To continue and resolve the conflict: ")
71+
click.echo(" $ cd cpython")
72+
click.echo(" $ git status # to find out which files need attention")
73+
click.echo(" # Fix the conflict")
74+
click.echo(" $ git status # should now say `all conflicts fixed`")
75+
click.echo(" $ cd ..")
76+
click.echo(" $ python -m cherry_picker --continue")
77+
78+
click.echo("")
79+
click.echo("To abort the cherry-pick and cleanup: ")
80+
click.echo(" $ python -m cherry_picker --abort")
81+
4682

47-
cmd = "git checkout master"
83+
def abort_cherry_pick(*, dry_run=False):
84+
"""
85+
run `git cherry-pick --abort` and then clean up the branch
86+
"""
87+
if run_cmd("git cherry-pick --abort", dry_run=dry_run):
88+
cleanup_branch(get_current_branch(), dry_run=dry_run)
89+
90+
91+
def continue_cherry_pick(username, pr_remote, *, dry_run=False):
92+
"""
93+
git push origin <current_branch>
94+
open the PR
95+
clean up branch
96+
97+
"""
98+
cherry_pick_branch = get_current_branch()
99+
if cherry_pick_branch != 'master':
100+
101+
# this has the same effect as `git cherry-pick --continue`
102+
cmd = f"git commit -am 'Resolved.' --allow-empty"
48103
run_cmd(cmd, dry_run=dry_run)
49104

50-
cmd = f"git branch -D {cherry_pick_branch}"
51-
if run_cmd(cmd, dry_run=dry_run):
52-
if not dry_run:
53-
click.echo(f"branch {cherry_pick_branch} has been deleted.")
54-
else:
55-
click.echo(f"branch {cherry_pick_branch} NOT deleted.")
105+
base_branch = get_base_branch(cherry_pick_branch)
106+
pr_url = get_pr_url(username, base_branch, cherry_pick_branch)
107+
push_to_remote(pr_url, pr_remote, cherry_pick_branch, dry_run=dry_run)
108+
109+
cleanup_branch(cherry_pick_branch, dry_run=dry_run)
110+
else:
111+
click.echo(u"Refuse to push to master \U0001F61B")
112+
113+
114+
def get_base_branch(cherry_pick_branch):
115+
"""
116+
return '2.7' from 'backport-sha-2.7'
117+
"""
118+
return cherry_pick_branch[cherry_pick_branch.rfind('-')+1:]
119+
120+
121+
def cleanup_branch(cherry_pick_branch, *, dry_run=False):
122+
"""
123+
git checkout master
124+
git branch -D <branch to delete>
125+
"""
126+
cmd = "git checkout master"
127+
run_cmd(cmd, dry_run=dry_run)
128+
129+
cmd = f"git branch -D {cherry_pick_branch}"
130+
if run_cmd(cmd, dry_run=dry_run):
131+
if not dry_run:
132+
click.echo(f"branch {cherry_pick_branch} has been deleted.")
133+
else:
134+
click.echo(f"branch {cherry_pick_branch} NOT deleted.")
56135

136+
def push_to_remote(pr_url, pr_remote, cherry_pick_branch, *, dry_run=False):
137+
cmd = f"git push {pr_remote} {cherry_pick_branch}"
138+
if not run_cmd(cmd, dry_run=dry_run):
139+
click.echo(f"Failed to push to {pr_remote} \u2639")
140+
else:
141+
open_pr(pr_url, dry_run=dry_run)
57142

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

96180

97-
def open_pr(forked_repo, base_branch, cherry_pick_branch, *, dry_run=False):
181+
def get_pr_url(forked_repo, base_branch, cherry_pick_branch):
182+
"""
183+
construct the url for the pull request
184+
"""
185+
return f"https://github.com/python/cpython/compare/{base_branch}...{forked_repo}:{cherry_pick_branch}?expand=1"
186+
187+
188+
def open_pr(url, *, dry_run=False):
98189
"""
99-
construct the url for pull request and open it in the web browser
190+
open url in the web browser
100191
"""
101-
url = f"https://github.com/python/cpython/compare/{base_branch}...{forked_repo}:{cherry_pick_branch}?expand=1"
102192
if dry_run:
103193
click.echo(f" dry-run: Create new PR: {url}")
104194
return
105195
webbrowser.open_new_tab(url)
106196

107197

198+
def get_current_branch():
199+
"""
200+
Return the current branch
201+
"""
202+
cmd = "git symbolic-ref HEAD | sed 's!refs\/heads\/!!'"
203+
output = subprocess.check_output(cmd, shell=True)
204+
return output.strip().decode()
205+
206+
108207
def get_sorted_branch(branches):
109208
return sorted(
110209
branches,

cherry_picker/readme.rst

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Usage::
22
3-
python -m cherry_picker [--push REMOTE] [--dry-run] <commit_sha1> <branches>
3+
python -m cherry_picker [--push REMOTE] [--dry-run] [--abort/--continue] <commit_sha1> <branches>
44
55

66

@@ -43,16 +43,33 @@ repository are pushed to `origin`. If this is incorrect, then the correct
4343
remote will need be specified using the ``--push`` option (e.g.
4444
``--push pr`` to use a remote named ``pr``).
4545

46+
4647
Cherry-picking :snake: :cherries: :pick:
4748
==============
4849

4950
(Setup first! See prev section)
5051

5152
::
5253

53-
(venv) $ python -m cherry_picker <commit_sha1> <branches>
54+
(venv) $ python -m cherry_picker [--dry-run] [--abort/--continue] <commit_sha1> <branches>
55+
56+
The commit sha1 is obtained from the merged pull request on ``master``.
57+
5458

55-
The commit sha1 is obtained from the merged pull request on ``master``.
59+
Options
60+
-------
61+
62+
::
63+
64+
-- dry-run Dry Run Mode. Prints out the commands, but not executed.
65+
-- abort Stop at the first cherry-pick failure. This is the default
66+
value.
67+
-- continue Continue backporting other branches if it encounters failure.
68+
-- push REMOTE Specify the branch to push into. Default is 'origin'.
69+
70+
71+
Example
72+
-------
5673

5774
For example, to cherry-pick ``6de2b7817f-some-commit-sha1-d064`` into
5875
``3.5`` and ``3.6``:
@@ -106,6 +123,7 @@ steps it would execute without actually executing any of them. For example::
106123
dry_run: git checkout master
107124
dry_run: git branch -D backport-1e32a1b-3.5
108125

126+
109127
Creating Pull Requests
110128
======================
111129

0 commit comments

Comments
 (0)