Pelican, Git Hooks, and github.io

Let me begin this post by saying that, in the midst of writing it, I realized that Amy Hanlon had already covered almost exactly the same material two days earlier. I’ve been scooped! I do recommend reading her post, as she is also the reason I tried out Pelican and Github Pages in the first place.

Every page on this blog is static. Each article is generated from a single Markdown source file using Pelican, and the html pages are then pushed to github.com/mavant/mavant.github.io, which in turn hosts them at mavant.com. One issue with this is that there’s no good way to host the Pelican source files in the same repository as the output files (aside from making the output directory a parent of the source directory). But we can certainly keep two separate repositories - one for the source files and one for the generated pages - and push to both of them at the same time. There are two ways we might go about this: We can alter the Pelican makefile, or we can add hooks to our git repository. I’ve opted for the latter, as it’s somewhat more generally useful in this context. In fact this could be used as an exceeedingly hackish alternative to git submodules (but still maybe less frustrating than submodules themselves).

Here’s how it works. Most (not all) stages of the git workflow have hooks defined - specific spots at which git pulls in (or ‘hooks’) a user-defined script to be executed locally or remotely, and these can be defined on a per-repository basis. Ours is going to be pretty simple.

The first step is to ensure that we aren’t duplicating the generated pages between our two repositories. This isn’t strictly necessary, but it makes things feel a bit neater, and anyway it’s very simple: We just add a single line to .gitignore in our source repository. If we’re already in our source repo directory, we just do this:

:::bash
echo "output/" >> .gitignore

Personally, I also like to exclude “*~”, because those saved-changes ~ files drive me up the wall, but that is probably better done in a ~/.gitignore_global. We could also define this rule in .git/info/exclude, which would ensure that this rule is not automatically included in clones of the repo (unlike .gitignore, which is checked in to the repository and tracked like any other file).

Before I move on to the second step, let me note: The assumption here is that you are using Pelican’s default output directory, “output”, and that “output” contains a git repository with yourname.github.io linked as origin/master. Furthermore, I am assuming that you’re linked to the github repo using SSH rather than HTTP, so that we will not have to ask for user input.

With that out of the way, the second step is to define a git hook. There are plenty of ways we could do this, but to keep this as simple as possible, I’m going to put all of this into one git hook, specifically pre-push (which executes on the local machine immediately prior to pushing to the remote repository). In short, whenever we push changes to the source repo, we also want to push changes to the pages repo, and before we do that we want to make sure we’ve generated the up-to-date static site. This consumes a whopping four lines of bash script:

:::python
#!/bin/bash
# This file should be located in your Pelican source directory at .git/hooks/pre-push
# First figure out what the commit message will be for the child repository. I like to use a summary of all the changes that have been made to the source repo since it was last pushed.
lastcommit=`git log origin/master..HEAD --oneline`
# Build the publication-quality version of the site with Pelican.
make publish
# Drop down into the output directory, add all changes (including deletions), commit with our summary message, and push to the origin.
cd output && git add -A && git commit -m "$lastcommit" && git push origin master
# Exit with a zero status - note that if we exit with a non-zero status, git will not push the source repo.
exit 0

That’s it. Just chmod +x .git/hooks/pre-push, and you’re good to go. The sum total of commands required to get this blog post up and running were

:::bash
vim content/4.md
git commit -am "Fourth post!"
git push origin master