gitolite is a wonderful tool that makes hosting git repositories pain-free. Gitolite also supports adding custom hooks but for whatever reason I found it a little hard to get my head around for what I wanted (a post-receive hook per repository).

For that reason I've written up a guide to setting up custom post-receive hooks for your repositories to automate various actions. Let's get started.

Preparing Gitolite.

Note. I am assuming Gitolite v3 for this, but it may be similar for v2 with some tweaking.

Our hooking system will exist almost entirely inside the Gitolite admin repository, but for that to work we first need to make some changes to .gitolite.rc over on the server side. So go ahead and open that up and make the following changes:

  1. Change the LOCAL_CODE directive to "$ENV{HOME}/.gitolite/local-code" (this allows our hook to fire)
  2. Change the GIT_CONFIG_KEYS directive to ".*" (this is to be able to use hooks.run later on)

Save the file and run gitolite setup at the shell to apply these changes. Now we can go ahead and set up the local-code directory structure in the admin repository back on your local machine to look like the following:

gitolite-admin
├── conf
├── keydir
└── local-code
    └── hooks
        └── common
            └── hooks.d

The hooking system.

This system has two components, the post-receive hook that executes the appropriate hooks from the hooks.d directory and the hooks themselves. Here is the code for our post-receive file that handles calling whichever hooks are defined for the repository (place this code under local-code/hooks/common/):

post-receive

#!/bin/bash
run_hook () {
  echo -en "\e[1;33m$4..\e[00m "
  echo $1 $2 $3 | $GIT_DIR/hooks/hooks.d/$4
}

echo -en "\e[1;33mRunning hooks..\e[00m "

while read oldrev newrev refname; do
  if [ "$refname" =  "refs/heads/master" ]; then
    hooks=$(git cat-file blob $newrev:.hooks 2>/dev/null)
    if [ -n "$hooks" ]; then
      # Repo-local hooks defined in .hooks.
      for hook in $hooks; do
        run_hook $oldrev $newrev $refname $hook
      done
    fi

    # Global hooks for this repo (ie. set in Gitolite config).
    hooks=$(git config --get hooks.run)
    [ -z "$hooks" ] && continue

    for hook in $hooks; do
      run_hook $oldrev $newrev $refname $hook
    done
  fi
done

echo -e "\e[1;32mDone.\e[00m"

I believe this is pretty self-explanatory but please comment if you'd like further explanation for any of it :)

Enabling hooks in your repositories.

As mentioned earlier these hooks are designed to be per repository so they are defined directly in gitolite.conf. For example, I use the following for my websites (note the config line):

repo    web/..*
  C = @zanea
  RW+ = @zanea
  config hooks.run = nginx-deploy notify

You can also specify hooks on a single line in a .hooks file inside the repository itself (useful for wild repos).

A simple deploy hook.

This is geared up for my setup (using nginx) but is easy to customize (and lives under local-code/hooks/common/hooks.d/):

nginx-deploy

#!/bin/bash
read oldrev newrev refname

# Get project name from current directory (without .git)
PROJECT=$(basename "$PWD")
PROJECT=${PROJECT%.git}

# Where the checkout should reside
WWW_DIR=/www/${PROJECT}

# Where logs should reside
LOG_DIR=/www/logs/${PROJECT}
mkdir -p $LOG_DIR ; chmod 770 $LOG_DIR/../ ; chmod 770 $LOG_DIR

# Checkout the website
mkdir -p $WWW_DIR
GIT_WORK_TREE=$WWW_DIR git checkout -q -f $newrev
chmod -R 770 $WWW_DIR

# Replace some template values in our nginx.conf
sed -i -e "s_{{WEBDIR}}_${WWW_DIR}_g" -e "s_{{LOGDIR}}_${LOG_DIR}_g" -e "s_{{PROJECT}}_${PROJECT}_g" ${WWW_DIR}/nginx.conf

# Symlink the nginx config to the nginx directory and tell nginx to reload
sudo ln -sf ${WWW_DIR}/nginx.conf /etc/nginx/sites-enabled/${PROJECT}.conf && sudo nginx -s reload

echo -e "\e[1;32mSuccessfully deployed ${PROJECT}.\e[00m"

With this in place along with an appropriate Gitolite config, you should now be able to run git commit -a; git push from your admin repository to set up your hooks! Give it a try using this barebones hook below:

test-print

#!/bin/bash
read oldrev newrev refname
echo "$(pwd), $GIT_DIR, $oldrev $newrev $refname"

Now if all goes well, when you push to a repository with the test-print hook defined you should see the script output in your terminal.

A world of possibilities (and automation) awaits.

One of my biggest reasons for wanting to use hooks in the first place is to automate code style checking and to generate documentation for projects at CodeShack. We tend to use a lot of PHP and luckily there are plenty of utilities out there to perform style checking of PHP code such as PHPMD (PHP Mess Detector), phpcpd (PHP Copy/Paste Detector), and PHP_CodeSniffer. There are also tools such as phpunit and phpDocumentor 2 for unit testing and documentation.

Using git hooks we can monitor these tools for any bad output and report a bug directly to Redmine under the appropriate project, as well as keep project documentation always up to date, all as the result of pushing new code to a repository :)


(Maybe) Related posts: