Created
October 27, 2016 18:23
-
-
Save psyrendust/ccf58166e9a80bb836d9efa6375b2f41 to your computer and use it in GitHub Desktop.
GitHub patching script
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bash | |
# | |
# gitpatchit - Easily create a patch and apply it to a destination branch. | |
# | |
# Create a patch file between a current branch and a destination branch. Then | |
# test and apply the patch to a temp branch. Finally rebase the temp branch onto | |
# the destination branch and cleanup the patch file and remove the temp branch. | |
# | |
# Usage: | |
# gitpatchit -h | |
# | |
#------------------------------------------------------------------------------- | |
# Does the following: | |
# 1. Create a temporary patch directory. | |
# 2. Update the <destination> branch. | |
# 3. Create a <temp> branch. | |
# 4. Create the patch file. | |
# 5. Check the diffstat of the patch against the <temp> branch. | |
# 6. Check the patch for errors against the <temp> branch. | |
# 7. Apply the patch to the <temp> branch. | |
# 8. Rebase the <temp> branch onto the <destination> branch. | |
# 9. Start the cleanup process. | |
# 10. Delete the <temp> branch. | |
# 11. Delete the temporary patch directory. | |
#------------------------------------------------------------------------------- | |
set -e | |
version="0.1.0" | |
# ------------------------------------------------------------------------------ | |
# Helper commands | |
# ------------------------------------------------------------------------------ | |
# Universal logger for this script | |
logError() { | |
printf '\033[0;31m%s\033[0m %s\n' "[ ERRO ]" "$1" | |
} | |
logInfo() { | |
printf '\033[0;36m%s\033[0m %s\n' "[ INFO ]" "$1" | |
# printf "[INFO] $1\n" | |
} | |
logOk() { | |
printf '\033[0;32m%s\033[0m %s\n' "[ OK ]" "$1" | |
} | |
logPrompt() { | |
printf '\033[1;33m%s\033[0m %s\n' "[PROMPT]" "$1" | |
# printf "[PROMPT] $1\n" | |
} | |
# From the CWD get the root of the Git repo | |
getGitRoot() { | |
local gitRoot="$(git rev-parse --show-toplevel 2>/dev/null)" | |
[[ -z "$gitRoot" ]] && echo "[WARNING] ** CWD is not a Git repo! **" && exit | |
echo "$gitRoot" | |
} | |
# Get the current Git branch | |
getGitCurrBranch() { | |
local ref=$(command git symbolic-ref HEAD 2> /dev/null) || \ | |
local ref=$(command git rev-parse --short HEAD 2> /dev/null) || return | |
echo "${ref#refs/heads/}" | |
} | |
getGitHubPatch() { | |
curl -H "Authorization: token $GITHUB_AUTH_TOKEN" -H "Accept: application/vnd.github.VERSION.patch" -L "$1" > "$patchFile" | |
} | |
getGitHubPrNumber() { | |
local pullrequesturl=${1#*github.com/} | |
echo ${pullrequesturl#*pull/} | |
} | |
# Convert a GitHub PR URL to an API PR URL | |
# From: https://github.com/:owner/:repo/pull/:number | |
# To : https://api.github.com/repos/:owner/:repo/pulls/:number | |
githubPrToApiUrl() { | |
local numreg='^[0-9]+$' | |
local pullrequesturl=${1#*github.com/} | |
local ownerrepo=${pullrequesturl%/pull*} | |
local owner=${ownerrepo%/*} | |
local repo=${ownerrepo#*/} | |
local number="$(getGitHubPrNumber $1)" | |
if [[ -n $owner ]]; then | |
if [[ -n $repo ]]; then | |
if [[ $number =~ $numreg ]]; then | |
echo "https://api.github.com/repos/$owner/$repo/pulls/$number" | |
else | |
gitHubUrlError "number" "$1" | |
fi | |
else | |
gitHubUrlError "repo" "$1" | |
fi | |
else | |
gitHubUrlError "owner" "$1" | |
fi | |
} | |
gitHubUrlError() { | |
echo "[ERROR] Bad url format, (missing $1)\n Expected: 'https://github.com/:owner/:repo/pull/:number'\n Received: '$2'" | |
} | |
# Make sure we start on the current branch | |
startOnCurrBranch() { | |
if [ $(getGitCurrBranch) != $currBranch ]; then | |
logInfo "Switching to '$currBranch' branch" | |
git checkout "$currBranch" | |
fi | |
} | |
# Make sure we start on the destination branch | |
startOnDestBranch() { | |
if [ $(getGitCurrBranch) != $destBranch ]; then | |
logInfo "Switching to '$destBranch' branch" | |
git checkout "$destBranch" | |
fi | |
} | |
# Make sure we start on the temp branch | |
startOnTempBranch() { | |
if [ $(getGitCurrBranch) != $tempBranch ]; then | |
logInfo "Switching to '$tempBranch' branch" | |
git checkout "$tempBranch" | |
fi | |
} | |
# ------------------------------------------------------------------------------ | |
# Sanity Check | |
# ------------------------------------------------------------------------------ | |
# Get the root directory of the Git repo | |
gitRoot="$(getGitRoot)" | |
# Exit if we are not in a Git repo | |
[[ $gitRoot == "[WARNING]"* ]] && echo "\n$gitRoot" && return | |
# ------------------------------------------------------------------------------ | |
# Script commands | |
# ------------------------------------------------------------------------------ | |
createPatchDir() { | |
logInfo "Create a local patch directory in '$patchDir'" | |
mkdir -p "$patchDir" | |
} | |
# Make sure we are up to date | |
updateDestBranch() { | |
startOnDestBranch | |
logInfo "Update $destBranch branch from $remote" | |
git pull --rebase $remote $destBranch; | |
} | |
# Make a staging branch from develop | |
createTempBranch() { | |
startOnDestBranch | |
if [[ -n $(git branch | grep "$tempBranch") ]]; then | |
logInfo "A branch named '$tempBranch' already exists." | |
if [[ -z $argNoPrompt ]]; then | |
logPrompt "Would you like to delete the branch named '$tempBranch'?" | |
select yn in "Yes" "No"; do | |
case $yn in | |
Yes ) break;; | |
No ) exit;; | |
esac | |
done | |
fi | |
deleteTempBranch | |
fi | |
logInfo "Creating $tempBranch branch" | |
git branch "$tempBranch" "$destBranch" | |
} | |
# Create the patch | |
createPatch() { | |
if [[ -n $argPrUrl ]]; then | |
# Get patch from GitHub PR URL | |
local url="$(githubPrToApiUrl "$argPrUrl")" | |
logInfo "Create patch from: $url" | |
getGitHubPatch $url | |
else | |
startOnCurrBranch | |
logInfo "Create patch from: $tempBranch" | |
git format-patch "$tempBranch" --stdout > "$patchFile" | |
# git format-patch -o "$patchDir" "$tempBranch" | |
fi | |
logInfo "Created patch at: $patchFile" | |
if [[ -z $argNoPrompt ]]; then | |
logPrompt "Would you like to view the patch?" | |
select yn in "Yes" "No"; do | |
case $yn in | |
Yes ) cat "$patchFile" && break;; | |
No ) break;; | |
esac | |
done | |
logPrompt "Would you like to continue?" | |
select yn in "Yes" "No"; do | |
case $yn in | |
Yes ) break;; | |
No ) exit;; | |
esac | |
done | |
fi | |
} | |
checkDiffStat() { | |
startOnTempBranch | |
logInfo "Check the diffstat of the patch against '$tempBranch'" | |
git apply --stat "$patchFile" | |
if [[ -z $argNoPrompt ]]; then | |
logPrompt "Would you like to continue?" | |
select yn in "Yes" "No"; do | |
case $yn in | |
Yes ) break;; | |
No ) exit;; | |
esac | |
done | |
fi | |
} | |
checkPatchForErrors() { | |
startOnTempBranch | |
logInfo "Check the patch against '$tempBranch' and look for errors" | |
git apply --check "$patchFile" | |
if [[ -z $argNoPrompt ]]; then | |
logPrompt "Would you like to apply the patch to '$tempBranch'?" | |
select yn in "Yes" "No"; do | |
case $yn in | |
Yes ) break;; | |
No ) exit;; | |
esac | |
done | |
fi | |
} | |
applyPatchToTempBranch() { | |
startOnTempBranch | |
logInfo "Appling the patch to '$tempBranch'" | |
git am -3 --whitespace=fix --signoff < "$patchFile" | |
if [[ -z $argNoPrompt ]]; then | |
logPrompt "Would you like to rebase '$tempBranch' onto '$destBranch'?" | |
select yn in "Yes" "No"; do | |
case $yn in | |
Yes ) break;; | |
No ) exit;; | |
esac | |
done | |
fi | |
} | |
rebaseTempBranchOntoDestBranch() { | |
startOnDestBranch | |
logInfo "Rebase the changes from '$tempBranch' onto '$destBranch'" | |
git rebase "$tempBranch" | |
} | |
startCleanup() { | |
logInfo "Running cleanup" | |
deleteTempBranch | |
deletePatchDir | |
} | |
deleteTempBranch() { | |
startOnDestBranch | |
if [[ -n $(git branch | grep "$tempBranch") ]]; then | |
logInfo "Delete the '$tempBranch' branch" | |
git branch -D "$tempBranch" | |
fi | |
} | |
deletePatchDir() { | |
logInfo "Delete temp patch directory" | |
[[ -d "$patchDir" ]] && rm -rf "$patchDir" | |
} | |
# ------------------------------------------------------------------------------ | |
# Usage Info | |
# ------------------------------------------------------------------------------ | |
usage() { | |
cat << EOF | |
gitpatchit - Easily create a patch and apply it to a destination branch. | |
Create a patch file between a current branch and a destination branch. Then | |
test and apply the patch to a temp branch. Finally rebase the temp branch onto | |
the destination branch and cleanup the patch file and remove the temp branch. | |
USAGE: | |
gitpatchit [options] | |
FLAG OPTIONS: | |
-h Show this message. | |
-v Display the version of this script. | |
-k Delete the '.git/patches' folder and the <temp> branch and exit. | |
-n Do not display any prompts | |
ARGUMENT OPTIONS: | |
-c The <current> branch to create the patch from. Defaults to the current branch. | |
-d The <destination> branch to rebase staging onto. Defaults to 'develop'. | |
-r The <remote> to push and pull from. Defaults to 'origin'. | |
-s Allows you to start at a specific step. Useful for when the script | |
fails before completing all steps. See AVAILABLE STEPS below. | |
-t The <temp> branch to apply the patch to. Defaults to 'staging'. | |
-u The GitHub Pull Request URL to create the patch from. | |
AVAILABLE STEPS: | |
1 Create a temporary patch directory. | |
2 Update the <destination> branch. | |
3 Create a <temp> branch. | |
4 Create the patch file. | |
5 Check the diffstat of the patch against the <temp> branch. | |
6 Check the patch for errors against the <temp> branch. | |
7 Apply the patch to the <temp> branch. | |
8 Rebase the <temp> branch onto the <destination> branch. | |
9 Start the cleanup process. | |
10 Delete the <temp> branch. | |
11 Delete the temporary patch directory. | |
EXAMPLES: | |
1. Create a patch with all defaults, from 'feat/woot' branch onto 'develop'. | |
2. Create a patch with all defaults and no prompts. | |
3. Create a patch with no prompts, from 'feat/woot' branch onto 'master' | |
and use a temp branch named 'hotfix'. | |
4. Create a patch with all defaults from GitHub PR URL. | |
gitpatchit -c feat/woot | |
gitpatchit -n | |
gitpatchit -nc feat/woot -d master -t hotfix | |
gitpatchit -u https://github.com/westfield/magellan/pull/31 | |
EOF | |
} | |
# ------------------------------------------------------------------------------ | |
# Variable setup | |
# ------------------------------------------------------------------------------ | |
# Defaults | |
defaultCurrBranch="$(getGitCurrBranch)" | |
defaultDestBranch="develop" | |
defaultTempBranch="staging" | |
defaultRemote="origin" | |
defaultStep=1 | |
# Argument variables | |
argNoPrompt= | |
argCleanup= | |
argCurrBranch= | |
argDestBranch= | |
argTempBranch= | |
argPrUrl= | |
argRemote= | |
argStep= | |
# Variables to use in script | |
currBranch= | |
destBranch= | |
tempBranch= | |
noprompts= | |
remote= | |
step= | |
# ------------------------------------------------------------------------------ | |
# Parse command line arguments and flags | |
# ------------------------------------------------------------------------------ | |
while getopts "hvknc:d:t:r:s:u:" OPTION | |
do | |
case $OPTION in | |
h) | |
usage | |
exit 1 | |
;; | |
v) | |
echo "$version" | |
exit 1 | |
;; | |
k) | |
argCleanup=1 | |
;; | |
n) | |
argNoPrompt=1 | |
;; | |
c) | |
argCurrBranch=$OPTARG | |
;; | |
d) | |
argDestBranch=$OPTARG | |
;; | |
t) | |
argTempBranch=$OPTARG | |
;; | |
r) | |
argRemote=$OPTARG | |
;; | |
s) | |
argStep=$OPTARG | |
;; | |
u) | |
argPrUrl=$OPTARG | |
;; | |
?) | |
usage | |
exit | |
;; | |
esac | |
done | |
# ------------------------------------------------------------------------------ | |
# Assign values to script vars from arguments or apply defaults | |
# ------------------------------------------------------------------------------ | |
[[ -n $argCurrBranch ]] && currBranch=$argCurrBranch || currBranch=$defaultCurrBranch | |
[[ -n $argDestBranch ]] && destBranch=$argDestBranch || destBranch=$defaultDestBranch | |
[[ -n $argTempBranch ]] && tempBranch=$argTempBranch || tempBranch=$defaultTempBranch | |
[[ -n $argRemote ]] && remote=$argRemote || remote=$defaultRemote | |
[[ -n $argStep ]] && step=$argStep || step=$defaultStep | |
[[ -n $argNoPrompt ]] && noprompts="true" || noprompts="false" | |
[[ -n $argPrUrl ]] && currBranch="pr$(getGitHubPrNumber $argPrUrl)" | |
patchDir="$gitRoot/.git/patches" | |
patchFile="$patchDir/$currBranch.patch"; | |
[[ -n $argCleanup ]] && startCleanup && exit 1; | |
# Output config setup | |
cat << EOF | |
========== CONFIG ========== | |
gitRoot : $gitRoot | |
patchDir : $patchDir | |
patchFile : $patchFile | |
currBranch : $currBranch | |
destBranch : $destBranch | |
tempBranch : $tempBranch | |
remote : $remote | |
step : $step | |
noprompts : $noprompts | |
pr url : $argPrUrl | |
============================ | |
EOF | |
if [[ -z $argNoPrompt ]]; then | |
logPrompt "Based on the config above would you like to continue?" | |
select yn in "Yes" "No"; do | |
case $yn in | |
Yes ) break;; | |
No ) exit;; | |
esac | |
done | |
fi | |
# Run the script | |
if [[ $step -le 1 ]]; then createPatchDir; fi && # Step 1 | |
if [[ $step -le 2 ]]; then updateDestBranch; fi && # Step 2 | |
if [[ $step -le 3 ]]; then createTempBranch; fi && # Step 3 | |
if [[ $step -le 4 ]]; then createPatch; fi && # Step 4 | |
# if [[ $step -le 5 ]]; then checkDiffStat; fi && # Step 5 | |
# if [[ $step -le 6 ]]; then checkPatchForErrors; fi && # Step 6 | |
# if [[ $step -le 7 ]]; then applyPatchToTempBranch; fi && # Step 7 | |
# if [[ $step -le 8 ]]; then rebaseTempBranchOntoDestBranch; fi && # Step 8 | |
logOk "All done!" || logError "Epic fail!" | |
startCleanup |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment