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