Using Git Hooks to Automate Blog Publishing

Background

This blog is produced from a plaintext Org mode file that is passed to an org module called ox-hugo. Essentially, ox-hugo looks at the org file and produces Markdown files, which are then processed by a static website generation framework called Hugo. It goes without saying that these are all fantastic, free tools.

My Prior Workflow

Originally, when ready to publish the blog, I would save the org file1–this would trigger an export process2 that produced the Markdown files. This would result in a Hugo-friendly set of files that Hugo could then chew through to produce a complete static website. I put together this workflow based largely on this great post from Peter Cheng, Publishing a Website from Emacs and Hugo.

The Bash script to render and upload the site looks like this:

#!/usr/bin/env bash

# Sync /public (containing finalised HTML and resources)
# to webserver (e.g. ianhocking.com)

# e - exit if command exits with error
# u - treat unset variables as an error
set -eu

username="username"
server="servername"

blogDir="/Users/ianuser/Dropbox/org/blog/programmer"

# Remove previous build of site
rm -r "$blogDir/public" || echo "No /public directory in blog directory $blogDir to delete"

# Build
cd $blogDir && hugo || echo "Cannot cd to blog directory $blogDir"

# Upload
rsync -r --verbose --compress --human-readable --progress --recursive public/. $username@$server:public_html/blog/
Code Snippet 1: Contents of the file deploy.sh

Why the Change?

Until yesterday, the Git3 repository containing the source of my site was private, but I decided that I wanted to put into the ox-hugo showcase, so now it’s available on Github here.

Git Hooks

Git allows hooks, which are simply scripts run by Git4. You can find a fuller explanation of them in various places. Automate Your Workflow with Git Hooks is a good one.

Essentially, with the directory holding your tracked files, you’ll see .git/hooks. Inside that, there are the hooks themselves.

Let’s take a look at them.

ls -g ~/Dropbox/org/blog/programmer/.git/hooks

Gives us:

applypatch-msg.sample
commit-msg.sample
copy_of_post-update
fsmonitor-watchman.sample
post-commit
post-update
pre-applypatch.sample
pre-commit.sample
pre-push
pre-push.sample
pre-rebase.sample
pre-receive.sample
prepare-commit-msg.sample
resources
update.sample

Some of these hooks are designed to be run by Git on a server (i.e. receiving updates) and others locally (i.e. when sending). The details of which hook suits which end of the process are provided by Git - githooks Documentation.

The two hooks I’m interested in are post-commit and pre-push. In the directory listing above, you can see that both of these have the suffix .sample removed; that means that Git will pay attention to them. Additionally, to make sure that the hooks execute (which are simple Bash scripts after all), we need to make sure that execution privileges have been assigned:

chmod +x ls ~/Dropbox/org/blog/programmer/.git/hooks/post-commit
chmod +x ls ~/Dropbox/org/blog/programmer/.git/hooks/pre-push

My New Workflow

When I commit a change to the repository, I’d like git commit to run the hook post-commit. This hook will itself call git push, which will notice the pre-push hook and then run my deployment script.

It’s vitally important that the deployment script itself doesn’t make changes to the working tree, or we’ll end up in a situation where Git branches diverge–a difficult error to troubleshoot. For this reason, my .gitignore excludes: content/, public and logs. ~

#!/bin/sh

{ echo '-- ' &&
	date &&
	echo 'Git hook .git/hooks/post-commit executed by git-commit' &&
	echo 'Hook will push master branch to remote origin' ; } >> /Users/ianuser/Dropbox/org/blog/programmer/logs/hooks.log

git push origin master
Code Snippet 2: .git/hooks/post-commit

#!/bin/sh

{ echo '-- ' &&
	date &&
	echo 'Git hook .git/hooks/pre-push executed by git-push' &&
	echo 'Hook will call deploy.sh' ; } >> /Users/ianuser/Dropbox/org/blog/programmer/logs/hooks.log

/Users/ianuser/Dropbox/org/blog/programmer/deploy.sh
Code Snippet 3: .git/hooks/pre-push

Finally, I’m now able to make a commit–this will get pushed to my Github repository, the site built, an the site uploaded to ianhocking.com.


  1. Or ‘write the buffer to disk’ in Emacs-speak. [return]
  2. This is set with the variable org-hugo-auto-export-mode in a dotfile in the blog directory (.dirs-locals.el). [return]
  3. Git is a system for tracking file changes. [return]
  4. Actually, any number of subcommands. [return]
comments powered by Disqus