##// END OF EJS Templates
Add repo support
Jonathan Frederic -
Show More
@@ -1,176 +1,189 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 """
2 """
3 Backport pull requests to a particular branch.
3 Backport pull requests to a particular branch.
4
4
5 Usage: backport_pr.py branch [PR] [PR2]
5 Usage: backport_pr.py [org/repository] branch [PR] [PR2]
6
6
7 e.g.:
7 e.g.:
8
8
9 python tools/backport_pr.py 0.13.1 123 155
9 python tools/backport_pr.py 0.13.1 123 155
10
10
11 to backport PR #123 onto branch 0.13.1
11 to backport PRs #123 and #155 onto branch 0.13.1
12
12
13 or
13 or
14
14
15 python tools/backport_pr.py 2.1
15 python tools/backport_pr.py 2.1
16
16
17 to see what PRs are marked for backport with milestone=2.1 that have yet to be applied
17 to see what PRs are marked for backport with milestone=2.1 that have yet to be applied
18 to branch 2.x.
18 to branch 2.x
19
20 or
21
22 python tools/backport_pr.py jupyter/notebook 0.13.1 123 155
23
24 to backport PRs #123 and #155 of the `jupyter/notebook` repo onto branch 0.13.1
25 of that repo.
19
26
20 """
27 """
21
28
22 from __future__ import print_function
29 from __future__ import print_function
23
30
24 import os
31 import os
25 import re
32 import re
26 import sys
33 import sys
27
34
28 from subprocess import Popen, PIPE, check_call, check_output
35 from subprocess import Popen, PIPE, check_call, check_output
29 try:
36 try:
30 from urllib.request import urlopen
37 from urllib.request import urlopen
31 except:
38 except:
32 from urllib import urlopen
39 from urllib import urlopen
33
40
34 from gh_api import (
41 from gh_api import (
35 get_issues_list,
42 get_issues_list,
36 get_pull_request,
43 get_pull_request,
37 get_pull_request_files,
44 get_pull_request_files,
38 is_pull_request,
45 is_pull_request,
39 get_milestone_id,
46 get_milestone_id,
40 )
47 )
41
48
42 def find_rejects(root='.'):
49 def find_rejects(root='.'):
43 for dirname, dirs, files in os.walk(root):
50 for dirname, dirs, files in os.walk(root):
44 for fname in files:
51 for fname in files:
45 if fname.endswith('.rej'):
52 if fname.endswith('.rej'):
46 yield os.path.join(dirname, fname)
53 yield os.path.join(dirname, fname)
47
54
48 def get_current_branch():
55 def get_current_branch():
49 branches = check_output(['git', 'branch'])
56 branches = check_output(['git', 'branch'])
50 for branch in branches.splitlines():
57 for branch in branches.splitlines():
51 if branch.startswith(b'*'):
58 if branch.startswith(b'*'):
52 return branch[1:].strip().decode('utf-8')
59 return branch[1:].strip().decode('utf-8')
53
60
54 def backport_pr(branch, num, project='ipython/ipython'):
61 def backport_pr(branch, num, project='ipython/ipython'):
55 current_branch = get_current_branch()
62 current_branch = get_current_branch()
56 if branch != current_branch:
63 if branch != current_branch:
57 check_call(['git', 'checkout', branch])
64 check_call(['git', 'checkout', branch])
58 check_call(['git', 'pull'])
65 check_call(['git', 'pull'])
59 pr = get_pull_request(project, num, auth=True)
66 pr = get_pull_request(project, num, auth=True)
60 files = get_pull_request_files(project, num, auth=True)
67 files = get_pull_request_files(project, num, auth=True)
61 patch_url = pr['patch_url']
68 patch_url = pr['patch_url']
62 title = pr['title']
69 title = pr['title']
63 description = pr['body']
70 description = pr['body']
64 fname = "PR%i.patch" % num
71 fname = "PR%i.patch" % num
65 if os.path.exists(fname):
72 if os.path.exists(fname):
66 print("using patch from {fname}".format(**locals()))
73 print("using patch from {fname}".format(**locals()))
67 with open(fname, 'rb') as f:
74 with open(fname, 'rb') as f:
68 patch = f.read()
75 patch = f.read()
69 else:
76 else:
70 req = urlopen(patch_url)
77 req = urlopen(patch_url)
71 patch = req.read()
78 patch = req.read()
72
79
73 lines = description.splitlines()
80 lines = description.splitlines()
74 if len(lines) > 5:
81 if len(lines) > 5:
75 lines = lines[:5] + ['...']
82 lines = lines[:5] + ['...']
76 description = '\n'.join(lines)
83 description = '\n'.join(lines)
77
84
78 msg = "Backport PR #%i: %s" % (num, title) + '\n\n' + description
85 msg = "Backport PR #%i: %s" % (num, title) + '\n\n' + description
79 check = Popen(['git', 'apply', '--check', '--verbose'], stdin=PIPE)
86 check = Popen(['git', 'apply', '--check', '--verbose'], stdin=PIPE)
80 a,b = check.communicate(patch)
87 a,b = check.communicate(patch)
81
88
82 if check.returncode:
89 if check.returncode:
83 print("patch did not apply, saving to {fname}".format(**locals()))
90 print("patch did not apply, saving to {fname}".format(**locals()))
84 print("edit {fname} until `cat {fname} | git apply --check` succeeds".format(**locals()))
91 print("edit {fname} until `cat {fname} | git apply --check` succeeds".format(**locals()))
85 print("then run tools/backport_pr.py {num} again".format(**locals()))
92 print("then run tools/backport_pr.py {num} again".format(**locals()))
86 if not os.path.exists(fname):
93 if not os.path.exists(fname):
87 with open(fname, 'wb') as f:
94 with open(fname, 'wb') as f:
88 f.write(patch)
95 f.write(patch)
89 return 1
96 return 1
90
97
91 p = Popen(['git', 'apply'], stdin=PIPE)
98 p = Popen(['git', 'apply'], stdin=PIPE)
92 a,b = p.communicate(patch)
99 a,b = p.communicate(patch)
93
100
94 filenames = [ f['filename'] for f in files ]
101 filenames = [ f['filename'] for f in files ]
95
102
96 check_call(['git', 'add'] + filenames)
103 check_call(['git', 'add'] + filenames)
97
104
98 check_call(['git', 'commit', '-m', msg])
105 check_call(['git', 'commit', '-m', msg])
99
106
100 print("PR #%i applied, with msg:" % num)
107 print("PR #%i applied, with msg:" % num)
101 print()
108 print()
102 print(msg)
109 print(msg)
103 print()
110 print()
104
111
105 if branch != current_branch:
112 if branch != current_branch:
106 check_call(['git', 'checkout', current_branch])
113 check_call(['git', 'checkout', current_branch])
107
114
108 return 0
115 return 0
109
116
110 backport_re = re.compile(r"(?:[Bb]ackport|[Mm]erge).*#(\d+)")
117 backport_re = re.compile(r"(?:[Bb]ackport|[Mm]erge).*#(\d+)")
111
118
112 def already_backported(branch, since_tag=None):
119 def already_backported(branch, since_tag=None):
113 """return set of PRs that have been backported already"""
120 """return set of PRs that have been backported already"""
114 if since_tag is None:
121 if since_tag is None:
115 since_tag = check_output(['git','describe', branch, '--abbrev=0']).decode('utf8').strip()
122 since_tag = check_output(['git','describe', branch, '--abbrev=0']).decode('utf8').strip()
116 cmd = ['git', 'log', '%s..%s' % (since_tag, branch), '--oneline']
123 cmd = ['git', 'log', '%s..%s' % (since_tag, branch), '--oneline']
117 lines = check_output(cmd).decode('utf8')
124 lines = check_output(cmd).decode('utf8')
118 return set(int(num) for num in backport_re.findall(lines))
125 return set(int(num) for num in backport_re.findall(lines))
119
126
120 def should_backport(labels=None, milestone=None, project='ipython/ipython'):
127 def should_backport(labels=None, milestone=None, project='ipython/ipython'):
121 """return set of PRs marked for backport"""
128 """return set of PRs marked for backport"""
122 if labels is None and milestone is None:
129 if labels is None and milestone is None:
123 raise ValueError("Specify one of labels or milestone.")
130 raise ValueError("Specify one of labels or milestone.")
124 elif labels is not None and milestone is not None:
131 elif labels is not None and milestone is not None:
125 raise ValueError("Specify only one of labels or milestone.")
132 raise ValueError("Specify only one of labels or milestone.")
126 if labels is not None:
133 if labels is not None:
127 issues = get_issues_list(project,
134 issues = get_issues_list(project,
128 labels=labels,
135 labels=labels,
129 state='closed',
136 state='closed',
130 auth=True,
137 auth=True,
131 )
138 )
132 else:
139 else:
133 milestone_id = get_milestone_id(project, milestone,
140 milestone_id = get_milestone_id(project, milestone,
134 auth=True)
141 auth=True)
135 issues = get_issues_list(project,
142 issues = get_issues_list(project,
136 milestone=milestone_id,
143 milestone=milestone_id,
137 state='closed',
144 state='closed',
138 auth=True,
145 auth=True,
139 )
146 )
140
147
141 should_backport = set()
148 should_backport = set()
142 for issue in issues:
149 for issue in issues:
143 if not is_pull_request(issue):
150 if not is_pull_request(issue):
144 continue
151 continue
145 pr = get_pull_request(project, issue['number'],
152 pr = get_pull_request(project, issue['number'],
146 auth=True)
153 auth=True)
147 if not pr['merged']:
154 if not pr['merged']:
148 print ("Marked PR closed without merge: %i" % pr['number'])
155 print ("Marked PR closed without merge: %i" % pr['number'])
149 continue
156 continue
150 if pr['base']['ref'] != 'master':
157 if pr['base']['ref'] != 'master':
151 continue
158 continue
152 should_backport.add(pr['number'])
159 should_backport.add(pr['number'])
153 return should_backport
160 return should_backport
154
161
155 if __name__ == '__main__':
162 if __name__ == '__main__':
156
163 project = 'ipython/ipython'
157 if len(sys.argv) < 2:
164 args = list(sys.argv)
165 if len(args) >= 2:
166 if '/' in args[1]:
167 project = args[1]
168 del args[1]
169
170 if len(args) < 2:
158 print(__doc__)
171 print(__doc__)
159 sys.exit(1)
172 sys.exit(1)
160
173
161 if len(sys.argv) < 3:
174 if len(args) < 3:
162 milestone = sys.argv[1]
175 milestone = args[1]
163 branch = milestone.split('.')[0] + '.x'
176 branch = milestone.split('.')[0] + '.x'
164 already = already_backported(branch)
177 already = already_backported(branch)
165 should = should_backport(milestone=milestone)
178 should = should_backport(milestone=milestone, project=project)
166 print ("The following PRs should be backported:")
179 print ("The following PRs should be backported:")
167 for pr in sorted(should.difference(already)):
180 for pr in sorted(should.difference(already)):
168 print (pr)
181 print (pr)
169 sys.exit(0)
182 sys.exit(0)
170
183
171 for prno in map(int, sys.argv[2:]):
184 for prno in map(int, args[2:]):
172 print("Backporting PR #%i" % prno)
185 print("Backporting PR #%i" % prno)
173 rc = backport_pr(sys.argv[1], prno)
186 rc = backport_pr(args[1], prno, project=project)
174 if rc:
187 if rc:
175 print("Backporting PR #%i failed" % prno)
188 print("Backporting PR #%i failed" % prno)
176 sys.exit(rc)
189 sys.exit(rc)
General Comments 0
You need to be logged in to leave comments. Login now