Skip to content

Instantly share code, notes, and snippets.

@psyrendust
Created October 27, 2016 18:23
Show Gist options
  • Save psyrendust/ccf58166e9a80bb836d9efa6375b2f41 to your computer and use it in GitHub Desktop.
Save psyrendust/ccf58166e9a80bb836d9efa6375b2f41 to your computer and use it in GitHub Desktop.
GitHub patching script
#!/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