##// END OF EJS Templates
Allow checking for backports via milestone
Skipper Seabold -
Show More
@@ -32,6 +32,7 b' from gh_api import ('
32 32 get_pull_request,
33 33 get_pull_request_files,
34 34 is_pull_request,
35 get_milestone_id,
35 36 )
36 37
37 38 def find_rejects(root='.'):
@@ -64,11 +65,11 b" def backport_pr(branch, num, project='ipython/ipython'):"
64 65 else:
65 66 req = urlopen(patch_url)
66 67 patch = req.read()
67
68
68 69 msg = "Backport PR #%i: %s" % (num, title) + '\n\n' + description
69 70 check = Popen(['git', 'apply', '--check', '--verbose'], stdin=PIPE)
70 71 a,b = check.communicate(patch)
71
72
72 73 if check.returncode:
73 74 print("patch did not apply, saving to {fname}".format(**locals()))
74 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 78 with open(fname, 'wb') as f:
78 79 f.write(patch)
79 80 return 1
80
81
81 82 p = Popen(['git', 'apply'], stdin=PIPE)
82 83 a,b = p.communicate(patch)
83
84
84 85 filenames = [ f['filename'] for f in files ]
85 86
86 87 check_call(['git', 'add'] + filenames)
87
88
88 89 check_call(['git', 'commit', '-m', msg])
89
90
90 91 print("PR #%i applied, with msg:" % num)
91 92 print()
92 93 print(msg)
93 94 print()
94
95
95 96 if branch != current_branch:
96 97 check_call(['git', 'checkout', current_branch])
97
98
98 99 return 0
99 100
100 101 backport_re = re.compile(r"[Bb]ackport.*?(\d+)")
@@ -107,18 +108,33 b' def already_backported(branch, since_tag=None):'
107 108 lines = check_output(cmd).decode('utf8')
108 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 112 """return set of PRs marked for backport"""
112 issues = get_issues_list("ipython/ipython",
113 labels=labels,
114 state='closed',
115 auth=True,
116 )
113 if labels is None and milestone is None:
114 raise ValueError("Specify one of labels or milestone.")
115 elif labels is not None and milestone is not None:
116 raise ValueError("Specify only one of labels or milestone.")
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 132 should_backport = set()
118 133 for issue in issues:
119 134 if not is_pull_request(issue):
120 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 138 if not pr['merged']:
123 139 print ("Marked PR closed without merge: %i" % pr['number'])
124 140 continue
@@ -126,11 +142,11 b' def should_backport(labels):'
126 142 return should_backport
127 143
128 144 if __name__ == '__main__':
129
145
130 146 if len(sys.argv) < 2:
131 147 print(__doc__)
132 148 sys.exit(1)
133
149
134 150 if len(sys.argv) < 3:
135 151 branch = sys.argv[1]
136 152 already = already_backported(branch)
@@ -139,5 +155,5 b" if __name__ == '__main__':"
139 155 for pr in should.difference(already):
140 156 print (pr)
141 157 sys.exit(0)
142
158
143 159 sys.exit(backport_pr(sys.argv[1], int(sys.argv[2])))
@@ -32,28 +32,28 b' class Obj(dict):'
32 32 return self[name]
33 33 except KeyError:
34 34 raise AttributeError(name)
35
35
36 36 def __setattr__(self, name, val):
37 37 self[name] = val
38 38
39 39 token = None
40 40 def get_auth_token():
41 41 global token
42
42
43 43 if token is not None:
44 44 return token
45
45
46 46 import keyring
47 47 token = keyring.get_password('github', fake_username)
48 48 if token is not None:
49 49 return token
50
50
51 51 print("Please enter your github username and password. These are not "
52 52 "stored, only used to get an oAuth token. You can revoke this at "
53 53 "any time on Github.")
54 54 user = input("Username: ")
55 55 pw = getpass.getpass("Password: ")
56
56
57 57 auth_request = {
58 58 "scopes": [
59 59 "public_repo",
@@ -88,13 +88,13 b" def post_gist(content, description='', filename='file', auth=False):"
88 88 }
89 89 }
90 90 }).encode('utf-8')
91
91
92 92 headers = make_auth_header() if auth else {}
93 93 response = requests.post("https://api.github.com/gists", data=post_data, headers=headers)
94 94 response.raise_for_status()
95 95 response_data = json.loads(response.text)
96 96 return response_data['html_url']
97
97
98 98 def get_pull_request(project, num, auth=False):
99 99 """get pull request info by number
100 100 """
@@ -156,6 +156,23 b' def get_issues_list(project, auth=False, **params):'
156 156 pages = get_paged_request(url, headers=headers, **params)
157 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 176 def is_pull_request(issue):
160 177 """Return True if the given issue is a pull request."""
161 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 250 name = os.path.basename(filename)
234 251 with open(filename, 'rb') as f:
235 252 filedata = f.read()
236
253
237 254 url = "https://api.github.com/repos/{project}/downloads".format(project=project)
238
255
239 256 payload = json.dumps(dict(name=name, size=len(filedata),
240 257 description=description))
241 258 response = requests.post(url, data=payload, headers=make_auth_header())
242 259 response.raise_for_status()
243 260 reply = json.loads(response.content)
244 261 s3_url = reply['s3_url']
245
262
246 263 fields = dict(
247 264 key=reply['path'],
248 265 acl=reply['acl'],
General Comments 0
You need to be logged in to leave comments. Login now