{-# LANGUAGE LambdaCase #-} {-# LANGUAGE MultiWayIf #-} -- | -- Module : $Header$ -- Copyright : (c) 2015 Adam C. Foltzer -- License : BSD3 -- Maintainer : acfoltzer@galois.com -- Stability : provisional -- Portability : portable -- -- Some handy Template Haskell splices for including the current git -- hash and branch in the code of your project. Useful for including -- in panic messages, @--version@ output, or diagnostic info for more -- informative bug reports. -- -- > {-# LANGUAGE TemplateHaskell #-} -- > import Development.GitRev -- > -- > panic :: String -> a -- > panic msg = error panicMsg -- > where panicMsg = -- > concat [ "[panic ", $(gitBranch), "@", $(gitHash) -- > , " (", $(gitCommitDate), ")" -- > , " (", $(gitCommitCount), " commits in HEAD)" -- > , dirty, "] ", msg ] -- > dirty | $(gitDirty) = " (uncommitted files present)" -- > | otherwise = "" -- > -- > main = panic "oh no!" -- -- > % cabal exec runhaskell Example.hs -- > Example.hs: [panic master@2ae047ba5e4a6f0f3e705a43615363ac006099c1 (Mon Jan 11 11:50:59 2016 -0800) (14 commits in HEAD) (uncommitted files present)] oh no! module Development.GitRev (gitHash, gitBranch, gitDirty, gitCommitCount, gitCommitDate) where import Control.Exception import Control.Monad import Data.Maybe import Language.Haskell.TH import Language.Haskell.TH.Syntax import System.Directory import System.Exit import System.FilePath import System.Process import Prelude () import Prelude.Compat -- | Run git with the given arguments and no stdin, returning the -- stdout output. If git isn't available or something goes wrong, -- return the second argument. runGit :: [String] -> String -> IndexUsed -> Q String runGit args def useIdx = do let oops :: SomeException -> IO (ExitCode, String, String) oops _e = return (ExitFailure 1, def, "") gitFound <- runIO $ isJust <$> findExecutable "git" if gitFound then do -- a lot of bookkeeping to record the right dependencies pwd <- runIO getGitDirectory let hd = pwd ".git" "HEAD" index = pwd ".git" "index" packedRefs = pwd ".git" "packed-refs" hdExists <- runIO $ doesFileExist hd when hdExists $ do -- the HEAD file either contains the hash of a detached head -- or a pointer to the file that contains the hash of the head splitAt 5 `fmap` runIO (readFile hd) >>= \case -- pointer to ref ("ref: ", relRef) -> do let ref = pwd ".git" relRef refExists <- runIO $ doesFileExist ref when refExists $ addDependentFile ref -- detached head _hash -> addDependentFile hd -- add the index if it exists to set the dirty flag indexExists <- runIO $ doesFileExist index when (indexExists && useIdx == IdxUsed) $ addDependentFile index -- if the refs have been packed, the info we're looking for -- might be in that file rather than the one-file-per-ref case -- handled above packedExists <- runIO $ doesFileExist packedRefs when packedExists $ addDependentFile packedRefs runIO $ do (code, out, _err) <- readProcessWithExitCode "git" args "" `catch` oops case code of ExitSuccess -> return (takeWhile (/= '\n') out) ExitFailure _ -> return def else return def -- | Determine where our git directory is, in case we're in a -- submodule. getGitDirectory :: IO FilePath getGitDirectory = do pwd <- getCurrentDirectory let dotGit = pwd ".git" oops = return dotGit -- it's gonna fail, that's fine isDir <- doesDirectoryExist dotGit isFile <- doesFileExist dotGit if | isDir -> return dotGit | not isFile -> oops | isFile -> splitAt 8 `fmap` readFile dotGit >>= \case ("gitdir: ", relDir) -> do isRelDir <- doesDirectoryExist relDir if isRelDir then return relDir else oops _ -> oops -- | Type to flag if the git index is used or not in a call to runGit data IndexUsed = IdxUsed -- ^ The git index is used | IdxNotUsed -- ^ The git index is /not/ used deriving (Eq) -- | Return the hash of the current git commit, or @UNKNOWN@ if not in -- a git repository gitHash :: ExpQ gitHash = stringE =<< runGit ["rev-parse", "HEAD"] "UNKNOWN" IdxNotUsed -- | Return the branch (or tag) name of the current git commit, or @UNKNOWN@ -- if not in a git repository. For detached heads, this will just be -- "HEAD" gitBranch :: ExpQ gitBranch = stringE =<< runGit ["rev-parse", "--abbrev-ref", "HEAD"] "UNKNOWN" IdxNotUsed -- | Return @True@ if there are non-committed files present in the -- repository gitDirty :: ExpQ gitDirty = do output <- runGit ["status", "--porcelain"] "" IdxUsed case output of "" -> conE falseName _ -> conE trueName -- | Return the number of commits in the current head gitCommitCount :: ExpQ gitCommitCount = stringE =<< runGit ["rev-list", "HEAD", "--count"] "UNKNOWN" IdxNotUsed -- | Return the commit date of the current head gitCommitDate :: ExpQ gitCommitDate = stringE =<< runGit ["log", "HEAD", "-1", "--format=%cd"] "UNKNOWN" IdxNotUsed