Browse Source
da6d18b
devtools: replace github-merge with python version (Wladimir J. van der Laan)
0.13
Wladimir J. van der Laan
9 years ago
4 changed files with 227 additions and 189 deletions
@ -0,0 +1,223 @@
@@ -0,0 +1,223 @@
|
||||
#!/usr/bin/env python2 |
||||
# Copyright (c) 2016 Bitcoin Core Developers |
||||
# Distributed under the MIT software license, see the accompanying |
||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php. |
||||
|
||||
# This script will locally construct a merge commit for a pull request on a |
||||
# github repository, inspect it, sign it and optionally push it. |
||||
|
||||
# The following temporary branches are created/overwritten and deleted: |
||||
# * pull/$PULL/base (the current master we're merging onto) |
||||
# * pull/$PULL/head (the current state of the remote pull request) |
||||
# * pull/$PULL/merge (github's merge) |
||||
# * pull/$PULL/local-merge (our merge) |
||||
|
||||
# In case of a clean merge that is accepted by the user, the local branch with |
||||
# name $BRANCH is overwritten with the merged result, and optionally pushed. |
||||
from __future__ import division,print_function,unicode_literals |
||||
import os,sys |
||||
from sys import stdin,stdout,stderr |
||||
import argparse |
||||
import subprocess |
||||
|
||||
# External tools (can be overridden using environment) |
||||
GIT = os.getenv('GIT','git') |
||||
BASH = os.getenv('BASH','bash') |
||||
|
||||
def git_config_get(option, default=None): |
||||
''' |
||||
Get named configuration option from git repository. |
||||
''' |
||||
try: |
||||
return subprocess.check_output([GIT,'config','--get',option]).rstrip() |
||||
except subprocess.CalledProcessError as e: |
||||
return default |
||||
|
||||
def retrieve_pr_title(repo,pull): |
||||
''' |
||||
Retrieve pull request title from github. |
||||
Return None if no title can be found, or an error happens. |
||||
''' |
||||
import urllib2,json |
||||
try: |
||||
req = urllib2.Request("https://api.github.com/repos/"+repo+"/pulls/"+pull) |
||||
result = urllib2.urlopen(req) |
||||
result = json.load(result) |
||||
return result['title'] |
||||
except Exception as e: |
||||
print('Warning: unable to retrieve pull title from github: %s' % e) |
||||
return None |
||||
|
||||
def ask_prompt(text): |
||||
print(text,end=" ",file=stderr) |
||||
reply = stdin.readline().rstrip() |
||||
print("",file=stderr) |
||||
return reply |
||||
|
||||
def parse_arguments(branch): |
||||
epilog = ''' |
||||
In addition, you can set the following git configuration variables: |
||||
githubmerge.repository (mandatory), |
||||
user.signingkey (mandatory), |
||||
githubmerge.host (default: git@github.com), |
||||
githubmerge.branch (default: master), |
||||
githubmerge.testcmd (default: none). |
||||
''' |
||||
parser = argparse.ArgumentParser(description='Utility to merge, sign and push github pull requests', |
||||
epilog=epilog) |
||||
parser.add_argument('pull', metavar='PULL', type=int, nargs=1, |
||||
help='Pull request ID to merge') |
||||
parser.add_argument('branch', metavar='BRANCH', type=str, nargs='?', |
||||
default=branch, help='Branch to merge against (default: '+branch+')') |
||||
return parser.parse_args() |
||||
|
||||
def main(): |
||||
# Extract settings from git repo |
||||
repo = git_config_get('githubmerge.repository') |
||||
host = git_config_get('githubmerge.host','git@github.com') |
||||
branch = git_config_get('githubmerge.branch','master') |
||||
testcmd = git_config_get('githubmerge.testcmd') |
||||
signingkey = git_config_get('user.signingkey') |
||||
if repo is None: |
||||
print("ERROR: No repository configured. Use this command to set:", file=stderr) |
||||
print("git config githubmerge.repository <owner>/<repo>", file=stderr) |
||||
exit(1) |
||||
if signingkey is None: |
||||
print("ERROR: No GPG signing key set. Set one using:",file=stderr) |
||||
print("git config --global user.signingkey <key>",file=stderr) |
||||
exit(1) |
||||
|
||||
host_repo = host+":"+repo # shortcut for push/pull target |
||||
|
||||
# Extract settings from command line |
||||
args = parse_arguments(branch) |
||||
pull = str(args.pull[0]) |
||||
branch = args.branch |
||||
|
||||
# Initialize source branches |
||||
head_branch = 'pull/'+pull+'/head' |
||||
base_branch = 'pull/'+pull+'/base' |
||||
merge_branch = 'pull/'+pull+'/merge' |
||||
local_merge_branch = 'pull/'+pull+'/local-merge' |
||||
|
||||
devnull = open(os.devnull,'w') |
||||
try: |
||||
subprocess.check_call([GIT,'checkout','-q',branch]) |
||||
except subprocess.CalledProcessError as e: |
||||
print("ERROR: Cannot check out branch %s." % (branch), file=stderr) |
||||
exit(3) |
||||
try: |
||||
subprocess.check_call([GIT,'fetch','-q',host_repo,'+refs/pull/'+pull+'/*:refs/heads/pull/'+pull+'/*']) |
||||
except subprocess.CalledProcessError as e: |
||||
print("ERROR: Cannot find pull request #%s on %s." % (pull,host_repo), file=stderr) |
||||
exit(3) |
||||
try: |
||||
subprocess.check_call([GIT,'log','-q','-1','refs/heads/'+head_branch], stdout=devnull, stderr=stdout) |
||||
except subprocess.CalledProcessError as e: |
||||
print("ERROR: Cannot find head of pull request #%s on %s." % (pull,host_repo), file=stderr) |
||||
exit(3) |
||||
try: |
||||
subprocess.check_call([GIT,'log','-q','-1','refs/heads/'+merge_branch], stdout=devnull, stderr=stdout) |
||||
except subprocess.CalledProcessError as e: |
||||
print("ERROR: Cannot find merge of pull request #%s on %s." % (pull,host_repo), file=stderr) |
||||
exit(3) |
||||
try: |
||||
subprocess.check_call([GIT,'fetch','-q',host_repo,'+refs/heads/'+branch+':refs/heads/'+base_branch]) |
||||
except subprocess.CalledProcessError as e: |
||||
print("ERROR: Cannot find branch %s on %s." % (branch,host_repo), file=stderr) |
||||
exit(3) |
||||
subprocess.check_call([GIT,'checkout','-q',base_branch]) |
||||
subprocess.call([GIT,'branch','-q','-D',local_merge_branch], stderr=devnull) |
||||
subprocess.check_call([GIT,'checkout','-q','-b',local_merge_branch]) |
||||
|
||||
try: |
||||
# Create unsigned merge commit. |
||||
title = retrieve_pr_title(repo,pull) |
||||
if title: |
||||
firstline = 'Merge #%s: %s' % (pull,title) |
||||
else: |
||||
firstline = 'Merge #%s' % (pull,) |
||||
message = firstline + '\n\n' |
||||
message += subprocess.check_output([GIT,'log','--no-merges','--topo-order','--pretty=format:%h %s (%an)',base_branch+'..'+head_branch]) |
||||
try: |
||||
subprocess.check_call([GIT,'merge','-q','--commit','--no-edit','--no-ff','-m',message,head_branch]) |
||||
except subprocess.CalledProcessError as e: |
||||
print("ERROR: Cannot be merged cleanly.",file=stderr) |
||||
subprocess.check_call([GIT,'merge','--abort']) |
||||
exit(4) |
||||
logmsg = subprocess.check_output([GIT,'log','--pretty=format:%s','-n','1']) |
||||
if logmsg.rstrip() != firstline.rstrip(): |
||||
print("ERROR: Creating merge failed (already merged?).",file=stderr) |
||||
exit(4) |
||||
|
||||
# Run test command if configured. |
||||
if testcmd: |
||||
# Go up to the repository's root. |
||||
toplevel = subprocess.check_output([GIT,'rev-parse','--show-toplevel']) |
||||
os.chdir(toplevel) |
||||
if subprocess.call(testcmd,shell=True): |
||||
print("ERROR: Running %s failed." % testcmd,file=stderr) |
||||
exit(5) |
||||
|
||||
# Show the created merge. |
||||
diff = subprocess.check_output([GIT,'diff',merge_branch+'..'+local_merge_branch]) |
||||
subprocess.check_call([GIT,'diff',base_branch+'..'+local_merge_branch]) |
||||
if diff: |
||||
print("WARNING: merge differs from github!",file=stderr) |
||||
reply = ask_prompt("Type 'ignore' to continue.") |
||||
if reply.lower() == 'ignore': |
||||
print("Difference with github ignored.",file=stderr) |
||||
else: |
||||
exit(6) |
||||
reply = ask_prompt("Press 'd' to accept the diff.") |
||||
if reply.lower() == 'd': |
||||
print("Diff accepted.",file=stderr) |
||||
else: |
||||
print("ERROR: Diff rejected.",file=stderr) |
||||
exit(6) |
||||
else: |
||||
# Verify the result manually. |
||||
print("Dropping you on a shell so you can try building/testing the merged source.",file=stderr) |
||||
print("Run 'git diff HEAD~' to show the changes being merged.",file=stderr) |
||||
print("Type 'exit' when done.",file=stderr) |
||||
if os.path.isfile('/etc/debian_version'): # Show pull number on Debian default prompt |
||||
os.putenv('debian_chroot',pull) |
||||
subprocess.call([BASH,'-i']) |
||||
reply = ask_prompt("Type 'm' to accept the merge.") |
||||
if reply.lower() == 'm': |
||||
print("Merge accepted.",file=stderr) |
||||
else: |
||||
print("ERROR: Merge rejected.",file=stderr) |
||||
exit(7) |
||||
|
||||
# Sign the merge commit. |
||||
reply = ask_prompt("Type 's' to sign off on the merge.") |
||||
if reply == 's': |
||||
try: |
||||
subprocess.check_call([GIT,'commit','-q','--gpg-sign','--amend','--no-edit']) |
||||
except subprocess.CalledProcessError as e: |
||||
print("Error signing, exiting.",file=stderr) |
||||
exit(1) |
||||
else: |
||||
print("Not signing off on merge, exiting.",file=stderr) |
||||
exit(1) |
||||
|
||||
# Put the result in branch. |
||||
subprocess.check_call([GIT,'checkout','-q',branch]) |
||||
subprocess.check_call([GIT,'reset','-q','--hard',local_merge_branch]) |
||||
finally: |
||||
# Clean up temporary branches. |
||||
subprocess.call([GIT,'checkout','-q',branch]) |
||||
subprocess.call([GIT,'branch','-q','-D',head_branch],stderr=devnull) |
||||
subprocess.call([GIT,'branch','-q','-D',base_branch],stderr=devnull) |
||||
subprocess.call([GIT,'branch','-q','-D',merge_branch],stderr=devnull) |
||||
subprocess.call([GIT,'branch','-q','-D',local_merge_branch],stderr=devnull) |
||||
|
||||
# Push the result. |
||||
reply = ask_prompt("Type 'push' to push the result to %s, branch %s." % (host_repo,branch)) |
||||
if reply.lower() == 'push': |
||||
subprocess.check_call([GIT,'push',host_repo,'refs/heads/'+branch]) |
||||
|
||||
if __name__ == '__main__': |
||||
main() |
||||
|
@ -1,185 +0,0 @@
@@ -1,185 +0,0 @@
|
||||
#!/bin/bash |
||||
|
||||
# This script will locally construct a merge commit for a pull request on a |
||||
# github repository, inspect it, sign it and optionally push it. |
||||
|
||||
# The following temporary branches are created/overwritten and deleted: |
||||
# * pull/$PULL/base (the current master we're merging onto) |
||||
# * pull/$PULL/head (the current state of the remote pull request) |
||||
# * pull/$PULL/merge (github's merge) |
||||
# * pull/$PULL/local-merge (our merge) |
||||
|
||||
# In case of a clean merge that is accepted by the user, the local branch with |
||||
# name $BRANCH is overwritten with the merged result, and optionally pushed. |
||||
|
||||
REPO="$(git config --get githubmerge.repository)" |
||||
if [[ "d$REPO" == "d" ]]; then |
||||
echo "ERROR: No repository configured. Use this command to set:" >&2 |
||||
echo "git config githubmerge.repository <owner>/<repo>" >&2 |
||||
echo "In addition, you can set the following variables:" >&2 |
||||
echo "- githubmerge.host (default git@github.com)" >&2 |
||||
echo "- githubmerge.branch (default master)" >&2 |
||||
echo "- githubmerge.testcmd (default none)" >&2 |
||||
exit 1 |
||||
fi |
||||
|
||||
HOST="$(git config --get githubmerge.host)" |
||||
if [[ "d$HOST" == "d" ]]; then |
||||
HOST="git@github.com" |
||||
fi |
||||
|
||||
BRANCH="$(git config --get githubmerge.branch)" |
||||
if [[ "d$BRANCH" == "d" ]]; then |
||||
BRANCH="master" |
||||
fi |
||||
|
||||
TESTCMD="$(git config --get githubmerge.testcmd)" |
||||
|
||||
PULL="$1" |
||||
|
||||
if [[ "d$PULL" == "d" ]]; then |
||||
echo "Usage: $0 pullnumber [branch]" >&2 |
||||
exit 2 |
||||
fi |
||||
|
||||
if [[ "d$2" != "d" ]]; then |
||||
BRANCH="$2" |
||||
fi |
||||
|
||||
# Initialize source branches. |
||||
git checkout -q "$BRANCH" |
||||
if git fetch -q "$HOST":"$REPO" "+refs/pull/$PULL/*:refs/heads/pull/$PULL/*"; then |
||||
if ! git log -q -1 "refs/heads/pull/$PULL/head" >/dev/null 2>&1; then |
||||
echo "ERROR: Cannot find head of pull request #$PULL on $HOST:$REPO." >&2 |
||||
exit 3 |
||||
fi |
||||
if ! git log -q -1 "refs/heads/pull/$PULL/merge" >/dev/null 2>&1; then |
||||
echo "ERROR: Cannot find merge of pull request #$PULL on $HOST:$REPO." >&2 |
||||
exit 3 |
||||
fi |
||||
else |
||||
echo "ERROR: Cannot find pull request #$PULL on $HOST:$REPO." >&2 |
||||
exit 3 |
||||
fi |
||||
if git fetch -q "$HOST":"$REPO" +refs/heads/"$BRANCH":refs/heads/pull/"$PULL"/base; then |
||||
true |
||||
else |
||||
echo "ERROR: Cannot find branch $BRANCH on $HOST:$REPO." >&2 |
||||
exit 3 |
||||
fi |
||||
git checkout -q pull/"$PULL"/base |
||||
git branch -q -D pull/"$PULL"/local-merge 2>/dev/null |
||||
git checkout -q -b pull/"$PULL"/local-merge |
||||
TMPDIR="$(mktemp -d -t ghmXXXXX)" |
||||
|
||||
function cleanup() { |
||||
git checkout -q "$BRANCH" |
||||
git branch -q -D pull/"$PULL"/head 2>/dev/null |
||||
git branch -q -D pull/"$PULL"/base 2>/dev/null |
||||
git branch -q -D pull/"$PULL"/merge 2>/dev/null |
||||
git branch -q -D pull/"$PULL"/local-merge 2>/dev/null |
||||
rm -rf "$TMPDIR" |
||||
} |
||||
|
||||
# Create unsigned merge commit. |
||||
( |
||||
echo "Merge pull request #$PULL" |
||||
echo "" |
||||
git log --no-merges --topo-order --pretty='format:%h %s (%an)' pull/"$PULL"/base..pull/"$PULL"/head |
||||
)>"$TMPDIR/message" |
||||
if git merge -q --commit --no-edit --no-ff -m "$(<"$TMPDIR/message")" pull/"$PULL"/head; then |
||||
if [ "d$(git log --pretty='format:%s' -n 1)" != "dMerge pull request #$PULL" ]; then |
||||
echo "ERROR: Creating merge failed (already merged?)." >&2 |
||||
cleanup |
||||
exit 4 |
||||
fi |
||||
else |
||||
echo "ERROR: Cannot be merged cleanly." >&2 |
||||
git merge --abort |
||||
cleanup |
||||
exit 4 |
||||
fi |
||||
|
||||
# Run test command if configured. |
||||
if [[ "d$TESTCMD" != "d" ]]; then |
||||
# Go up to the repository's root. |
||||
while [ ! -d .git ]; do cd ..; done |
||||
if ! $TESTCMD; then |
||||
echo "ERROR: Running $TESTCMD failed." >&2 |
||||
cleanup |
||||
exit 5 |
||||
fi |
||||
# Show the created merge. |
||||
git diff pull/"$PULL"/merge..pull/"$PULL"/local-merge >"$TMPDIR"/diff |
||||
git diff pull/"$PULL"/base..pull/"$PULL"/local-merge |
||||
if [[ "$(<"$TMPDIR"/diff)" != "" ]]; then |
||||
echo "WARNING: merge differs from github!" >&2 |
||||
read -p "Type 'ignore' to continue. " -r >&2 |
||||
if [[ "d$REPLY" =~ ^d[iI][gG][nN][oO][rR][eE]$ ]]; then |
||||
echo "Difference with github ignored." >&2 |
||||
else |
||||
cleanup |
||||
exit 6 |
||||
fi |
||||
fi |
||||
read -p "Press 'd' to accept the diff. " -n 1 -r >&2 |
||||
echo |
||||
if [[ "d$REPLY" =~ ^d[dD]$ ]]; then |
||||
echo "Diff accepted." >&2 |
||||
else |
||||
echo "ERROR: Diff rejected." >&2 |
||||
cleanup |
||||
exit 6 |
||||
fi |
||||
else |
||||
# Verify the result. |
||||
echo "Dropping you on a shell so you can try building/testing the merged source." >&2 |
||||
echo "Run 'git diff HEAD~' to show the changes being merged." >&2 |
||||
echo "Type 'exit' when done." >&2 |
||||
if [[ -f /etc/debian_version ]]; then # Show pull number in prompt on Debian default prompt |
||||
export debian_chroot="$PULL" |
||||
fi |
||||
bash -i |
||||
read -p "Press 'm' to accept the merge. " -n 1 -r >&2 |
||||
echo |
||||
if [[ "d$REPLY" =~ ^d[Mm]$ ]]; then |
||||
echo "Merge accepted." >&2 |
||||
else |
||||
echo "ERROR: Merge rejected." >&2 |
||||
cleanup |
||||
exit 7 |
||||
fi |
||||
fi |
||||
|
||||
# Sign the merge commit. |
||||
read -p "Press 's' to sign off on the merge. " -n 1 -r >&2 |
||||
echo |
||||
if [[ "d$REPLY" =~ ^d[Ss]$ ]]; then |
||||
if [[ "$(git config --get user.signingkey)" == "" ]]; then |
||||
echo "ERROR: No GPG signing key set, not signing. Set one using:" >&2 |
||||
echo "git config --global user.signingkey <key>" >&2 |
||||
cleanup |
||||
exit 1 |
||||
else |
||||
if ! git commit -q --gpg-sign --amend --no-edit; then |
||||
echo "Error signing, exiting." |
||||
cleanup |
||||
exit 1 |
||||
fi |
||||
fi |
||||
else |
||||
echo "Not signing off on merge, exiting." |
||||
cleanup |
||||
exit 1 |
||||
fi |
||||
|
||||
# Clean up temporary branches, and put the result in $BRANCH. |
||||
git checkout -q "$BRANCH" |
||||
git reset -q --hard pull/"$PULL"/local-merge |
||||
cleanup |
||||
|
||||
# Push the result. |
||||
read -p "Type 'push' to push the result to $HOST:$REPO, branch $BRANCH. " -r >&2 |
||||
if [[ "d$REPLY" =~ ^d[Pp][Uu][Ss][Hh]$ ]]; then |
||||
git push "$HOST":"$REPO" refs/heads/"$BRANCH" |
||||
fi |
Loading…
Reference in new issue