Last (git) modification time in Hakyll
While working on this site, I wanted to list metadata for each page describing both the time the page was originally written, and the date on which it was last modified. The first is fairly easy, and is included in Hakyll by default: Given a page at a path such as: writing/2018-08-03-last-git-modification-time-in-haskyll.markdown
, the Hakyll compiler will extract the 2018-08-03
portion of the filepath, and fill in the $date$
portion of the template with the formatted date.
The second, listing the modification time, was somewhat more difficult, and took a bit of hacking to get working nicely. To start with, we need some way to extract the modification times. This is performed by the following method:
buildModifications :: Pattern -> Rules [(Identifier, String)]
buildModifications pattern = do
ids <- getMatches pattern
pairs <- preprocess $ foldM getLastModified [] ids
return pairs
where
getLastModified l id' = do
t <- readProcess "git"
["log", "-1", "--format=%ad", "--date=format:%b %d, %Y", (toFilePath id')] ""
return $ (id', t) : l
Essentially, this function takes a pattern (describing a list of files) and, for each of them, runs the git
executable to find when the file was last modified according to git. We don’t really care about the actual modification time, but when we officially “committed” the changes to the site. Finally, we wrap the result in a Rules
monad, which implements the MonadMetadata
typeclass, smoothly integrating pattern matching and IO (through judicious use of preprocess
).
Once we have the modification times, we need to use a context to describe how, and when we should substitute in the times:
modificationCtx :: [(Identifier, String)] -> Context String
modificationCtx modificationTimes = field "lastModified" $ \item -> do
let time = find (\x -> (fst x) == (itemIdentifier item)) modificationTimes >>= return . snd
return $ fromMaybe "no recent modifications" $ time
This method is fairly straightforward: it takes a given item, looks up the lastModified
field in the body, looks up the time (if there is one) of the identifier of the item, and returns a context replacing lastModified
with the lookup operation on the item.
For fun, here’s a simple context where we can simply insert the path of the item’s identifier where requested:
githubCtx :: Context String
githubCtx = field "filepath" $ \item -> do
let path = (toFilePath . itemIdentifier) item
return $ path
To see them both in action, check out the site source.