##// END OF EJS Templates
Allow checking for backports via milestone
Skipper Seabold -
Show More
@@ -32,6 +32,7 b' from gh_api import ('
32 get_pull_request,
32 get_pull_request,
33 get_pull_request_files,
33 get_pull_request_files,
34 is_pull_request,
34 is_pull_request,
35 get_milestone_id,
35 )
36 )
36
37
37 def find_rejects(root='.'):
38 def find_rejects(root='.'):
@@ -64,11 +65,11 b" def backport_pr(branch, num, project='ipython/ipython'):"
64 else:
65 else:
65 req = urlopen(patch_url)
66 req = urlopen(patch_url)
66 patch = req.read()
67 patch = req.read()
67
68
68 msg = "Backport PR #%i: %s" % (num, title) + '\n\n' + description
69 msg = "Backport PR #%i: %s" % (num, title) + '\n\n' + description
69 check = Popen(['git', 'apply', '--check', '--verbose'], stdin=PIPE)
70 check = Popen(['git', 'apply', '--check', '--verbose'], stdin=PIPE)
70 a,b = check.communicate(patch)
71 a,b = check.communicate(patch)
71
72
72 if check.returncode:
73 if check.returncode:
73 print("patch did not apply, saving to {fname}".format(**locals()))
74 print("patch did not apply, saving to {fname}".format(**locals()))
74 print("edit {fname} until `cat {fname} | git apply --check` succeeds".format(**locals()))
75 print("edit {fname} until `cat {fname} | git apply --check` succeeds".format(**locals()))
@@ -77,24 +78,24 b" def backport_pr(branch, num, project='ipython/ipython'):"
77 with open(fname, 'wb') as f:
78 with open(fname, 'wb') as f:
78 f.write(patch)
79 f.write(patch)
79 return 1
80 return 1
80
81
81 p = Popen(['git', 'apply'], stdin=PIPE)
82 p = Popen(['git', 'apply'], stdin=PIPE)
82 a,b = p.communicate(patch)
83 a,b = p.communicate(patch)
83
84
84 filenames = [ f['filename'] for f in files ]
85 filenames = [ f['filename'] for f in files ]
85
86
86 check_call(['git', 'add'] + filenames)
87 check_call(['git', 'add'] + filenames)
87
88
88 check_call(['git', 'commit', '-m', msg])
89 check_call(['git', 'commit', '-m', msg])
89
90
90 print("PR #%i applied, with msg:" % num)
91 print("PR #%i applied, with msg:" % num)
91 print()
92 print()
92 print(msg)
93 print(msg)
93 print()
94 print()
94
95
95 if branch != current_branch:
96 if branch != current_branch:
96 check_call(['git', 'checkout', current_branch])
97 check_call(['git', 'checkout', current_branch])
97
98
98 return 0
99 return 0
99
100
100 backport_re = re.compile(r"[Bb]ackport.*?(\d+)")
101 backport_re = re.compile(r"[Bb]ackport.*?(\d+)")
@@ -107,18 +108,33 b' def already_backported(branch, since_tag=None):'
107 lines = check_output(cmd).decode('utf8')
108 lines = check_output(cmd).decode('utf8')
108 return set(int(num) for num in backport_re.findall(lines))
109 return set(int(num) for num in backport_re.findall(lines))
109
110
110 def should_backport(labels):
111 def should_backport(labels=None, milestone=None):
111 """return set of PRs marked for backport"""
112 """return set of PRs marked for backport"""
112 issues = get_issues_list("ipython/ipython",
113 if labels is None and milestone is None:
113 labels=labels,
114 raise ValueError("Specify one of labels or milestone.")
114 state='closed',
115 elif labels is not None and milestone is not None:
115 auth=True,
116 raise ValueError("Specify only one of labels or milestone.")
116 )
117 if labels is not None:
118 issues = get_issues_list("ipython/ipython",
119 labels=labels,
120 state='closed',
121 auth=True,
122 )
123 else:
124 milestone_id = get_milestone_id("ipython/ipython", milestone,
125 auth=True)
126 issues = get_issues_list("ipython/ipython",
127 milestone=milestone_id,
128 state='closed',
129 auth=True,
130 )
131
117 should_backport = set()
132 should_backport = set()
118 for issue in issues:
133 for issue in issues:
119 if not is_pull_request(issue):
134 if not is_pull_request(issue):
120 continue
135 continue
121 pr = get_pull_request("ipython/ipython", issue['number'], auth=True)
136 pr = get_pull_request("ipython/ipython", issue['number'],
137 auth=True)
122 if not pr['merged']:
138 if not pr['merged']:
123 print ("Marked PR closed without merge: %i" % pr['number'])
139 print ("Marked PR closed without merge: %i" % pr['number'])
124 continue
140 continue
@@ -126,11 +142,11 b' def should_backport(labels):'
126 return should_backport
142 return should_backport
127
143
128 if __name__ == '__main__':
144 if __name__ == '__main__':
129
145
130 if len(sys.argv) < 2:
146 if len(sys.argv) < 2:
131 print(__doc__)
147 print(__doc__)
132 sys.exit(1)
148 sys.exit(1)
133
149
134 if len(sys.argv) < 3:
150 if len(sys.argv) < 3:
135 branch = sys.argv[1]
151 branch = sys.argv[1]
136 already = already_backported(branch)
152 already = already_backported(branch)
@@ -139,5 +155,5 b" if __name__ == '__main__':"
139 for pr in should.difference(already):
155 for pr in should.difference(already):
140 print (pr)
156 print (pr)
141 sys.exit(0)
157 sys.exit(0)
142
158
143 sys.exit(backport_pr(sys.argv[1], int(sys.argv[2])))
159 sys.exit(backport_pr(sys.argv[1], int(sys.argv[2])))
@@ -32,28 +32,28 b' class Obj(dict):'
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",
@@ -88,13 +88,13 b" def post_gist(content, description='', filename='file', auth=False):"
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 """
@@ -156,6 +156,23 b' def get_issues_list(project, auth=False, **params):'
156 pages = get_paged_request(url, headers=headers, **params)
156 pages = get_paged_request(url, headers=headers, **params)
157 return pages
157 return pages
158
158
159 def get_milestones(project, auth=False, **params):
160 url = "https://api.github.com/repos/{project}/milestones".format(project=project)
161 if auth:
162 headers = make_auth_header()
163 else:
164 headers = None
165 pages = get_paged_request(url, headers=headers, **params)
166 return pages
167
168 def get_milestone_id(project, milestone, auth=False, **params):
169 pages = get_milestones(project, auth=auth, **params)
170 for page in pages:
171 if page['title'] == milestone:
172 return page['number']
173 else:
174 raise ValueError("milestone %s not found" % milestone)
175
159 def is_pull_request(issue):
176 def is_pull_request(issue):
160 """Return True if the given issue is a pull request."""
177 """Return True if the given issue is a pull request."""
161 return bool(issue.get('pull_request', {}).get('html_url', None))
178 return bool(issue.get('pull_request', {}).get('html_url', None))
@@ -233,16 +250,16 b' def post_download(project, filename, name=None, description=""):'
233 name = os.path.basename(filename)
250 name = os.path.basename(filename)
234 with open(filename, 'rb') as f:
251 with open(filename, 'rb') as f:
235 filedata = f.read()
252 filedata = f.read()
236
253
237 url = "https://api.github.com/repos/{project}/downloads".format(project=project)
254 url = "https://api.github.com/repos/{project}/downloads".format(project=project)
238
255
239 payload = json.dumps(dict(name=name, size=len(filedata),
256 payload = json.dumps(dict(name=name, size=len(filedata),
240 description=description))
257 description=description))
241 response = requests.post(url, data=payload, headers=make_auth_header())
258 response = requests.post(url, data=payload, headers=make_auth_header())
242 response.raise_for_status()
259 response.raise_for_status()
243 reply = json.loads(response.content)
260 reply = json.loads(response.content)
244 s3_url = reply['s3_url']
261 s3_url = reply['s3_url']
245
262
246 fields = dict(
263 fields = dict(
247 key=reply['path'],
264 key=reply['path'],
248 acl=reply['acl'],
265 acl=reply['acl'],
General Comments 0
You need to be logged in to leave comments. Login now