Skip to content

Commit da6d18b

Browse files
committed
devtools: replace github-merge with python version
This is meant to be a direct translation of the bash script, with the difference that it retrieves the PR title from github, thus creating pull messages like: Merge bitcoin#12345: Expose transaction temperature over RPC
1 parent 668906f commit da6d18b

File tree

4 files changed

+227
-189
lines changed

4 files changed

+227
-189
lines changed

contrib/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ Repository Tools
1111

1212
### [Developer tools](/contrib/devtools) ###
1313
Specific tools for developers working on this repository.
14-
Contains the script `github-merge.sh` for merging github pull requests securely and signing them using GPG.
14+
Contains the script `github-merge.py` for merging github pull requests securely and signing them using GPG.
1515

1616
### [Verify-Commits](/contrib/verify-commits) ###
17-
Tool to verify that every merge commit was signed by a developer using the above `github-merge.sh` script.
17+
Tool to verify that every merge commit was signed by a developer using the above `github-merge.py` script.
1818

1919
### [Linearize](/contrib/linearize) ###
2020
Construct a linear, no-fork, best version of the blockchain.

contrib/devtools/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,14 @@ Usage: `git-subtree-check.sh DIR COMMIT`
5656

5757
`COMMIT` may be omitted, in which case `HEAD` is used.
5858

59-
github-merge.sh
59+
github-merge.py
6060
===============
6161

6262
A small script to automate merging pull-requests securely and sign them with GPG.
6363

6464
For example:
6565

66-
./github-merge.sh bitcoin/bitcoin 3077
66+
./github-merge.py 3077
6767

6868
(in any git repository) will help you merge pull request #3077 for the
6969
bitcoin/bitcoin repository.

contrib/devtools/github-merge.py

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
#!/usr/bin/env python2
2+
# Copyright (c) 2016 Bitcoin Core Developers
3+
# Distributed under the MIT software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
6+
# This script will locally construct a merge commit for a pull request on a
7+
# github repository, inspect it, sign it and optionally push it.
8+
9+
# The following temporary branches are created/overwritten and deleted:
10+
# * pull/$PULL/base (the current master we're merging onto)
11+
# * pull/$PULL/head (the current state of the remote pull request)
12+
# * pull/$PULL/merge (github's merge)
13+
# * pull/$PULL/local-merge (our merge)
14+
15+
# In case of a clean merge that is accepted by the user, the local branch with
16+
# name $BRANCH is overwritten with the merged result, and optionally pushed.
17+
from __future__ import division,print_function,unicode_literals
18+
import os,sys
19+
from sys import stdin,stdout,stderr
20+
import argparse
21+
import subprocess
22+
23+
# External tools (can be overridden using environment)
24+
GIT = os.getenv('GIT','git')
25+
BASH = os.getenv('BASH','bash')
26+
27+
def git_config_get(option, default=None):
28+
'''
29+
Get named configuration option from git repository.
30+
'''
31+
try:
32+
return subprocess.check_output([GIT,'config','--get',option]).rstrip()
33+
except subprocess.CalledProcessError as e:
34+
return default
35+
36+
def retrieve_pr_title(repo,pull):
37+
'''
38+
Retrieve pull request title from github.
39+
Return None if no title can be found, or an error happens.
40+
'''
41+
import urllib2,json
42+
try:
43+
req = urllib2.Request("https://api.github.com/repos/"+repo+"/pulls/"+pull)
44+
result = urllib2.urlopen(req)
45+
result = json.load(result)
46+
return result['title']
47+
except Exception as e:
48+
print('Warning: unable to retrieve pull title from github: %s' % e)
49+
return None
50+
51+
def ask_prompt(text):
52+
print(text,end=" ",file=stderr)
53+
reply = stdin.readline().rstrip()
54+
print("",file=stderr)
55+
return reply
56+
57+
def parse_arguments(branch):
58+
epilog = '''
59+
In addition, you can set the following git configuration variables:
60+
githubmerge.repository (mandatory),
61+
user.signingkey (mandatory),
62+
githubmerge.host (default: [email protected]),
63+
githubmerge.branch (default: master),
64+
githubmerge.testcmd (default: none).
65+
'''
66+
parser = argparse.ArgumentParser(description='Utility to merge, sign and push github pull requests',
67+
epilog=epilog)
68+
parser.add_argument('pull', metavar='PULL', type=int, nargs=1,
69+
help='Pull request ID to merge')
70+
parser.add_argument('branch', metavar='BRANCH', type=str, nargs='?',
71+
default=branch, help='Branch to merge against (default: '+branch+')')
72+
return parser.parse_args()
73+
74+
def main():
75+
# Extract settings from git repo
76+
repo = git_config_get('githubmerge.repository')
77+
host = git_config_get('githubmerge.host','[email protected]')
78+
branch = git_config_get('githubmerge.branch','master')
79+
testcmd = git_config_get('githubmerge.testcmd')
80+
signingkey = git_config_get('user.signingkey')
81+
if repo is None:
82+
print("ERROR: No repository configured. Use this command to set:", file=stderr)
83+
print("git config githubmerge.repository <owner>/<repo>", file=stderr)
84+
exit(1)
85+
if signingkey is None:
86+
print("ERROR: No GPG signing key set. Set one using:",file=stderr)
87+
print("git config --global user.signingkey <key>",file=stderr)
88+
exit(1)
89+
90+
host_repo = host+":"+repo # shortcut for push/pull target
91+
92+
# Extract settings from command line
93+
args = parse_arguments(branch)
94+
pull = str(args.pull[0])
95+
branch = args.branch
96+
97+
# Initialize source branches
98+
head_branch = 'pull/'+pull+'/head'
99+
base_branch = 'pull/'+pull+'/base'
100+
merge_branch = 'pull/'+pull+'/merge'
101+
local_merge_branch = 'pull/'+pull+'/local-merge'
102+
103+
devnull = open(os.devnull,'w')
104+
try:
105+
subprocess.check_call([GIT,'checkout','-q',branch])
106+
except subprocess.CalledProcessError as e:
107+
print("ERROR: Cannot check out branch %s." % (branch), file=stderr)
108+
exit(3)
109+
try:
110+
subprocess.check_call([GIT,'fetch','-q',host_repo,'+refs/pull/'+pull+'/*:refs/heads/pull/'+pull+'/*'])
111+
except subprocess.CalledProcessError as e:
112+
print("ERROR: Cannot find pull request #%s on %s." % (pull,host_repo), file=stderr)
113+
exit(3)
114+
try:
115+
subprocess.check_call([GIT,'log','-q','-1','refs/heads/'+head_branch], stdout=devnull, stderr=stdout)
116+
except subprocess.CalledProcessError as e:
117+
print("ERROR: Cannot find head of pull request #%s on %s." % (pull,host_repo), file=stderr)
118+
exit(3)
119+
try:
120+
subprocess.check_call([GIT,'log','-q','-1','refs/heads/'+merge_branch], stdout=devnull, stderr=stdout)
121+
except subprocess.CalledProcessError as e:
122+
print("ERROR: Cannot find merge of pull request #%s on %s." % (pull,host_repo), file=stderr)
123+
exit(3)
124+
try:
125+
subprocess.check_call([GIT,'fetch','-q',host_repo,'+refs/heads/'+branch+':refs/heads/'+base_branch])
126+
except subprocess.CalledProcessError as e:
127+
print("ERROR: Cannot find branch %s on %s." % (branch,host_repo), file=stderr)
128+
exit(3)
129+
subprocess.check_call([GIT,'checkout','-q',base_branch])
130+
subprocess.call([GIT,'branch','-q','-D',local_merge_branch], stderr=devnull)
131+
subprocess.check_call([GIT,'checkout','-q','-b',local_merge_branch])
132+
133+
try:
134+
# Create unsigned merge commit.
135+
title = retrieve_pr_title(repo,pull)
136+
if title:
137+
firstline = 'Merge #%s: %s' % (pull,title)
138+
else:
139+
firstline = 'Merge #%s' % (pull,)
140+
message = firstline + '\n\n'
141+
message += subprocess.check_output([GIT,'log','--no-merges','--topo-order','--pretty=format:%h %s (%an)',base_branch+'..'+head_branch])
142+
try:
143+
subprocess.check_call([GIT,'merge','-q','--commit','--no-edit','--no-ff','-m',message,head_branch])
144+
except subprocess.CalledProcessError as e:
145+
print("ERROR: Cannot be merged cleanly.",file=stderr)
146+
subprocess.check_call([GIT,'merge','--abort'])
147+
exit(4)
148+
logmsg = subprocess.check_output([GIT,'log','--pretty=format:%s','-n','1'])
149+
if logmsg.rstrip() != firstline.rstrip():
150+
print("ERROR: Creating merge failed (already merged?).",file=stderr)
151+
exit(4)
152+
153+
# Run test command if configured.
154+
if testcmd:
155+
# Go up to the repository's root.
156+
toplevel = subprocess.check_output([GIT,'rev-parse','--show-toplevel'])
157+
os.chdir(toplevel)
158+
if subprocess.call(testcmd,shell=True):
159+
print("ERROR: Running %s failed." % testcmd,file=stderr)
160+
exit(5)
161+
162+
# Show the created merge.
163+
diff = subprocess.check_output([GIT,'diff',merge_branch+'..'+local_merge_branch])
164+
subprocess.check_call([GIT,'diff',base_branch+'..'+local_merge_branch])
165+
if diff:
166+
print("WARNING: merge differs from github!",file=stderr)
167+
reply = ask_prompt("Type 'ignore' to continue.")
168+
if reply.lower() == 'ignore':
169+
print("Difference with github ignored.",file=stderr)
170+
else:
171+
exit(6)
172+
reply = ask_prompt("Press 'd' to accept the diff.")
173+
if reply.lower() == 'd':
174+
print("Diff accepted.",file=stderr)
175+
else:
176+
print("ERROR: Diff rejected.",file=stderr)
177+
exit(6)
178+
else:
179+
# Verify the result manually.
180+
print("Dropping you on a shell so you can try building/testing the merged source.",file=stderr)
181+
print("Run 'git diff HEAD~' to show the changes being merged.",file=stderr)
182+
print("Type 'exit' when done.",file=stderr)
183+
if os.path.isfile('/etc/debian_version'): # Show pull number on Debian default prompt
184+
os.putenv('debian_chroot',pull)
185+
subprocess.call([BASH,'-i'])
186+
reply = ask_prompt("Type 'm' to accept the merge.")
187+
if reply.lower() == 'm':
188+
print("Merge accepted.",file=stderr)
189+
else:
190+
print("ERROR: Merge rejected.",file=stderr)
191+
exit(7)
192+
193+
# Sign the merge commit.
194+
reply = ask_prompt("Type 's' to sign off on the merge.")
195+
if reply == 's':
196+
try:
197+
subprocess.check_call([GIT,'commit','-q','--gpg-sign','--amend','--no-edit'])
198+
except subprocess.CalledProcessError as e:
199+
print("Error signing, exiting.",file=stderr)
200+
exit(1)
201+
else:
202+
print("Not signing off on merge, exiting.",file=stderr)
203+
exit(1)
204+
205+
# Put the result in branch.
206+
subprocess.check_call([GIT,'checkout','-q',branch])
207+
subprocess.check_call([GIT,'reset','-q','--hard',local_merge_branch])
208+
finally:
209+
# Clean up temporary branches.
210+
subprocess.call([GIT,'checkout','-q',branch])
211+
subprocess.call([GIT,'branch','-q','-D',head_branch],stderr=devnull)
212+
subprocess.call([GIT,'branch','-q','-D',base_branch],stderr=devnull)
213+
subprocess.call([GIT,'branch','-q','-D',merge_branch],stderr=devnull)
214+
subprocess.call([GIT,'branch','-q','-D',local_merge_branch],stderr=devnull)
215+
216+
# Push the result.
217+
reply = ask_prompt("Type 'push' to push the result to %s, branch %s." % (host_repo,branch))
218+
if reply.lower() == 'push':
219+
subprocess.check_call([GIT,'push',host_repo,'refs/heads/'+branch])
220+
221+
if __name__ == '__main__':
222+
main()
223+

0 commit comments

Comments
 (0)