{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ViewPatterns #-}
module Data.Git.Monad
(
MonadGit(..)
, GitT
, Git
, runGit
, runGitT
, initRepo
, repoPath
, findBlob
, findTag
, findTree
, findTreeish
, findCommit
, grepCommit
, resolveSha
, resolveBlob
, writeBlob
, writeTree
, writeCommit
, writeTag
, packing
, readBranch
, readHead
, writeBranch
, writeHead
, detachHead
, listBranches
, readPackedRefs
, peelRef
, peeled
) where
import Prelude hiding (fail)
import Codec.Compression.Zlib
import Control.Monad.Catch
import Control.Monad.Fail
import Control.Monad.State hiding (fail)
import Control.Monad.Trans.Maybe
import qualified Data.ByteString as B
import qualified Data.ByteString.Builder as BB
import qualified Data.ByteString.Lazy as BL
import Data.Foldable
import Data.Map (Map)
import qualified Data.Map as M
import qualified Data.Set as S
import System.IO.Error (isDoesNotExistError)
import System.Posix.Directory.Traversals (traverseDirectory)
import System.Posix.FilePath
import System.Posix.Files.ByteString
import Data.Git.Formats
import Data.Git.Hash
import Data.Git.Internal.FileUtil
import Data.Git.Internal.Object (parseObject)
import Data.Git.Internal.Pack (isPackIndex)
import Data.Git.Internal.Parsers
import Data.Git.Internal.Types (GitConf(..), GitT(..))
import Data.Git.Object
import Data.Git.Pack
import Data.Git.Paths
import Data.Git.Ref
import Data.Git.RefName
import Data.Git.Types
class Monad m => MonadGit m where
lookupSha :: Sha1 -> m (Maybe Object)
writeObject :: Object -> m Sha1
flushObjects :: m ()
flushObjects = return ()
lookupRef :: Ref -> m (Maybe Sha1)
listRefs :: m (Map Ref (Maybe Sha1))
writeRef :: Ref -> Sha1 -> m ()
writeSymRef :: Ref
-> Ref
-> m ()
registerPack :: PackFile -> m ()
findThing :: MonadGit m => (Object -> Maybe a) -> Sha1 -> m (Maybe a)
findThing thing s = lookupSha s >>= return . maybe Nothing thing
findBlob :: MonadGit m => Sha1 -> m (Maybe Blob)
findBlob = findThing asBlob
findTree :: MonadGit m => Sha1 -> m (Maybe Tree)
findTree = findThing asTree
findCommit :: MonadGit m => Sha1 -> m (Maybe Commit)
findCommit = findThing asCommit
findTag :: MonadGit m => Sha1 -> m (Maybe Tag)
findTag = findThing asTag
findTreeishSha :: MonadGit m => Sha1 -> m (Maybe Sha1)
findTreeishSha s = lookupSha s >>= \case
Just (TreeObj _) -> return $ Just s
Just (CommitObj c) -> return . Just $ commitTree c
Just (TagObj t) -> findTreeishSha $ tagObject t
_ -> return Nothing
findTreeish :: MonadGit m => Sha1 -> m (Maybe Tree)
findTreeish s = lookupSha s >>= \case
Just (TreeObj t) -> return $ Just t
Just (CommitObj c) -> findTree $ commitTree c
Just (TagObj t) -> findTreeish $ tagObject t
_ -> return Nothing
resolveSha :: (MonadFail m, MonadGit m) => Sha1 -> [PathComponent] -> m (Maybe Sha1)
resolveSha s [] = findTreeishSha s
resolveSha s p = runMaybeT $ do t <- MaybeT (findTreeish s)
go t p
where go _ [] = fail "empty path should not occur in this part of resolveSha, wasn't a Treeish?"
go (Tree t) [b] = MaybeT . return $ lookupThing b t
go (Tree t) (d:bs) = do dir <- MaybeT . return $ M.lookup (Entry d TreeMode) t
t' <- MaybeT $ findTree dir
go t' bs
lookupThing b t = msum [ M.lookup (Entry b m) t | m <- modes ]
modes = [ BlobMode, ExecMode, TreeMode, SubmMode, LinkMode ]
resolveBlob :: (MonadFail m, MonadGit m) => Sha1 -> [PathComponent] -> m (Maybe Blob)
resolveBlob s p = runMaybeT $ do b <- MaybeT $ resolveSha s p
MaybeT $ findBlob b
grepCommit :: MonadGit m => (Commit -> Bool) -> Sha1 -> m (Maybe Sha1)
grepCommit f ref = do
jc <- findCommit ref
case jc of
Just c -> do
ps <- grepCommit f `mapM` commitParents c
return $ if f c then Just ref else asum ps
Nothing -> pure Nothing
writeBlob :: MonadGit m => Blob -> m Sha1
writeBlob = writeObject . BlobObj
writeTree :: MonadGit m => Tree -> m Sha1
writeTree = writeObject . TreeObj
writeCommit :: MonadGit m => Commit -> m Sha1
writeCommit = writeObject . CommitObj
writeTag :: MonadGit m => Tag -> m Sha1
writeTag = writeObject . TagObj
readBranch :: MonadGit m => RefName -> m (Maybe Sha1)
readBranch = lookupRef . Branch
writeBranch :: MonadIO m => RefName -> Sha1 -> GitT m ()
writeBranch p = writeRef (Branch p)
readHead :: MonadGit m => m (Maybe Sha1)
readHead = lookupRef HEAD
detachHead :: MonadGit m => Sha1 -> m ()
detachHead = writeRef HEAD
writeHead :: MonadGit m => Ref -> m ()
writeHead = writeSymRef HEAD
listBranches :: MonadGit m => m (S.Set RefName)
listBranches = listRefs >>= \rs -> return $ S.fromList [b | (Branch b, _) <- M.toList rs]
peelRef :: MonadGit m => Ref -> m (Maybe Sha1)
peelRef (TagRef _ (Just s)) = return (Just s)
peelRef r = do
s <- lookupRef r
o <- maybe (pure Nothing) findTag s
maybe (pure s) chaseTag o
where chaseTag Tag {tagType=tt, tagObject=to}
| tt == TagType = findTag to >>= maybe (pure Nothing) chaseTag
| otherwise = return $ Just to
peeled :: MonadGit m => Ref -> m Ref
peeled t@(TagRef rn Nothing) = TagRef rn <$> peelRef t
peeled t = return t
instance MonadIO m => MonadGit (GitT m) where
lookupSha s = do
s' <- getLooseSha s
maybe (gets $ msum . fmap (`findPackSha` s) . packs) (return . parseMaybe parseObject) s'
writeObject = writeLooseObject
lookupRef r = do
path <- repoPath r
ref <- liftIO $ readRefFile path
case ref of
Nothing -> (join . fmap (lookup r)) <$> readPackedRefs
Just (ShaRef s) -> return $ Just s
Just (SymRef r') -> lookupRef r'
listRefs = do
prs <- maybe mempty M.fromList <$> readPackedRefs
lrs <- looseRefs
return $ lrs `M.union` (Just <$> prs)
writeRef r s = do
path <- repoPath r
liftIO . createRawDirectoryIfMissing True . takeDirectory $ path
liftIO . writeRawFileS path . flip B.snoc 10 . getSha1Hex . toHex $ s
writeSymRef from to = do
fromPath <- repoPath from
liftIO . createRawDirectoryIfMissing True . takeDirectory $ fromPath
liftIO . writeRawFileS fromPath . flip B.snoc 10 . B.append "ref: " $ inRepo to
registerPack p = GitT $ modify (\g -> g { packs = p:(packs g) })
getLooseSha :: MonadIO m => Sha1 -> GitT m (Maybe BL.ByteString)
getLooseSha s = do
p <- repoPath $ looseObjectPath s
mwhenFileExists p (liftIO . fmap decompress . readRawFileL $ p)
writeLooseSha :: MonadIO m => Sha1 -> BL.ByteString -> GitT m Sha1
writeLooseSha s bs = do p <- repoPath $ looseObjectPath s
liftIO $ createRawDirectoryIfMissing True (takeDirectory p)
>> writeRawFileL p bs
>> return s
writeLooseObject :: MonadIO m => Object -> GitT m Sha1
writeLooseObject o = writeLooseSha (sha1 b) (compress b)
where b = BB.toLazyByteString . buildLooseObject $ o
packIndices :: MonadIO m => GitT m [RawFilePath]
packIndices = do
pd <- repoPath packDir
liftIO . handleJust (guard . isDoesNotExistError) (const $ return []) .
fmap (filter isPackIndex) . getRawDirectoryContents $ pd
addPackFile :: MonadIO m => RawFilePath -> GitT m ()
addPackFile fp = do gc <- get
pd <- repoPath packDir
pf <- liftIO $ readPackFile (pd </> dropExtension fp)
put gc { packs = pf : packs gc }
loadPackFiles :: MonadIO m => GitT m ()
loadPackFiles = packIndices >>= mapM_ addPackFile
readPackedRefs :: MonadIO m => GitT m (Maybe [(Ref, Sha1)])
readPackedRefs = repoPath packedRefsPath >>= liftIO . readPackedRefsFile
looseBranches, looseTags, looseRemotes, looseRefs :: MonadIO m => GitT m (Map Ref (Maybe Sha1))
looseBranches = findLooseRefs "refs/heads"
looseTags = findLooseRefs "refs/tags"
looseRemotes = findLooseRefs "refs/remotes"
looseRefs = fold <$> sequence [looseBranches, looseTags, looseRemotes]
findLooseRefs :: MonadIO m => RawFilePath -> GitT m (Map Ref (Maybe Sha1))
findLooseRefs p = do path <- gets gitDir
whenFileExists (path </> p) mempty $ do
liftIO . withRawCurrentDirectory path $
traverseDirectory addBranch mempty p
where addBranch acc (mkRef -> Just ref) = do
reg <- isRegularFile <$> getFileStatus (inRepo ref)
return $ if reg then M.insert ref Nothing acc else acc
addBranch acc _ = return acc
instance (MonadGit m, MonadIO m) => MonadGit (PackingT m) where
lookupSha = lift . lookupSha
writeObject = packObject
lookupRef = lift . lookupRef
listRefs = lift listRefs
writeRef r s = lift $ writeRef r s
writeSymRef from to = lift $ writeSymRef from to
flushObjects = flushPackFile registerPack
registerPack = lift . registerPack
packing :: MonadIO m => PackingT (GitT m) a -> GitT m a
packing git = do
template <- repoPath $ packDir </> "pack-"
runPackingT registerPack template git
initRepo :: Maybe RawFilePath -> IO ()
initRepo Nothing = initRepo $ Just ".git"
initRepo (Just d) = do
createRawDirectoryIfMissing True d
withRawCurrentDirectory d $ do
createRawDirectoryIfMissing False "refs"
createRawDirectoryIfMissing False "objects"
writeRawFileS "HEAD" "ref: refs/heads/master"
runGitT :: MonadIO m => RawFilePath -> GitT m a -> m a
runGitT p git = evalStateT (unGitT $ loadPackFiles >> git) conf
where conf = GitConf p []
runGit :: RawFilePath -> Git a -> IO a
runGit = runGitT