1
1
#!/usr/bin/env python3
2
- # Copyright (c) 2016 The Bitcoin Core developers
2
+ # Copyright (c) 2016-2017 Bitcoin Core Developers
3
3
# Distributed under the MIT software license, see the accompanying
4
4
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5
5
14
14
15
15
# In case of a clean merge that is accepted by the user, the local branch with
16
16
# name $BRANCH is overwritten with the merged result, and optionally pushed.
17
- from __future__ import division ,print_function ,unicode_literals
18
17
import os
19
18
from sys import stdin ,stdout ,stderr
20
19
import argparse
21
20
import hashlib
22
21
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
28
27
29
28
# External tools (can be overridden using environment)
30
29
GIT = os .getenv ('GIT' ,'git' )
@@ -45,24 +44,61 @@ def git_config_get(option, default=None):
45
44
'''
46
45
try :
47
46
return subprocess .check_output ([GIT ,'config' ,'--get' ,option ]).rstrip ().decode ('utf-8' )
48
- except subprocess .CalledProcessError as e :
47
+ except subprocess .CalledProcessError :
49
48
return default
50
49
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 ):
52
57
'''
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.
55
60
'''
56
61
try :
57
- req = Request ("https://api.github.com/repos/" + repo + "/pulls/" + pull )
58
- result = urlopen (req )
59
62
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
61
80
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
62
86
except Exception as e :
63
87
print ('Warning: unable to retrieve pull information from github: %s' % e )
64
88
return None
65
89
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
+
66
102
def ask_prompt (text ):
67
103
print (text ,end = " " ,file = stderr )
68
104
stderr .flush ()
@@ -127,12 +163,26 @@ def tree_sha512sum(commit='HEAD'):
127
163
raise IOError ('Non-zero return value executing git cat-file' )
128
164
return overall .hexdigest ()
129
165
166
+ def get_acks_from_comments (head_commit , comments ):
167
+ assert len (head_commit ) == 6
168
+ ack_str = '\n \n ACKs 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 ])
130
179
131
180
def parse_arguments ():
132
181
epilog = '''
133
182
In addition, you can set the following git configuration variables:
134
183
githubmerge.repository (mandatory),
135
184
user.signingkey (mandatory),
185
+ user.ghtoken (default: none).
136
186
githubmerge.host (default: [email protected] ),
137
187
githubmerge.branch (no default),
138
188
githubmerge.testcmd (default: none).
@@ -151,27 +201,35 @@ def main():
151
201
host = git_config_get (
'githubmerge.host' ,
'[email protected] ' )
152
202
opt_branch = git_config_get ('githubmerge.branch' ,None )
153
203
testcmd = git_config_get ('githubmerge.testcmd' )
204
+ ghtoken = git_config_get ('user.ghtoken' )
154
205
signingkey = git_config_get ('user.signingkey' )
155
206
if repo is None :
156
207
print ("ERROR: No repository configured. Use this command to set:" , file = stderr )
157
208
print ("git config githubmerge.repository <owner>/<repo>" , file = stderr )
158
- exit (1 )
209
+ sys . exit (1 )
159
210
if signingkey is None :
160
211
print ("ERROR: No GPG signing key set. Set one using:" ,file = stderr )
161
212
print ("git config --global user.signingkey <key>" ,file = stderr )
162
- exit (1 )
213
+ sys . exit (1 )
163
214
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
165
219
166
220
# Extract settings from command line
167
221
args = parse_arguments ()
168
222
pull = str (args .pull [0 ])
169
223
170
224
# Receive pull information from github
171
- info = retrieve_pr_info (repo ,pull )
225
+ info = retrieve_pr_info (repo ,pull , ghtoken )
172
226
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 ()
175
233
# precedence order for destination branch argument:
176
234
# - command line argument
177
235
# - githubmerge.branch setting
@@ -185,32 +243,28 @@ def main():
185
243
merge_branch = 'pull/' + pull + '/merge'
186
244
local_merge_branch = 'pull/' + pull + '/local-merge'
187
245
188
- devnull = open (os .devnull ,'w' )
246
+ devnull = open (os .devnull , 'w' , encoding = "utf8" )
189
247
try :
190
248
subprocess .check_call ([GIT ,'checkout' ,'-q' ,branch ])
191
- except subprocess .CalledProcessError as e :
249
+ except subprocess .CalledProcessError :
192
250
print ("ERROR: Cannot check out branch %s." % (branch ), file = stderr )
193
- exit (3 )
251
+ sys . exit (3 )
194
252
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 )
199
258
try :
200
259
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 :
202
261
print ("ERROR: Cannot find head of pull request #%s on %s." % (pull ,host_repo ), file = stderr )
203
- exit (3 )
262
+ sys . exit (3 )
204
263
try :
205
264
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 :
207
266
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 )
214
268
subprocess .check_call ([GIT ,'checkout' ,'-q' ,base_branch ])
215
269
subprocess .call ([GIT ,'branch' ,'-q' ,'-D' ,local_merge_branch ], stderr = devnull )
216
270
subprocess .check_call ([GIT ,'checkout' ,'-q' ,'-b' ,local_merge_branch ])
@@ -226,45 +280,46 @@ def main():
226
280
firstline = 'Merge #%s' % (pull ,)
227
281
message = firstline + '\n \n '
228
282
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 \n Pull 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 )
229
285
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 :
232
288
print ("ERROR: Cannot be merged cleanly." ,file = stderr )
233
289
subprocess .check_call ([GIT ,'merge' ,'--abort' ])
234
- exit (4 )
290
+ sys . exit (4 )
235
291
logmsg = subprocess .check_output ([GIT ,'log' ,'--pretty=format:%s' ,'-n' ,'1' ]).decode ('utf-8' )
236
292
if logmsg .rstrip () != firstline .rstrip ():
237
293
print ("ERROR: Creating merge failed (already merged?)." ,file = stderr )
238
- exit (4 )
294
+ sys . exit (4 )
239
295
240
296
symlink_files = get_symlink_files ()
241
297
for f in symlink_files :
242
298
print ("ERROR: File %s was a symlink" % f )
243
299
if len (symlink_files ) > 0 :
244
- exit (4 )
300
+ sys . exit (4 )
245
301
246
302
# Put tree SHA512 into the message
247
303
try :
248
304
first_sha512 = tree_sha512sum ()
249
305
message += '\n \n Tree-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 )
253
309
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 )
258
314
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 )
261
316
print ()
262
317
263
318
# Run test command if configured.
264
319
if testcmd :
265
320
if subprocess .call (testcmd ,shell = True ):
266
321
print ("ERROR: Running %s failed." % testcmd ,file = stderr )
267
- exit (5 )
322
+ sys . exit (5 )
268
323
269
324
# Show the created merge.
270
325
diff = subprocess .check_output ([GIT ,'diff' ,merge_branch + '..' + local_merge_branch ])
@@ -275,13 +330,7 @@ def main():
275
330
if reply .lower () == 'ignore' :
276
331
print ("Difference with github ignored." ,file = stderr )
277
332
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 )
285
334
else :
286
335
# Verify the result manually.
287
336
print ("Dropping you on a shell so you can try building/testing the merged source." ,file = stderr )
@@ -290,29 +339,25 @@ def main():
290
339
if os .path .isfile ('/etc/debian_version' ): # Show pull number on Debian default prompt
291
340
os .putenv ('debian_chroot' ,pull )
292
341
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 )
299
342
300
343
second_sha512 = tree_sha512sum ()
301
344
if first_sha512 != second_sha512 :
302
345
print ("ERROR: Tree hash changed unexpectedly" ,file = stderr )
303
- exit (8 )
346
+ sys . exit (8 )
304
347
305
348
# 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 )
316
361
317
362
# Put the result in branch.
318
363
subprocess .check_call ([GIT ,'checkout' ,'-q' ,branch ])
@@ -326,9 +371,14 @@ def main():
326
371
subprocess .call ([GIT ,'branch' ,'-q' ,'-D' ,local_merge_branch ],stderr = devnull )
327
372
328
373
# 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 )
332
381
333
382
if __name__ == '__main__' :
334
383
main ()
384
+
0 commit comments