{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE OverloadedStrings #-}
module Data.Git.Storage
( Git
, packedNamed
, gitRepoPath
, configs
, openRepo
, closeRepo
, withRepo
, withCurrentRepo
, findRepoMaybe
, findRepo
, isRepo
, initRepo
, getDescription
, setDescription
, iterateIndexes
, findReference
, findReferencesWithPrefix
, getObjectRaw
, getObjectRawAt
, getObject
, getObject_
, getObjectAt
, getObjectType
, setObject
) where
import Control.Exception
import qualified Control.Exception as E
import Control.Monad
import Data.List ((\\), isPrefixOf)
import Data.Either (partitionEithers)
import Data.IORef
import Data.Word
import Data.Typeable
import Data.Git.Named
import Data.Git.Imports
import Data.Git.OS
import Data.Git.Path (packedRefsPath)
import Data.Git.Delta
import Data.Git.Storage.FileReader
import Data.Git.Storage.PackIndex
import Data.Git.Storage.Object
import Data.Git.Storage.Pack
import Data.Git.Storage.Loose
import Data.Git.Storage.CacheFile
import Data.Git.Ref
import Data.Git.Config
import qualified Data.ByteString.Lazy as L
import qualified Data.Map as M
data PackIndexReader = PackIndexReader PackIndexHeader FileReader
type CachedPackedRef hash = CacheFile (PackedRefs (M.Map RefName (Ref hash)))
data Git hash = Git
{ gitRepoPath :: LocalPath
, indexReaders :: IORef [(Ref hash, PackIndexReader)]
, packReaders :: IORef [(Ref hash, FileReader)]
, packedNamed :: CachedPackedRef hash
, configs :: IORef [Config]
}
openRepo :: LocalPath -> IO (Git SHA1)
openRepo path = Git path <$> newIORef []
<*> newIORef []
<*> packedRef
<*> (readConfigs >>= newIORef)
where packedRef = newCacheVal (packedRefsPath path)
(readPackedRefs path M.fromList)
(PackedRefs M.empty M.empty M.empty)
readConfigs = do
global <- E.try readGlobalConfig :: IO (Either IOException Config)
local <- E.try (readConfig path)
return $ snd $ partitionEithers [local,global]
closeRepo :: Git hash -> IO ()
closeRepo (Git { indexReaders = ireaders, packReaders = preaders }) = do
mapM_ (closeIndexReader . snd) =<< readIORef ireaders
mapM_ (fileReaderClose . snd) =<< readIORef preaders
where closeIndexReader (PackIndexReader _ fr) = fileReaderClose fr
findRepoMaybe :: IO (Maybe LocalPath)
findRepoMaybe = do
menvDir <- E.catch (Just <$> getEnvAsPath "GIT_DIR") (\(_:: SomeException) -> return Nothing)
case menvDir of
Nothing -> getWorkingDirectory >>= checkDir 0
Just envDir -> isRepo envDir >>= \e -> return (if e then Just envDir else Nothing)
where checkDir :: Int -> LocalPath -> IO (Maybe LocalPath)
checkDir 128 _ = return Nothing
checkDir n wd = do
let filepath = wd </> ".git"
e <- isRepo filepath
if e then return (Just filepath) else checkDir (n+1) (if absolute wd then parent wd else wd </> "..")
findRepo :: IO LocalPath
findRepo = do
menvDir <- E.catch (Just <$> getEnvAsPath "GIT_DIR") (\(_:: SomeException) -> return Nothing)
case menvDir of
Nothing -> getWorkingDirectory >>= checkDir 0
Just envDir -> do
e <- isRepo envDir
when (not e) $ error "environment GIT_DIR is not a git repository"
return envDir
where checkDir :: Int -> LocalPath -> IO LocalPath
checkDir 128 _ = error "not a git repository"
checkDir n wd = do
let filepath = wd </> ".git"
e <- isRepo filepath
if e then return filepath else checkDir (n+1) (if absolute wd then parent wd else wd </> "..")
withRepo :: LocalPath -> (Git SHA1 -> IO c) -> IO c
withRepo path f = bracket (openRepo path) closeRepo f
withCurrentRepo :: (Git SHA1 -> IO a) -> IO a
withCurrentRepo f = findRepo >>= \path -> withRepo path f
isRepo :: LocalPath -> IO Bool
isRepo path = do
dir <- isDirectory path
subDirs <- mapM (isDirectory . (path </>))
[ "hooks", "info"
, "objects", "refs"
, "refs"</> "heads", "refs"</> "tags"]
return $ and ([dir] ++ subDirs)
initRepo :: LocalPath -> IO ()
initRepo path = do
exists <- isDirectory path
when exists $ error "destination directory already exists"
createParentDirectory path
createDirectory False path
mapM_ (createDirectory False . (path </>))
[ "branches", "hooks", "info"
, "logs", "objects", "refs"
, "refs"</> "heads", "refs"</> "tags"]
getDescription :: Git hash -> IO (Maybe String)
getDescription git = do
isdescription <- isFile descriptionPath
if (isdescription)
then do
content <- readTextFile descriptionPath
return $ Just content
else return Nothing
where descriptionPath = (gitRepoPath git) </> "description"
setDescription :: Git hash -> String -> IO ()
setDescription git desc = do
writeTextFile descriptionPath desc
where descriptionPath = (gitRepoPath git) </> "description"
iterateIndexes :: HashAlgorithm hash
=> Git hash
-> (b -> (Ref hash, PackIndexReader) -> IO (b, Bool))
-> b -> IO b
iterateIndexes git f initAcc = do
allIndexes <- packIndexEnumerate (gitRepoPath git)
readers <- readIORef (indexReaders git)
(a,terminate) <- loop initAcc readers
if terminate
then return a
else readRemainingIndexes a (allIndexes \\ map fst readers)
where loop acc [] = return (acc, False)
loop acc (r:rs) = do
(nacc, terminate) <- f acc r
if terminate
then return (nacc,True)
else loop nacc rs
readRemainingIndexes acc [] = return acc
readRemainingIndexes acc (idxref:idxs) = do
fr <- packIndexOpen (gitRepoPath git) idxref
idx <- packIndexReadHeader fr
let idxreader = PackIndexReader idx fr
let r = (idxref, idxreader)
modifyIORef (indexReaders git) (\l -> r : l)
(nacc, terminate) <- f acc r
if terminate
then return nacc
else readRemainingIndexes nacc idxs
findReference :: HashAlgorithm hash => Git hash -> Ref hash -> IO (ObjectLocation hash)
findReference git ref = maybe NotFound id <$> (findLoose `mplusIO` findInIndexes)
where
findLoose = do
isLoose <- looseExists (gitRepoPath git) ref
if isLoose then return (Just $ Loose ref) else return Nothing
findInIndexes = iterateIndexes git isinIndex Nothing
isinIndex acc (idxref, (PackIndexReader idxhdr indexreader)) = do
mloc <- packIndexGetReferenceLocation idxhdr indexreader ref
case mloc of
Nothing -> return (acc, False)
Just loc -> return (Just $ Packed idxref loc, True)
mplusIO :: IO (Maybe a) -> IO (Maybe a) -> IO (Maybe a)
mplusIO f g = f >>= \vopt -> case vopt of
Nothing -> g
Just v -> return $ Just v
findReferencesWithPrefix :: HashAlgorithm hash => Git hash -> String -> IO [Ref hash]
findReferencesWithPrefix git pre
| invalidLength = error ("not a valid prefix: " ++ show pre)
| not (isHexString pre) = error ("reference prefix contains non hexchar: " ++ show pre)
| otherwise = do
looseRefs <- looseEnumerateWithPrefixFilter (gitRepoPath git) (take 2 pre) matchRef
packedRefs <- concat <$> iterateIndexes git idxPrefixMatch []
return (looseRefs ++ packedRefs)
where
matchRef ref = pre `isPrefixOf` toHexString ref
invalidLength = length pre < 2 || length pre > 39
idxPrefixMatch acc (_, (PackIndexReader idxhdr indexreader)) = do
refs <- packIndexGetReferencesWithPrefix idxhdr indexreader pre
return (refs:acc,False)
readRawFromPack :: HashAlgorithm hash => Git hash -> Ref hash -> Word64 -> IO (FileReader, (PackedObjectRaw hash))
readRawFromPack git pref offset = do
readers <- readIORef (packReaders git)
reader <- maybe getDefault return $ lookup pref readers
po <- packReadRawAtOffset reader offset
return (reader, po)
where getDefault = do p <- packOpen (gitRepoPath git) pref
modifyIORef (packReaders git) ((pref, p):)
return p
readFromPack :: HashAlgorithm hash => Git hash -> Ref hash -> Word64 -> Bool -> IO (Maybe (ObjectInfo hash))
readFromPack git pref o resolveDelta = do
(reader, x) <- readRawFromPack git pref o
if resolveDelta then resolve reader o x else return $ Just $ generifyHeader x
where generifyHeader :: PackedObjectRaw hash -> ObjectInfo hash
generifyHeader (po, objData) = ObjectInfo { oiHeader = hdr, oiData = objData, oiChains = [] }
where hdr = (poiType po, poiActualSize po, poiExtra po)
resolve reader offset (po, objData) = do
case (poiType po, poiExtra po) of
(TypeDeltaOff, Just ptr@(PtrOfs doff)) -> do
let delta = deltaRead (L.toChunks objData)
let noffset = offset - doff
base <- resolve reader noffset =<< packReadRawAtOffset reader noffset
return $ addToChain ptr $ applyDelta delta base
(TypeDeltaRef, Just ptr@(PtrRef bref)) -> do
let delta = deltaRead (L.toChunks objData)
base <- getObjectRaw git bref True
return $ addToChain ptr $ applyDelta delta base
_ ->
return $ Just $ generifyHeader (po, objData)
addToChain ptr (Just oi) = Just (oi { oiChains = ptr : oiChains oi })
addToChain _ Nothing = Nothing
applyDelta :: Maybe Delta -> Maybe (ObjectInfo hash) -> Maybe (ObjectInfo hash)
applyDelta (Just delta@(Delta _ rSize _)) (Just objInfo) = Just $ objInfo
{ oiHeader = (\(a,_,c) -> (a,rSize,c)) $ oiHeader objInfo
, oiData = deltaApply (oiData objInfo) delta
}
applyDelta _ _ = Nothing
getObjectRawAt :: HashAlgorithm hash => Git hash -> ObjectLocation hash -> Bool -> IO (Maybe (ObjectInfo hash))
getObjectRawAt _ NotFound _ = return Nothing
getObjectRawAt git (Loose ref) _ = Just . (\(h,d)-> ObjectInfo h d[]) <$> looseReadRaw (gitRepoPath git) ref
getObjectRawAt git (Packed pref o) resolveDelta = readFromPack git pref o resolveDelta
getObjectRaw :: HashAlgorithm hash => Git hash -> Ref hash -> Bool -> IO (Maybe (ObjectInfo hash))
getObjectRaw git ref resolveDelta = do
loc <- findReference git ref
getObjectRawAt git loc resolveDelta
getObjectType :: HashAlgorithm hash => Git hash -> Ref hash -> IO (Maybe ObjectType)
getObjectType git ref = findReference git ref >>= getObjectTypeAt
where getObjectTypeAt NotFound = return Nothing
getObjectTypeAt (Loose _) = Just . (\(t,_,_) -> t) <$> looseReadHeader (gitRepoPath git) ref
getObjectTypeAt (Packed pref o) =
fmap ((\(ty,_,_) -> ty) . oiHeader) <$> readFromPack git pref o True
getObjectAt :: HashAlgorithm hash => Git hash -> ObjectLocation hash -> Bool -> IO (Maybe (Object hash))
getObjectAt git loc resolveDelta = maybe Nothing toObj <$> getObjectRawAt git loc resolveDelta
where toObj (ObjectInfo { oiHeader = (ty, _, extra), oiData = objData }) = packObjectFromRaw (ty, extra, objData)
getObject :: HashAlgorithm hash
=> Git hash
-> Ref hash
-> Bool
-> IO (Maybe (Object hash))
getObject git ref resolveDelta = maybe Nothing toObj <$> getObjectRaw git ref resolveDelta
where toObj (ObjectInfo { oiHeader = (ty, _, extra), oiData = objData }) = packObjectFromRaw (ty, extra, objData)
getObject_ :: (Typeable hash, HashAlgorithm hash)
=> Git hash
-> Ref hash
-> Bool
-> IO (Object hash)
getObject_ git ref resolveDelta = maybe (throwIO $ RefNotFound ref) return
=<< getObject git ref resolveDelta
setObject :: HashAlgorithm hash
=> Git hash
-> Object hash
-> IO (Ref hash)
setObject git obj = looseWrite (gitRepoPath git) obj