Skip to content

Commit b0d7d07

Browse files
committed
Update github-merge.py
1 parent fe7bf50 commit b0d7d07

File tree

1 file changed

+128
-78
lines changed

1 file changed

+128
-78
lines changed

contrib/devtools/github-merge.py

+128-78
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/usr/bin/env python3
2-
# Copyright (c) 2016 The Bitcoin Core developers
2+
# Copyright (c) 2016-2017 Bitcoin Core Developers
33
# Distributed under the MIT software license, see the accompanying
44
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
55

@@ -14,17 +14,16 @@
1414

1515
# In case of a clean merge that is accepted by the user, the local branch with
1616
# name $BRANCH is overwritten with the merged result, and optionally pushed.
17-
from __future__ import division,print_function,unicode_literals
1817
import os
1918
from sys import stdin,stdout,stderr
2019
import argparse
2120
import hashlib
2221
import subprocess
23-
import json,codecs
24-
try:
25-
from urllib.request import Request,urlopen
26-
except:
27-
from urllib2 import Request,urlopen
22+
import sys
23+
import json
24+
import codecs
25+
from urllib.request import Request, urlopen
26+
from urllib.error import HTTPError
2827

2928
# External tools (can be overridden using environment)
3029
GIT = os.getenv('GIT','git')
@@ -45,24 +44,61 @@ def git_config_get(option, default=None):
4544
'''
4645
try:
4746
return subprocess.check_output([GIT,'config','--get',option]).rstrip().decode('utf-8')
48-
except subprocess.CalledProcessError as e:
47+
except subprocess.CalledProcessError:
4948
return default
5049

51-
def retrieve_pr_info(repo,pull):
50+
def get_response(req_url, ghtoken):
51+
req = Request(req_url)
52+
if ghtoken is not None:
53+
req.add_header('Authorization', 'token ' + ghtoken)
54+
return urlopen(req)
55+
56+
def retrieve_json(req_url, ghtoken, use_pagination=False):
5257
'''
53-
Retrieve pull request information from github.
54-
Return None if no title can be found, or an error happens.
58+
Retrieve json from github.
59+
Return None if an error happens.
5560
'''
5661
try:
57-
req = Request("https://api.github.com/repos/"+repo+"/pulls/"+pull)
58-
result = urlopen(req)
5962
reader = codecs.getreader('utf-8')
60-
obj = json.load(reader(result))
63+
if not use_pagination:
64+
return json.load(reader(get_response(req_url, ghtoken)))
65+
66+
obj = []
67+
page_num = 1
68+
while True:
69+
req_url_page = '{}?page={}'.format(req_url, page_num)
70+
result = get_response(req_url_page, ghtoken)
71+
obj.extend(json.load(reader(result)))
72+
73+
link = result.headers.get('link', None)
74+
if link is not None:
75+
link_next = [l for l in link.split(',') if 'rel="next"' in l]
76+
if len(link_next) > 0:
77+
page_num = int(link_next[0][link_next[0].find("page=")+5:link_next[0].find(">")])
78+
continue
79+
break
6180
return obj
81+
except HTTPError as e:
82+
error_message = e.read()
83+
print('Warning: unable to retrieve pull information from github: %s' % e)
84+
print('Detailed error: %s' % error_message)
85+
return None
6286
except Exception as e:
6387
print('Warning: unable to retrieve pull information from github: %s' % e)
6488
return None
6589

90+
def retrieve_pr_info(repo,pull,ghtoken):
91+
req_url = "https://api.github.com/repos/"+repo+"/pulls/"+pull
92+
return retrieve_json(req_url,ghtoken)
93+
94+
def retrieve_pr_comments(repo,pull,ghtoken):
95+
req_url = "https://api.github.com/repos/"+repo+"/issues/"+pull+"/comments"
96+
return retrieve_json(req_url,ghtoken,use_pagination=True)
97+
98+
def retrieve_pr_reviews(repo,pull,ghtoken):
99+
req_url = "https://api.github.com/repos/"+repo+"/pulls/"+pull+"/reviews"
100+
return retrieve_json(req_url,ghtoken,use_pagination=True)
101+
66102
def ask_prompt(text):
67103
print(text,end=" ",file=stderr)
68104
stderr.flush()
@@ -127,12 +163,26 @@ def tree_sha512sum(commit='HEAD'):
127163
raise IOError('Non-zero return value executing git cat-file')
128164
return overall.hexdigest()
129165

166+
def get_acks_from_comments(head_commit, comments):
167+
assert len(head_commit) == 6
168+
ack_str ='\n\nACKs for commit {}:\n'.format(head_commit)
169+
for c in comments:
170+
review = [l for l in c['body'].split('\r\n') if 'ACK' in l and head_commit in l]
171+
if review:
172+
ack_str += ' {}:\n'.format(c['user']['login'])
173+
ack_str += ' {}\n'.format(review[0])
174+
return ack_str
175+
176+
def print_merge_details(pull, title, branch, base_branch, head_branch):
177+
print('%s#%s%s %s %sinto %s%s' % (ATTR_RESET+ATTR_PR,pull,ATTR_RESET,title,ATTR_RESET+ATTR_PR,branch,ATTR_RESET))
178+
subprocess.check_call([GIT,'log','--graph','--topo-order','--pretty=format:'+COMMIT_FORMAT,base_branch+'..'+head_branch])
130179

131180
def parse_arguments():
132181
epilog = '''
133182
In addition, you can set the following git configuration variables:
134183
githubmerge.repository (mandatory),
135184
user.signingkey (mandatory),
185+
user.ghtoken (default: none).
136186
githubmerge.host (default: [email protected]),
137187
githubmerge.branch (no default),
138188
githubmerge.testcmd (default: none).
@@ -151,27 +201,35 @@ def main():
151201
host = git_config_get('githubmerge.host','[email protected]')
152202
opt_branch = git_config_get('githubmerge.branch',None)
153203
testcmd = git_config_get('githubmerge.testcmd')
204+
ghtoken = git_config_get('user.ghtoken')
154205
signingkey = git_config_get('user.signingkey')
155206
if repo is None:
156207
print("ERROR: No repository configured. Use this command to set:", file=stderr)
157208
print("git config githubmerge.repository <owner>/<repo>", file=stderr)
158-
exit(1)
209+
sys.exit(1)
159210
if signingkey is None:
160211
print("ERROR: No GPG signing key set. Set one using:",file=stderr)
161212
print("git config --global user.signingkey <key>",file=stderr)
162-
exit(1)
213+
sys.exit(1)
163214

164-
host_repo = host+":"+repo # shortcut for push/pull target
215+
if host.startswith(('https:','http:')):
216+
host_repo = host+"/"+repo+".git"
217+
else:
218+
host_repo = host+":"+repo
165219

166220
# Extract settings from command line
167221
args = parse_arguments()
168222
pull = str(args.pull[0])
169223

170224
# Receive pull information from github
171-
info = retrieve_pr_info(repo,pull)
225+
info = retrieve_pr_info(repo,pull,ghtoken)
172226
if info is None:
173-
exit(1)
174-
title = info['title']
227+
sys.exit(1)
228+
comments = retrieve_pr_comments(repo,pull,ghtoken) + retrieve_pr_reviews(repo,pull,ghtoken)
229+
if comments is None:
230+
sys.exit(1)
231+
title = info['title'].strip()
232+
body = info['body'].strip()
175233
# precedence order for destination branch argument:
176234
# - command line argument
177235
# - githubmerge.branch setting
@@ -185,32 +243,28 @@ def main():
185243
merge_branch = 'pull/'+pull+'/merge'
186244
local_merge_branch = 'pull/'+pull+'/local-merge'
187245

188-
devnull = open(os.devnull,'w')
246+
devnull = open(os.devnull, 'w', encoding="utf8")
189247
try:
190248
subprocess.check_call([GIT,'checkout','-q',branch])
191-
except subprocess.CalledProcessError as e:
249+
except subprocess.CalledProcessError:
192250
print("ERROR: Cannot check out branch %s." % (branch), file=stderr)
193-
exit(3)
251+
sys.exit(3)
194252
try:
195-
subprocess.check_call([GIT,'fetch','-q',host_repo,'+refs/pull/'+pull+'/*:refs/heads/pull/'+pull+'/*'])
196-
except subprocess.CalledProcessError as e:
197-
print("ERROR: Cannot find pull request #%s on %s." % (pull,host_repo), file=stderr)
198-
exit(3)
253+
subprocess.check_call([GIT,'fetch','-q',host_repo,'+refs/pull/'+pull+'/*:refs/heads/pull/'+pull+'/*',
254+
'+refs/heads/'+branch+':refs/heads/'+base_branch])
255+
except subprocess.CalledProcessError:
256+
print("ERROR: Cannot find pull request #%s or branch %s on %s." % (pull,branch,host_repo), file=stderr)
257+
sys.exit(3)
199258
try:
200259
subprocess.check_call([GIT,'log','-q','-1','refs/heads/'+head_branch], stdout=devnull, stderr=stdout)
201-
except subprocess.CalledProcessError as e:
260+
except subprocess.CalledProcessError:
202261
print("ERROR: Cannot find head of pull request #%s on %s." % (pull,host_repo), file=stderr)
203-
exit(3)
262+
sys.exit(3)
204263
try:
205264
subprocess.check_call([GIT,'log','-q','-1','refs/heads/'+merge_branch], stdout=devnull, stderr=stdout)
206-
except subprocess.CalledProcessError as e:
265+
except subprocess.CalledProcessError:
207266
print("ERROR: Cannot find merge of pull request #%s on %s." % (pull,host_repo), file=stderr)
208-
exit(3)
209-
try:
210-
subprocess.check_call([GIT,'fetch','-q',host_repo,'+refs/heads/'+branch+':refs/heads/'+base_branch])
211-
except subprocess.CalledProcessError as e:
212-
print("ERROR: Cannot find branch %s on %s." % (branch,host_repo), file=stderr)
213-
exit(3)
267+
sys.exit(3)
214268
subprocess.check_call([GIT,'checkout','-q',base_branch])
215269
subprocess.call([GIT,'branch','-q','-D',local_merge_branch], stderr=devnull)
216270
subprocess.check_call([GIT,'checkout','-q','-b',local_merge_branch])
@@ -226,45 +280,46 @@ def main():
226280
firstline = 'Merge #%s' % (pull,)
227281
message = firstline + '\n\n'
228282
message += subprocess.check_output([GIT,'log','--no-merges','--topo-order','--pretty=format:%h %s (%an)',base_branch+'..'+head_branch]).decode('utf-8')
283+
message += '\n\nPull request description:\n\n ' + body.replace('\n', '\n ') + '\n'
284+
message += get_acks_from_comments(head_commit=subprocess.check_output([GIT,'log','-1','--pretty=format:%H',head_branch]).decode('utf-8')[:6], comments=comments)
229285
try:
230-
subprocess.check_call([GIT,'merge','-q','--commit','--no-edit','--no-ff','-m',message.encode('utf-8'),head_branch])
231-
except subprocess.CalledProcessError as e:
286+
subprocess.check_call([GIT,'merge','-q','--commit','--no-edit','--no-ff','--no-gpg-sign','-m',message.encode('utf-8'),head_branch])
287+
except subprocess.CalledProcessError:
232288
print("ERROR: Cannot be merged cleanly.",file=stderr)
233289
subprocess.check_call([GIT,'merge','--abort'])
234-
exit(4)
290+
sys.exit(4)
235291
logmsg = subprocess.check_output([GIT,'log','--pretty=format:%s','-n','1']).decode('utf-8')
236292
if logmsg.rstrip() != firstline.rstrip():
237293
print("ERROR: Creating merge failed (already merged?).",file=stderr)
238-
exit(4)
294+
sys.exit(4)
239295

240296
symlink_files = get_symlink_files()
241297
for f in symlink_files:
242298
print("ERROR: File %s was a symlink" % f)
243299
if len(symlink_files) > 0:
244-
exit(4)
300+
sys.exit(4)
245301

246302
# Put tree SHA512 into the message
247303
try:
248304
first_sha512 = tree_sha512sum()
249305
message += '\n\nTree-SHA512: ' + first_sha512
250-
except subprocess.CalledProcessError as e:
251-
printf("ERROR: Unable to compute tree hash")
252-
exit(4)
306+
except subprocess.CalledProcessError:
307+
print("ERROR: Unable to compute tree hash")
308+
sys.exit(4)
253309
try:
254-
subprocess.check_call([GIT,'commit','--amend','-m',message.encode('utf-8')])
255-
except subprocess.CalledProcessError as e:
256-
printf("ERROR: Cannot update message.",file=stderr)
257-
exit(4)
310+
subprocess.check_call([GIT,'commit','--amend','--no-gpg-sign','-m',message.encode('utf-8')])
311+
except subprocess.CalledProcessError:
312+
print("ERROR: Cannot update message.", file=stderr)
313+
sys.exit(4)
258314

259-
print('%s#%s%s %s %sinto %s%s' % (ATTR_RESET+ATTR_PR,pull,ATTR_RESET,title,ATTR_RESET+ATTR_PR,branch,ATTR_RESET))
260-
subprocess.check_call([GIT,'log','--graph','--topo-order','--pretty=format:'+COMMIT_FORMAT,base_branch+'..'+head_branch])
315+
print_merge_details(pull, title, branch, base_branch, head_branch)
261316
print()
262317

263318
# Run test command if configured.
264319
if testcmd:
265320
if subprocess.call(testcmd,shell=True):
266321
print("ERROR: Running %s failed." % testcmd,file=stderr)
267-
exit(5)
322+
sys.exit(5)
268323

269324
# Show the created merge.
270325
diff = subprocess.check_output([GIT,'diff',merge_branch+'..'+local_merge_branch])
@@ -275,13 +330,7 @@ def main():
275330
if reply.lower() == 'ignore':
276331
print("Difference with github ignored.",file=stderr)
277332
else:
278-
exit(6)
279-
reply = ask_prompt("Press 'd' to accept the diff.")
280-
if reply.lower() == 'd':
281-
print("Diff accepted.",file=stderr)
282-
else:
283-
print("ERROR: Diff rejected.",file=stderr)
284-
exit(6)
333+
sys.exit(6)
285334
else:
286335
# Verify the result manually.
287336
print("Dropping you on a shell so you can try building/testing the merged source.",file=stderr)
@@ -290,29 +339,25 @@ def main():
290339
if os.path.isfile('/etc/debian_version'): # Show pull number on Debian default prompt
291340
os.putenv('debian_chroot',pull)
292341
subprocess.call([BASH,'-i'])
293-
reply = ask_prompt("Type 'm' to accept the merge.")
294-
if reply.lower() == 'm':
295-
print("Merge accepted.",file=stderr)
296-
else:
297-
print("ERROR: Merge rejected.",file=stderr)
298-
exit(7)
299342

300343
second_sha512 = tree_sha512sum()
301344
if first_sha512 != second_sha512:
302345
print("ERROR: Tree hash changed unexpectedly",file=stderr)
303-
exit(8)
346+
sys.exit(8)
304347

305348
# Sign the merge commit.
306-
reply = ask_prompt("Type 's' to sign off on the merge.")
307-
if reply == 's':
308-
try:
309-
subprocess.check_call([GIT,'commit','-q','--gpg-sign','--amend','--no-edit'])
310-
except subprocess.CalledProcessError as e:
311-
print("Error signing, exiting.",file=stderr)
312-
exit(1)
313-
else:
314-
print("Not signing off on merge, exiting.",file=stderr)
315-
exit(1)
349+
print_merge_details(pull, title, branch, base_branch, head_branch)
350+
while True:
351+
reply = ask_prompt("Type 's' to sign off on the above merge, or 'x' to reject and exit.").lower()
352+
if reply == 's':
353+
try:
354+
subprocess.check_call([GIT,'commit','-q','--gpg-sign','--amend','--no-edit'])
355+
break
356+
except subprocess.CalledProcessError:
357+
print("Error while signing, asking again.",file=stderr)
358+
elif reply == 'x':
359+
print("Not signing off on merge, exiting.",file=stderr)
360+
sys.exit(1)
316361

317362
# Put the result in branch.
318363
subprocess.check_call([GIT,'checkout','-q',branch])
@@ -326,9 +371,14 @@ def main():
326371
subprocess.call([GIT,'branch','-q','-D',local_merge_branch],stderr=devnull)
327372

328373
# Push the result.
329-
reply = ask_prompt("Type 'push' to push the result to %s, branch %s." % (host_repo,branch))
330-
if reply.lower() == 'push':
331-
subprocess.check_call([GIT,'push',host_repo,'refs/heads/'+branch])
374+
while True:
375+
reply = ask_prompt("Type 'push' to push the result to %s, branch %s, or 'x' to exit without pushing." % (host_repo,branch)).lower()
376+
if reply == 'push':
377+
subprocess.check_call([GIT,'push',host_repo,'refs/heads/'+branch])
378+
break
379+
elif reply == 'x':
380+
sys.exit(1)
332381

333382
if __name__ == '__main__':
334383
main()
384+

0 commit comments

Comments
 (0)