miss-0: src/Data/Git/Monad.hs
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ViewPatterns #-}
{-|
Functionality we're currently missing (an assuredly incomplete list):
* @.git@ textfile (gitdir: some-path)
* using $GIT_DIRECTORY
* @objects/info/alternates@ or $GIT_ALTERNATE_OBJECT_DIRECTORIES
-}
module Data.Git.Monad
(
-- * The Git Monad
MonadGit(..)
, GitT
, Git
, runGit
, runGitT
-- * Repository Management
, initRepo
, repoPath
-- * Object Reading
, findBlob
, findTag
, findTree
, findTreeish
, findCommit
, grepCommit
, resolveSha
, resolveBlob
-- * Object Writing
, writeBlob
, writeTree
, writeCommit
, writeTag
-- * Writing Packfiles
, packing
-- * Ref Handling
, 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
-- | Monads that let you work with git repositories.
class Monad m => MonadGit m where
-- | Try to look up an object by its 'Sha1'.
lookupSha :: Sha1 -> m (Maybe Object)
-- | Write an 'Object' to storage, returning its 'Sha1'. We should have the law:
-- @
-- writeObject o >>= \s -> flushObjects >> lookupSha s == return (Just o)
-- @
writeObject :: Object -> m Sha1
-- | Flush written 'Object's to disk. Defaults to a no-op.
flushObjects :: m ()
flushObjects = return ()
-- | Try to get the 'Sha1' from a 'Ref'.
lookupRef :: Ref -> m (Maybe Sha1)
-- | A 'Map' from 'Ref's to the 'Sha1's at which they point, optionally. An instance may choose
-- not to provide hashes for some 'Ref's (e.g., only providing hashes for packed refs, but not
-- loose ones).
listRefs :: m (Map Ref (Maybe Sha1))
-- | Write a 'Ref' to the given 'Sha1'.
writeRef :: Ref -> Sha1 -> m ()
-- | Write a symref from the first argument to the second.
writeSymRef :: Ref -- ^ write a symref here
-> Ref -- ^ pointing to here
-> m ()
-- | Register a packfile with git so it knows to search it.
-- Primarily a support function for PackingT, and perhaps should be seperated out.
registerPack :: PackFile -> m ()
findThing :: MonadGit m => (Object -> Maybe a) -> Sha1 -> m (Maybe a)
findThing thing s = lookupSha s >>= return . maybe Nothing thing
-- | Lookup a 'Sha1'. 'Nothing' when the object does not exist or is not a 'Blob'.
findBlob :: MonadGit m => Sha1 -> m (Maybe Blob)
findBlob = findThing asBlob
-- | Lookup a 'Sha1'. 'Nothing' when the object does not exist or is not a 'Tree'.
findTree :: MonadGit m => Sha1 -> m (Maybe Tree)
findTree = findThing asTree
-- | Lookup a 'Sha1'. 'Nothing' when the object does not exist or is not a 'Commit'.
findCommit :: MonadGit m => Sha1 -> m (Maybe Commit)
findCommit = findThing asCommit
-- | Lookup a 'Sha1'. 'Nothing' when the object does not exist or is not a 'Tag'.
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
-- | A "treeish" is an object that can be recursively dereferenced to a 'Tree'. This includes
-- 'Tree's themselves, 'Commit's, and (usually) 'Tag's.
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
-- | Given a 'Sha1' that refers to a tree-ish (see 'findTreeish') and a list of path components,
-- find the 'Sha1' of the object in the tree at that path.
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 ]
-- | As 'resolveSha', expecting a 'Blob' at the given location.
resolveBlob :: (MonadFail m, MonadGit m) => Sha1 -> [PathComponent] -> m (Maybe Blob)
resolveBlob s p = runMaybeT $ do b <- MaybeT $ resolveSha s p
MaybeT $ findBlob b
-- | Search through the 'Commit' with the given 'Sha1' and its ancestors, for the first commit
-- satisfying the given predicate.
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
-- TODO: Shallow checkout?
Nothing -> pure Nothing
-- | Write a 'Blob', returning its 'Sha1'.
writeBlob :: MonadGit m => Blob -> m Sha1
writeBlob = writeObject . BlobObj
-- | Write a 'Tree', returning its 'Sha1'.
writeTree :: MonadGit m => Tree -> m Sha1
writeTree = writeObject . TreeObj
-- | Write a 'Commit', returning its 'Sha1'.
writeCommit :: MonadGit m => Commit -> m Sha1
writeCommit = writeObject . CommitObj
-- | Write a 'Tag', returning its 'Sha1'.
writeTag :: MonadGit m => Tag -> m Sha1
writeTag = writeObject . TagObj
-- | Read a 'Sha1' out of a branch (in @refs/heads/@)
readBranch :: MonadGit m => RefName -> m (Maybe Sha1)
readBranch = lookupRef . Branch
-- | Set a branch (in @refs/heads/@) to a particular 'Sha1'.
writeBranch :: MonadIO m => RefName -> Sha1 -> GitT m ()
writeBranch p = writeRef (Branch p)
-- | Read the 'Sha1' in @HEAD@
readHead :: MonadGit m => m (Maybe Sha1)
readHead = lookupRef HEAD
-- | Set @HEAD@ to a specific 'Sha1'. Leaves the repo in a "detached HEAD" state.
detachHead :: MonadGit m => Sha1 -> m ()
detachHead = writeRef HEAD
-- | Point @HEAD@ at a 'Ref'
writeHead :: MonadGit m => Ref -> m ()
writeHead = writeSymRef HEAD
-- | List all branches.
listBranches :: MonadGit m => m (S.Set RefName)
listBranches = listRefs >>= \rs -> return $ S.fromList [b | (Branch b, _) <- M.toList rs]
-- | Attempt to peel (recursively dereference) a ref (usually a tag) down to the 'Sha1' of a non-tag
-- object. TODO: improve this documentation.
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
-- | The peeled version of a 'Ref'.
peeled :: MonadGit m => Ref -> m Ref
peeled t@(TagRef rn Nothing) = TagRef rn <$> peelRef t
peeled t = return t
-- | A concrete 'MonadGit' instance writing loose objects.
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
-- | Read this repository's @packed-refs@ file, if it's there.
--
-- NB: Loose refs have priority over packed refs, so if (for example) a branch exists both loose
-- and packed in the repository and is associated with different hashes, it points to whatever the
-- loose one says. *However*, this function intentionally does *not* honor that.
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 -- strip of the leading "./"
reg <- isRegularFile <$> getFileStatus (inRepo ref)
return $ if reg then M.insert ref Nothing acc else acc
addBranch acc _ = return acc
-- | Like the instance for 'GitT', but writing packfiles.
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
-- | Run a 'GitT' computation, writing objects to a packfile instead of loose.
-- Currently objects are not findable until flushObjects is called.
packing :: MonadIO m => PackingT (GitT m) a -> GitT m a
packing git = do
template <- repoPath $ packDir </> "pack-"
runPackingT registerPack template git
-- | Minimal rendition of @git init@. When given 'Nothing', creates a @.git/@ the current working
-- directory. When given @'Just' path@, initializes a repository at @path@. Thus, @initRepo Nothing
-- == initRepo (Just ".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"
-- | Do some git computations in the given git directory.
runGitT :: MonadIO m => RawFilePath -> GitT m a -> m a
runGitT p git = evalStateT (unGitT $ loadPackFiles >> git) conf
where conf = GitConf p []
-- | Do some git computations in the given git directory.
runGit :: RawFilePath -> Git a -> IO a
runGit = runGitT