darcs-2.18.1: src/Darcs/Util/Tree/Hashed.hs
-- Copyright (C) 2009-2011 Petr Rockai
--
-- BSD3
-- | A few darcs-specific utility functions. These are used for reading and
-- writing darcs and darcs-compatible hashed trees.
module Darcs.Util.Tree.Hashed
( -- * Obtaining Trees.
--
-- | Please note that Trees obtained this way will contain Stub
-- items. These need to be executed (they are IO actions) in order to be
-- accessed. Use 'expand' to do this. However, many operations are
-- perfectly fine to be used on a stubbed Tree (and it is often more
-- efficient to do everything that can be done before expanding a Tree).
readDarcsHashed
-- * Writing trees.
, writeDarcsHashed
-- * Interact with hashed tree
, hashedTreeIO
-- * Other
, readDarcsHashedNosize
, darcsAddMissingHashes
, darcsTreeHash
, darcsUpdateHashes
, followPristineHashes
) where
import qualified Data.ByteString.Char8 as BC
import qualified Data.ByteString.Lazy as BL
import Data.List ( sortBy )
import Data.Maybe ( fromMaybe )
import Darcs.Prelude
import Darcs.Util.Cache
( Cache
, fetchFileUsingCache
, writeFileUsingCache
)
import Darcs.Util.Hash
( Hash
, encodeBase16
, encodeHash
, sha256
, showHash
)
import Darcs.Util.Parser
import Darcs.Util.Path ( Name, decodeWhiteName, encodeWhiteName )
import Darcs.Util.Progress ( debugMessage, finishedOneIO, withSizedProgress )
import Darcs.Util.Tree
( Blob(..)
, ItemType(..)
, Tree(..)
, TreeItem(..)
, addMissingHashes
, expand
, itemHash
, list
, listImmediate
, makeTreeWithHash
, readBlob
, updateSubtrees
, updateTree
)
import Darcs.Util.Tree.Monad ( TreeIO, runTreeMonad )
import Darcs.Util.ValidHash
( PristineHash
, decodeValidHash
, encodeValidHash
, fromHash
, getHash
, getSize
)
----------------------------------------------
-- Darcs directory format.
--
-- Precondition: all (immediate) items in the tree have hashes
darcsFormatDir :: Tree m -> BL.ByteString
darcsFormatDir =
BL.fromChunks . map formatItem . sortBy cmp . listImmediate
where
cmp (a, _) (b, _) = compare a b
formatItem (name, item) = BC.unlines
[ case item of
File _ -> kwFile
_ -> kwDir
, encodeWhiteName name
, case itemHash item of
Nothing -> error "precondition of darcsFormatDir"
Just h -> encodeBase16 h
]
darcsParseDir
:: FilePath -> BC.ByteString -> Either String [(ItemType, Name, PristineHash)]
darcsParseDir path = withPath path . parseAll (many pDir)
where
pDir = do
t <- pHeader
char '\n'
n <- pName
char '\n'
h <- pHash
char '\n'
return (t, n, h)
pHeader = (BlobType <$ string kwFile) <|> (TreeType <$ string kwDir)
pName = do
name <- takeTillChar '\n'
either fail return (decodeWhiteName name)
pHash = do
hash <- takeTillChar '\n'
maybe (fail "expected valid hash") return (decodeValidHash (BC.unpack hash))
kwFile, kwDir :: BC.ByteString
kwFile = BC.pack "file:"
kwDir = BC.pack "directory:"
----------------------------------------
-- Utilities.
--
-- | Compute a darcs-compatible hash value for a tree-like structure.
darcsTreeHash :: Tree m -> Hash
darcsTreeHash = sha256 . darcsFormatDir
darcsUpdateDirHashes :: Tree m -> Tree m
darcsUpdateDirHashes = updateSubtrees update
where update t = t { treeHash = Just (darcsTreeHash t) }
darcsUpdateHashes :: Monad m => Tree m -> m (Tree m)
darcsUpdateHashes = updateTree update
where update (SubTree t) =
-- why not recursively ensure that hashes exist here?
return . SubTree $ t { treeHash = Just (darcsTreeHash t) }
update (File blob@(Blob con _)) =
do hash <- sha256 <$> readBlob blob
return $ File (Blob con (Just hash))
update stub = return stub
darcsHash :: Monad m => TreeItem m -> m Hash
darcsHash (SubTree t) = return (darcsTreeHash t)
darcsHash (File blob) = sha256 <$> readBlob blob
darcsHash (Stub unstub _) = darcsTreeHash <$> unstub
darcsAddMissingHashes :: (Monad m) => Tree m -> m (Tree m)
darcsAddMissingHashes = addMissingHashes darcsHash
-------------------------------------------
-- Reading darcs pristine data
--
-- | Read and parse a darcs-style hashed directory listing from a given @cache@
-- and with a given @hash@.
readDarcsHashedDir :: Cache
-> PristineHash
-> IO [(ItemType, Name, PristineHash)]
readDarcsHashedDir cache ph = do
debugMessage $ "readDarcsHashedDir: " ++ encodeValidHash ph
(file, content) <- fsReadHashedFile cache ph
either fail return $ darcsParseDir file content
-- | Read a darcs-style hashed tree.
readDarcsHashed' :: Bool -> Cache -> PristineHash -> IO (Tree IO)
readDarcsHashed' sizefail cache root = do
items' <- readDarcsHashedDir cache root
subs <- sequence [
do let h = getHash ph
case getSize ph of
Just _ | sizefail ->
fail ("Unexpectedly encountered size-prefixed hash in " ++ encodeValidHash root)
_ -> return ()
case tp of
BlobType -> return (d, File $
Blob (readBlob' ph) (Just h))
TreeType ->
do let t = readDarcsHashed cache ph
return (d, Stub t (Just h))
| (tp, d, ph) <- items' ]
return $ makeTreeWithHash subs (getHash root)
where readBlob' = fmap (BL.fromStrict . snd) . fsReadHashedFile cache
readDarcsHashed :: Cache -> PristineHash -> IO (Tree IO)
readDarcsHashed = readDarcsHashed' False
readDarcsHashedNosize :: Cache -> PristineHash -> IO (Tree IO)
readDarcsHashedNosize = readDarcsHashed' True
----------------------------------------------------
-- Writing darcs-style hashed trees.
--
-- | Write a Tree into a darcs-style hashed directory.
writeDarcsHashed :: Tree IO -> Cache -> IO PristineHash
writeDarcsHashed tree' cache = do
debugMessage "writeDarcsHashed"
t <- darcsUpdateDirHashes <$> expand tree'
let items = list t
withSizedProgress "Getting pristine" (length items) $ \k -> do
sequence_ [readAndWriteBlob k b | (_, File b) <- items]
let dirs = darcsFormatDir t : [darcsFormatDir d | (_, SubTree d) <- items]
mapM_ (dump k) dirs
return (fromHash (darcsTreeHash t))
where
readAndWriteBlob k b = readBlob b >>= dump k
dump k x = fsCreateHashedFile cache x >>= finishedOneIO k . encodeValidHash
-- | Create a hashed file from a 'Cache' and file content. In case the file
-- exists it is kept untouched and is assumed to have the right content.
fsCreateHashedFile :: Cache -> BL.ByteString -> IO PristineHash
fsCreateHashedFile cache content =
writeFileUsingCache cache (BL.toStrict content)
fsReadHashedFile :: Cache -> PristineHash -> IO (FilePath, BC.ByteString)
fsReadHashedFile = fetchFileUsingCache
-- | Run a 'TreeIO' @action@ in a hashed setting. Any changes will be written
-- out to the cache. Please note that actual filesystem files are never removed.
hashedTreeIO :: TreeIO a -- ^ action
-> Tree IO -- ^ initial
-> Cache
-> IO (a, Tree IO)
hashedTreeIO action tree cache = runTreeMonad action tree (const dumpItem)
where
dumpItem (File b) = File <$> dumpFile b
dumpItem (Stub unstub _) = SubTree <$> (unstub >>= dumpTree)
dumpItem (SubTree s) = SubTree <$> dumpTree s
-- This code is somewhat tricky. The original Tree may have come from
-- anywhere e.g. a plain Tree. So when we modify the content of a
-- file, we not only write a new hashed file, but also modify the
-- Blob itself, so that the embedded read action read this new hashed
-- file.
dumpFile :: Blob IO -> IO (Blob IO)
dumpFile (Blob getBlob mhash) = do
content <- getBlob
let hash = fromMaybe (sha256 content) mhash
debugMessage $ "hashedTreeIO.dumpFile: old hash=" ++ encodeHash hash
let getBlob' =
BL.fromStrict . snd <$>
fsReadHashedFile cache (fromHash hash)
nhash <- fsCreateHashedFile cache content
debugMessage $ "hashedTreeIO.dumpFile: new hash=" ++ encodeValidHash nhash
return $ Blob getBlob' (Just hash)
dumpTree :: Tree IO -> IO (Tree IO)
dumpTree t = do
debugMessage $ "hashedTreeIO.dumpTree: old hash=" ++ showHash (treeHash t)
t' <- darcsAddMissingHashes t
nhash <- fsCreateHashedFile cache (darcsFormatDir t')
debugMessage $ "hashedTreeIO.dumpTree: new hash=" ++ encodeValidHash nhash
return t'
-- | Return all 'PristineHash'es reachable from the given root set, which must
-- consist of directory hashes only.
followPristineHashes :: Cache -> [PristineHash] -> IO [PristineHash]
followPristineHashes cache = followAll
where
followAll roots = concat <$> mapM followOne roots
followOne root = do
x <- readDarcsHashedDir cache root
let subs = [ ph | (TreeType, _, ph) <- x ]
hashes = root : [ ph | (_, _, ph) <- x ]
(hashes ++) <$> followAll subs