Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
executable file 281 lines (219 sloc) 8.13 KB
#!/usr/bin/env bash
function help() {
script="$(basename "$0") $pr"
cat <<HELP
-=[ GitHub Pull Request Helper - "Cowboy" Ben Alman - ]=-
Usage: $(basename "$0") pull_request_id [ step ]
-=[ Description ]============================================================-
The "step" argument can be used to force execution of a particular step,
but by default this script will automatically choose the next step, from
STEP 1 to STEP 2 and finally to STEP 3. You'll have to execute STEP 4
manually, once everything is done.
-=[ Detailed workflow example ]==============================================-
For this example, assume PR $pr has been filed against the "$local_ref" branch.
-=[ STEP 1 ]=-
You run "$script" and this executes:
-=[ STEP 2 ]=-
You run "$script" and this executes:
-=[ STEP 3 ]=-
You run "$script" and this executes:
-=[ STEP 4 ]=-
You run "$script 4" (explicitly specifying the 4) and this executes:
-=[ Very important notes ]===================================================-
Before running this script, ensure that your local "$local_ref" branch is
At the beginning of STEP 1, any existing "pr$pr" and "pr$pr-squash"
branches will be deleted.
At the beginning of STEP 2, any existing pr$pr-squash" branch will be
If you want to hide per-step explanations, set ENV var GPR_SHH=1. For
example, create an alias like: alias gpr='GPR_SHH=1 gpr'
-=[ License ]================================================================-
Copyright (c) 2012 "Cowboy" Ben Alman
Licensed under the MIT license.
[[ "$1" ]]; exit
function help_step1() {
echo " Fetch and rebase PR $pr into \"pr$pr\" branch"
[[ "$1" ]] && return
cat <<HELP
1. Fetch the repo and branch associated with PR $pr.
2. Checkout a new "pr$pr" branch at the HEAD of the fetched branch.
3. Rebase "$local_ref" branch onto "pr$pr" branch.
4. Display a list of files changed in the PR.
Note that you may need to resolve conflicts. If the rebase is successful,
test the PR and commit fixes. Commit as many times as necessary; It doesn't
matter because all PR-related commits will be squash merged in STEP 2.
function help_step2() {
echo " Perform squash merge into \"pr$pr-squash\" branch"
[[ "$1" ]] && return
cat <<HELP
1. Checkout a new "pr$pr-squash" branch from "$local_ref" branch.
2. Squash merge "pr$pr" branch into "pr$pr-squash" branch. (no commit)
3. Append "Closes gh-$pr." to SQUASH_MSG.
4. Commit using the PR branch's HEAD author name.
Since all the commits in the PR have been squashed into one commit, you will
need to edit the commit message. When done, inspect the commit log, ensuring
everything is perfect; you should see a single, beautiful commit.
function help_step3() {
echo " Perform final merge into \"$local_ref\" branch"
[[ "$1" ]] && return
cat <<HELP
1. Checkout "$local_ref" branch.
2. Merge "$pr-squash" branch into "$local_ref" branch.
If STEP 2 was successful, there should be no conflicts here. If there are,
you're doing it wrong. Just double-check the commit log and push when done.
function help_step4() {
echo " Cleanup temporary branches and tags"
[[ "$1" ]] && return
cat <<HELP
1. Temporary "pr$pr" and "pr$pr-squash" branches are deleted.
2. Temporary "_pr${pr}_author_head" tag is deleted.
function header() {
if [[ "$GPR_SHH" ]]; then
echo "-=[ STEP $1 ]=================================================================-"
echo "-=[ STEP $1 Overview ]========================================================-"
echo "-=[ Actual ]=================================================================-"
[[ ! "$1" || "$1" == "-h" || "$1" == "--help" ]] && help $1
# Generate and store an OAUTH token.
if [[ "$1" == "auth" ]]; then
read -p 'Enter GitHub username: '
json="$(curl -fsSL --data '{"note":"gpr","scopes":["repo"]}' -u "$REPLY")"
if [[ "$json" ]]; then
token="$(node -pe "($json).token")"
git config --global --remove-section gpr 2>/dev/null
git config --global --add gpr.token $token
echo "Authorization successful, token saved."
echo "Error authorizing with GitHub, please try again."
exit 5
pr="$1"; shift
script="$(basename "$0") $pr"
repo="$(git remote show -n origin | perl -ne '/Fetch URL: .*github\.com[:\/](.*\/.*)\.git/ && print $1')"
# Let's fetch some JSON.
token="$(git config --get gpr.token)"
json="$(curl -fsSL "$repo/pulls/$pr?access_token=$token" 2>/dev/null)"
if [[ $? != 0 || ! "$json" ]]; then
echo "Error fetching GitHub API data for $repo PR $pr!"
echo "If you're trying to access a private repo and haven't yet done so, please run"
echo "the \"$(basename "$0") auth\" command to generate a GitHub auth token."
exit 2
# Let's parse some JSON.
remote_url="$(node -pe "($json).head.repo.git_url")"
remote_ref="$(node -pe "($json).head.ref")"
local_url="$(node -pe "($json).base.repo.git_url")"
local_ref="$(node -pe "($json).base.ref")"
num_commits="$(node -pe "($json).commits")"
# Let's get the project's .git folder.
git_dir="$(git rev-parse --show-toplevel)/.git"
function del_branch() {
if [[ "$(git branch | grep " $1\$")" ]]; then
git checkout "$local_ref" 2>/dev/null
git branch -D "$1" 2>/dev/null
# Use the specified step, otherwise attempt to auto-detect it.
if [[ "$1" ]]; then
elif [[ "$(git branch | grep " $branch-squash\$")" ]]; then
# STEP 3 should never auto-execute twice.
if [[ "$(git branch --contains "$(git rev-parse $branch-squash)" | grep " $local_ref\$")" ]]; then
echo "Error merging branch \"$branch-squash\" into \"$local_ref\" branch! (already done)"
echo "Redo the last step with: $script 3"
exit 4
elif [[ "$(git branch | grep " $branch\$")" ]]; then
# Let's do some stuff.
if [[ $step == 1 ]]; then
header 1
# Clean up any prior work on this PR.
del_branch "$branch"
del_branch "$branch-squash"
# Fetch remote, create a branch, etc.
if [[ "$remote_url" == "$local_url" ]]; then
git fetch origin "$remote_ref"
git fetch "$remote_url" "$remote_ref"
git checkout -b "$branch" FETCH_HEAD
# Save ref to last PR author commit for later use
git tag --force "_${branch}_author_head" FETCH_HEAD
# Rebase!
git rebase "$local_ref"
if [[ $? != 0 ]]; then
echo "Error while attempting rebase!"
exit 3
echo "Changed files in HEAD~$num_commits:"
git --no-pager diff --name-only HEAD~"$num_commits"
echo "-=[ Next Steps ]=============================================================-"
echo "$(help_step2 1) with: $script"
echo " Or redo the current step with: $script 1"
elif [[ $step == 2 ]]; then
header 2
# Clean up any prior squashes for this PR.
del_branch "$branch-squash"
# Create branch and squash merge all commits.
git checkout -b "$branch-squash" "$local_ref"
git merge --squash "$branch"
# Append useful information to commit message.
echo -e "\nCloses gh-$pr." >> "$squash_msg_file"
# Retrieve author name and email from stored commit, and commit.
author="$(git log "_${branch}_author_head" -n1 --format="%an <%ae>")"
git commit --author="$author"
echo "-=[ Next Steps ]=============================================================-"
echo "$(help_step3 1) with: $script"
echo " Or redo the current step with: $script 2"
elif [[ $step == 3 ]]; then
header 3
# Actually merge squashed commits into branch.
git checkout "$local_ref"
git merge "$branch-squash"
echo "-=[ Next Steps ]=============================================================-"
echo "$(help_step4 1) with: $script 4"
echo " Or redo the current step with: $script 3"
elif [[ $step == 4 ]]; then
header 4
del_branch "$branch"
del_branch "$branch-squash"
git tag -d "_${branch}_author_head" 2>/dev/null
echo "All done."