2
\$\begingroup\$

This is my first piece of code to post here. I wrote this script as a post-receive hook for a Git repository but I’m not so concerned with the Git parts (I’d like to have the part marked with TODO working but it’s not a priority).

It’s my first time to use either named pipes or the trap command to clean up temporary files. As the first comment states, I’ve written the script to be POSIX compatible – though I’ve only run it using Bash in POSIX mode.

Finally, I’m also happy to get feedback on any stylistic issues or common conventions that I could be using.

#!/bin/sh
# This shell script is POSIX compatible.  If using Bash, the use of named pipes
# could be replaced by process substitution (see further comments).

# This Git post-receive hook:
# 1. Sends an email to the project collaborators with details of commits being
# pushed to this repository.
# 2. Checks out changes in the branches into different working directories to
# be served as virtual sites by Apache and provides immediate feedback (on
# stdout) to the git user pushing to the repository.

readonly PROJECT=project_name
readonly RECIPIENTS="[email protected],[email protected]"

# Send email message with info about the changes being pushed.

# Keep track of which branch(es) are being pushed to since `refname` is only
# available while a line from stdin is being processed.
# Create an "empty" variable for each branch that may be checked out.
master_branch=''
beta_branch=''

# A named pipe is used to store the details of each commit from each branch
# being pushed. The original idea was to pipe the output of the do...while loop
# into the mail command. However, using a pipe means that the first part of the
# pipe (the loop) is executed in a subshell environment with the result that
# changes made to shell variables are lost when the subshell terminates.  A
# named pipe (or process substitution) is required to avoid creating a subshell
# whose variables aren't visible outside the subshell.
# See http://mywiki.wooledge.org/BashFAQ/024

readonly NAMED_PIPE=/tmp/${PROJECT}_pipe.$$
# Set trap to ensure the named pipe is cleaned up no matter how the script exits.
trap "rm -f $NAMED_PIPE" EXIT
# Create the named pipe if it doesn't already exist (it shouldn't).
if [ ! -p $NAMED_PIPE ]; then
    mkfifo $NAMED_PIPE
fi

# Run the mail command in the background so it stays open for as long as the
# named pipe has data.
mail "$RECIPIENTS" -s "Pushed commits for $PROJECT" < $NAMED_PIPE &

# For each branch being pushed to, there's a corresponding line on stdin.
while read oldrev newrev refname
do
    printf "\n====================================================\n"
    # If a new branch is being pushed to; this would result in a
    # `fatal: Invalid symmetric difference expression" error.
    # In this case, $oldrev is set to 0000000000000000000000000000000000000000
    # http://stackoverflow.com/questions/3511057/git-receive-update-hooks-and-new-branches
    if [ "$oldrev" != 0000000000000000000000000000000000000000 ]; then
        # Alternative solution is to use Bash to compare SHAs as hexadecimal
        # numbers using `-eq` but string comparison is simpler & more portable.
        printf "\nNew commits for branch: %s" ${refname##*/}
        printf "\n----------------------------------------\n\n"
        git log --no-color --stat ${oldrev}...${newrev}
        # git rev-list "$oldrev..$newrev"
    else
        printf "\n\nNew branch: %s\n" ${refname##*/}
        # TODO: Show the commits in the new branch, i.e., revisions
        # reachable from $newrev but not from any pre-existing branches.
    fi

    # Record which branch(es) are being updated.
    if [ "${refname##*/}" = "master" ]; then
        master_branch="true"
    elif [ "${refname##*/}" = "development" ]; then
        beta_branch="true"
    fi
done > $NAMED_PIPE
# done > >(mail "$RECIPIENTS" -s "Pushed commits for $PROJECT") # Bash process substitution.


# Check out the branches to their appropriate directories while printing
# command output to stdout. Note that one or more branches may have changes.

if [ "$master_branch" ]; then
    echo  Checking out the master branch into primary_site.staging
    git --work-tree /var/www/vhosts/primary_site.staging/ checkout -f master
    echo
fi

if [ "$beta_branch" ]; then
    echo  Checking out the development branch into beta_site.staging
    git --work-tree /var/www/vhosts/beta_site.staging/ checkout -f development
    echo
fi
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$
  • Make sure your script will stop early: set -o errexit -o noclobber -o nounset, but be aware of the caveats.
  • The fact that the master_branch and beta_branch variables are mutually exclusive is a hint that they can be merged:

    branch="${refname##*/}"
    
    case "$branch" in
        master)
            ...
            ;;
        development)
            ...
            ;;
    esac
    
  • You should use mktemp rather than PID to create a guaranteed unique file. A common best practice is to create a temporary directory since it's an atomic operation.

  • Use More Quotes™!

Other than this, it's very well written.

\$\endgroup\$
1
  • \$\begingroup\$ Thanks a lot for the feedback. Setting errexit, etc. makes sense and the case statement is cleaner and more readable. However, the temporary file I’m creating is a named pipe so I can’t use mktemp (AFAIK mknod is the only alternative to mkfifo for creating a named pipe). Finally, I’ve been looking at some of the scripts submitted to this site and reviewed by Janos; he/she warns against over-quoting so I’m trying to tread a middle ground. :) \$\endgroup\$ Nov 6, 2015 at 13:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.