I’ve been having a lot of fun with the new, open-source version of this site and have been looking for opportunities to experiment with things I’d have little reason to do on client sites. For instance, I wanted to see if I could get the current git checksum as a string using PHP (without resorting to shell commands) so that I could display it in the comments of my WordPress template. Once I was able to achieve that, I took it one step further and used it to invalidate browsers’ cached versions of my scripts and styles anytime I made an update to the theme.
Getting the hash
Getting the current revision out of your .git directory isn’t hard, you just need to know where to look. As I understand it, .git/refs/heads
is used to track the current HEAD of each branch of a git repository. If you open .git/refs/heads/master
, you should see the SHA-1 checksum (hash) for the current commit within the master branch. An easy way to read this into a string would be:
1 2 3 4 5 |
/* * This assumes that the script is running in the root of your * repository. Adjust the path to .git as appropriate. */ $commit = file_get_contents( '.git/refs/heads/master' ); |
Cache busting
Typically when a browser caches an asset like a javascript file or a stylesheet it depends on that cached file having the same URI. Since these types of files rarely uses query string arguments (?foo=bar
), developers commonly append version numbers (ideal), the current date (not-so-ideal), or other data (ideality varies) that can be changed to invalidate a cache, forcing the browser to download the latest version. In my opinion the version number would be the ideal query string amendment; if a new version of the script is pushed, the browser should download that version and forget about its cached copy. Realistically a lot of sites probably don’t have numbered versions for site files – that’s where our git commit can come in handy.
Using the query string concept and our previous example, we could append the current git commit to the end of our assets – in this case, a stylesheet called mystyles.css – so that browsers will download a fresh copy of these assets anytime the current HEAD changes:
1 2 3 4 5 6 7 8 |
/* * This assumes that the script is running in the root of your * repository. Adjust the path to .git as appropriate. */ $commit = file_get_contents( '.git/refs/heads/master' ); <link href="/css/mystyles.css?version=<?php echo $commit; ?>" type="text/css" rel="stylesheet" media="screen" /> |
The only major drawback here is that every asset with the current commit appended to it will be invalidated whenever the current HEAD changes, even if that file was unaffected. I’d argue that it’s still better than a datestamp (which would cause browsers to re-download whenever the date changes). Like anything on the web, do what will be best for you and your visitors.
Tying this into WordPress
I’ve expanded our little file_get_contents()
call into something a little more robust, which allows us to specify a branch and optionally truncate the returned commit to a particular length:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
/** * Get the hash of the current git HEAD. * * We can use this to version our scripts and styles to bust caches * when updates are made. * * @param string $branch The git branch to check. * @param int $length Optionally only return the first $length * characters of the hash. * @return string|boolean Either the hash or false. */ function grunwell_get_current_git_commit( $branch='master', $length=false ) { if ( ! defined( 'GRUNWELL_CURRENT_GIT_COMMIT' ) ) { $hash = file_get_contents( sprintf( '%s.git/refs/heads/%s', ABSPATH, $branch ) ); define( 'GRUNWELL_CURRENT_GIT_COMMIT', ( $hash ? $hash : false ) ); } return $length ? substr( GRUNWELL_CURRENT_GIT_COMMIT, 0, $length ) : GRUNWELL_CURRENT_GIT_COMMIT ); } |
This will see if the GRUNWELL_CURRENT_GIT_COMMIT
constant is defined and, if it isn’t, will read .git/refs/heads/{BRANCH NAME}
and save the value to the aforementioned constant. Using the constant allows us to a) avoid reading the file every time we want the hash, b) not have to use global variables, and c) prevent the value from being overwritten – if the hash changes mid-page load, something much more serious is up. As before, if you don’t keep your .git directory in the document root, you may need to adjust this to work in your environment.
To make this useful within WordPress, we can pass this hash as the $version
argument in the script/style registration and enqueue functions (wp_register_script()
, wp_enqueue_script()
, etc.). This will append ?ver={HASH}
to the end of external scripts and styles, which should be enough to force browsers to refresh (unless they’ve already grabbed the latest).
If our “mystyles.css” file were in our WordPress theme “my-theme”:
1 2 3 4 5 6 7 8 9 10 11 12 |
wp_register_style( 'base', get_bloginfo( 'template_url' ) . '/css/base.css', null, grunwell_get_current_git_commit( 'master', 8 ), 'all' ); // will now output: <link rel="stylesheet" id="base-css" type="text/css" media="all" href="http://example.com.com/wp-content/themes/my-theme/css/mystyles.css?ver={HASH}" /> |
Current commit shortcode
Another fun (and I mean that in the geekiest sense of the word) thing you could do is build a “current commit” shortcode so you can dynamically output the current site revision:
1 |
add_shortcode( 'current-commit', 'grunwell_get_current_git_commit' ); |
Wrapping up
There are a few practical (and a million geeky) things to do with the current commit’s hash, cache busting just happens to be one of them. What else can you think to do (or have you done before) with the hash?
Additional resources
- Can we prevent CSS caching? – An article on cache invalidation by Chris Coyier
- wp_register_script(), wp_enqueue_script(), wp_register_style(), and wp_enqueue_style() in the WordPress Codex
Tyler
Great post. We will be testing out these techniques in the coming days. Another option is to use file mod time, rather than the current time.
petitchevalroux
Thanks for this idea. An improved version of it may be using last commit hash of the file. This way you don’t flush cache if you commit something else. See http://stackoverflow.com/questions/4784575/how-do-i-find-the-most-recent-git-commit-that-modified-a-file