module Git ( module Git.CommitHash, getCommitHashes, getRootDir, getChangedFilesOf, Commit (..), Author (..), getCommitOf, readTextFileOfText, readTextFileOfBS, resolveRef, getCommitsBetween, diffOf, ) where import Control.Exception (IOException, catch, throw, throwIO) import Data.Binary (Binary) import Data.Binary.Instances () import Data.ByteString.Lazy qualified as LB import Data.List.NonEmpty (NonEmpty) import Data.List.NonEmpty qualified as N import Data.Maybe (fromMaybe) import Data.Text qualified as T import Data.Text.Encoding qualified as T import Data.Text.Lazy qualified as LT import Data.Text.Lazy.Encoding qualified as LT import Data.Text.Lazy.IO qualified as LT import Data.Time.Clock (UTCTime, getCurrentTime) import Exception qualified as E import GHC.Generics (Generic) import Git.CommitHash import Patch qualified as A import Process (proc, sh, sh_) import Text.Printf (printf) getCommitHashes :: IO (NonEmpty T.Text) getCommitHashes = fromMaybe (throw E.NoCommits) . N.nonEmpty . reverse . T.lines . T.decodeUtf8 . LB.toStrict <$> sh "git log --format=%H" getRootDir :: IO FilePath getRootDir = T.unpack . stripTrailingNL . T.decodeUtf8 . LB.toStrict <$> sh (proc "git rev-parse --show-toplevel") where stripTrailingNL s = fromMaybe s $ T.stripSuffix "\n" s getChangedFilesOf :: CommitHash -> IO [FilePath] getChangedFilesOf WorkingTree = do map T.unpack . T.lines . T.decodeUtf8 . LB.toStrict <$> sh "git ls-files --modified" getChangedFilesOf (Commit hash) = do map T.unpack . T.lines . T.decodeUtf8 . LB.toStrict <$> sh (proc "git show -p --name-only --format= %" hash) data Commit = Commit' { commitHash :: CommitHash, date :: UTCTime, author :: Author } deriving (Show, Generic, Binary, Eq) data Author = Author { name :: T.Text, email :: T.Text } deriving (Show, Generic, Binary, Eq) getCommitOf :: CommitHash -> IO Commit getCommitOf commitHash@WorkingTree = do date <- getCurrentTime authorName <- T.decodeUtf8 . LB.toStrict <$> sh "git config user.name" authorEmail <- T.decodeUtf8 . LB.toStrict <$> sh "git config user.email" pure Commit' { author = Author authorName authorEmail, .. } getCommitOf commitHash@(Commit hash) = do ( T.splitOn "\NUL" . head . T.lines . T.decodeUtf8 . LB.toStrict <$> sh ( proc "git show --quiet --format=%%ai%%x00%%ae%%x00%%an %" hash ) ) >>= \case rawDate : authorEmail : authorName : _ -> let date = read (T.unpack rawDate) in pure Commit' { author = Author authorName authorEmail, .. } _ -> throwIO E.NoCommits readTextFileOfText :: CommitHash -> FilePath -> IO LT.Text readTextFileOfText = readTextFileOf LT.readFile LT.decodeUtf8 readTextFileOfBS :: CommitHash -> FilePath -> IO LB.ByteString readTextFileOfBS = readTextFileOf LB.readFile id -- REVIEW Suggestion: we could use `id` instead of `\x -> x` -- -- REVIEW OK! -- -- RESOLVED readTextFileOf :: (FilePath -> IO a) -> (LB.ByteString -> a) -> CommitHash -> FilePath -> IO a readTextFileOf readFile _ WorkingTree filePath = catch (readFile filePath) (\(_ :: IOException) -> throwIO (E.CannotReadFile filePath)) readTextFileOf _ decode (Commit hash) filePath = catch (decode <$> sh (proc "git show %:%" hash filePath)) (\(_ :: E.ProcessException) -> throwIO (E.CannotReadFile (printf "%s:%s" hash filePath))) resolveRef :: T.Text -> IO CommitHash resolveRef = fmap (Commit . T.strip . T.decodeUtf8 . LB.toStrict) . sh . proc "git rev-parse %" -- | `getCommitsBetween prevCommit commit` returns the commits from `prevCommit` to `commit`. The result excludes `prevCommit`, but includes `commit`. -- -- If `prevCommit` is not an ancestor of `commit`, this functions throws `NoAncestor commit prevCommit`. getCommitsBetween :: CommitHash -> CommitHash -> IO [CommitHash] getCommitsBetween WorkingTree commit@(Commit _) = throwIO (E.NoAncestor WorkingTree commit) getCommitsBetween WorkingTree WorkingTree = pure [WorkingTree] getCommitsBetween prevCommit WorkingTree = fmap (++ [WorkingTree]) . getCommitsBetween prevCommit =<< resolveRef "HEAD" getCommitsBetween prevCommit@(Commit prevHash) commit@(Commit hash) = do catch (sh_ (proc "git merge-base --is-ancestor % %" prevHash hash)) (\(_ :: E.ProcessException) -> throwIO (E.NoAncestor commit prevCommit)) map (Commit . T.strip) . T.lines . T.decodeUtf8 . LB.toStrict <$> sh (proc "git log --format=%%H %..%" prevHash hash) diffOf :: CommitHash -> CommitHash -> IO A.Patch diffOf prevHash hash = A.parse . T.decodeUtf8 . LB.toStrict <$> sh (proc "git diff % %" (toTextUnsafe prevHash) (toTextUnsafe hash))