packages feed

gitrev-typed-0.1: src/Development/GitRev/Internal/Git.hs

-- | 'Q' primitives for git actions.
--
-- @since 0.1
module Development.GitRev.Internal.Git
  ( -- * Built-in
    GitError (..),
    gitBranchQ,
    gitCommitCountQ,
    gitCommitDateQ,
    gitDescribeQ,
    gitDiffQ,
    gitDirtyQ,
    gitDirtyTrackedQ,
    gitHashQ,
    gitShortHashQ,
    gitTreeQ,

    -- * Git primitives
    runGitQ,
    runGitPostProcessQ,
    IndexUsed (..),
  )
where

import Control.Exception
  ( Exception (displayException),
  )
import Data.Bifunctor (Bifunctor (first))
import Development.GitRev.Internal.Git.Common
  ( GitProcessArgs
      ( MkGitProcessArgs,
        fromStringTotal,
        gitRootArgs,
        runProcessGit,
        toOsPath,
        toStringTotal
      ),
    IndexUsed (IdxNotUsed, IdxUsed),
  )
import Development.GitRev.Internal.Git.Common qualified as Common
import Development.GitRev.Internal.OsString qualified as OsStringI
import Language.Haskell.TH (Q)
import Language.Haskell.TH.Syntax (Lift)
import System.Process qualified as Process

-- $setup
-- >>> import Development.GitRev.Typed (qToCode)

-- | Returns the current git branch.
--
-- ==== __Examples__
--
-- >>> $$(qToCode gitBranchQ)
-- Right ...
--
-- @since 0.1
gitBranchQ :: Q (Either GitError String)
gitBranchQ = runGitQ ["rev-parse", "--abbrev-ref", "HEAD"] IdxNotUsed

-- | Returns the git commit count.
--
-- ==== __Examples__
--
-- >>> $$(qToCode gitCommitCountQ)
-- Right ...
--
-- @since 0.1
gitCommitCountQ :: Q (Either GitError String)
gitCommitCountQ = runGitQ ["rev-list", "HEAD", "--count"] IdxNotUsed

-- | Returns the latest git commit date.
--
-- ==== __Examples__
--
-- >>> $$(qToCode gitCommitDateQ)
-- Right ...
--
-- @since 0.1
gitCommitDateQ :: Q (Either GitError String)
gitCommitDateQ = runGitQ ["log", "HEAD", "-1", "--format=%cd"] IdxNotUsed

-- | Returns the git description.
--
-- ==== __Examples__
--
-- >>> $$(qToCode gitDescribeQ)
-- Right ...
--
-- @since 0.1
gitDescribeQ :: Q (Either GitError String)
gitDescribeQ = runGitQ ["describe", "--long", "--always"] IdxNotUsed

-- | Return the diff of the working copy with HEAD.
--
-- ==== __Examples__
--
-- >>> $$(qToCode gitDiffQ)
-- Right ...
--
-- @since 0.1
gitDiffQ :: Q (Either GitError String)
gitDiffQ = runGitPostProcessQ id ["diff", "HEAD"] IdxNotUsed

-- | Returns the git dirty status.
--
-- ==== __Examples__
--
-- >>> $$(qToCode gitDirtyQ)
-- Right ...
--
-- @since 0.1
gitDirtyQ :: Q (Either GitError Bool)
gitDirtyQ = fmap Common.nonEmpty <$> runGitQ ["status", "--porcelain"] IdxUsed

-- | Returns the git dirty status, ignoring untracked files.
--
-- ==== __Examples__
--
-- >>> $$(qToCode gitDirtyTrackedQ)
-- Right ...
--
-- @since 0.1
gitDirtyTrackedQ :: Q (Either GitError Bool)
gitDirtyTrackedQ =
  fmap Common.nonEmpty
    <$> runGitQ ["status", "--porcelain", "--untracked-files=no"] IdxUsed

-- | Returns the latest git hash.
--
-- ==== __Examples__
--
-- >>> $$(qToCode gitHashQ)
-- Right ...
--
-- @since 0.1
gitHashQ :: Q (Either GitError String)
gitHashQ = runGitQ ["rev-parse", "HEAD"] IdxNotUsed

-- | Returns the latest git short hash.
--
-- ==== __Examples__
--
-- >>> $$(qToCode gitShortHashQ)
-- Right ...
--
-- @since 0.1
gitShortHashQ :: Q (Either GitError String)
gitShortHashQ = runGitQ ["rev-parse", "--short", "HEAD"] IdxNotUsed

-- | Returns the hash of the current tree.
--
-- ==== __Examples__
--
-- >>> $$(qToCode gitTreeQ)
-- Right ...
--
-- @since 0.1
gitTreeQ :: Q (Either GitError String)
gitTreeQ = runGitQ ["show", "HEAD", "--format=%T", "--no-patch"] IdxNotUsed

-- | Errors that can be encountered with git.
--
-- @since 0.1
newtype GitError = MkGitError
  { -- | @since 0.1
    reason :: String
  }
  deriving stock
    ( -- | @since 0.1
      Eq,
      -- | @since 0.1
      Lift,
      -- | @since 0.1
      Show
    )

-- | @since 0.1
instance Exception GitError where
  displayException (MkGitError s) = "Git error: " ++ s

-- | Runs git with the arguments. If 'IdxUsed' is passed, it is tracked for
-- recompilation purposes.
--
-- ==== __Examples__
--
-- >>> :{
--   -- Returns 'YYYY-MM-DD' rather than e.g. gitCommitDateQ's
--   -- 'Fri May 2 13:29:59 2025 +1200'.
--   gitCommitDateShortQ :: Q (Either GitError String)
--   gitCommitDateShortQ = runGitQ ["log", "HEAD", "-1", "--format=%cs"] IdxNotUsed
-- :}
--
-- @since 0.1
runGitQ ::
  -- | Arguments to git.
  [String] ->
  -- | Whether the index is used.
  IndexUsed ->
  Q (Either GitError String)
runGitQ = runGitPostProcessQ tillNewLineStr

-- | Like 'runGitQ', except it applies the given function to the result.
-- Normal 'runGitQ' takes everything up until the first new line or carriage
-- return.
--
-- ==== __Examples__
--
-- >>> :{
--   runGitNoProcessQ :: [String] -> IndexUsed -> Q (Either GitError String)
--   runGitNoProcessQ = runGitPostProcessQ id
-- :}
--
-- @since 0.1
runGitPostProcessQ ::
  -- | Function to run on the result.
  (String -> String) ->
  -- | Arguments to git.
  [String] ->
  -- | Whether the index is used.
  IndexUsed ->
  Q (Either GitError String)
runGitPostProcessQ postProcess args idxUsed =
  first mapGitError
    <$> Common.runGitPostprocess
      gitProcessArgs
      postProcess
      args
      idxUsed

mapGitError :: Common.GitError String -> GitError
mapGitError (Common.MkGitError s) = MkGitError s

gitProcessArgs :: GitProcessArgs String
gitProcessArgs =
  MkGitProcessArgs
    { fromStringTotal = id,
      gitRootArgs = ["rev-parse", "--show-toplevel"],
      runProcessGit = \args -> Process.readProcessWithExitCode "git" args "",
      toOsPath = OsStringI.encodeThrowM,
      toStringTotal = id
    }

tillNewLineStr :: String -> String
tillNewLineStr = takeWhile (\c -> c /= '\n' && c /= '\r')