How can I populate the Git commit ID into a file when I commit?

I would like to create Git hook(s) that will populate the commit id of the commit I am about to make into a file (basically variable substitution) in my source code. Is this possible with Git? Or is the fact that by resolving the variable to the git id, I am going to be changing the sha 1, thereby winding up with a "chicken or the egg" problem.


Solution 1:

The solution I have used for a similar situation is this:

  1. Put the string $Id$ somewhere in the file you want to have identified (e.g. test.html), probably within a comment or other non-functional section of the file where it won't cause issues.
  2. In your .gitattributes, flag the file in question with the ident keyword (e.g. *.html ident).

The result of this is that when git checkout copies the file out of the object database into your working directory, it expands the $Id$ string to read $Id: <sha-1 of file>$, and git add reverses that transformation when you want to check it in, so the versions of that file in your object database only ever contain $Id$, not the expanded forms.

That's a start, but unfortunately, finding the commit that contains a file with a specific hash is not so easy, and not necessarily one-to-one either. So, in addition, I also tag those files with the export-subst attribute (e.g. *.html ident export-subst in .gitattributes), and add an additional string, like $Format:%ci$ ($Format:%h$) somewhere in the file as well.

git checkout and git add don't affect these tags, though, so the versions in my repository always have exactly that string. In order to get those tags expanded, you have to use git archive to create a tar-ball (or .zip) of a specific version of your project, which you then use to deploy that version - you won't be able to just copy the files, or make install or whatever, since git archive is the only thing that will expand those tags.

The two tags I gave as an example expand to YYYY-MM-DD HH:MM:SS +TZOFFSET (HASH), where the HASH in this case is the actual commit hash, so it's more useful.

You can find other potentially usefull $Format:$ specifiers in the git log help page under the --pretty-format specifiers.

Solution 2:

It's impossible to do what you want: the commit's SHA-1 hash is calculated over the whole repository snapshot including each member file, so there's the chicken and egg problem — to calculate the commit's hash you need to know the contents of all the files which comprise it.

Solution 3:

You can do this with the post-commit hook. Here's an excerpt from the git-scm website

After the entire commit process is completed, the post-commit hook runs. It doesn’t take any parameters, but you can easily get the last commit by running git log -1 HEAD. Generally, this script is used for notification or something similar.

It would be a case of getting the output of git log -1 HEAD, then using a tool like sed to replace variables in your file. However, this modifies your working directory, and unless you're going to throw those changes away then you'd end up with a permanently modified working directory.

If you just want to use the current commit hash in a variable somewhere in your code, you could just execute git log -1 HEAD or cat .git/HEAD and store the output in your variable

If you only want the id (hash) like in the question title, you can use the --format flag. git log -1 HEAD --format=%H

Solution 4:

You can create a filter which does substitution on files on commit and checkout. These are called "smudge" and "clean" filters and their operation is controlled through .gitattributes. For example:

*.c     filter=yourfilter

This tells git to run the yourfilter filter for all .c files. You then have to tell git what yourfilter means:

git config --global filter.yourfilter.clean script1
git config --global filter.yourfilter.smudge script2

You'd then write a script (sed, Perl, Python, or anything) to replace an expression like $LastSha$ with $LastSha: <sha>$ on checkout ("smudge"). The other script reverses the expansion before commit ("clean".)

Search the Pro Git book for "Keyword Expansion" for a detailed example.

Solution 5:

OK, inspired by Jon Cairns' answer, I came up with this little snippet you could put in your Makefile.

version.h:
        git log -n 1 --format=format:"#define GIT_COMMIT \"%h\"%n" HEAD > $@

It's not a completely general solution, but it could come in handy. I know a place or two where I'll be using it.