1{-# LANGUAGE NamedFieldPuns #-} 2 3{-| 4License : GPL-2 5 6The patch-index stores additional information that is extracted from 7the PatchSet for the repository to speed up certain commands (namely 8@log@ and @annotate@). More precisely, for every file tracked by the 9repository, it stores the list of patches that touch it. 10 11When created, patch-index lives in @_darcs\/patch_index\/@, and it 12should be automatically maintained each time the set of patches of 13the repository is updated. 14 15Patch-index can also be explicitely disabled by creating a file 16@_darcs\/no_patch_index@. "Explicitely disabed" means that no command 17should attempt to automatically create the patch-index. 18 19See <http://darcs.net/Internals/PatchIndex> for more information. 20-} 21module Darcs.Repository.PatchIndex 22 ( doesPatchIndexExist 23 , isPatchIndexDisabled 24 , isPatchIndexInSync 25 , canUsePatchIndex 26 , createPIWithInterrupt 27 , createOrUpdatePatchIndexDisk 28 , deletePatchIndex 29 , attemptCreatePatchIndex 30 , PatchFilter 31 , maybeFilterPatches 32 , getRelevantSubsequence 33 , dumpPatchIndex 34 , piTest 35 ) where 36 37import Darcs.Prelude 38 39import Control.Exception ( catch ) 40import Control.Monad ( forM_, unless, when ) 41import Control.Monad.State.Strict ( evalState, execState, State, gets, modify ) 42 43import Data.Binary ( Binary, encodeFile, decodeFileOrFail ) 44import qualified Data.ByteString as B 45import Data.Int ( Int8 ) 46import Data.List ( group, mapAccumL, sort, nub, (\\) ) 47import Data.Maybe ( fromJust, fromMaybe, isJust ) 48import qualified Data.Map as M 49import qualified Data.Set as S 50import Data.Word ( Word32 ) 51 52import System.Directory 53 ( createDirectory 54 , doesDirectoryExist 55 , doesFileExist 56 , removeDirectoryRecursive 57 , removeFile 58 , renameDirectory 59 ) 60import System.FilePath( (</>) ) 61import System.IO ( openFile, IOMode(WriteMode), hClose ) 62 63import Darcs.Patch ( RepoPatch, listTouchedFiles ) 64import Darcs.Patch.Apply ( ApplyState(..) ) 65import Darcs.Patch.Index.Types 66import Darcs.Patch.Index.Monad ( applyToFileMods, makePatchID ) 67import Darcs.Patch.Inspect ( PatchInspect ) 68import Darcs.Patch.PatchInfoAnd ( PatchInfoAnd, info ) 69import Darcs.Patch.Progress (progressFL ) 70import Darcs.Patch.Set ( PatchSet, patchSet2FL, Origin, patchSet2FL ) 71import Darcs.Patch.Witnesses.Ordered ( mapFL, RL(..), FL(..), reverseRL ) 72import Darcs.Patch.Witnesses.Sealed 73 ( Sealed2(..) 74 , Sealed(..) 75 , seal 76 , seal2 77 , unseal 78 , unseal2 79 ) 80import Darcs.Patch.Witnesses.Unsafe ( unsafeCoerceP, unsafeCoercePEnd ) 81 82import Darcs.Repository.Format ( formatHas, RepoProperty( HashedInventory ) ) 83import Darcs.Repository.InternalTypes ( Repository, repoLocation, repoFormat ) 84 85import Darcs.Util.Global ( darcsdir ) 86import Darcs.Util.Hash ( sha256sum, showAsHex ) 87import Darcs.Util.Lock ( withPermDir ) 88import Darcs.Util.Path ( AnchoredPath, displayPath, toFilePath, isPrefix ) 89import Darcs.Util.Progress ( debugMessage ) 90import Darcs.Util.SignalHandler ( catchInterrupt ) 91import Darcs.Util.Tree ( Tree(..) ) 92 93type Map = M.Map 94type Set = S.Set 95 96data FileIdSpan = FidSpan 97 !FileId -- ^ the fileid has some fixed name in the 98 !PatchId -- ^ span starting here 99 !(Maybe PatchId) -- ^ and (maybe) ending here 100 deriving (Show, Eq, Ord) 101 102data FilePathSpan = FpSpan 103 !AnchoredPath -- ^ the file path has some fixed fileid in the 104 !PatchId -- ^ span starting here 105 !(Maybe PatchId) -- ^ and (maybe) ending here 106 deriving (Show, Eq, Ord) 107 108-- | info about a given fileid, e.g.. is a file or a directory 109data FileInfo = FileInfo 110 { isFile :: Bool 111 , touching :: Set Word32 -- ^ first word of patch hash 112 } deriving (Show, Eq, Ord) 113 114-- | timespans where a certain filename corresponds to a file with a given id 115type FileIdSpans = Map AnchoredPath [FileIdSpan] 116 117-- | timespans where a file with a certain id corresponds to given filenames 118type FilePathSpans = Map FileId [FilePathSpan] 119 120-- | information file with a given ID 121type InfoMap = Map FileId FileInfo 122 123-- | the patch-index 124data PatchIndex = PatchIndex 125 { pids :: [PatchId] 126 -- ^ all the 'PatchId's tracked by this patch index, with the most 127 -- recent patch at the head of the list (note, stored in the 128 -- reverse order to this on disk for backwards compatibility 129 -- with an older format). 130 , fidspans :: FileIdSpans 131 , fpspans :: FilePathSpans 132 , infom :: InfoMap 133 } 134 135-- | On-disk version of patch index 136-- version 1 is the one introduced in darcs 2.10 137-- 2 changes the pids order to newer-to-older 138-- 3 changes FileName to AnchoredPath everywhere, which has 139-- different Binary (and Ord) instances 140version :: Int8 141version = 3 142 143type PIM a = State PatchIndex a 144 145-- | 'applyPatchMods pmods pindex' applies a list of PatchMods to the given 146-- patch index pindex 147applyPatchMods :: [(PatchId, [PatchMod AnchoredPath])] -> PatchIndex -> PatchIndex 148applyPatchMods pmods pindex = 149 flip execState pindex $ mapM_ goList pmods 150 where goList :: (PatchId, [PatchMod AnchoredPath]) -> PIM () 151 goList (pid, mods) = do 152 modify (\pind -> pind{pids = pid:pids pind}) 153 mapM_ (curry go pid) (nubSeq mods) 154 -- nubSeq handles invalid patch in darcs repo: 155 -- move with identical target name "rename darcs_patcher to darcs-patcher." 156 nubSeq = map head . group 157 go :: (PatchId, PatchMod AnchoredPath) -> PIM () 158 go (pid, PCreateFile fn) = do 159 fid <- createFidStartSpan fn pid 160 startFpSpan fid fn pid 161 createInfo fid True 162 insertTouch fid pid 163 go (pid, PCreateDir fn) = do 164 fid <- createFidStartSpan fn pid 165 startFpSpan fid fn pid 166 createInfo fid False 167 insertTouch fid pid 168 go (pid, PTouch fn) = do 169 fid <- lookupFid fn 170 insertTouch fid pid 171 go (pid, PRename oldfn newfn) = do 172 fid <- lookupFid oldfn 173 stopFpSpan fid pid 174 startFpSpan fid newfn pid 175 insertTouch fid pid 176 stopFidSpan oldfn pid 177 startFidSpan newfn pid fid 178 go (pid, PRemove fn) = do 179 fid <- lookupFid fn 180 insertTouch fid pid 181 stopFidSpan fn pid 182 stopFpSpan fid pid 183 go (pid, PDuplicateTouch fn) = do 184 fidm <- gets fidspans 185 case M.lookup fn fidm of 186 Just (FidSpan fid _ _:_) -> insertTouch fid pid 187 Nothing -> return () 188 Just [] -> error $ "applyPatchMods: impossible, no entry for "++show fn 189 ++" in FileIdSpans in duplicate, empty list" 190 191-- | create new filespan for created file 192createFidStartSpan :: AnchoredPath -> PatchId -> PIM FileId 193createFidStartSpan fn pstart = do 194 fidspans <- gets fidspans 195 case M.lookup fn fidspans of 196 Nothing -> do 197 let fid = FileId fn 1 198 modify (\pind -> pind {fidspans=M.insert fn [FidSpan fid pstart Nothing] fidspans}) 199 return fid 200 Just fspans -> do 201 let fid = FileId fn (length fspans+1) 202 modify (\pind -> pind {fidspans=M.insert fn (FidSpan fid pstart Nothing:fspans) fidspans}) 203 return fid 204 205-- | start new span for name fn for file fid starting with patch pid 206startFpSpan :: FileId -> AnchoredPath -> PatchId -> PIM () 207startFpSpan fid fn pstart = modify (\pind -> pind {fpspans=M.alter alt fid (fpspans pind)}) 208 where alt Nothing = Just [FpSpan fn pstart Nothing] 209 alt (Just spans) = Just (FpSpan fn pstart Nothing:spans) 210 211-- | stop current span for file name fn 212stopFpSpan :: FileId -> PatchId -> PIM () 213stopFpSpan fid pend = modify (\pind -> pind {fpspans=M.alter alt fid (fpspans pind)}) 214 where alt Nothing = error $ "impossible: no span for " ++ show fid 215 alt (Just []) = error $ "impossible: no span for " ++ show fid++", empty list" 216 alt (Just (FpSpan fp pstart Nothing:spans)) = 217 Just (FpSpan fp pstart (Just pend):spans) 218 alt _ = error $ "impossible: span already ended for " ++ show fid 219 220-- | start new span for name fn for file fid starting with patch pid 221startFidSpan :: AnchoredPath -> PatchId -> FileId -> PIM () 222startFidSpan fn pstart fid = modify (\pind -> pind {fidspans=M.alter alt fn (fidspans pind)}) 223 where alt Nothing = Just [FidSpan fid pstart Nothing] 224 alt (Just spans) = Just (FidSpan fid pstart Nothing:spans) 225 226-- | stop current span for file name fn 227stopFidSpan :: AnchoredPath -> PatchId -> PIM () 228stopFidSpan fn pend = modify (\pind -> pind {fidspans=M.alter alt fn (fidspans pind)}) 229 where alt Nothing = error $ "impossible: no span for " ++ show fn 230 alt (Just []) = error $ "impossible: no span for " ++ show fn++", empty list" 231 alt (Just (FidSpan fid pstart Nothing:spans)) = 232 Just (FidSpan fid pstart (Just pend):spans) 233 alt _ = error $ "impossible: span already ended for " ++ show fn 234 235-- | insert touching patchid for given file id 236createInfo :: FileId -> Bool -> PIM () 237createInfo fid isF = modify (\pind -> pind {infom=M.alter alt fid (infom pind)}) 238 where alt Nothing = Just (FileInfo isF S.empty) 239 alt (Just _) = Just (FileInfo isF S.empty) -- forget old false positives 240 241-- | insert touching patchid for given file id 242insertTouch :: FileId -> PatchId -> PIM () 243insertTouch fid pid = modify (\pind -> pind {infom=M.alter alt fid (infom pind)}) 244 where alt Nothing = error "impossible: Fileid does not exist" 245 alt (Just (FileInfo isF pids)) = Just (FileInfo isF (S.insert (short pid) pids)) 246 247-- | lookup current fid of filepath 248lookupFid :: AnchoredPath -> PIM FileId 249lookupFid fn = do 250 maybeFid <- lookupFid' fn 251 case maybeFid of 252 Nothing -> error $ "couldn't find " ++ displayPath fn ++ " in patch index" 253 Just fid -> return fid 254 255-- | lookup current fid of filepatch, returning a Maybe to allow failure 256lookupFid' :: AnchoredPath -> PIM (Maybe FileId) 257lookupFid' fn = do 258 fidm <- gets fidspans 259 case M.lookup fn fidm of 260 Just (FidSpan fid _ _:_) -> return $ Just fid 261 _ -> return Nothing 262 263 264-- | lookup all the file ids of a given path 265lookupFidf' :: AnchoredPath -> PIM [FileId] 266lookupFidf' fn = do 267 fidm <- gets fidspans 268 case M.lookup fn fidm of 269 Just spans -> return $ map (\(FidSpan fid _ _) -> fid) spans 270 Nothing -> 271 error $ "lookupFidf': no entry for " ++ show fn ++ " in FileIdSpans" 272 273-- | return all fids of matching subpaths 274-- of the given filepath 275lookupFids :: AnchoredPath -> PIM [FileId] 276lookupFids fn = do 277 fid_spans <- gets fidspans 278 file_idss <- mapM lookupFidf' $ 279 filter (isPrefix fn) (fpSpans2filePaths' fid_spans) 280 return $ nub $ concat file_idss 281 282-- | returns a single file id if the given path is a file 283-- if it is a directory, if returns all the file ids of all paths inside it, 284-- at any point in repository history 285lookupFids' :: AnchoredPath -> PIM [FileId] 286lookupFids' fn = do 287 info_map <- gets infom 288 fps_spans <- gets fpspans 289 a <- lookupFid' fn 290 if isJust a then do 291 let fid = fromJust a 292 case M.lookup fid info_map of 293 Just (FileInfo True _) -> return [fid] 294 Just (FileInfo False _) -> 295 let file_names = map (\(FpSpan x _ _) -> x) (fps_spans M.! fid) 296 in nub . concat <$> mapM lookupFids file_names 297 Nothing -> error "lookupFids' : could not find file" 298 else return [] 299 300-- | Creates patch index that corresponds to all patches in repo. 301createPatchIndexDisk 302 :: (RepoPatch p, ApplyState p ~ Tree) 303 => Repository rt p wR wU wT 304 -> PatchSet rt p Origin wR 305 -> IO () 306createPatchIndexDisk repository ps = do 307 let patches = mapFL Sealed2 $ progressFL "Create patch index" $ patchSet2FL ps 308 createPatchIndexFrom repository $ patches2patchMods patches S.empty 309 310-- | convert patches to patchmods 311patches2patchMods :: (Apply p, PatchInspect p, ApplyState p ~ Tree) 312 => [Sealed2 (PatchInfoAnd rt p)] -> Set AnchoredPath -> [(PatchId, [PatchMod AnchoredPath])] 313patches2patchMods patches fns = snd $ mapAccumL go fns patches 314 where 315 go filenames (Sealed2 p) = (filenames', (pid, pmods_effect ++ pmods_dup)) 316 where pid = makePatchID . info $ p 317 (filenames', pmods_effect) = applyToFileMods p filenames 318 -- applyToFileMods only returns patchmods that actually modify a file, 319 -- i.e., never duplicate patches 320 touched pm = case pm of {PTouch f -> [f]; PRename a b -> [a,b]; 321 PCreateDir f -> [f]; PCreateFile f -> [f]; 322 PRemove f -> [f]; _ -> []} 323 touched_all = listTouchedFiles p 324 touched_effect = concatMap touched pmods_effect 325 -- listTouchedFiles returns all files that touched by these 326 -- patches, even if they have no effect, e.g. by duplicate patches 327 pmods_dup = map PDuplicateTouch . S.elems 328 $ S.difference (S.fromList touched_all) 329 (S.fromList touched_effect) 330 331-- | return set of current filenames in patch index 332fpSpans2fileNames :: FilePathSpans -> Set AnchoredPath 333fpSpans2fileNames fpSpans = 334 S.fromList [fn | (FpSpan fn _ Nothing:_)<- M.elems fpSpans] 335 336-- | remove all patch effects of given patches from patch index. 337-- assumes that the given list of patches is a suffix of the 338-- patches tracked by the patch-index 339removePidSuffix :: Map PatchId Int -> [PatchId] -> PatchIndex -> PatchIndex 340removePidSuffix _ [] pindex = pindex 341removePidSuffix pid2idx oldpids@(oldpid:_) (PatchIndex pids fidspans fpspans infom) = 342 PatchIndex (pids \\ oldpids) 343 (M.mapMaybe removefid fidspans) 344 (M.mapMaybe removefp fpspans) 345 infom -- leave hashes in infom, false positives are harmless 346 where 347 findIdx pid = fromMaybe (error "impossible case") (M.lookup pid pid2idx) 348 oldidx = findIdx oldpid 349 from `after` idx = findIdx from > idx 350 mto `afterM` idx | Just to <- mto, findIdx to > idx = True 351 | otherwise = False 352 removefid fidsps = if null fidsps' then Nothing else Just fidsps' 353 where 354 fidsps' = concatMap go fidsps 355 go (FidSpan fid from mto) 356 | from `after` oldidx && mto `afterM` oldidx = [FidSpan fid from mto] 357 | from `after` oldidx = [FidSpan fid from Nothing] 358 | otherwise = [] 359 removefp fpsps = if null fpsps' then Nothing else Just fpsps' 360 where 361 fpsps' = concatMap go fpsps 362 go (FpSpan fn from mto) 363 | from `after` oldidx && mto `afterM` oldidx = [FpSpan fn from mto] 364 | from `after` oldidx = [FpSpan fn from Nothing] 365 | otherwise = [] 366 367-- | update the patch index to the current state of the repository 368updatePatchIndexDisk 369 :: (RepoPatch p, ApplyState p ~ Tree) 370 => Repository rt p wR wU wT 371 -> PatchSet rt p Origin wR 372 -> IO () 373updatePatchIndexDisk repo patches = do 374 let repodir = repoLocation repo 375 (_,_,pid2idx,pindex) <- loadPatchIndex repodir 376 -- check that patch index is up to date 377 let flpatches = progressFL "Update patch index" $ patchSet2FL patches 378 let pidsrepo = mapFL (makePatchID . info) flpatches 379 (oldpids,_,len_common) = uncommon (reverse $ pids pindex) pidsrepo 380 pindex' = removePidSuffix pid2idx oldpids pindex 381 filenames = fpSpans2fileNames (fpspans pindex') 382 cdir = repodir </> indexDir 383 -- reread to prevent holding onto patches for too long 384 let newpatches = drop len_common $ mapFL seal2 flpatches 385 newpmods = patches2patchMods newpatches filenames 386 inv_hash <- getInventoryHash repodir 387 storePatchIndex cdir inv_hash (applyPatchMods newpmods pindex') 388 where 389 -- return uncommon suffixes and length of common prefix of as and bs 390 uncommon = uncommon' 0 391 uncommon' x (a:as) (b:bs) 392 | a == b = uncommon' (x+1) as bs 393 | otherwise = (a:as,b:bs,x) 394 uncommon' x as bs = (as,bs,x) 395 396-- | 'createPatchIndexFrom repo pmods' creates a patch index from the given 397-- patchmods. 398createPatchIndexFrom :: Repository rt p wR wU wT 399 -> [(PatchId, [PatchMod AnchoredPath])] -> IO () 400createPatchIndexFrom repo pmods = do 401 inv_hash <- getInventoryHash repodir 402 storePatchIndex cdir inv_hash (applyPatchMods pmods emptyPatchIndex) 403 where repodir = repoLocation repo 404 cdir = repodir </> indexDir 405 emptyPatchIndex = PatchIndex [] M.empty M.empty M.empty 406 407getInventoryHash :: FilePath -> IO String 408getInventoryHash repodir = do 409 inv <- B.readFile (repodir </> darcsdir </> "hashed_inventory") 410 return $ sha256sum inv 411 412-- | Load patch-index from disk along with some meta data. 413loadPatchIndex :: FilePath -> IO (Int8, String, Map PatchId Int, PatchIndex) 414loadPatchIndex repodir = do 415 let pindex_dir = repodir </> indexDir 416 (v,inv_hash) <- loadRepoState (pindex_dir </> repoStateFile) 417 pids <- loadPatchIds (pindex_dir </> pidsFile) 418 let pid2idx = M.fromList $ zip pids [(1::Int)..] 419 infom <- loadInfoMap (pindex_dir </> touchMapFile) 420 fidspans <- loadFidMap (pindex_dir </> fidMapFile) 421 fpspans <- loadFpMap (pindex_dir </> fpMapFile) 422 return (v, inv_hash, pid2idx, PatchIndex pids fidspans fpspans infom) 423 424-- | If patch-index is useful as it is now, read it. If not, create or update it, then read it. 425loadSafePatchIndex :: (RepoPatch p, ApplyState p ~ Tree) 426 => Repository rt p wR wU wT 427 -> PatchSet rt p Origin wR -- ^ PatchSet of the repository, used if we need to create the patch-index. 428 -> IO PatchIndex 429loadSafePatchIndex repo ps = do 430 let repodir = repoLocation repo 431 can_use <- isPatchIndexInSync repo 432 (_,_,_,pi) <- 433 if can_use 434 then loadPatchIndex repodir 435 else do createOrUpdatePatchIndexDisk repo ps 436 loadPatchIndex repodir 437 return pi 438 439-- | Read-only. Checks if patch-index exists for this repository 440-- it works by checking if: 441-- 442-- 1. @_darcs\/patch_index\/@ and its corresponding files are all present 443-- 2. patch index version is the one handled by this version of Darcs 444doesPatchIndexExist :: FilePath -> IO Bool 445doesPatchIndexExist repodir = do 446 filesArePresent <- and <$> mapM (doesFileExist . (pindex_dir </>)) 447 [repoStateFile, pidsFile, touchMapFile, fidMapFile, fpMapFile] 448 if filesArePresent 449 then do v <- piVersion 450 return (v == version) -- consider PI only of on-disk format is the current one 451 else return False 452 where pindex_dir = repodir </> indexDir 453 piVersion = fst <$> loadRepoState (pindex_dir </> repoStateFile) 454 455-- | Read-only. Checks if @_darcs\/noPatchIndex@ exists, that is, if patch-index is explicitely disabled. 456isPatchIndexDisabled :: FilePath -> IO Bool 457isPatchIndexDisabled repodir = doesFileExist (repodir </> darcsdir </> noPatchIndex) 458 459-- | Create or update patch index 460-- 461-- 1. if @_darcs\/no_patch_index@ exists, delete it 462-- 2. if patch index exists, update it 463-- 3. if not, create it from scratch 464createOrUpdatePatchIndexDisk :: (RepoPatch p, ApplyState p ~ Tree) 465 => Repository rt p wR wU wT -> PatchSet rt p Origin wR -> IO () 466createOrUpdatePatchIndexDisk repo ps = do 467 let repodir = repoLocation repo 468 removeFile (repodir </> darcsdir </> noPatchIndex) `catch` \(_ :: IOError) -> return () 469 dpie <- doesPatchIndexExist repodir 470 if dpie 471 then updatePatchIndexDisk repo ps 472 else createPatchIndexDisk repo ps 473 474-- | Read-only. Checks the two following things: 475-- 476-- 1. 'doesPatchIndexExist' 477-- 2. 'isPatchIndexDisabled' 478-- 479-- Then only if it exists and it is not explicitely disabled, returns @True@, else returns @False@ 480-- (or an error if it exists and is explicitely disabled at the same time). 481canUsePatchIndex :: Repository rt p wR wU wT -> IO Bool 482canUsePatchIndex repo = do 483 let repodir = repoLocation repo 484 piExists <- doesPatchIndexExist repodir 485 piDisabled <- isPatchIndexDisabled repodir 486 case (piExists, piDisabled) of 487 (True, False) -> return True 488 (False, True) -> return False 489 (True, True) -> fail "patch index exists, and patch index is disabled. run optimize enable-patch-index or disable-patch-index to rectify." 490 (False, False) -> return False 491 492-- | Creates patch-index (ignoring whether it is explicitely disabled). 493-- If it is ctrl-c'ed, then aborts, delete patch-index and mark it as disabled. 494createPIWithInterrupt :: (RepoPatch p, ApplyState p ~ Tree) 495 => Repository rt p wR wU wT -> PatchSet rt p Origin wR -> IO () 496createPIWithInterrupt repo ps = do 497 let repodir = repoLocation repo 498 putStrLn "Creating a patch index, please wait. To stop press Ctrl-C" 499 (do 500 createPatchIndexDisk repo ps 501 putStrLn "Created patch index.") `catchInterrupt` (putStrLn "Patch Index Disabled" >> deletePatchIndex repodir) 502 503-- | Checks if patch-index exists and is in sync with repository (more precisely with @_darcs\/hashed_inventory@). 504-- That is, checks if patch-index can be used as it is now. 505isPatchIndexInSync :: Repository rt p wR wU wT -> IO Bool 506isPatchIndexInSync repo = do 507 let repodir = repoLocation repo 508 dpie <- doesPatchIndexExist repodir 509 if dpie 510 then do 511 (_, inv_hash_pindex, _, _) <- loadPatchIndex repodir 512 inv_hash <- getInventoryHash repodir 513 return (inv_hash == inv_hash_pindex) 514 else return False 515 516-- | Stores patch-index on disk. 517storePatchIndex :: FilePath -> String -> PatchIndex -> IO () 518storePatchIndex cdir inv_hash (PatchIndex pids fidspans fpspans infom) = do 519 createDirectory cdir `catch` \(_ :: IOError) -> return () 520 tmpdir <- withPermDir cdir $ \dir -> do 521 debugMessage "About to create patch index..." 522 let tmpdir = toFilePath dir 523 storeRepoState (tmpdir </> repoStateFile) inv_hash 524 storePatchIds (tmpdir </> pidsFile) pids 525 storeInfoMap (tmpdir </> touchMapFile) infom 526 storeFidMap (tmpdir </> fidMapFile) fidspans 527 storeFpMap (tmpdir </> fpMapFile) fpspans 528 debugMessage "Patch index created" 529 return tmpdir 530 removeDirectoryRecursive cdir `catch` \(_ :: IOError) -> return () 531 renameDirectory tmpdir cdir 532 533decodeFile :: Binary a => FilePath -> IO a 534decodeFile path = do 535 result <- decodeFileOrFail path 536 case result of 537 Left (offset, msg) -> 538 fail $ 539 "Patch index is corrupt (file "++path++" at offset "++show offset++"): "++msg++ 540 "\nPlease remove the corrupt file and then try again." 541 Right r -> return r 542 543storeRepoState :: FilePath -> String -> IO () 544storeRepoState fp inv_hash = encodeFile fp (version,inv_hash) 545 546loadRepoState :: FilePath -> IO (Int8, String) 547loadRepoState = decodeFile 548 549storePatchIds :: FilePath -> [PatchId] -> IO () 550storePatchIds = encodeFile 551 552loadPatchIds :: FilePath -> IO [PatchId] 553loadPatchIds = decodeFile 554 555storeFidMap :: FilePath -> FileIdSpans -> IO () 556storeFidMap fp fidm = 557 encodeFile fp $ M.map (map (\(FidSpan a b c) -> (a, b, toIdxM c))) fidm 558 where toIdxM Nothing = zero 559 toIdxM (Just pid) = pid 560 561loadFidMap :: FilePath -> IO FileIdSpans 562loadFidMap fp = M.map (map (\(a,b,c) -> FidSpan a b (toPidM c))) <$> decodeFile fp 563 where toPidM pid | pid == zero = Nothing 564 | otherwise = Just pid 565 566storeFpMap :: FilePath -> FilePathSpans -> IO () 567storeFpMap fp fidm = 568 encodeFile fp $ M.map (map (\(FpSpan a b c) -> (a, b, toIdxM c))) fidm 569 where toIdxM Nothing = zero 570 toIdxM (Just pid) = pid 571 572loadFpMap :: FilePath -> IO FilePathSpans 573loadFpMap fp = M.map (map (\(a,b,c) -> FpSpan a b (toPidM c))) <$> decodeFile fp 574 where toPidM pid | pid == zero = Nothing 575 | otherwise = Just pid 576 577storeInfoMap :: FilePath -> InfoMap -> IO () 578storeInfoMap fp infom = 579 encodeFile fp $ M.map (\fi -> (isFile fi, touching fi)) infom 580 581loadInfoMap :: FilePath -> IO InfoMap 582loadInfoMap fp = M.map (\(isF,pids) -> FileInfo isF pids) <$> decodeFile fp 583 584indexDir, repoStateFile, pidsFile, fidMapFile, fpMapFile, 585 touchMapFile, noPatchIndex :: String 586indexDir = darcsdir </> "patch_index" 587repoStateFile = "repo_state" 588pidsFile = "patch_ids" 589fidMapFile = "fid_map" 590fpMapFile = "fp_map" 591touchMapFile = "touch_map" 592noPatchIndex = "no_patch_index" 593 594-- | Deletes patch-index (@_darcs\/patch_index\/@ and its contents) and mark repository as disabled (creates @_darcs\/no_patch_index@). 595deletePatchIndex :: FilePath -> IO () 596deletePatchIndex repodir = do 597 exists <- doesDirectoryExist indexDir 598 when exists $ 599 removeDirectoryRecursive indexDir 600 `catch` \(e :: IOError) -> fail $ "Error: Could not delete patch index\n" ++ show e 601 (openFile (repodir </> darcsdir </> noPatchIndex) WriteMode >>= hClose) 602 `catch` \(e :: IOError) -> fail $ "Error: Could not disable patch index\n" ++ show e 603 604dumpRepoState :: [PatchId] -> String 605dumpRepoState = unlines . map pid2string 606 607dumpFileIdSpans :: FileIdSpans -> String 608dumpFileIdSpans fidspans = 609 unlines [displayPath fn++" -> "++showFileId fid++" from "++pid2string from++" to "++maybe "-" pid2string mto 610 | (fn, fids) <- M.toList fidspans, FidSpan fid from mto <- fids] 611 612dumpFilePathSpans :: FilePathSpans -> String 613dumpFilePathSpans fpspans = 614 unlines [showFileId fid++" -> "++ displayPath fn++" from "++pid2string from++" to "++maybe "-" pid2string mto 615 | (fid, fns) <- M.toList fpspans, FpSpan fn from mto <- fns] 616 617dumpTouchingMap :: InfoMap -> String 618dumpTouchingMap infom = unlines [showFileId fid++(if isF then "" else "/")++" -> "++ showAsHex w32 619 | (fid,FileInfo isF w32s) <- M.toList infom, w32 <- S.elems w32s] 620 621-- | return set of current filepaths in patch index 622fpSpans2filePaths :: FilePathSpans -> InfoMap -> [FilePath] 623fpSpans2filePaths fpSpans infom = 624 sort [displayPath fn ++ (if isF then "" else "/") | (fid,FpSpan fn _ Nothing:_) <- M.toList fpSpans, 625 let Just (FileInfo isF _) = M.lookup fid infom] 626 627-- | return set of current filepaths in patch index, for internal use 628fpSpans2filePaths' :: FileIdSpans -> [AnchoredPath] 629fpSpans2filePaths' fidSpans = [fp | (fp, _) <- M.toList fidSpans] 630 631-- | Checks if patch index can be created and build it with interrupt. 632attemptCreatePatchIndex 633 :: (RepoPatch p, ApplyState p ~ Tree) 634 => Repository rt p wR wU wT -> PatchSet rt p Origin wR -> IO () 635attemptCreatePatchIndex repo ps = do 636 canCreate <- canCreatePI repo 637 when canCreate $ createPIWithInterrupt repo ps 638 639-- | Checks whether a patch index can (and should) be created. If we are not in 640-- an old-fashioned repo, and if we haven't been told not to, then we should 641-- create a patch index if it doesn't already exist. 642canCreatePI :: Repository rt p wR wU wT -> IO Bool 643canCreatePI repo = 644 (not . or) <$> sequence [ doesntHaveHashedInventory (repoFormat repo) 645 , isPatchIndexDisabled repodir 646 , doesPatchIndexExist repodir 647 ] 648 where 649 repodir = repoLocation repo 650 doesntHaveHashedInventory = return . not . formatHas HashedInventory 651 652-- | Returns an RL in which the order of patches matters. Useful for the 653-- @annotate@ command. If patch-index does not exist and is not explicitely 654-- disabled, silently create it. (Also, if it is out-of-sync, which should not 655-- happen, silently update it). 656getRelevantSubsequence 657 :: (RepoPatch p, ApplyState p ~ Tree, a ~ PatchInfoAnd rt p) 658 => Sealed ((RL a) wK) 659 -- ^ Sequence of patches you want to filter 660 -> Repository rt p wR wU wR 661 -- ^ The repository (to attempt loading patch-index from its path) 662 -> PatchSet rt p Origin wR 663 -- ^ PatchSet of repository (in case we need to create patch-index) 664 -> [AnchoredPath] 665 -- ^ File(s) about which you want patches from given sequence 666 -> IO (Sealed ((RL a) Origin)) 667 -- ^ Filtered sequence of patches 668getRelevantSubsequence pxes repository ps fns = do 669 pi@(PatchIndex _ _ _ infom) <- loadSafePatchIndex repository ps 670 let fids = map (\fn -> evalState (lookupFid fn) pi) fns 671 pidss = map ((\(FileInfo _ a) -> a) . fromJust . (`M.lookup` infom)) fids 672 pids = S.unions pidss 673 let flpxes = reverseRL $ unseal unsafeCoercePEnd pxes 674 return . seal $ keepElems flpxes NilRL pids 675 where 676 keepElems :: (RepoPatch p, ApplyState p ~ Tree, a ~ PatchInfoAnd rt p) 677 => FL a wX wY -> RL a wB wX -> S.Set Word32 -> RL a wP wQ 678 keepElems NilFL acc _ = unsafeCoerceP acc 679 keepElems (x :>: xs) acc pids 680 | short (makePatchID $ info x) `S.member` pids = keepElems xs (acc :<: x) pids 681 | otherwise = keepElems (unsafeCoerceP xs) acc pids 682 683type PatchFilter rt p = [AnchoredPath] -> [Sealed2 (PatchInfoAnd rt p)] -> IO [Sealed2 (PatchInfoAnd rt p)] 684 685-- | If a patch index is available, returns a filter that takes a list of files and returns 686-- a @PatchFilter@ that only keeps patches that modify the given list of files. 687-- If patch-index cannot be used, return the original input. 688-- If patch-index does not exist and is not explicitely disabled, silently create it. 689-- (Also, if it is out-of-sync, which should not happen, silently update it). 690maybeFilterPatches 691 :: (RepoPatch p, ApplyState p ~ Tree) 692 => Repository rt p wR wU wT -- ^ The repository 693 -> PatchSet rt p Origin wR -- ^ PatchSet of patches of repository (in case patch-index needs to be created) 694 -> PatchFilter rt p -- ^ PatchFilter ready to be used by SelectChanges. 695maybeFilterPatches repo ps fps ops = do 696 usePI <- canUsePatchIndex repo 697 if usePI 698 then do 699 pi@(PatchIndex _ _ _ infom) <- loadSafePatchIndex repo ps 700 let fids = concatMap ((\fn -> evalState (lookupFids' fn) pi)) fps 701 npids = S.unions $ map (touching.fromJust.(`M.lookup` infom)) fids 702 return $ filter 703 (flip S.member npids . (unseal2 (short . makePatchID . info))) ops 704 else return ops 705 706-- | Dump information in patch index. Patch-index should be checked to exist beforehand. Read-only. 707dumpPatchIndex :: FilePath -> IO () 708dumpPatchIndex repodir = do 709 (_,inv_hash,_,PatchIndex pids fidspans fpspans infom) <- loadPatchIndex repodir 710 putStrLn $ unlines $ 711 [ "Inventory hash:" ++ inv_hash 712 , "=================" 713 , "Repo state:" 714 , "===========" 715 , dumpRepoState pids 716 , "Fileid spans:" 717 , "=============" 718 , dumpFileIdSpans fidspans 719 , "Filepath spans:" 720 , "==============" 721 , dumpFilePathSpans fpspans 722 , "Info Map:" 723 , "=========" 724 , dumpTouchingMap infom 725 , "Files:" 726 , "==============" 727 ] ++ fpSpans2filePaths fpspans infom 728 729-- | Read-only sanity check on patch-index. Patch-index should be checked to exist beforehand. It may not be in sync with repository. 730piTest :: FilePath -> IO () 731piTest repodir = do 732 (_,_,_,PatchIndex rpids fidspans fpspans infom) <- loadPatchIndex repodir 733 let pids = reverse rpids 734 735 -- test fidspans 736 putStrLn "fidspans" 737 putStrLn "===========" 738 forM_ (M.toList fidspans) $ \(fn, spans) -> do 739 let g :: FileIdSpan -> [PatchId] 740 g (FidSpan _ x (Just y)) = [y,x] 741 g (FidSpan _ x _) = [x] 742 ascTs = reverse . nub . concat $ map g spans 743 unless (isInOrder ascTs pids) (fail $ "In order test failed! filename: " ++ show fn) 744 forM_ spans $ \(FidSpan fid _ _) -> unless (M.member fid fpspans) (fail $ "Valid file id test failed! fid: " ++ show fid) 745 putStrLn "fidspans tests passed" 746 747 -- test fpspans 748 putStrLn "fpspans" 749 putStrLn "===========" 750 forM_ (M.toList fpspans) $ \(fid, spans) -> do 751 let g :: FilePathSpan -> [PatchId] 752 g (FpSpan _ x (Just y)) = [y,x] 753 g (FpSpan _ x _) = [x] 754 ascTs = reverse . nub . concat $ map g spans 755 unless (isInOrder ascTs pids) (fail $ "In order test failed! fileid: " ++ show fid) 756 forM_ spans $ \(FpSpan fn _ _) -> unless (M.member fn fidspans) (fail $ "Valid file name test failed! file name: " ++ show fn) 757 let f :: FilePathSpan -> FilePathSpan -> Bool 758 f (FpSpan _ x _) (FpSpan _ _ (Just y)) = x == y 759 f _ _ = error "adj test of fpspans fail" 760 unless (and $ zipWith f spans (tail spans)) (fail $ "Adjcency test failed! fid: " ++ show fid) 761 putStrLn "fpspans tests passed" 762 763 -- test infomap 764 putStrLn "infom" 765 putStrLn "===========" 766 putStrLn $ "Valid fid test: " ++ (show.and $ map (`M.member` fpspans) (M.keys infom)) 767 putStrLn $ "Valid pid test: " ++ (show.flip S.isSubsetOf (S.fromList $ map short pids) . S.unions . map touching . M.elems $ infom) 768 where 769 isInOrder :: Eq a => [a] -> [a] -> Bool 770 isInOrder (x:xs) (y:ys) | x == y = isInOrder xs ys 771 | otherwise = isInOrder (x:xs) ys 772 isInOrder [] _ = True 773 isInOrder _ [] = False 774 775 776