{-# Language DeriveFunctor #-}
{-# Language LambdaCase #-}
{-# Language OverloadedStrings #-}
{-# Language RecordWildCards #-}
{-# Language ViewPatterns #-}
module Restless.Git
( Path (..)
, File (..)
, make
, save
, load
) where
import Control.Monad (void)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.ByteString (ByteString)
import Data.Foldable (toList)
import Data.Map (Map)
import Data.Monoid ((<>))
import Data.Set (Set)
import Data.Text (Text, unpack)
import Data.Text.Encoding (decodeUtf8)
import HSH (run, (-|-))
import qualified Data.ByteString as BS
import qualified Data.Set as Set
import qualified Data.Map as Map
data Path = Path [ByteString] ByteString
deriving (Eq, Ord, Show)
data File = File { filePath :: Path, fileData :: ByteString }
deriving (Eq, Ord, Show)
data Tree a = Tree (Map ByteString (Tree a)) (Map ByteString a)
deriving (Functor, Show)
instance Monoid (Tree a) where
mempty =
Tree mempty mempty
mappend (Tree a b) (Tree c d) =
Tree (Map.unionWith mappend a c)
(Map.union b d)
make
:: (Monad m, MonadIO m)
=> FilePath -> m ()
make repo = liftIO $ do
void $ git repo "init" []
void $ git repo "commit" ["-am", "initialize", "--allow-empty"]
save
:: (Monad m, MonadIO m)
=> FilePath -> Text -> Set File -> m ()
save dst message files = liftIO $ do
tree <-
saveTree dst (treeFromFiles files)
parent <-
latestCommitOid dst defaultRef
commit <-
createCommit dst parent tree (unpack message)
updateReference dst defaultRef commit
return ()
load :: (Monad m, MonadIO m) => FilePath -> m (Set File)
load src = liftIO $ do
ls <- git src "ls-tree" ["-r", "-z", defaultRef]
Set.fromList <$> mapM (loadFile src) (filter (/= "") (BS.split 0 ls))
treeFromFiles :: Foldable t => t File -> Tree ByteString
treeFromFiles = foldMap singletonTree . toList
singletonTree :: File -> Tree ByteString
singletonTree (File {..}) =
case filePath of
Path [] name ->
Tree mempty (Map.singleton name fileData)
Path (x:xs) name ->
let subtree = singletonTree (File (Path xs name) fileData)
in Tree (Map.singleton x subtree) mempty
data ObjectType = TreeObject | BlobObject
deriving Show
newtype SHA1 = SHA1 ByteString
deriving Show
data MkTree =
MkTree (Map ByteString (ObjectType, SHA1))
deriving Show
serializeMkTree :: MkTree -> ByteString
serializeMkTree (MkTree m) =
mconcat . map (uncurry mkTreeLine) . Map.toList $ m
mkTreeLine :: ByteString -> (ObjectType, SHA1) -> ByteString
mkTreeLine name (TreeObject, SHA1 sha1) =
"040000 tree " <> sha1 <> "\t" <> name <> "\0"
mkTreeLine name (BlobObject, SHA1 sha1) =
"100644 blob " <> sha1 <> "\t" <> name <> "\0"
saveTree :: FilePath -> Tree ByteString -> IO SHA1
saveTree dst (Tree folders files) = do
trees <- mapM (fmap ((,) TreeObject) . saveTree dst) folders
blobs <- mapM (fmap ((,) BlobObject) . createBlob dst) files
let input = serializeMkTree (MkTree (trees <> blobs))
asSHA1 <$> run ((\() -> input) -|- git' dst "mktree" ["-z"])
asSHA1 :: ByteString -> SHA1
asSHA1 = SHA1 . fst . BS.break (== 0xa)
defaultRef :: String
defaultRef = "refs/heads/master"
git :: String -> String -> [String] -> IO ByteString
git repo cmd args = do
x <- run $ ("git" :: String, ["-C", repo] ++ (cmd : args))
return x
sha1String :: SHA1 -> String
sha1String (SHA1 bs) = unpack (decodeUtf8 bs)
createCommit :: FilePath -> SHA1 -> SHA1 -> String -> IO SHA1
createCommit dst parent tree message =
asSHA1 <$>
git dst "commit-tree"
["-p", sha1String parent, "-m", message, sha1String tree]
updateReference :: String -> String -> SHA1 -> IO ()
updateReference dst ref next =
void $ git dst "update-ref" [ref, sha1String next]
git' :: FilePath -> String -> [String] -> (String, [String])
git' repo cmd args =
("git" :: String, ["-C", repo] ++ (cmd : args))
createBlob :: String -> ByteString -> IO SHA1
createBlob dst bytes =
asSHA1 <$> run ((\() -> bytes) -|- git' dst "hash-object" ["--stdin", "-w"])
loadFile :: FilePath -> ByteString -> IO File
loadFile src line = do
let
(a, b) = BS.splitAt 52 line
sha1 = SHA1 (BS.drop 12 a)
name = BS.take (BS.length b - 1) (BS.drop 1 b)
path =
case BS.split 0x2f name of
[] -> error "empty file name"
parts -> Path (init parts) (last parts)
bytes <- git src "cat-file" ["blob", sha1String sha1]
return (File path bytes)
latestCommitOid :: FilePath -> String -> IO SHA1
latestCommitOid dst ref =
asSHA1 <$> git dst "rev-parse" [ref]