The Advisory Boar

By Abhijit Menon-Sen <ams@toroid.org>
2009-08-20

Updating last-modified dates with a git hook

I wrote a git post-commit hook that looks at certain files in my repository whenever I change them, edits them a bit if it wants to, and commits any changes it made. The changes it makes are not very interesting, but such a hook could, for example, be used to maintain "Last modified: ..." lines in static HTML files as shown below.

Let's say we want to update all foo/*.html files that contain something like the following line:

<div class=lastmod>Last modified: ...</div>

The idea is simple: use git diff --name-only HEAD^ HEAD to get a list of the files that were changed by the last commit, pick the ones we're interested in, edit them using sed, and commit any changes we make.

#!/bin/sh

git log -1 --format=%s HEAD|grep -q "^Auto: " && exit

START="<div class=lastmod>Last modified: "
END="</div>"

FILES=$(git diff --name-only HEAD^ HEAD|grep -x 'foo/.*\.html')
grep -xl "$START.*$END" $FILES < /dev/null |\
while read file;
do
    DATE=$(date '+%Y-%m-%d')
    sed "s,^\($START\).*\($END\),\1$DATE\2," $file > /tmp/$$
    mv /tmp/$$ $file
    git add $file
done

git diff-index --quiet --cached HEAD || git commit -m "Auto: lastmod"

There are a few things worth noting here. The hook is re-executed after its own commit, so we must protect against recursion, which I do by not acting on a commit with an "Auto: ..." message. We must be prepared for the possibility that the commit does not touch any of the files we care about (i.e. that $FILES is empty), which I do by feeding < /dev/null to grep. We add edited files to the index without knowing if we actually changed the value, and use git diff-index to see if anything changed at the end.

With the hook in place, you see something like this when you commit a change to some HTML file in foo:

[master e14fbb9] Auto: lastmod
 1 files changed, 1 insertions(+), 1 deletions(-)
[master dc5dc4d] Towards a transformative hermeneutics of hook scripts
 1 files changed, 123 insertions(+), 14 deletions(-)

(Adding -q to the git commit in the hook will suppress the "Auto: ..." message.)

Unfortunately, as the notation implies, git diff --name-only HEAD^ HEAD does not work for the root commit in the repository, because HEAD^ makes no sense without a parent. But I can't find a good way to list the files affected by a given commit that handles this special case.

There are some minor complications. The extra commits won't make everyone happy, especially since git commit --amend may or may not operate on your last commit any more (git reset HEAD^^ may be useful here). The automatically updated lines may also cause extra conflicts if you ever try to merge unrelated changes to the affected files.

For a very different mechanism that can be used to do (only) CVS-style $Keyword$ expansion outside the repository when exporting a commit with git-archive, see gitattributes(5).

(Questions and other feedback are welcome. Send me email.)

Tags: git • Link: etc/git-last-modified