From e5adec35c65b5194d0d4465b4d095f34e80dcc94 Mon Sep 17 00:00:00 2001 From: "Wladimir J. van der Laan" Date: Fri, 22 Nov 2013 05:49:51 +0100 Subject: [PATCH] contrib: add sipa's github-merge script --- contrib/README.md | 4 + contrib/devtools/README.md | 38 +++++++ contrib/devtools/github-merge.sh | 173 +++++++++++++++++++++++++++++++ 3 files changed, 215 insertions(+) create mode 100644 contrib/devtools/README.md create mode 100755 contrib/devtools/github-merge.sh diff --git a/contrib/README.md b/contrib/README.md index d1cdd7eb3..167b5df4e 100644 --- a/contrib/README.md +++ b/contrib/README.md @@ -52,3 +52,7 @@ tests each pull and when master is tested using jenkins. ### [Verify SF Binaries](/contrib/verifysfbinaries) ### This script attempts to download and verify the signature file SHA256SUMS.asc from SourceForge. +### [Developer tools](/control/devtools) ### +Specific tools for developers working on this repository. +Contains the script `github-merge.sh` for merging github pull requests securely and signing them using GPG. + diff --git a/contrib/devtools/README.md b/contrib/devtools/README.md new file mode 100644 index 000000000..55d5d24cc --- /dev/null +++ b/contrib/devtools/README.md @@ -0,0 +1,38 @@ +Contents +=========== +This directory contains tools for developers working on this repository. + +github-merge.sh +---------------- + +A small script to automate merging pull-requests securely and sign them with GPG. + +For example: + + ./github-merge.sh bitcoin/bitcoin 3077 + +(in any git repository) will help you merge pull request #3077 for the +bitcoin/bitcoin repository. + +What it does: +* Fetch master and the pull request. +* Locally construct a merge commit. +* Show the diff that merge results in. +* Ask you to verify the resulting source tree (so you can do a make +check or whatever). +* Ask you whether to GPG sign the merge commit. +* Ask you whether to push the result upstream. + +This means that there are no potential race conditions (where a +pullreq gets updated while you're reviewing it, but before you click +merge), and when using GPG signatures, that even a compromised github +couldn't mess with the sources. + +Setup +--------- +Configuring the github-merge tool for the bitcoin repository is done in the following way: + + git config githubmerge.repository bitcoin/bitcoin + git config githubmerge.testcmd "make -j4 check" (adapt to whatever you want to use for testing) + git config --global user.signingkey mykeyid (if you want to GPG sign) + diff --git a/contrib/devtools/github-merge.sh b/contrib/devtools/github-merge.sh new file mode 100755 index 000000000..e42b71a54 --- /dev/null +++ b/contrib/devtools/github-merge.sh @@ -0,0 +1,173 @@ +#!/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 /" >&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 -1q "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 -1q "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 + 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 "WARNING: No GPG signing key set, not signing. Set one using:" >&2 + echo "git config --global user.signingkey " >&2 + git commit -q --signoff --amend --no-edit + else + git commit -q --gpg-sign --amend --no-edit + fi +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