##// END OF EJS Templates
get files list in backport_pr...
MinRK -
Show More
@@ -1,90 +1,98 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
5 Usage: backport_pr.py branch PR
6
6
7 e.g.:
7 e.g.:
8
8
9 backport_pr.py 0.13.1 123
9 backport_pr.py 0.13.1 123
10
10
11 to backport PR #123 onto branch 0.13.1
11 to backport PR #123 onto branch 0.13.1
12
12
13 """
13 """
14
14
15 from __future__ import print_function
15 from __future__ import print_function
16
16
17 import os
17 import os
18 import sys
18 import sys
19 from subprocess import Popen, PIPE, check_call, check_output
19 from subprocess import Popen, PIPE, check_call, check_output
20 from urllib import urlopen
20 from urllib import urlopen
21
21
22 from gh_api import get_pull_request
22 from gh_api import get_pull_request, get_pull_request_files
23
23
24 def find_rejects(root='.'):
24 def find_rejects(root='.'):
25 for dirname, dirs, files in os.walk(root):
25 for dirname, dirs, files in os.walk(root):
26 for fname in files:
26 for fname in files:
27 if fname.endswith('.rej'):
27 if fname.endswith('.rej'):
28 yield os.path.join(dirname, fname)
28 yield os.path.join(dirname, fname)
29
29
30 def get_current_branch():
30 def get_current_branch():
31 branches = check_output(['git', 'branch'])
31 branches = check_output(['git', 'branch'])
32 for branch in branches.splitlines():
32 for branch in branches.splitlines():
33 if branch.startswith('*'):
33 if branch.startswith('*'):
34 return branch[1:].strip()
34 return branch[1:].strip()
35
35
36 def backport_pr(branch, num, project='ipython/ipython'):
36 def backport_pr(branch, num, project='ipython/ipython'):
37 current_branch = get_current_branch()
37 current_branch = get_current_branch()
38 if branch != current_branch:
38 if branch != current_branch:
39 check_call(['git', 'checkout', branch])
39 check_call(['git', 'checkout', branch])
40 pr = get_pull_request(project, num)
40 pr = get_pull_request(project, num, auth=True)
41 files = get_pull_request_files(project, num, auth=True)
41 patch_url = pr['patch_url']
42 patch_url = pr['patch_url']
42 title = pr['title']
43 title = pr['title']
43 description = pr['body']
44 description = pr['body']
44 fname = "PR%i.patch" % num
45 fname = "PR%i.patch" % num
45 if os.path.exists(fname):
46 if os.path.exists(fname):
46 print("using patch from {fname}".format(**locals()))
47 print("using patch from {fname}".format(**locals()))
47 with open(fname) as f:
48 with open(fname) as f:
48 patch = f.read()
49 patch = f.read()
49 else:
50 else:
50 req = urlopen(patch_url)
51 req = urlopen(patch_url)
51 patch = req.read()
52 patch = req.read()
52
53
53 msg = "Backport PR #%i: %s" % (num, title) + '\n\n' + description
54 msg = "Backport PR #%i: %s" % (num, title) + '\n\n' + description
54 check = Popen(['git', 'apply', '--check', '--verbose'], stdin=PIPE)
55 check = Popen(['git', 'apply', '--check', '--verbose'], stdin=PIPE)
55 a,b = check.communicate(patch)
56 a,b = check.communicate(patch)
56
57
57 if check.returncode:
58 if check.returncode:
58 print("patch did not apply, saving to {fname}".format(**locals()))
59 print("patch did not apply, saving to {fname}".format(**locals()))
59 print("edit {fname} until `cat {fname} | git apply --check` succeeds".format(**locals()))
60 print("edit {fname} until `cat {fname} | git apply --check` succeeds".format(**locals()))
60 print("then run tools/backport_pr.py {num} again".format(**locals()))
61 print("then run tools/backport_pr.py {num} again".format(**locals()))
61 if not os.path.exists(fname):
62 if not os.path.exists(fname):
62 with open(fname, 'wb') as f:
63 with open(fname, 'wb') as f:
63 f.write(patch)
64 f.write(patch)
64 return 1
65 return 1
65
66
66 p = Popen(['git', 'apply'], stdin=PIPE)
67 p = Popen(['git', 'apply'], stdin=PIPE)
67 a,b = p.communicate(patch)
68 a,b = p.communicate(patch)
69
70 filenames = [ f['filename'] for f in files ]
71 add = Popen(['git', 'add'] + filenames)
72 add.wait()
73 if add.returncode:
74 print("git add %s failed!" % filenames)
75 return 1
68
76
69 commit = Popen(['git', 'commit', '-a', '-m', msg])
77 commit = Popen(['git', 'commit', '-m', msg])
70 commit.communicate()
78 commit.wait()
71 if commit.returncode:
79 if commit.returncode:
72 print("commit failed!")
80 print("commit failed!")
73 return 1
81 return 1
74 else:
82 else:
75 print("PR #%i applied, with msg:" % num)
83 print("PR #%i applied, with msg:" % num)
76 print()
84 print()
77 print(msg)
85 print(msg)
78 print()
86 print()
79
87
80 if branch != current_branch:
88 if branch != current_branch:
81 check_call(['git', 'checkout', current_branch])
89 check_call(['git', 'checkout', current_branch])
82
90
83 return 0
91 return 0
84
92
85 if __name__ == '__main__':
93 if __name__ == '__main__':
86 if len(sys.argv) < 3:
94 if len(sys.argv) < 3:
87 print(__doc__)
95 print(__doc__)
88 sys.exit(1)
96 sys.exit(1)
89
97
90 sys.exit(backport_pr(sys.argv[1], int(sys.argv[2])))
98 sys.exit(backport_pr(sys.argv[1], int(sys.argv[2])))
@@ -1,245 +1,254 b''
1 """Functions for Github API requests."""
1 """Functions for Github API requests."""
2 from __future__ import print_function
2 from __future__ import print_function
3
3
4 try:
4 try:
5 input = raw_input
5 input = raw_input
6 except NameError:
6 except NameError:
7 pass
7 pass
8
8
9 import os
9 import os
10 import re
10 import re
11 import sys
11 import sys
12
12
13 import requests
13 import requests
14 import getpass
14 import getpass
15 import json
15 import json
16
16
17 try:
17 try:
18 import requests_cache
18 import requests_cache
19 except ImportError:
19 except ImportError:
20 print("no cache")
20 print("no cache")
21 else:
21 else:
22 requests_cache.install_cache("gh_api")
22 requests_cache.install_cache("gh_api")
23
23
24 # Keyring stores passwords by a 'username', but we're not storing a username and
24 # Keyring stores passwords by a 'username', but we're not storing a username and
25 # password
25 # password
26 fake_username = 'ipython_tools'
26 fake_username = 'ipython_tools'
27
27
28 class Obj(dict):
28 class Obj(dict):
29 """Dictionary with attribute access to names."""
29 """Dictionary with attribute access to names."""
30 def __getattr__(self, name):
30 def __getattr__(self, name):
31 try:
31 try:
32 return self[name]
32 return self[name]
33 except KeyError:
33 except KeyError:
34 raise AttributeError(name)
34 raise AttributeError(name)
35
35
36 def __setattr__(self, name, val):
36 def __setattr__(self, name, val):
37 self[name] = val
37 self[name] = val
38
38
39 token = None
39 token = None
40 def get_auth_token():
40 def get_auth_token():
41 global token
41 global token
42
42
43 if token is not None:
43 if token is not None:
44 return token
44 return token
45
45
46 import keyring
46 import keyring
47 token = keyring.get_password('github', fake_username)
47 token = keyring.get_password('github', fake_username)
48 if token is not None:
48 if token is not None:
49 return token
49 return token
50
50
51 print("Please enter your github username and password. These are not "
51 print("Please enter your github username and password. These are not "
52 "stored, only used to get an oAuth token. You can revoke this at "
52 "stored, only used to get an oAuth token. You can revoke this at "
53 "any time on Github.")
53 "any time on Github.")
54 user = input("Username: ")
54 user = input("Username: ")
55 pw = getpass.getpass("Password: ")
55 pw = getpass.getpass("Password: ")
56
56
57 auth_request = {
57 auth_request = {
58 "scopes": [
58 "scopes": [
59 "public_repo",
59 "public_repo",
60 "gist"
60 "gist"
61 ],
61 ],
62 "note": "IPython tools",
62 "note": "IPython tools",
63 "note_url": "https://github.com/ipython/ipython/tree/master/tools",
63 "note_url": "https://github.com/ipython/ipython/tree/master/tools",
64 }
64 }
65 response = requests.post('https://api.github.com/authorizations',
65 response = requests.post('https://api.github.com/authorizations',
66 auth=(user, pw), data=json.dumps(auth_request))
66 auth=(user, pw), data=json.dumps(auth_request))
67 response.raise_for_status()
67 response.raise_for_status()
68 token = json.loads(response.text)['token']
68 token = json.loads(response.text)['token']
69 keyring.set_password('github', fake_username, token)
69 keyring.set_password('github', fake_username, token)
70 return token
70 return token
71
71
72 def make_auth_header():
72 def make_auth_header():
73 return {'Authorization': 'token ' + get_auth_token()}
73 return {'Authorization': 'token ' + get_auth_token()}
74
74
75 def post_issue_comment(project, num, body):
75 def post_issue_comment(project, num, body):
76 url = 'https://api.github.com/repos/{project}/issues/{num}/comments'.format(project=project, num=num)
76 url = 'https://api.github.com/repos/{project}/issues/{num}/comments'.format(project=project, num=num)
77 payload = json.dumps({'body': body})
77 payload = json.dumps({'body': body})
78 requests.post(url, data=payload, headers=make_auth_header())
78 requests.post(url, data=payload, headers=make_auth_header())
79
79
80 def post_gist(content, description='', filename='file', auth=False):
80 def post_gist(content, description='', filename='file', auth=False):
81 """Post some text to a Gist, and return the URL."""
81 """Post some text to a Gist, and return the URL."""
82 post_data = json.dumps({
82 post_data = json.dumps({
83 "description": description,
83 "description": description,
84 "public": True,
84 "public": True,
85 "files": {
85 "files": {
86 filename: {
86 filename: {
87 "content": content
87 "content": content
88 }
88 }
89 }
89 }
90 }).encode('utf-8')
90 }).encode('utf-8')
91
91
92 headers = make_auth_header() if auth else {}
92 headers = make_auth_header() if auth else {}
93 response = requests.post("https://api.github.com/gists", data=post_data, headers=headers)
93 response = requests.post("https://api.github.com/gists", data=post_data, headers=headers)
94 response.raise_for_status()
94 response.raise_for_status()
95 response_data = json.loads(response.text)
95 response_data = json.loads(response.text)
96 return response_data['html_url']
96 return response_data['html_url']
97
97
98 def get_pull_request(project, num, auth=False):
98 def get_pull_request(project, num, auth=False):
99 """get pull request info by number
99 """get pull request info by number
100 """
100 """
101 url = "https://api.github.com/repos/{project}/pulls/{num}".format(project=project, num=num)
101 url = "https://api.github.com/repos/{project}/pulls/{num}".format(project=project, num=num)
102 if auth:
102 if auth:
103 header = make_auth_header()
103 header = make_auth_header()
104 else:
104 else:
105 header = None
105 header = None
106 response = requests.get(url, headers=header)
106 response = requests.get(url, headers=header)
107 response.raise_for_status()
107 response.raise_for_status()
108 return json.loads(response.text, object_hook=Obj)
108 return json.loads(response.text, object_hook=Obj)
109
109
110 def get_pull_request_files(project, num, auth=False):
111 """get list of files in a pull request"""
112 url = "https://api.github.com/repos/{project}/pulls/{num}/files".format(project=project, num=num)
113 if auth:
114 header = make_auth_header()
115 else:
116 header = None
117 return get_paged_request(url, headers=header)
118
110 element_pat = re.compile(r'<(.+?)>')
119 element_pat = re.compile(r'<(.+?)>')
111 rel_pat = re.compile(r'rel=[\'"](\w+)[\'"]')
120 rel_pat = re.compile(r'rel=[\'"](\w+)[\'"]')
112
121
113 def get_paged_request(url, headers=None):
122 def get_paged_request(url, headers=None):
114 """get a full list, handling APIv3's paging"""
123 """get a full list, handling APIv3's paging"""
115 results = []
124 results = []
116 while True:
125 while True:
117 print("fetching %s" % url, file=sys.stderr)
126 print("fetching %s" % url, file=sys.stderr)
118 response = requests.get(url, headers=headers)
127 response = requests.get(url, headers=headers)
119 response.raise_for_status()
128 response.raise_for_status()
120 results.extend(response.json())
129 results.extend(response.json())
121 if 'next' in response.links:
130 if 'next' in response.links:
122 url = response.links['next']['url']
131 url = response.links['next']['url']
123 else:
132 else:
124 break
133 break
125 return results
134 return results
126
135
127 def get_pulls_list(project, state="closed", auth=False):
136 def get_pulls_list(project, state="closed", auth=False):
128 """get pull request list
137 """get pull request list
129 """
138 """
130 url = "https://api.github.com/repos/{project}/pulls?state={state}&per_page=100".format(project=project, state=state)
139 url = "https://api.github.com/repos/{project}/pulls?state={state}&per_page=100".format(project=project, state=state)
131 if auth:
140 if auth:
132 headers = make_auth_header()
141 headers = make_auth_header()
133 else:
142 else:
134 headers = None
143 headers = None
135 pages = get_paged_request(url, headers=headers)
144 pages = get_paged_request(url, headers=headers)
136 return pages
145 return pages
137
146
138 def get_issues_list(project, state="closed", auth=False):
147 def get_issues_list(project, state="closed", auth=False):
139 """get pull request list
148 """get pull request list
140 """
149 """
141 url = "https://api.github.com/repos/{project}/pulls?state={state}&per_page=100".format(project=project, state=state)
150 url = "https://api.github.com/repos/{project}/pulls?state={state}&per_page=100".format(project=project, state=state)
142 if auth:
151 if auth:
143 headers = make_auth_header()
152 headers = make_auth_header()
144 else:
153 else:
145 headers = None
154 headers = None
146 pages = get_paged_request(url, headers=headers)
155 pages = get_paged_request(url, headers=headers)
147 return pages
156 return pages
148
157
149 # encode_multipart_formdata is from urllib3.filepost
158 # encode_multipart_formdata is from urllib3.filepost
150 # The only change is to iter_fields, to enforce S3's required key ordering
159 # The only change is to iter_fields, to enforce S3's required key ordering
151
160
152 def iter_fields(fields):
161 def iter_fields(fields):
153 fields = fields.copy()
162 fields = fields.copy()
154 for key in ('key', 'acl', 'Filename', 'success_action_status', 'AWSAccessKeyId',
163 for key in ('key', 'acl', 'Filename', 'success_action_status', 'AWSAccessKeyId',
155 'Policy', 'Signature', 'Content-Type', 'file'):
164 'Policy', 'Signature', 'Content-Type', 'file'):
156 yield (key, fields.pop(key))
165 yield (key, fields.pop(key))
157 for (k,v) in fields.items():
166 for (k,v) in fields.items():
158 yield k,v
167 yield k,v
159
168
160 def encode_multipart_formdata(fields, boundary=None):
169 def encode_multipart_formdata(fields, boundary=None):
161 """
170 """
162 Encode a dictionary of ``fields`` using the multipart/form-data mime format.
171 Encode a dictionary of ``fields`` using the multipart/form-data mime format.
163
172
164 :param fields:
173 :param fields:
165 Dictionary of fields or list of (key, value) field tuples. The key is
174 Dictionary of fields or list of (key, value) field tuples. The key is
166 treated as the field name, and the value as the body of the form-data
175 treated as the field name, and the value as the body of the form-data
167 bytes. If the value is a tuple of two elements, then the first element
176 bytes. If the value is a tuple of two elements, then the first element
168 is treated as the filename of the form-data section.
177 is treated as the filename of the form-data section.
169
178
170 Field names and filenames must be unicode.
179 Field names and filenames must be unicode.
171
180
172 :param boundary:
181 :param boundary:
173 If not specified, then a random boundary will be generated using
182 If not specified, then a random boundary will be generated using
174 :func:`mimetools.choose_boundary`.
183 :func:`mimetools.choose_boundary`.
175 """
184 """
176 # copy requests imports in here:
185 # copy requests imports in here:
177 from io import BytesIO
186 from io import BytesIO
178 from requests.packages.urllib3.filepost import (
187 from requests.packages.urllib3.filepost import (
179 choose_boundary, six, writer, b, get_content_type
188 choose_boundary, six, writer, b, get_content_type
180 )
189 )
181 body = BytesIO()
190 body = BytesIO()
182 if boundary is None:
191 if boundary is None:
183 boundary = choose_boundary()
192 boundary = choose_boundary()
184
193
185 for fieldname, value in iter_fields(fields):
194 for fieldname, value in iter_fields(fields):
186 body.write(b('--%s\r\n' % (boundary)))
195 body.write(b('--%s\r\n' % (boundary)))
187
196
188 if isinstance(value, tuple):
197 if isinstance(value, tuple):
189 filename, data = value
198 filename, data = value
190 writer(body).write('Content-Disposition: form-data; name="%s"; '
199 writer(body).write('Content-Disposition: form-data; name="%s"; '
191 'filename="%s"\r\n' % (fieldname, filename))
200 'filename="%s"\r\n' % (fieldname, filename))
192 body.write(b('Content-Type: %s\r\n\r\n' %
201 body.write(b('Content-Type: %s\r\n\r\n' %
193 (get_content_type(filename))))
202 (get_content_type(filename))))
194 else:
203 else:
195 data = value
204 data = value
196 writer(body).write('Content-Disposition: form-data; name="%s"\r\n'
205 writer(body).write('Content-Disposition: form-data; name="%s"\r\n'
197 % (fieldname))
206 % (fieldname))
198 body.write(b'Content-Type: text/plain\r\n\r\n')
207 body.write(b'Content-Type: text/plain\r\n\r\n')
199
208
200 if isinstance(data, int):
209 if isinstance(data, int):
201 data = str(data) # Backwards compatibility
210 data = str(data) # Backwards compatibility
202 if isinstance(data, six.text_type):
211 if isinstance(data, six.text_type):
203 writer(body).write(data)
212 writer(body).write(data)
204 else:
213 else:
205 body.write(data)
214 body.write(data)
206
215
207 body.write(b'\r\n')
216 body.write(b'\r\n')
208
217
209 body.write(b('--%s--\r\n' % (boundary)))
218 body.write(b('--%s--\r\n' % (boundary)))
210
219
211 content_type = b('multipart/form-data; boundary=%s' % boundary)
220 content_type = b('multipart/form-data; boundary=%s' % boundary)
212
221
213 return body.getvalue(), content_type
222 return body.getvalue(), content_type
214
223
215
224
216 def post_download(project, filename, name=None, description=""):
225 def post_download(project, filename, name=None, description=""):
217 """Upload a file to the GitHub downloads area"""
226 """Upload a file to the GitHub downloads area"""
218 if name is None:
227 if name is None:
219 name = os.path.basename(filename)
228 name = os.path.basename(filename)
220 with open(filename, 'rb') as f:
229 with open(filename, 'rb') as f:
221 filedata = f.read()
230 filedata = f.read()
222
231
223 url = "https://api.github.com/repos/{project}/downloads".format(project=project)
232 url = "https://api.github.com/repos/{project}/downloads".format(project=project)
224
233
225 payload = json.dumps(dict(name=name, size=len(filedata),
234 payload = json.dumps(dict(name=name, size=len(filedata),
226 description=description))
235 description=description))
227 response = requests.post(url, data=payload, headers=make_auth_header())
236 response = requests.post(url, data=payload, headers=make_auth_header())
228 response.raise_for_status()
237 response.raise_for_status()
229 reply = json.loads(response.content)
238 reply = json.loads(response.content)
230 s3_url = reply['s3_url']
239 s3_url = reply['s3_url']
231
240
232 fields = dict(
241 fields = dict(
233 key=reply['path'],
242 key=reply['path'],
234 acl=reply['acl'],
243 acl=reply['acl'],
235 success_action_status=201,
244 success_action_status=201,
236 Filename=reply['name'],
245 Filename=reply['name'],
237 AWSAccessKeyId=reply['accesskeyid'],
246 AWSAccessKeyId=reply['accesskeyid'],
238 Policy=reply['policy'],
247 Policy=reply['policy'],
239 Signature=reply['signature'],
248 Signature=reply['signature'],
240 file=(reply['name'], filedata),
249 file=(reply['name'], filedata),
241 )
250 )
242 fields['Content-Type'] = reply['mime_type']
251 fields['Content-Type'] = reply['mime_type']
243 data, content_type = encode_multipart_formdata(fields)
252 data, content_type = encode_multipart_formdata(fields)
244 s3r = requests.post(s3_url, data=data, headers={'Content-Type': content_type})
253 s3r = requests.post(s3_url, data=data, headers={'Content-Type': content_type})
245 return s3r
254 return s3r
General Comments 0
You need to be logged in to leave comments. Login now