Last active
June 9, 2016 18:02
-
-
Save beporter/8074237fa5a71f77dc5c to your computer and use it in GitHub Desktop.
A git add-on script that automates fast-forwarding branches to each other, pushing to the default remote (origin) and triggering pre/post scripts. Read `usage()` for details.
This file contains 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 | |
# | |
# https://gist.github.com/beporter/8074237fa5a71f77dc5c | |
# [email protected] | |
# 2015-09-14 | |
#--------------------------------------------------------------------- | |
usage () | |
{ | |
cat <<EOT | |
${0##*/} | |
Convenience wrapper to fast-forward a local branch up to another | |
local branch and push it to the default remote (origin.) | |
Intended to be used with a git flow where specific branches | |
represent deployed versions of the app or specific merge | |
targets. For example, developers target a \`dev\` branch for | |
their topic work, a demo site runs against a \`staging\` branch, | |
and the production site tracks \`master\`. Dev is | |
usually the tip of the repo, with staging trailing somewhat | |
behind with finished but not yet "live" features being tested, | |
and master trails farther behind representing the stable code | |
that's been fully pushed to production. In this setup, dev | |
"feeds" staging, and staging feeds master. Conversely, master | |
"targets" its updates from staging and staging targets dev. | |
This command automates the following steps: | |
- Make sure the working dir is clean (abort if not). | |
- Process command line args: | |
- No args? Active branch is ahead branch; use | |
configured deploy values to determine the "target" | |
branch. | |
- 1 arg? Arg is the to-ff branch; use configured | |
deploy values to determine the feed branch. | |
- 2 args? First arg is the ahead branch, second arg | |
is the to-ff branch. (no configs consulted or | |
updated) | |
- Trigger a pre-deploy hook script, if present and executable, | |
passing the ahead branch and the to-ff branch as the first | |
and second args. If this script exits non-zero, the rest of | |
the deploy process is aborted. | |
- Make sure a local tracking branch exists for both branches. | |
- Make a note of the current "starting" branch. | |
- Switch to the to-ff branch. | |
- Merge the ahead branch into the to-ff, enforcing ff-only. | |
- Push the to-ff branch to the origin. | |
- Switch back to the starting branch. | |
- Trigger a post-deploy hook script, if present and executable, | |
passing the ahead branch and the to-ff branch as the first | |
and second args. | |
Usage: | |
${0##*/} [-h] | |
Look up the "target" branch for the currently checked out | |
branch and fast-forward it. | |
-h Print this help message. | |
${0##*/} <to-ff branch> | |
Look up the "feed" branch for the named branch and | |
fast-forward up to it. | |
${0##*/} <ahead branch> <to-ff branch> | |
Fast-forward the to-ff branch up to the ahead branch. | |
Configuration: | |
The script uses "target" and "feed" settings from the local | |
repo's .git/config to determine the assoications between | |
branches. If the necessary config value isn't set and isn't | |
provided on the command line, the script will prompt for it | |
and save the config for future use. | |
Installation: | |
Save this script as \`git-deploy\` anywhere in your \$PATH | |
and make it executable. | |
EOT | |
exit ${1:-0} # Exit with code 0 unless an arg is passed to the method. | |
} | |
if [ "$1" = '-h' ]; then | |
usage | |
fi | |
#--------------------------------------------------------------------- | |
# Echo to stderr. | |
echoerr() { | |
cat <<< "$@" 1>&2; | |
} | |
#--------------------------------------------------------------------- | |
# Uppercase the first letter in the provided string. | |
# | |
# Must be called like: $(_ucfirst "$VAR_TO_CHANGE") | |
# | |
# $1 = The string to manipulate. | |
ucfirst() { | |
echo "$(tr '[:lower:]' '[:upper:]' <<< ${1:0:1})${1:1}" | |
} | |
#--------------------------------------------------------------------- | |
# Determine if the local working directory is "clean" compared to | |
# the git index. | |
# | |
# Ref: https://stackoverflow.com/a/2659808/70876 | |
_git_local_changes() { | |
# Avoid some “false positives” caused by mismatching stat(2) | |
# information in subsequent commands. | |
git update-index -q --refresh || return 1 | |
# To check whether a repository has staged changes (not yet | |
# committed). | |
git diff-index --quiet --cached HEAD || return 1 | |
# To check whether a working tree has changes that could be | |
# staged. | |
git diff-files --quiet || return 1 | |
# To check whether the combination of the index and the | |
# tracked files in the working tree have changes with respect | |
# to HEAD. | |
git diff-index --quiet HEAD || return 1 | |
return 0 | |
# Check for For "untracked and unignored" files. (These checks | |
# won't stop switching branches or FFing so skip them.) | |
git ls-files --exclude-standard --others --error-unmatch . >/dev/null 2>&1; ec=$? | |
if test "$ec" = 0; then | |
# some untracked files | |
return 1 | |
elif test "$ec" = 1; then | |
# no untracked files | |
return 0 | |
else | |
# error from ls-files | |
return 1 | |
fi | |
} | |
#--------------------------------------------------------------------- | |
# Check the provided branch name. There must be a remote branch | |
# named after it. If there is, a local tracking branch must exist, | |
# and is created if it does not. If these checks pass, returns 0 | |
# (indicating success.) | |
# | |
# $1 = name of branch to check for or create if necessary. | |
# $2 = Name of branch to switch back to if $1 needed to be created. | |
_git_hydrate_tracking_branch() { | |
# If there's a local branch with the correct name, we're all | |
# set already. | |
if git show-ref --quiet --verify "refs/heads/$1"; then | |
echoerr "## Tracking branch exists for origin/$1." | |
return 0 | |
fi | |
# If we can create a tracking branch, switch back to the | |
# original and return success. | |
if git checkout --quiet --track -b "$1" "origin/$1"; then | |
echoerr "## Creating tracking branch for origin/$1." | |
git checkout --quiet "$2" | |
return 0 | |
fi | |
# Couldn't find a local branch and couldn't create one. | |
# Return failure. | |
return 1 | |
} | |
#--------------------------------------------------------------------- | |
# Prompt the user for a branch name and commit it to the local | |
# .git/config file. Must be called as | |
# $(_git_fetch_or_prompt_and_save_config ['feed'|'target'] [EXISTING_BRANCH]) | |
# | |
# $1 = Type of branch to get from the user. Either 'feed' or 'target'. | |
# $2 = The branch name we already know. Will be used when saving configs. | |
_git_fetch_or_prompt_and_save_config() { | |
BRANCH_TYPE=$1 | |
EXISTING_BRANCH=$2 | |
GIT_CONFIG_KEY="deploy.${EXISTING_BRANCH}-${BRANCH_TYPE}" | |
# First check in the local git config. If it's there, just return it. | |
ASSOC_BRANCH=$( git config --local --get $GIT_CONFIG_KEY 2>/dev/null ) | |
if [ "$?" -eq "0" ]; then | |
echo $ASSOC_BRANCH | |
return | |
fi | |
# Otherwise prompt user, making sure the input exists. | |
echoerr "!! No $BRANCH_TYPE branch found for \`$EXISTING_BRANCH\`." | |
BRANCH_FOUND=1 | |
until [ "$BRANCH_FOUND" -eq 0 ]; do | |
read -p ">> Enter $BRANCH_TYPE branch for \`$EXISTING_BRANCH\`: " ASSOC_BRANCH | |
_git_hydrate_tracking_branch $ASSOC_BRANCH $ACTIVE_BRANCH | |
BRANCH_FOUND=$? | |
done | |
# Write the value to the local config so it will be available next time. | |
if git config --local $GIT_CONFIG_KEY $ASSOC_BRANCH; then | |
echoerr "## $(ucfirst "${BRANCH_TYPE}") branch \`$ASSOC_BRANCH\` saved for \`$EXISTING_BRANCH\`." | |
else | |
echoerr "!! Failed to save $BRANCH_TYPE branch \`$ASSOC_BRANCH\` for \`$EXISTING_BRANCH\`." | |
fi | |
# Spit out the new branch name for capture and return. | |
echo $ASSOC_BRANCH | |
return | |
} | |
#--------------------------------------------------------------------- | |
# main() | |
# Make convenience functions available to sub-shells. | |
export -f echoerr | |
export -f _git_local_changes | |
# Abort if working dir is not clean. | |
if ! _git_local_changes; then | |
echoerr "!! Working directory is not clean. Please commit or stash before continuing." | |
git status | |
exit 2 | |
fi | |
# Make a note of the current "starting" branch. | |
ACTIVE_BRANCH=$( git rev-parse --quiet --abbrev-ref HEAD 2>/dev/null ) | |
# Handle command line args, if present. | |
if [ "$#" -gt 1 ]; then | |
AHEAD_BRANCH=$1 | |
BRANCH_TO_FF=$2 | |
elif [ "$#" -eq 1 ]; then | |
BRANCH_TO_FF=$1 | |
AHEAD_BRANCH=$(_git_fetch_or_prompt_and_save_config 'feed' $BRANCH_TO_FF) | |
_git_hydrate_tracking_branch $BRANCH_TO_FF $ACTIVE_BRANCH | |
else | |
AHEAD_BRANCH=$ACTIVE_BRANCH | |
BRANCH_TO_FF=$(_git_fetch_or_prompt_and_save_config 'target' $AHEAD_BRANCH) | |
fi | |
# Trigger a pre-deploy script, if present. If exit is >0, abort the rest. | |
if [ -x ".git/hooks/pre-deploy" ] && ! .git/hooks/pre-deploy $AHEAD_BRANCH $BRANCH_TO_FF; then | |
echoerr "!! Pre-deploy script exited non-zero. Aborting deploy." | |
exit $? | |
fi | |
# Checkout the branch to be updated, merge the previously-active | |
# (or named) branch, and push to origin. | |
echoerr "## Fast-forwarding $BRANCH_TO_FF up to $AHEAD_BRANCH and pushing to origin." | |
git checkout --quiet $BRANCH_TO_FF \ | |
&& git merge --quiet --ff-only $AHEAD_BRANCH \ | |
&& git push --quiet origin $BRANCH_TO_FF 2>/dev/null \ | |
git checkout --quiet $ACTIVE_BRANCH | |
# Trigger a post-deploy script, if present. | |
if [ -x ".git/hooks/post-deploy" ]; then | |
.git/hooks/post-deploy $AHEAD_BRANCH $BRANCH_TO_FF | |
fi | |
echoerr "## Done." |
This file contains 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 | |
# Save this script in .git/hooks/post-deploy and make it executable. | |
# $1 is the feed branch name. $2 is the target branch name. | |
# The functions `echoerr` and `_git_local_changes` are available from git-deploy. | |
# | |
# A "generic" post-deploy hook script intended to connect to the server using a | |
# "conventional" command name and run a deploy script on that server to update the code. | |
# | |
# Expects a command (or alias) to be available from PATH in the form of | |
# `${PROJECT_ROOT_NAME}_${TARGET_BRANCH} $TARGET_BRANCH` that provides command line | |
# access to the target environment's server and starts you in the corresponding project | |
# root directory. | |
echoerr "## Executing post-deploy hook script." | |
FEED_BRANCH=${1?"First argument must contain the feed branch name."} | |
TARGET_BRANCH=${2?"Second argument must contain the target branch name."} | |
PROJECT_ROOT_NAME="$( basename "`pwd`" )" | |
SERVER_CONNECT="${PROJECT_ROOT_NAME}_${TARGET_BRANCH}" | |
DEPLOY_COMMANDS=". ~/.profile; bin/deploy; exit;" | |
if [ -z "$(command -v $SERVER_CONNECT)" ]; then | |
echoerr "!! Server connect command \`$SERVER_CONNECT\` is not available." | |
exit 10 | |
fi | |
$SERVER_CONNECT "$DEPLOY_COMMANDS" |
This file contains 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 | |
# Save this script in .git/hooks/pre-deploy and make it executable. | |
# If this script exits non-zero, git-deploy will abort. | |
# $1 is the feed branch name. $2 is the target branch name. | |
# The functions `echoerr` and `_git_local_changes` are available from git-deploy. | |
echoerr "## Executing pre-deploy hook script." | |
exit 0 |
This file contains 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 | |
# Shortcut SSH connection script. | |
# Must exist in PATH for post-deploy to find it. | |
# This script must be named after the git project's root folder name, | |
# followed by an underscore, then the target branch/environment name. | |
# It must establish an SSH connection to the server for the given | |
# environment and pass all arguments to SSH as remote commands. | |
SSH_USER=ubuntu | |
SSH_HOST=some.host.com | |
SSH_PORT=22 | |
SSH_KEYFILE="/home/${USER}/.ssh/`basename ${0}`.pem" | |
SSH_FLAGS="-ACt" | |
ssh ${SSH_FLAGS} -i ${SSH_KEYFILE} ${SSH_USER}@${SSH_HOST} -p ${SSH_PORT} "$@" <&0 |
This file contains 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 | |
# Shortcut SSH connection script. | |
# Demonstrates how to pass commands to a different *remote* user | |
# account while still retaining the ability to use the script as | |
# a shortcut for making an interactive connection. | |
SSH_USER=me | |
SSH_HOST=shared.stagingserver.com | |
SSH_PORT=22 | |
SSH_KEYFILE="/home/${USER}/.ssh/`basename ${0}`.pem" | |
SSH_FLAGS="-ACt" | |
STAGING_USER="acme" | |
# Check the number of command line args. | |
if [ "$#" -gt 0 ]; then | |
# Only perform the provided commands and exit. | |
# (Used for git-deploy's post-deploy hook script.) | |
SSH_COMMANDS="echo 'cd public_html; $@' | sudo su - ${STAGING_USER}; exit" | |
else | |
# Start an interactive session. | |
SSH_COMMANDS="sudo su - ${STAGING_USER}; exit" | |
fi | |
# Execute the ssh connection and commands. | |
ssh ${SSH_FLAGS} -i ${SSH_KEYFILE} ${SSH_USER}@${SSH_HOST} -p ${SSH_PORT} ${SSH_COMMANDS} <&0 |
This file contains 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
# This will add bash tab completion to the `git deploy` command. | |
# | |
# Add the following custom completion function to your | |
# `~/.profile` or wherever will get sourced. | |
# | |
# NOTE: This is limited to pre-configured feed/target branches | |
# in the current repo's `.git/config` file. | |
_git_deploy() { | |
local cur=${COMP_WORDS[COMP_CWORD]} | |
local options=$(git config --local --get-regexp deploy\. | cut -f2 -d ' ') | |
COMPREPLY=( $(compgen -W "$options" -- $cur) ) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment