1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
1
4
import click
2
5
import os
3
6
import subprocess
4
7
import webbrowser
5
8
6
9
7
10
@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." )
9
13
@click .option ('--push' , 'pr_remote' , metavar = 'REMOTE' ,
10
14
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 = "" )
12
21
@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 " )
14
24
if not os .path .exists ('./pyconfig.h.in' ):
15
25
os .chdir ('./cpython/' )
26
+
16
27
upstream = get_git_fetch_remote ()
17
28
username = get_forked_repo_name (pr_remote )
18
29
19
30
if dry_run :
20
31
click .echo ("Dry run requested, listing expected command sequence" )
21
32
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 )
22
38
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 )
25
42
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." )
26
47
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 )
28
51
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 } '" )
31
54
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 )
36
60
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 )
42
65
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
+
46
82
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"
48
103
run_cmd (cmd , dry_run = dry_run )
49
104
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." )
56
135
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 )
57
142
58
143
def get_git_fetch_remote ():
59
144
"""Get the remote name to use for upstream branches
@@ -70,7 +155,6 @@ def get_git_fetch_remote():
70
155
def get_forked_repo_name (pr_remote ):
71
156
"""
72
157
Return 'myusername' out of https://github.com/myusername/cpython
73
- :return:
74
158
"""
75
159
cmd = f"git config --get remote.{ pr_remote } .url"
76
160
raw_result = subprocess .check_output (cmd .split (), stderr = subprocess .STDOUT )
@@ -94,17 +178,32 @@ def run_cmd(cmd, *, dry_run=False):
94
178
return True
95
179
96
180
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 ):
98
189
"""
99
- construct the url for pull request and open it in the web browser
190
+ open url in the web browser
100
191
"""
101
- url = f"https://github.com/python/cpython/compare/{ base_branch } ...{ forked_repo } :{ cherry_pick_branch } ?expand=1"
102
192
if dry_run :
103
193
click .echo (f" dry-run: Create new PR: { url } " )
104
194
return
105
195
webbrowser .open_new_tab (url )
106
196
107
197
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
+
108
207
def get_sorted_branch (branches ):
109
208
return sorted (
110
209
branches ,
0 commit comments