{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE CPP #-}
module Data.Git.Storage.Object
( ObjectLocation(..)
, ObjectType(..)
, ObjectHeader
, ObjectData
, ObjectPtr(..)
, Object(..)
, ObjectInfo(..)
, Objectable(..)
, objectToType
, objectTypeMarshall
, objectTypeUnmarshall
, objectTypeIsDelta
, objectIsDelta
, objectToTree
, objectToCommit
, objectToTag
, objectToBlob
, treeParse
, commitParse
, tagParse
, blobParse
, objectParseTree
, objectParseCommit
, objectParseTag
, objectParseBlob
, objectWriteHeader
, objectWrite
, objectHash
) where
import Data.Git.Ref
import Data.Git.Types
import Data.Git.Imports
import qualified Data.Git.Parser as P
import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as BC
import qualified Data.ByteString.Lazy as L
import Data.List (intersperse)
import Data.Word
import Text.Printf
#if MIN_VERSION_bytestring(0,10,0)
import Data.ByteString.Lazy.Builder hiding (word8)
#else
import qualified Data.ByteString.Lazy.Char8 as LC
string7 :: String -> L.ByteString
string7 = LC.pack
byteString :: ByteString -> L.ByteString
byteString = LC.fromChunks . (:[])
toLazyByteString = id
#endif
data ObjectLocation hash = NotFound | Loose (Ref hash) | Packed (Ref hash) Word64
deriving (Show,Eq)
data ObjectPtr hash = PtrRef (Ref hash) | PtrOfs Word64 deriving (Show,Eq)
type ObjectHeader hash = (ObjectType, Word64, Maybe (ObjectPtr hash))
type ObjectData = L.ByteString
data ObjectInfo hash = ObjectInfo
{ oiHeader :: ObjectHeader hash
, oiData :: ObjectData
, oiChains :: [ObjectPtr hash]
} deriving (Show,Eq)
data Object hash =
ObjCommit (Commit hash)
| ObjTag (Tag hash)
| ObjBlob (Blob hash)
| ObjTree (Tree hash)
| ObjDeltaOfs (DeltaOfs hash)
| ObjDeltaRef (DeltaRef hash)
deriving (Show,Eq)
class Objectable a where
getType :: a hash -> ObjectType
getRaw :: a hash -> L.ByteString
isDelta :: a hash -> Bool
toObject :: a hash -> Object hash
objectToType :: Object hash -> ObjectType
objectToType (ObjTree _) = TypeTree
objectToType (ObjBlob _) = TypeBlob
objectToType (ObjCommit _) = TypeCommit
objectToType (ObjTag _) = TypeTag
objectToType (ObjDeltaOfs _) = TypeDeltaOff
objectToType (ObjDeltaRef _) = TypeDeltaRef
objectTypeMarshall :: ObjectType -> String
objectTypeMarshall TypeTree = "tree"
objectTypeMarshall TypeBlob = "blob"
objectTypeMarshall TypeCommit = "commit"
objectTypeMarshall TypeTag = "tag"
objectTypeMarshall _ = error "deltas cannot be marshalled"
objectTypeUnmarshall :: ByteString -> ObjectType
objectTypeUnmarshall "tree" = TypeTree
objectTypeUnmarshall "blob" = TypeBlob
objectTypeUnmarshall "commit" = TypeCommit
objectTypeUnmarshall "tag" = TypeTag
objectTypeUnmarshall _ = error "unknown object type"
objectTypeIsDelta :: ObjectType -> Bool
objectTypeIsDelta TypeDeltaOff = True
objectTypeIsDelta TypeDeltaRef = True
objectTypeIsDelta _ = False
objectIsDelta :: Object hash -> Bool
objectIsDelta (ObjDeltaOfs _) = True
objectIsDelta (ObjDeltaRef _) = True
objectIsDelta _ = False
objectToTree :: Object hash -> Maybe (Tree hash)
objectToTree (ObjTree tree) = Just tree
objectToTree _ = Nothing
objectToCommit :: Object hash -> Maybe (Commit hash)
objectToCommit (ObjCommit commit) = Just commit
objectToCommit _ = Nothing
objectToTag :: Object hash -> Maybe (Tag hash)
objectToTag (ObjTag tag) = Just tag
objectToTag _ = Nothing
objectToBlob :: Object hash -> Maybe (Blob hash)
objectToBlob (ObjBlob blob) = Just blob
objectToBlob _ = Nothing
octal :: P.Parser Int
octal = B.foldl' step 0 `fmap` P.takeWhile1 isOct
where isOct w = w >= 0x30 && w <= 0x37
step a w = a * 8 + fromIntegral (w - 0x30)
modeperm :: P.Parser ModePerm
modeperm = ModePerm . fromIntegral <$> octal
treeParse :: HashAlgorithm hash => P.Parser (Tree hash)
treeParse = Tree <$> parseEnts
where parseEnts = P.hasMore >>= \b -> if b then liftM2 (:) parseEnt parseEnts else return []
parseEnt = liftM3 (,,) modeperm parseEntName (P.byte 0 >> P.referenceBin)
parseEntName = entName <$> (P.skipASCII ' ' >> P.takeWhile (/= 0))
blobParse :: P.Parser (Blob hash)
blobParse = (Blob . L.fromChunks . (:[]) <$> P.takeAll)
commitParse :: HashAlgorithm hash => P.Parser (Commit hash)
commitParse = do
tree <- P.string "tree " >> P.referenceHex
P.skipEOL
parents <- many parseParentRef
author <- P.string "author " >> parsePerson
committer <- P.string "committer " >> parsePerson
encoding <- optional (P.string "encoding " >> P.tillEOL)
extras <- many parseExtra
P.skipEOL
message <- P.takeAll
return $ Commit tree parents author committer encoding extras message
where
parseParentRef = do
tree <- P.string "parent " >> P.referenceHex
P.skipEOL
return tree
parseExtra = do
b <- P.anyByte
if b == 0xa
then fail "no extra"
else do
r <- P.tillEOL
P.skipEOL
v <- concatLines <$> many (P.string " " *> P.tillEOL <* P.skipEOL)
return $ CommitExtra (b `B.cons` r) v
concatLines = B.concat . intersperse (B.pack [0xa])
tagParse :: HashAlgorithm hash => P.Parser (Tag hash)
tagParse = do
object <- P.string "object " >> P.referenceHex
P.skipEOL
type_ <- objectTypeUnmarshall <$> (P.string "type " >> P.tillEOL)
P.skipEOL
tag <- P.string "tag " >> P.tillEOL
P.skipEOL
tagger <- P.string "tagger " >> parsePerson
P.skipEOL
signature <- P.takeAll
return $ Tag object type_ tag tagger signature
parsePerson :: P.Parser Person
parsePerson = do
name <- B.init <$> P.takeUntilASCII '<'
P.skipASCII '<'
email <- P.takeUntilASCII '>'
_ <- P.string "> "
time <- P.decimal :: P.Parser Integer
_ <- P.string " "
timezoneSign <- maybe 1 id <$> optional ((const 1 <$> ascii '+') <|> (const (-1) <$> ascii '-'))
timezoneFmt <- P.decimal
let (h,m) = timezoneFmt `divMod` 100
timezone = timezoneSign * (h * 60 + m)
P.skipEOL
return $ Person name email (gitTime time timezone)
ascii :: Char -> P.Parser ()
ascii c = P.byte (asciiChar c)
asciiChar :: Char -> Word8
asciiChar c
| cp < 0x80 = fromIntegral cp
| otherwise = error ("char " <> show c <> " not valid ASCII")
where cp = fromEnum c
objectParseTree, objectParseCommit, objectParseTag, objectParseBlob :: HashAlgorithm hash => P.Parser (Object hash)
objectParseTree = ObjTree <$> treeParse
objectParseCommit = ObjCommit <$> commitParse
objectParseTag = ObjTag <$> tagParse
objectParseBlob = ObjBlob <$> blobParse
objectWriteHeader :: ObjectType -> Word64 -> ByteString
objectWriteHeader ty sz = BC.pack (objectTypeMarshall ty ++ " " ++ show sz ++ [ '\0' ])
objectWrite :: Object hash -> L.ByteString
objectWrite (ObjCommit commit) = commitWrite commit
objectWrite (ObjTag tag) = tagWrite tag
objectWrite (ObjBlob blob) = blobWrite blob
objectWrite (ObjTree tree) = treeWrite tree
objectWrite _ = error "delta cannot be marshalled"
treeWrite :: Tree hash -> L.ByteString
treeWrite (Tree ents) = toLazyByteString $ mconcat $ concatMap writeTreeEnt ents
where writeTreeEnt (ModePerm perm,name,ref) =
[ string7 (printf "%o" perm)
, string7 " "
, byteString $ getEntNameBytes name
, string7 "\0"
, byteString $ toBinary ref
]
commitWrite :: Commit hash -> L.ByteString
commitWrite (Commit tree parents author committer encoding extra msg) =
toLazyByteString $ mconcat els
where
toNamedRef s r = mconcat [string7 s, byteString (toHex r),eol]
toParent = toNamedRef "parent "
toCommitExtra (CommitExtra k v) = [byteString k, eol] ++
(concatMap (\l -> [byteString " ", byteString l, eol]) $ linesLast v)
linesLast b
| B.length b > 0 && B.last b == 0xa = BC.lines b ++ [ "" ]
| otherwise = BC.lines b
els = [toNamedRef "tree " tree ]
++ map toParent parents
++ [byteString $ writeName "author" author, eol
,byteString $ writeName "committer" committer, eol
,maybe (byteString B.empty) (byteString) encoding
]
++ concatMap toCommitExtra extra
++ [eol
,byteString msg
]
tagWrite :: Tag hash -> L.ByteString
tagWrite (Tag ref ty tag tagger signature) =
toLazyByteString $ mconcat els
where els = [ string7 "object ", byteString (toHex ref), eol
, string7 "type ", string7 (objectTypeMarshall ty), eol
, string7 "tag ", byteString tag, eol
, byteString $ writeName "tagger" tagger, eol
, eol
, byteString signature
]
eol :: Builder
eol = string7 "\n"
blobWrite :: Blob hash -> L.ByteString
blobWrite (Blob bData) = bData
instance Objectable Blob where
getType _ = TypeBlob
getRaw = blobWrite
toObject = ObjBlob
isDelta = const False
instance Objectable Commit where
getType _ = TypeCommit
getRaw = commitWrite
toObject = ObjCommit
isDelta = const False
instance Objectable Tag where
getType _ = TypeTag
getRaw = tagWrite
toObject = ObjTag
isDelta = const False
instance Objectable Tree where
getType _ = TypeTree
getRaw = treeWrite
toObject = ObjTree
isDelta = const False
instance Objectable DeltaOfs where
getType _ = TypeDeltaOff
getRaw = error "delta offset cannot be marshalled"
toObject = ObjDeltaOfs
isDelta = const True
instance Objectable DeltaRef where
getType _ = TypeDeltaRef
getRaw = error "delta ref cannot be marshalled"
toObject = ObjDeltaRef
isDelta = const True
objectHash :: HashAlgorithm hash => ObjectType -> Word64 -> L.ByteString -> Ref hash
objectHash ty w lbs = hashLBS $ L.fromChunks (objectWriteHeader ty w : L.toChunks lbs)
writeName :: ByteString -> Person -> ByteString
writeName label (Person name email locTime) =
B.concat [label, " ", name, " <", email, "> ", BC.pack timeStr]
where timeStr = show locTime