Skip to content
This repository
branch: master
Ben Alman September 26, 2013
executable file 281 lines (219 sloc) 8.328 kb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280
#!/usr/bin/env bash

function help() {
  pr=123
  local_ref=foo
  script="$(basename "$0") $pr"
  cat <<HELP
-=[ GitHub Pull Request Helper - "Cowboy" Ben Alman - http://benalman.com/ ]=-

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:

$(help_step1)

-=[ STEP 2 ]=-

You run "$script" and this executes:

$(help_step2)

-=[ STEP 3 ]=-

You run "$script" and this executes:

$(help_step3)

-=[ STEP 4 ]=-

You run "$script 4" (explicitly specifying the 4) and this executes:

$(help_step4)

-=[ Very important notes ]===================================================-

Before running this script, ensure that your local "$local_ref" branch is
up-to-date!

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
deleted.

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.
http://benalman.com/about/license/
HELP
  [[ "$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.
HELP
}

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.
HELP
}

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.
HELP
}

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.
HELP
}

function header() {
  if [[ "$GPR_SHH" ]]; then
echo "-=[ STEP $1 ]=================================================================-"
  else
echo "-=[ STEP $1 Overview ]========================================================-"
    help_step$1
    echo
echo "-=[ Actual ]=================================================================-"
  fi
}

[[ ! "$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"]}' https://api.github.com/authorizations -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."
    exit
else
echo "Error authorizing with GitHub, please try again."
    exit 5
  fi
fi

pr="$1"; shift
script="$(basename "$0") $pr"
branch="pr$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 "https://api.github.com/repos/$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
fi

# 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
  fi
}

# Use the specified step, otherwise attempt to auto-detect it.
if [[ "$1" ]]; then
step=$1
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
echo "Redo the last step with: $script 3"
    exit 4
  fi
step=3
elif [[ "$(git branch | grep " $branch\$")" ]]; then
step=2
else
step=1
fi

# 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"
  else
git fetch "$remote_url" "$remote_ref"
  fi
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
  fi

echo
echo "Changed files in HEAD~$num_commits:"
  git --no-pager diff --name-only HEAD~"$num_commits"

  echo
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.
  squash_msg_file="$git_dir/SQUASH_MSG"
  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
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
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
echo "All done."
fi
Something went wrong with that request. Please try again.