{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-|
Description: A monad for manipulating trees of files from git.
An 'EditTree' is a convenient representation of a 'Tree'. The leaves of an 'EditTree' are either
the contents of a 'Blob', a sub-'EditTree', or a 'Sha1'. This makes it easier to work with large
'Tree's, because subobjects are stored as hashes until you modify them.
-}
module Data.Git.EditTree where
import Prelude hiding (fail)
import Control.Monad.Fail
import Control.Monad.State hiding (fail)
import qualified Data.ByteString as B
import Data.ByteString.Lazy (ByteString)
import qualified Data.ByteString.Lazy as BL
import Data.Git
import Data.Map (Map)
import qualified Data.Map as Map
import System.Posix.FilePath
-- | A nice representation of a 'Tree'.
type EditTree = Map TreeEntry TreePart
-- | The leaves of an 'EditTree'---either a hash, 'Blob' data, or a subtree.
data TreePart = PartSha Sha1 | PartData ByteString | PartTree EditTree
-- | A monad for editing 'EditTree's.
newtype TreeEdit m a = TreeEdit { runTreeEdit :: StateT EditTree m a }
deriving (Functor, Applicative, Monad, MonadState EditTree, MonadIO, MonadTrans, MonadFail)
-- | Turn the given treeish 'Sha1' into an 'EditTree' whose leaves are all hashes.
loadEditTree :: (MonadGit m, MonadFail m) => Sha1 -> m EditTree
loadEditTree r = do (Just (Tree ents)) <- findTreeish r
return $ fmap PartSha ents
-- | Traverse an 'EditTree', writing new objects, and return the 'Sha1' of the new 'Tree'.
writeEditTree :: MonadGit m => EditTree -> m Sha1
writeEditTree = writeTree . Tree <=< traverse partToSha
where partToSha (PartSha s) = return s
partToSha (PartData b) = writeBlob $ Blob b
partToSha (PartTree t) = writeEditTree t
-- | Run a 'TreeEdit' computation against an 'EditTree', *without* writing the new objects out.
don'tEditTree :: MonadGit m => EditTree -> TreeEdit m a -> m EditTree
don'tEditTree et te = execStateT (runTreeEdit te) et
-- | Run a 'TreeEdit' computation against an 'EditTree', writing new objects as they occur.
editTree :: MonadGit m => EditTree -> TreeEdit m a -> m EditTree
editTree et te = do et' <- don'tEditTree et te
_ <- writeEditTree et'
return et'
-- | Delete an entry from the 'EditTree'.
rm :: Monad m => TreeEntry -> TreeEdit m ()
rm = modify . Map.delete
-- | Run a 'TreeEdit' in the subtree at the given path.
cd :: (MonadFail m, MonadGit m) => RawFilePath -> TreeEdit m a -> TreeEdit m a
cd dir act = splitPathComponents dir >>= (`cd'` act)
-- | As 'cd', but with a list of path components.
cd' :: (MonadFail m, MonadGit m) => [PathComponent] -> TreeEdit m a -> TreeEdit m a
cd' [] te = te
cd' (d:ds) te = cd1 d (cd' ds te)
-- | A one-level version of 'cd'.
cd1 :: (MonadFail m, MonadGit m) => PathComponent -> TreeEdit m a -> TreeEdit m a
cd1 d te = do old <- get
let dir = Entry d TreeMode
case old Map.! dir of
PartTree et -> put et
PartSha r -> put =<< lift (loadEditTree r)
_ -> error "cd1 exploded"
ret <- te
modify (\et -> Map.insert dir (PartTree et) old)
return ret
-- | Place a new leaf with the given filename.
create :: Monad m => TreeEntry -> TreePart -> TreeEdit m ()
create name ent = modify (Map.insert name ent)
-- | Create a subtree with the given filename.
mkdir :: MonadFail m => PathComponent -> TreeEdit m ()
mkdir dir = modify (Map.insertWith (flip const) (Entry dir TreeMode) (PartTree mempty))
-- | Create a path into the tree and do some 'TreeEdit's in that location.
cdCreating :: (MonadFail m, MonadGit m) => RawFilePath -> TreeEdit m a -> TreeEdit m a
cdCreating path te = (`cdCreating'` te) =<< splitPathComponents path
-- | As 'cdCreating', but with a list of path components.
cdCreating' :: (MonadFail m, MonadGit m) => [PathComponent] -> TreeEdit m a -> TreeEdit m a
cdCreating' path te = go path
where go [] = te
go (d:ds) = mkdir d >> cd1 d (go ds)
-- | A shortcut to create a bunch of files at once.
createFiles :: Monad m => Map PathComponent B.ByteString -> TreeEdit m ()
createFiles m = sequence_ [create (Entry file BlobMode) $ PartData (BL.fromStrict blob)
| (file, blob) <- Map.toList m]