Adding last modified timestamps with Git

A common ask of Jekyll, Gatsby, and other static site generator users is, “how do I automatically set the date in YAML front matter?” Today I learned you can do just that with a Git pre-commit hook.

If your project has been setup with git init you should have a Git hooks directory with several sample files to inspect.

├── .git
│   └── hooks
│       ├── applypatch-msg.sample
│       ├── commit-msg.sample
│       ├── post-update.sample
│       ├── pre-applypatch.sample
│       ├── pre-commit.sample
│       ├── ...

You can either create a new file named pre-commit inside of the hooks directory (or rename the .sample file). Then add the following shell script:

#!/bin/sh
# Contents of .git/hooks/pre-commit
# Replace `last_modified_at` timestamp with current time

git diff --cached --name-status | egrep -i "^(A|M).*\.(md)$" | while read a b; do
  cat $b | sed "/---.*/,/---.*/s/^last_modified_at:.*$/last_modified_at: $(date -u "+%Y-%m-%dT%H:%M:%S")/" > tmp
  mv tmp $b
  git add $b
done

Now when you commit a modified file with Git, the value of last_modified_at will be replaced with the current time i.e., YYYY-MM-DDThh:mm:ss. If you’re using a different front matter variable for modified timestamps, adjust the script above accordingly.

Before commitAfter commit
last_modified_at:last_modified_at: 2021-08-04T20:34:59

I’ve always followed the convention of using date for published timestamp and last_modified_at for the modified timestamp. Mostly because core Jekyll plugins like jekyll-sitemap and jekyll-feed support that front matter value.

---
title: "My awesome Markdown post"
date: 2020-01-01
last_modified_at: 2021-08-04T20:34:59
---

I’d be interested if the script could be improved on to append the date if last_modified_at hasn’t already been added, and replace the value if it has. Let me know below if you have any improves there.

I’m also not sure if this pre-commit hook is automatically installed when the Git repository is cloned. Do I need to install it on both my iMac running macOS and laptop running Windows 10? Or does it come along for the ride when push/pulling from remote?

5 mentions

  1. Simone Silvestroni

    Brilliant, thanks. I’ve checked, and based on git-scm:

    It’s important to note that client-side hooks are not copied when you clone a repository. If your intent with these scripts is to enforce a policy, you’ll probably want to do that on the server side; see the example in An Example Git-Enforced Policy.

  2. Simone Silvestroni

    Something weird happened after I implemented the hook. Code-wise was perfect, last_modified_at worked like a charm. However, after I’ve added the same hook to my colleague’s pre-commit hook, she committed a big chunk of images, which turned all of them into 0-bytes files.

    After a test, the same happened to me, so I removed the hook and the issue went away. Did this happened to you?

  3. Michael Rose

    @Simone - I’ve only used this method to do some light testing and didn’t see any issues.

    I did come across more robust versions of precommit hooks that appeared to check against file type, which would probably get around your issue of messing with binary files like images. For example this blog post has a version that looks for modified .html files. I’m sure it could be adapted for .md.

    All that said, not sure if I completely trust either scripts 💯 and still add last modified dates in a somewhat manual way using the Insert Date String VS Code plugin to drop in a new timestamp after I touch a file.

  4. Simone Silvestroni

    Thanks for your help. Just checked that VS extension… it might be time to leave Atom for good. After I left Sublime Text for good a few years ago. And that was after I ditched Textmate 😬

  5. Aldrin Cheung

    I made a script that would update all “*.md” files to add in last_modified_at tag to every single one of them. It worked for all my existing .md files. If there’s any exceptions please let me know.

    https://pastebin.com/jz3zYL6p

    #!/usr/bin/bash
    # Contents of .git/hooks/pre-commit
    # Replace `last_modified_at` timestamp with current time
    
    
    for b in $(find ./ -name '*.md'); do
      if [ $(cat $b | head -n 1 | grep '\-\-\-') ]; then
          echo "already have meta"
      else
        cat $b | sed '1s/^/---\n---\n/' > tmp
        mv tmp $b
      fi
      
      if grep -q last_modified_at $b; then
          echo "no change"
      else
        cat $b | sed '1 {/---.*/,/---.*/{0,/---/{s/---/---\nlast_modified_at:/}}}' > tmp
        mv tmp $b
      fi
    
      cat $b | sed "/---.*/,/---.*/s/^last_modified_at:.*$/last_modified_at: $(date -r $b "+%Y-%m-%dT%H:%M:%S")/" > tmp
      mv tmp $b
    done

Related

Using SSI to detect cookies

In my never ending quest to micro-optimize the hell out of my site, I ran into a snag when trying to use SSI directives to improve the loading of critical CSS and cached stylesheets.