Last active
June 14, 2016 11:41
-
-
Save dale-c-anderson/b791c79de07d604b76db to your computer and use it in GitHub Desktop.
Git Hook Chaining
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
#!/bin/bash -ue | |
# To use: | |
# wget -q -O - https://gist.githubusercontent.com/dale-c-anderson/b791c79de07d604b76db/raw/hook-chain-install.sh | bash | |
# @TODO: Allow use of either wget or curl (try the other if the first doesn't exist) | |
function main () { | |
DIRNAME=$(basename "$PWD") | |
if [ ! "$DIRNAME" = "hooks" ]; then | |
cerr "ERR: You need to be in a git hooks directory to install hook-chain." | |
exit 1 | |
fi | |
type wget > /dev/null 2>&1 || { | |
cerr "ERR: Wget not installed or not in path; cannot install hook-chain." | |
exit 1 | |
} | |
SAMPLEFILESCOUNT=$(find ./ -type f -executable -name '*.sample' | wc -l) | |
if [ "$SAMPLEFILESCOUNT" -gt 0 ]; then | |
echo "FYI: Executable hooks named '<hook-type>.sample' will be ignored by hook-chain." | |
echo "To reduce clutter and confusion, it is recommended to either remove the sample hooks, or unset their execute bits." | |
fi | |
( [ -f hook-chain ] || [ -L hook-chain ] || [ -f hook-chain.sh ] || [ -L hook-chain.sh ] ) && { | |
echo "Hook-chain appears to already be installed." | |
exit 0 | |
} | |
wget -nv -O hook-chain https://gist.githubusercontent.com/dale-c-anderson/b791c79de07d604b76db/raw/hook-chain.sh || { | |
cerr "ERR: Download failed. Hook-chain was not installed." | |
exit 1 | |
} | |
chmod +x hook-chain || { | |
cerr "ERR: Hook-chain was downloaded, but could not be made executable. You'll need to do this manually before hook-chain will work." | |
} | |
HOOKS="applypatch-msg pre-applypatch post-applypatch pre-commit prepare-commit-msg commit-msg post-commit pre-rebase post-checkout post-merge pre-receive update post-receive post-update pre-auto-gc post-rewrite pre-push" | |
MOVED_BY_INSTALLER=0 | |
for HOOK in $HOOKS; do | |
# The order of these tests matter. | |
if test -L "./$HOOK"; then | |
echo "$HOOK skipped: it's already a symlink to something else." | |
continue | |
elif test -f "./$HOOK"; then | |
# This is hook with a standard name that, if executable, will need to be moved so a symlink | |
# can be created in its place. Stay in the loop so we can act on it in the next section. | |
true | |
elif test -f "./$HOOK.sample"; then | |
# Ignore sample hooks. | |
continue | |
elif other_suitable_hook_scripts_exist "$HOOK"; then | |
# This is already named in a way can be processed by hook-chain: i.e. 'post-receive.notify-developers'. | |
# It doesn't need to be moved, but if the file is executable, a symlink to hook-chain will need to be created for | |
# the hook. Stay in the loop so we can act on it in the next section. | |
true | |
else | |
# Ignore hooks that don't yet exist. We could theoretically make a link to hook | |
# chain for all the possible hooks, but that would be presumptuous, not to mention messy. | |
continue | |
fi | |
# This next bit here is the whole point of having an install script. | |
if test -x "./$HOOK"; then | |
# Standard filename. Move it and create a link. | |
( mv -v "$HOOK" "$HOOK.unspecified-action" && ln -s hook-chain "$HOOK" ) || { | |
cerr "ERR: Could not move and relink $HOOK. Check permissions and try again." | |
} | |
MOVED_BY_INSTALLER=$((MOVED_BY_INSTALLER+1)) | |
elif other_suitable_hook_scripts_exist "$HOOK"; then | |
# Already named in a format that's usable by hook-chain. Just set up a link. | |
# shellcheck disable=SC2086 | |
echo "Linking '$HOOK' because of" ./$HOOK.* | |
ln -s hook-chain "$HOOK" || cerr "ERR: Could not link $HOOK. Check permissions and try again." | |
else | |
echo "$HOOK skipped: it's not executable." | |
continue | |
fi | |
done | |
if git rev-parse --is-bare-repository > /dev/null; then | |
# Create symlinks for pre/and post receive, regardless of whether there are hooks to use or not. | |
# Since pre and post receive hooks occur frequently, this saves a little effort on behalf of person installing hook-chain. | |
# This is an installer, after all. The user is going to be annoyed if there's more work required of them after it's finished. | |
test -L "pre-receive" || ln -s "hook-chain" "pre-receive" | |
test -L "post-receive" || ln -s "hook-chain" "post-receive" | |
fi | |
echo "Hook-chain is installed." | |
if [ $MOVED_BY_INSTALLER -gt 0 ]; then | |
echo "You should now rename any '.unspecified-action' scripts so the filename reflects it's real purpose." | |
echo "For example: $ mv post-receive.unspecified-action post-receive.notify-developers" | |
fi | |
} | |
function cerr () { | |
>&2 echo "$@" | |
} | |
# Returns 0 for true, any other number for false. | |
function other_suitable_hook_scripts_exist () { | |
HOOKTYPE="$1" | |
RETVAL=999 | |
for f in ./"$HOOKTYPE."*; do | |
# Thanks to http://stackoverflow.com/a/6364244/267455 | |
## Check if the glob gets expanded to existing files. | |
## If not, f here will be exactly the pattern above | |
## and the exists test will evaluate to false. | |
if [ -e "$f" ]; then | |
RETVAL=0 | |
else | |
RETVAL=1 | |
fi | |
## This is all we needed to know, so we can break after the first iteration | |
break | |
done | |
return $RETVAL | |
} | |
main "$@" || exit 1 |
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
#!/bin/bash | |
############################################################################### | |
# Git Hook Chaining, Dale Anderson, 2015-02-17 | |
# based on code found at: http://stackoverflow.com/a/8734391/267455 | |
# and: https://github.com/henrik/dotfiles/blob/master/git_template/hooks/pre-commit | |
#------------------------------------------------------------------------------ | |
# | |
# To install: | |
# | |
# 1) Copy this file in to your git hooks dir, and make it executable. | |
# | |
# 2) If you have an existing hook, rename it as "hook-type.action" | |
# e.g: $ mv post-receive post-receive.email-developers | |
# | |
# 3) Create your primary hook as a symlink to this file. | |
# e.g: $ ln -s hook-chain.sh post-receive | |
# | |
# | |
# 4) Optionally specify a location to log output of all hooks in git config: | |
# e.g: $ git config hookchain.log /path/to/writable/location/hookchain.log | |
# | |
# That's it. Now this script will execute every chained hook it finds. | |
# | |
# You can create as many more executable "hook-type.some-action" files you want, | |
# and they'll all get executed in alphabetical order. | |
# | |
# Repeat steps 2 and 3 for every hook you need to run multiple scripts for. All | |
# your hooks can be symlinked to the same master hook-chain script, and they'll | |
# all behave the same way. | |
# | |
#------------------------------------------------------------------------------ | |
function get_verified_log_location () { | |
# Returns the location of a writable log file, or /dev/null | |
# See if it's been specified. | |
CHAINLOG="$(git config hookchain.log)" || { | |
echo "/dev/null" | |
return | |
} | |
# Ensure we can write to it. Failing to write to a bad location will | |
# cause a false failure for a chained hook, causing annoyance to the folks | |
# who actually want to get some work done. | |
touch "$CHAINLOG" || { | |
>&2 echo "invalid git config hookhookchain.log: $CHAINLOG" | |
echo "/dev/null" | |
return | |
} | |
# If we got this far, the log file is writable. | |
echo "$CHAINLOG" | |
} | |
# Whatever this script was called as (pre-receive, post-receive, update, etc), | |
# that's the pattern of file names we'll be looking for to execute | |
# (post-receive.something, pre-receive.something, etc) | |
hookname=$(basename "$0") | |
# Capture what came from STDIN so it can be passed through to our chained hooks | |
data=$(cat) | |
# Keep track of exit codes so we can return an error if any hook failed. | |
exitcodes=() | |
# Optionally log the output of hooks. If no location specified, or if not writable, will write to /dev/null. | |
CHAINLOG="$(get_verified_log_location)" | |
# Since we are piping the hook through tee for logging, make sure | |
# a failed hook exit code is still captured. | |
set -o pipefail | |
# Process all the 'hookname.action' files we find and store the exit codes. | |
for hook in "$GIT_DIR/hooks/$hookname."*; do | |
if test -x "$hook"; then | |
if [[ "$hook" == "$GIT_DIR/hooks/$hookname.sample" ]]; then | |
# Ignore all the 'hookname.sample' files, as those were created by git, and it's not likely someone has named their hook that way. | |
echo "Hook-chain: ignoring $hook" | |
else | |
# This next line is how we impersonate the hook. | |
# We pipe anything we got from stdin, for hooks like pre-receive and post-receive. | |
# We forward any command line arguments we got after it, for hooks like update. | |
# Between both of these, this file can successfully impersonate any type of hook. | |
echo "$data" | "$hook" "$@" 2>&1 | tee -a "$CHAINLOG" | |
STATUS=$? | |
exitcodes+=($STATUS) | |
# Let the user know if a chained hook exited with an error. | |
[ $STATUS -eq 0 ] || { | |
echo "### Warning ### $hookname chained hook '$hook' exited with status $STATUS" | tee -a "$CHAINLOG" | |
} | |
fi | |
fi | |
done | |
# If any exit code isn't 0, exit with that code. | |
for ((i=0; i < ${#exitcodes[@]}; i++)); do | |
CODE=${exitcodes[$i]} | |
[ "$CODE" -eq 0 ] || exit "$CODE" | |
done |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment