-- Copyright (C) 2006-2007 David Roundy -- -- This program is free software; you can redistribute it and/or modify -- it under the terms of the GNU General Public License as published by -- the Free Software Foundation; either version 2, or (at your option) -- any later version. -- -- This program is distributed in the hope that it will be useful, -- but WITHOUT ANY WARRANTY; without even the implied warranty of -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -- GNU General Public License for more details. -- -- You should have received a copy of the GNU General Public License -- along with this program; if not, write to the Free Software Foundation, -- Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. {-# LANGUAGE OverloadedStrings #-} module Darcs.Repository.Hashed ( revertTentativeChanges , finalizeTentativeChanges , addToTentativeInventory , readPatches , readTentativePatches , writeAndReadPatch , writeTentativeInventory , copyHashedInventory , writePatchIfNecessary , tentativelyAddPatch , tentativelyRemovePatches , tentativelyRemovePatches_ , tentativelyAddPatch_ , tentativelyAddPatches , tentativelyAddPatches_ , reorderInventory , UpdatePristine(..) , repoXor ) where import Darcs.Prelude import Control.Monad ( unless, when ) import Data.List ( foldl' ) import System.Directory ( copyFile , createDirectoryIfMissing , renameFile ) import System.FilePath.Posix ( () ) import System.IO.Unsafe ( unsafeInterleaveIO ) import Darcs.Patch ( RepoPatch, effect, invert, invertFL, readPatch ) import Darcs.Patch.Apply ( Apply(..) ) import Darcs.Patch.Depends ( cleanLatestTag , removeFromPatchSet , slightlyOptimizePatchset , fullyOptimizePatchSet ) import Darcs.Patch.Format ( PatchListFormat ) import Darcs.Patch.Info ( displayPatchInfo, makePatchname, piName ) import Darcs.Patch.Invertible ( mkInvertible ) import Darcs.Patch.PatchInfoAnd ( PatchInfoAnd , createHashed , hopefully , info , patchInfoAndPatch ) import Darcs.Patch.Progress ( progressFL ) import Darcs.Patch.Read ( ReadPatch ) import Darcs.Patch.Rebase.Suspended ( addFixupsToSuspended , removeFixupsFromSuspended ) import Darcs.Patch.Set ( Origin, PatchSet(..), patchSet2RL ) import Darcs.Patch.Witnesses.Ordered ( FL(..) , foldlwFL , foldrwFL , mapRL , sequenceFL_ , (+>+) , (+>>+) ) import Darcs.Patch.Witnesses.Sealed ( Sealed(..) ) import Darcs.Patch.Witnesses.Unsafe ( unsafeCoerceP ) import Darcs.Repository.Flags ( OptimizeDeep(..) , RemoteDarcs , UpdatePending(..) , remoteDarcs ) import Darcs.Repository.Format ( RepoProperty(HashedInventory) , formatHas ) import Darcs.Repository.InternalTypes ( AccessType(..) , Repository , SAccessType(..) , repoAccessType , repoCache , repoFormat , repoLocation , unsafeCoerceR , withRepoDir ) import Darcs.Repository.Inventory ( peekPristineHash , pokePristineHash , readPatchesFromInventoryFile , showInventoryEntry , writeInventory , writePatchIfNecessary ) import qualified Darcs.Repository.Old as Old ( oldRepoFailMsg, readOldRepo ) import Darcs.Repository.Paths import Darcs.Repository.Pending ( readTentativePending , writeTentativePending ) import Darcs.Repository.Pristine ( applyToTentativePristine , convertSizePrefixedPristine ) import Darcs.Repository.Rebase ( withTentativeRebase ) import Darcs.Repository.Traverse ( cleanRepository ) import Darcs.Repository.Unrevert ( removeFromUnrevertContext ) import Darcs.Util.ByteString ( gzReadFilePS ) import Darcs.Util.Cache ( Cache, fetchFileUsingCache ) import Darcs.Util.File ( Cachable(Uncachable), copyFileOrUrl ) import Darcs.Util.Hash ( SHA1, sha1Xor, sha1zero ) import Darcs.Util.Lock ( appendDocBinFile , writeAtomicFilePS , writeDocBinFile ) import Darcs.Util.Printer ( renderString ) import Darcs.Util.Progress ( beginTedious, debugMessage, endTedious ) import Darcs.Util.SignalHandler ( withSignalsBlocked ) import Darcs.Util.Tree ( Tree ) -- |revertTentativeChanges swaps the tentative and "real" hashed inventory -- files, and then updates the tentative pristine with the "real" inventory -- hash. revertTentativeChanges :: Repository 'RO p wU wR -> IO () revertTentativeChanges repo = do copyFile hashedInventoryPath tentativeHashedInventoryPath inv <- gzReadFilePS tentativeHashedInventoryPath pristineHash <- convertSizePrefixedPristine (repoCache repo) (peekPristineHash inv) writeDocBinFile tentativePristinePath $ pokePristineHash pristineHash mempty {- -- this is not needed, as we never again access the pristine hash in -- tentativeHashedInventoryPath, only that in tentativePristinePath writeDocBinFile tentativeHashedInventoryPath $ pokePristineHash pristineHash inv -} -- |finalizeTentativeChanges trys to atomically swap the tentative -- inventory/pristine pointers with the "real" pointers; it first re-reads the -- inventory to optimize it, presumably to take account of any new tags, and -- then writes out the new tentative inventory, and finally does the atomic -- swap. In general, we can't clean the pristine cache at the same time, since -- a simultaneous get might be in progress. finalizeTentativeChanges :: RepoPatch p => Repository 'RW p wU wR -> IO () finalizeTentativeChanges r = do debugMessage "Optimizing the inventory..." -- Read the tentative patches ps <- readTentativePatches r writeTentativeInventory r ps i <- gzReadFilePS tentativeHashedInventoryPath p <- gzReadFilePS tentativePristinePath -- Write out the "optimised" tentative inventory. writeDocBinFile tentativeHashedInventoryPath $ pokePristineHash (peekPristineHash p) i -- Atomically swap. renameFile tentativeHashedInventoryPath hashedInventoryPath -- | Add (append) a patch to the tentative inventory. -- Warning: this allows to add any arbitrary patch! -- Used by convert import and 'tentativelyAddPatch_'. addToTentativeInventory :: RepoPatch p => Cache -> PatchInfoAnd p wX wY -> IO () addToTentativeInventory c p = do hash <- snd <$> writePatchIfNecessary c p appendDocBinFile tentativeHashedInventoryPath $ showInventoryEntry (info p, hash) -- | Read the recorded 'PatchSet' of a hashed 'Repository'. readPatchesHashed :: (PatchListFormat p, ReadPatch p) => Repository rt p wU wR -> IO (PatchSet p Origin wR) readPatchesHashed repo = case repoAccessType repo of SRO -> readPatchesFromInventoryFile hashedInventoryPath repo SRW -> readPatchesFromInventoryFile tentativeHashedInventoryPath repo -- | Read the tentative 'PatchSet' of a (hashed) 'Repository'. readTentativePatches :: (PatchListFormat p, ReadPatch p) => Repository 'RW p wU wR -> IO (PatchSet p Origin wR) readTentativePatches = readPatchesHashed -- |Copy the hashed inventory from the given location to the given repository, -- possibly using the given remote darcs binary. copyHashedInventory :: Repository 'RO p wU wR -> RemoteDarcs -> String -> IO () copyHashedInventory outrepo rdarcs inloc | remote <- remoteDarcs rdarcs = do let outloc = repoLocation outrepo createDirectoryIfMissing False (outloc inventoriesDirPath) copyFileOrUrl remote (inloc hashedInventoryPath) (outloc hashedInventoryPath) Uncachable debugMessage "Done copying hashed inventory." -- |writeAndReadPatch makes a patch lazy, by writing it out to disk (thus -- forcing it), and then re-reads the patch lazily. writeAndReadPatch :: RepoPatch p => Cache -> PatchInfoAnd p wX wY -> IO (PatchInfoAnd p wX wY) writeAndReadPatch c p = do (i, h) <- writePatchIfNecessary c p unsafeInterleaveIO $ readp h i where parse i h = do debugMessage $ "Rereading patch file for: " ++ piName i (fn, ps) <- fetchFileUsingCache c h case readPatch ps of Right x -> return x Left e -> fail $ unlines [ "Couldn't parse patch file " ++ fn , "which is" , renderString $ displayPatchInfo i , e ] readp h i = do Sealed x <- createHashed h (parse i) return . patchInfoAndPatch i $ unsafeCoerceP x -- | Write a 'PatchSet' to the tentative inventory. writeTentativeInventory :: RepoPatch p => Repository 'RW p wU wR -> PatchSet p Origin wX -> IO () writeTentativeInventory repo patchSet = do debugMessage "in writeTentativeInventory..." createDirectoryIfMissing False inventoriesDirPath let cache = repoCache repo tediousName = "Writing inventory" beginTedious tediousName hash <- writeInventory tediousName cache $ slightlyOptimizePatchset patchSet endTedious tediousName debugMessage "still in writeTentativeInventory..." (_filepath, content) <- fetchFileUsingCache cache hash writeAtomicFilePS tentativeHashedInventoryPath content tentativelyAddPatch :: (RepoPatch p, ApplyState p ~ Tree) => Repository 'RW p wU wR -> UpdatePending -> PatchInfoAnd p wR wY -> IO (Repository 'RW p wU wY) tentativelyAddPatch = tentativelyAddPatch_ UpdatePristine tentativelyAddPatches :: (RepoPatch p, ApplyState p ~ Tree) => Repository 'RW p wU wR -> UpdatePending -> FL (PatchInfoAnd p) wR wY -> IO (Repository 'RW p wU wY) tentativelyAddPatches = tentativelyAddPatches_ UpdatePristine data UpdatePristine = UpdatePristine | DontUpdatePristine | DontUpdatePristineNorRevert deriving Eq tentativelyAddPatches_ :: (RepoPatch p, ApplyState p ~ Tree) => UpdatePristine -> Repository 'RW p wU wR -> UpdatePending -> FL (PatchInfoAnd p) wR wY -> IO (Repository 'RW p wU wY) tentativelyAddPatches_ upr r upe ps = do let r' = unsafeCoerceR r withTentativeRebase r r' (foldlwFL (removeFixupsFromSuspended . hopefully) ps) withRepoDir r $ do sequenceFL_ (addToTentativeInventory (repoCache r)) ps when (upr == UpdatePristine) $ do applyToTentativePristine r $ mkInvertible $ progressFL "Applying to pristine" ps when (upe == YesUpdatePending) $ do debugMessage "Updating pending..." Sealed pend <- readTentativePending r writeTentativePending r' $ invertFL (effect ps) +>>+ pend return r' tentativelyAddPatch_ :: (RepoPatch p, ApplyState p ~ Tree) => UpdatePristine -> Repository 'RW p wU wR -> UpdatePending -> PatchInfoAnd p wR wY -> IO (Repository 'RW p wU wY) tentativelyAddPatch_ upr r upe p = tentativelyAddPatches_ upr r upe (p :>: NilFL) tentativelyRemovePatches :: (RepoPatch p, ApplyState p ~ Tree) => Repository 'RW p wU wR -> UpdatePending -> FL (PatchInfoAnd p) wX wR -> IO (Repository 'RW p wU wX) tentativelyRemovePatches = tentativelyRemovePatches_ UpdatePristine tentativelyRemovePatches_ :: (RepoPatch p, ApplyState p ~ Tree) => UpdatePristine -> Repository 'RW p wU wR -> UpdatePending -> FL (PatchInfoAnd p) wX wR -> IO (Repository 'RW p wU wX) tentativelyRemovePatches_ upr r upe ps | formatHas HashedInventory (repoFormat r) = do withRepoDir r $ do ref <- readTentativePatches r unless (upr == DontUpdatePristineNorRevert) $ removeFromUnrevertContext ref ps debugMessage "Removing changes from tentative inventory..." r' <- removeFromTentativeInventory r ps withTentativeRebase r r' (foldrwFL (addFixupsToSuspended . hopefully) ps) when (upr == UpdatePristine) $ applyToTentativePristine r $ invert $ mkInvertible $ progressFL "Applying inverse to pristine" ps when (upe == YesUpdatePending) $ do debugMessage "Adding changes to pending..." Sealed pend <- readTentativePending r writeTentativePending r' $ effect ps +>+ pend return r' | otherwise = fail Old.oldRepoFailMsg -- | Attempt to remove an FL of patches from the tentative inventory. -- -- Precondition: it must be possible to remove the patches, i.e. -- -- * the patches are in the repository -- -- * any necessary commutations will succeed removeFromTentativeInventory :: forall p wU wR wX. RepoPatch p => Repository 'RW p wU wR -> FL (PatchInfoAnd p) wX wR -> IO (Repository 'RW p wU wX) removeFromTentativeInventory repo to_remove = do debugMessage $ "Start removeFromTentativeInventory" allpatches :: PatchSet p Origin wR <- readTentativePatches repo remaining :: PatchSet p Origin wX <- case removeFromPatchSet to_remove allpatches of Nothing -> error "Hashed.removeFromTentativeInventory: precondition violated" Just r -> return r let repo' = unsafeCoerceR repo writeTentativeInventory repo' remaining debugMessage $ "Done removeFromTentativeInventory" return repo' -- | Writes out a fresh copy of the inventory that minimizes the -- amount of inventory that need be downloaded when people pull from -- the repository. The exact beavior depends on the 3rd parameter: -- -- For 'OptimizeShallow' it breaks up the inventory on the most recent tag. -- This speeds up most commands when run remotely, both because a -- smaller file needs to be transfered (only the most recent -- inventory). It also gives a guarantee that all the patches prior -- to a given tag are included in that tag, so less commutation and -- history traversal is needed. This latter issue can become very -- important in large repositories. -- -- For 'OptimizeDeep', the whole repo is traversed, from oldest to newest -- patch. Every tag we encounter is made clean, but only if that doesn't make -- any previous clean tag unclean. Every clean tags gets its own inventory. -- This speeds up "deep" operations, too, such as cloning a specific tag. -- It does not necessarily make the latest tag clean, but the benefits are -- similar to the shallow case. reorderInventory :: (RepoPatch p, ApplyState p ~ Tree) => Repository 'RW p wU wR -> OptimizeDeep -> IO () reorderInventory r deep | formatHas HashedInventory (repoFormat r) = do let optimize = case deep of OptimizeDeep -> fullyOptimizePatchSet OptimizeShallow -> cleanLatestTag readPatches r >>= return . optimize >>= writeTentativeInventory r cleanRepository r withSignalsBlocked $ finalizeTentativeChanges r | otherwise = fail Old.oldRepoFailMsg -- | Read inventories and patches from a 'Repository' and return them as a -- 'PatchSet'. Note that patches and inventories are read lazily. readPatches :: RepoPatch p => Repository rt p wU wR -> IO (PatchSet p Origin wR) readPatches r | formatHas HashedInventory (repoFormat r) = readPatchesHashed r | otherwise = do Sealed ps <- Old.readOldRepo (repoLocation r) return $ unsafeCoerceP ps -- | XOR of all hashes of the patches' metadata. -- It enables to quickly see whether two repositories -- have the same patches, independently of their order. -- It relies on the assumption that the same patch cannot -- be present twice in a repository. -- This checksum is not cryptographically secure, -- see http://robotics.stanford.edu/~xb/crypto06b/ . repoXor :: RepoPatch p => Repository rt p wU wR -> IO SHA1 repoXor repo = do hashes <- mapRL (makePatchname . info) . patchSet2RL <$> readPatches repo return $ foldl' sha1Xor sha1zero hashes