Git hooks

Earlier this month I wrote:

It seems inevitable that I’ll move, sooner or later, to Git. There are still a couple of gaps that would need to be filled in, but 90% of what I need is there.

Yesterday I talked about getting up a web interface for my public repositories; I’ve migrated three so far. The gitweb interface largely supercedes the existing "Subversion log" interface, at least for my open source projects.

Today I was able to implement the key infrastructure change necessary to start moving my private repositories to Git too. Due to their nature, I can’t provide access to these repositories via gitweb, so I need to hook them into the existing "Subversion log" mechanisms so that people can be informed of changes that are going on in the codebase.

The first automatic Git-driven update to the log can be seen here.

This is done with a Git post-receive hook script that updates the log (actually just a Movable Type weblog) via XML-RPC. Doing this is a little bit trickier than it is with Subversion.

Subversion hooks are simple: for each commit your hook script runs exactly once; and because Subversion is a client-server system all of commits occur in real time, meaning you don’t have to worry about the timestamps on your commits (they always just occurred when the hook script runs).

Git, on the other hand, will run your hook script once for each update to a branch (and for other things too, like branch creation and deletion, tagging and so forth). But if you push out a bunch of commits in one go, Git will fire the hook script only once; you then have to build up a revision list using the "plumbing" to take into account things like already-reported commits, and the possibility that your starting and end points might not even be on the same branch. And you don’t get easy-to-read, human-parseable revision numbers like you do in Subversion: you get hardcore cryptographic hashes.

In the end, the bit of logic that seems to do the job is this one (written in Ruby):

          @list = %x{
            git rev-parse --not --all |
            grep -v $(git rev-parse #{ref_name}) |
            git rev-list --reverse --stdin $(git merge-base #{old_rev} #{new_rev})..#{new_rev}
          }

So this seems to work, but it’s not as straight forward as the Subversion way. On the other hand, there’s a hell of a lot of scope for doing heavily customized stuff; look at the post-receive-email example hook script in the Git contrib directory. I guess it’s like many things in Git: tricky but powerful.

I like the look of the new entries because Git encourages (but doesn’t impose) the use of more detailed commit messages with a "subject" line followed by a blank separator line and then a "body", and it also provides tools for conveniently extracting those parts of the message (see man git-show). The only thing I’d like to change is the line wrapping behaviour; I’ve been hard-wrapping the messages so that they look good in the Terminal, but I’d like for the hard-wrapping to be ignored on the web… (Update: fixed here.)

Next step will be to continue migrating public and private repositories over to Git. Eventually I’ll rename the Subversion log to "Git log" too. (Update: that’s now done too, although the old svn-log URLs will continue to work.)

See also

Other posts on Git.