% Copyright (C) 2002-2004 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. \chapter{Repository format} \label{repository_format} A repository consists of a working directory, which has within it a directory called \verb!_darcs!. There must also be subdirectories within \verb!_darcs! named \verb!current! and \verb!patches!. The \verb!current! directory contains the version of the tree which has been recorded, while \verb!patches! contains the actual patches which are in the repository. \emph{WARNING!} Viewing files in current is perfectly acceptable, but if you view them with an editor (e.g. vi or emacs), that editor may create temporary files in the \verb!_darcs/current! dirctory, which will temporarily cause your repository to be inconsistent. So \emph{don't record any patches while viewing files in \_darcs/current with an editor!} A better plan would be to restrict yourself to viewing these files with a pager such as more or less. Also within \verb!_darcs! is the \verb!inventory! file, which lists all the patches that are in the repo. Moreover, it also gives the order of the representation of the patches as they are stored. Given a source of patches, i.e.\ any other set of repositories which have between them all the patches contained in a given repo, that repo can be reproduced based on only the information in the \verb!inventory! file. Under those circumstances, the order of the patches specified in the \verb!inventory! file would be unimportant, as this order is only needed to provide context for the interperetation of the stored patches in this repository. \begin{code} module Repository ( PatchSequence, slurp_pending, slurp_recorded, slurp_all_but_darcs, mmap_slurp_all_but_darcs, read_pending, write_pending, sift_for_pending, write_inventory, add_to_inventory, read_repo, get_unrecorded, is_repo, sync_repo, get_markedup_file, copy_repo_patches, am_in_repo, am_not_in_repo, maybe_in_repo, PatchSet, read_patch, write_patch, absolute_dir, get_checkpoint, get_checkpoint_by_default, write_checkpoint, write_recorded_checkpoint, write_checkpoint_patch, apply_patches, ) where import IO ( hFlush, stdout ) import Directory ( createDirectory, setCurrentDirectory, doesFileExist, doesDirectoryExist, removeFile ) import Workaround ( getCurrentDirectory ) import DarcsUtils ( withCurrentDirectory ) import System.IO.Unsafe ( unsafeInterleaveIO ) import Monad ( liftM, when ) import Maybe ( maybeToList ) import Control.Exception ( block ) import FastPackedString ( PackedString, packString, gzReadFilePS, breakOnPS, nullPS ) import Lock ( withTempDir ) import SlurpDirectory ( Slurpy, empty_slurpy, slurp, mmap_slurp, co_slurp, slurp_unboring, slurp_write, slurp_write_and_read_dirty, slurp_has, slurp_remove, ) import Patch ( Patch, invert, patch2patchinfo, apply_to_slurpy, flatten_to_primitives, join_patches, flatten, is_setpref, infopatch, reorder, is_hunk, merger_equivalent, commute, canonize, gzWritePatch, readPatchPS, writePatch, MarkedUpFile, LineMark(..), markup_file, empty_markedup_file, ) import PatchInfo ( PatchInfo, make_filename, readPatchInfoPS, human_friendly, ) import Diff ( smart_diff, sync ) import External ( gzFetchFilePS, fetchFilePS, copyFileOrUrl, Cachable(..) ) import Lock ( writeBinFile, appendBinFile ) import DarcsFlags ( DarcsFlag(Verbose, AnyOrder, NoCompress, WorkDir, LookForAdds, Boring, Partial, Complete, Quiet) ) import RepoTypes ( PatchSet, PatchSequence ) import Depends ( slightly_optimize_patchset, get_patches_beyond_tag, get_patches_in_tag ) import RepoPrefs ( filetype_function, darcsdir_filter, boring_file_filter ) import FileName ( fp2fn ) import DarcsUtils ( catchall ) #include "impossible.h" \end{code} \begin{code} --am_in_repo is a tricky version of is_repo that is used for --command_prereq, which moves in to the repo root directory and returns a --function to translate relative paths to compensate. am_not_in_repo :: [DarcsFlag] -> IO (Maybe FilePath, String) am_not_in_repo _ = do air <- doesFileExist "_darcs/inventory" `mand` doesDirectoryExist "_darcs/patches" `mand` doesDirectoryExist "_darcs/current" if air then return (Nothing, "") else return $ (Just "", "") am_in_repo :: [DarcsFlag] -> IO (Maybe FilePath, String) am_in_repo (WorkDir d:_) = do setCurrentDirectory d `catchall` (fail $ "can't set directory to "++d) am_in_repo [] am_in_repo (_:fs) = am_in_repo fs am_in_repo [] = a_i_r "" a_i_r :: FilePath -> IO (Maybe FilePath, String) a_i_r dir = do air <- doesFileExist "_darcs/inventory" `mand` doesDirectoryExist "_darcs/patches" `mand` doesDirectoryExist "_darcs/current" if air then return $ (Just dir, "") else do cd <- getCurrentDirectory setCurrentDirectory ".." cd' <- getCurrentDirectory if cd' /= cd then a_i_r $ reverse (takeWhile (/='/') $ reverse cd)///dir else return (Nothing, "You need to be in a repository directory" ++ " to run this command.") mand :: IO Bool -> IO Bool -> IO Bool a `mand` b = do isa <- a if isa then b else return False maybe_in_repo :: [DarcsFlag] -> IO (Maybe FilePath, String) maybe_in_repo (WorkDir d:_) = do setCurrentDirectory d `catchall` (fail $ "can't set directory to "++d) maybe_in_repo [] maybe_in_repo (_:fs) = maybe_in_repo fs maybe_in_repo [] = m_i_r "" where m_i_r dir = do air <- doesFileExist "_darcs/inventory" `mand` doesDirectoryExist "_darcs/patches" `mand` doesDirectoryExist "_darcs/current" if air then return $ (Just dir, "") else do cd <- getCurrentDirectory setCurrentDirectory ".." cd' <- getCurrentDirectory if cd' /= cd then m_i_r $ reverse (takeWhile (/='/') $ reverse cd)///dir else return (Just "", "") (///) :: FilePath -> FilePath -> FilePath ""///b = b a///"" = a a///b = a ++ "/" ++ b is_repo :: FilePath -> IO Bool is_repo d = do ps <- read_repo d if ps /= [] then return True else do isdir <- doesDirectoryExist d if not isdir then return False else do has_darcs <- doesDirectoryExist $ d++ "/_darcs" if not has_darcs then return False else do has_inv <- doesFileExist $ d++ "/_darcs/inventory" has_patches <- doesDirectoryExist $ d++ "/_darcs/patches" has_cur <- doesDirectoryExist $ d++ "/_darcs/current" return $ has_cur && has_patches && has_inv \end{code} There is a very special patch which may be stored in \verb!patches! which is called `pending'. This patch describes any changes which have not yet been recorded, and cannot be determined by a simple diff. For example file additions or renames are placed in pending until they are recorded. Similarly, token replaces are stored in pending until they are recorded. \begin{code} read_pending :: IO (Maybe Patch) write_pending :: Patch -> IO () read_pending = do pend <- gzReadFilePS "_darcs/patches/pending" `catch` (\_ -> return $ packString "") case readPatchPS pend of Nothing -> return Nothing Just (p,_) -> if p == join_patches [] then return Nothing else return $ Just p write_pending p = writePatch "_darcs/patches/pending" p sift_for_pending :: Patch -> Patch sift_for_pending patch = join_patches $ sfp [] $ reverse $ flatten $ fixup $ join_patches $ sfp [] $ reverse $ flatten_to_primitives $ merger_equivalent patch where sfp sofar [] = sofar sfp sofar (p:ps) | is_hunk p = case commute (join_patches sofar, p) of Just (_, sofar') -> sfp (flatten sofar') ps Nothing -> sfp (p:sofar) ps sfp sofar (p:ps) = sfp (p:sofar) ps fixup p = case canonize p of Nothing -> join_patches [] Just p' -> p' \end{code} \begin{code} stubbornly :: IO () -> IO () stubbornly do_something = do_something `catchall` return () write_patch :: [DarcsFlag] -> Patch -> IO () write_patch (NoCompress:_) p = case patch2patchinfo p of Nothing -> fail "Patch is not a named patch!" Just pinfo -> block $ do stubbornly $ removeFile $ "_darcs/patches/"++make_filename pinfo writePatch ("_darcs/patches/"++make_filename pinfo) p write_patch [] p = case patch2patchinfo p of Nothing -> fail "Patch is not a named patch!" Just pinfo -> block $ do stubbornly $ removeFile $ "_darcs/patches/"++make_filename pinfo gzWritePatch ("_darcs/patches/"++make_filename pinfo) p write_patch (_:ds) p = write_patch ds p \end{code} \begin{code} sync_repo :: IO () sync_repo = do ocur <- mmap_slurp "_darcs/current" owork <- co_slurp ocur "." sync "./_darcs/current" ocur owork get_unrecorded :: [DarcsFlag] -> IO (Maybe Patch) get_unrecorded opts = do cur <- slurp_pending "." work <- if LookForAdds `elem` opts then do nboring <- if Boring `elem` opts then return $ darcsdir_filter else boring_file_filter slurp_unboring (myfilt cur nboring) "." else co_slurp cur "." pend <- read_pending when (Verbose `elem` opts) $ putStr "diffing dir...\n" ftf <- filetype_function case smart_diff opts ftf cur work of Nothing -> case pend of Just empty | flatten empty == [] -> return Nothing _ -> return pend Just di-> case pend of Nothing -> return $ Just di Just pp -> if AnyOrder `elem` opts then return $ liftM join_patches $ unempty $ flatten $ join_patches [pp,di] else return $ liftM (reorder . join_patches) $ unempty $ flatten $ join_patches [pp,di] where myfilt s nboring f = slurp_has f s || nboring [f] /= [] unempty :: [a] -> Maybe [a] unempty [] = Nothing unempty l = Just l slurp_recorded :: FilePath -> IO Slurpy slurp_recorded d = mmap_slurp $ d++"/_darcs/current" mmap_slurp_all_but_darcs :: FilePath -> IO Slurpy mmap_slurp_all_but_darcs d = do s <- mmap_slurp d case slurp_remove (fp2fn "./_darcs") s of Nothing -> return s Just s' -> return s' slurp_all_but_darcs :: FilePath -> IO Slurpy slurp_all_but_darcs d = do s <- slurp d case slurp_remove (fp2fn "./_darcs") s of Nothing -> return s Just s' -> return s' \end{code} \begin{comment} \end{comment} \begin{code} slurp_pending :: FilePath -> IO Slurpy slurp_pending d = do cur <- mmap_slurp $ d ++ "/_darcs/current" mbpend <- read_pending case mbpend of Just pend -> case apply_to_slurpy pend cur of Just pendcur -> return pendcur Nothing -> do putStr "Yikes, pending has conflicts!\n" return cur Nothing -> return cur \end{code} \begin{code} --format_inventory is not exported for use outside of the Repository module --itself. format_inventory :: PatchSequence -> String format_inventory [] = "" format_inventory ((pinfo,_):ps) = show pinfo++"\n"++format_inventory ps write_inventory :: FilePath -> PatchSet -> IO () -- Note that write_inventory optimizes the inventory it writes out by -- checking on tag dependencies. -- FIXME: There is also a problem that write_inventory always writes -- out the entire inventory, including the parts that you haven't -- changed... write_inventory dir ps = block $ do stubbornly $ createDirectory (dir++"/_darcs/inventories") simply_write_inventory "inventory" dir $ slightly_optimize_patchset ps simply_write_inventory :: String -> FilePath -> PatchSet -> IO () simply_write_inventory name dir [] = writeBinFile (dir++"/_darcs/"++name) "" simply_write_inventory name dir [ps] = do writeBinFile (dir++"/_darcs/"++name) $ format_inventory $ reverse ps simply_write_inventory _ _ ([]:_) = fail $ "Bug in simply_write_inventory, please report!" simply_write_inventory name dir (ps:pss) = do tagname <- return $ make_filename $ fst $ head $ reverse ps simply_write_inventory ("inventories/"++tagname) dir pss writeBinFile (dir++"/_darcs/"++name) $ "Starting with tag:\n" ++ format_inventory (reverse ps) add_to_inventory :: FilePath -> PatchInfo -> IO () add_to_inventory dir pinfo = appendBinFile (dir++"/_darcs/inventory") $ show pinfo++"\n" \end{code} \begin{code} copy_repo_patches :: [DarcsFlag] -> FilePath -> FilePath -> IO () copy_repo_patches opts dir out = do putInfo "Copying patches...\n" realdir <- absolute_dir dir patches <- read_repo "." mpi <- if Partial `elem` opts then do cps <- read_checkpoints realdir case cps of [] -> return Nothing ((pinfo,_):_) -> return $ Just pinfo -- FIXME above should get last pinfo *before* desired -- tag... else return Nothing pns <- return $ map (make_filename . fst) $ since_checkpoint mpi $ concat patches sequence_ $ map (\pn -> do putVorDot $ "Copying "++pn++"\n" copyFileOrUrl (realdir++"/_darcs/patches/"++pn) (out++"/_darcs/patches/"++pn) Cachable) pns finishDots where putInfo s = when (not $ Quiet `elem` opts) $ putStr s putVorDot s = if Verbose `elem` opts then putStr s else if Quiet `elem` opts then return () else do putStr "." hFlush stdout finishDots = when (not $ Verbose `elem` opts || Quiet `elem` opts) $ putStr "\n" since_checkpoint :: Maybe PatchInfo -> [(PatchInfo, Maybe Patch)] -> [(PatchInfo, Maybe Patch)] since_checkpoint Nothing ps = ps since_checkpoint (Just ch) ((pinfo, mp):ps) | ch == pinfo = [(pinfo, mp)] | otherwise = (pinfo, mp) : since_checkpoint (Just ch) ps since_checkpoint _ [] = [] read_repo :: String -> IO PatchSet read_repo d = do realdir <- absolute_dir d read_repo_private realdir "inventory" read_repo_private :: FilePath -> FilePath -> IO PatchSet read_repo_private d iname = do i <- fetchFilePS (d++"/_darcs/"++iname) Uncachable (rest,str) <- case breakOnPS '\n' i of (swt,pistr) | swt == packString "Starting with tag:" -> do r <- rr $ head $ read_patch_ids pistr return (r,pistr) _ -> return ([],i) pis <- return $ reverse $ read_patch_ids str isdir <- doesDirectoryExist d these <- if isdir then read_patches_local d pis else read_patches_remote d pis return $ these : rest where rr pinfo = unsafeInterleaveIO $ read_repo_private d $ "inventories/"++make_filename pinfo read_patch :: String -> PatchInfo -> IO Patch read_patch repo i = do s <- gzFetchFilePS pn Cachable case readPatchPS s of Just (p,_) -> return p Nothing -> fail $ "couldn't read "++pn where pn = repo++"/_darcs/patches/"++make_filename i -- Note that read_repo_patches will soon be deprecated and removed in -- favor of read_repo. read_repo_patches :: String -> IO PatchSequence read_repo_patches d = do pset <- read_repo d return $ reverse $ concat pset read_patches_remote :: FilePath -> [PatchInfo] -> IO PatchSequence read_patches_remote _ [] = return [] read_patches_remote dir (i:is) = do mp <- unsafeInterleaveIO $ do s <- gzFetchFilePS (dir++"/_darcs/patches/"++make_filename i) Cachable return $ fst `liftM` (readPatchPS s) `catch` \_ -> return Nothing rest <- read_patches_remote dir is return $ (i,mp) : rest read_patches_local :: FilePath -> [PatchInfo] -> IO PatchSequence read_patches_local _ [] = return [] read_patches_local dir (i:is) = do mp <- unsafeInterleaveIO $ do s <- gzReadFilePS $ dir++"/_darcs/patches/"++make_filename i return $ fst `liftM` (readPatchPS s) `catch` \_ -> return Nothing rest <- read_patches_local dir is return $ (i,mp) : rest read_patch_ids :: PackedString -> [PatchInfo] read_patch_ids inv | nullPS inv = [] read_patch_ids inv = case readPatchInfoPS inv of Just (pinfo,r) -> pinfo : read_patch_ids r Nothing -> [] absolute_dir :: FilePath -> IO FilePath absolute_dir dir = do isdir <- doesDirectoryExist dir if not isdir then return dir -- hope it's an URL else do realdir <- withCurrentDirectory dir getCurrentDirectory -- This one is absolute! return realdir \end{code} \begin{code} read_checkpoints :: String -> IO [(PatchInfo, Maybe Slurpy)] read_checkpoints d = do realdir <- absolute_dir d pistr <- fetchFilePS (realdir++"/_darcs/checkpoints/inventory") Uncachable `catchall` (return $ packString "") pis <- return $ reverse $ read_patch_ids pistr slurpies <- sequence $ map (fetch_checkpoint realdir) pis return $ zip pis slurpies where fetch_checkpoint r pinfo = unsafeInterleaveIO $ do pstr <- gzFetchFilePS (r++"/_darcs/checkpoints/"++make_filename pinfo) Cachable case fst `liftM` readPatchPS pstr of Nothing -> return Nothing Just p -> return $ apply_to_slurpy p empty_slurpy get_checkpoint :: [DarcsFlag] -> String -> IO (Maybe Patch) get_checkpoint opts r = if Partial `elem` opts then get_check_internal r else return Nothing get_checkpoint_by_default :: [DarcsFlag] -> String -> IO (Maybe Patch) get_checkpoint_by_default opts r = if Complete `elem` opts then return Nothing else get_check_internal r get_check_internal :: String -> IO (Maybe Patch) get_check_internal r = do pistr <- fetchFilePS (r++"/_darcs/checkpoints/inventory") Uncachable `catchall` (return $ packString "") case reverse $ read_patch_ids pistr of [] -> return Nothing (pinfo:_) -> ((fst `liftM`). readPatchPS) `liftM` gzFetchFilePS (r++"/_darcs/checkpoints/"++make_filename pinfo) Cachable format_inv :: [PatchInfo] -> String format_inv [] = "" format_inv (pinfo:ps) = show pinfo++"\n"++format_inv ps write_recorded_checkpoint :: PatchInfo -> IO () write_recorded_checkpoint pinfo = do ps <- (map (fromJust.snd).reverse.concat) `liftM` read_repo "." ftf <- filetype_function s <- slurp_recorded "." write_checkpoint_patch $ infopatch pinfo $ join_patches $ changepps ps ++ maybeToList (smart_diff [LookForAdds] ftf empty_slurpy s) where changeps p = filter is_setpref $ flatten_to_primitives p changepps ps = concat $ map changeps $ ps write_checkpoint :: PatchInfo -> IO () write_checkpoint pinfo = do repodir <- getCurrentDirectory ps <- (reverse.map (fromJust.snd).concat.get_patches_in_tag pinfo) `liftM` read_repo "." ftf <- filetype_function with_tag pinfo $ do s <- mmap_slurp "." setCurrentDirectory repodir write_checkpoint_patch $ infopatch pinfo $ join_patches $ changepps ps ++ maybeToList (smart_diff [LookForAdds] ftf empty_slurpy s) where changeps p = filter is_setpref $ flatten_to_primitives p changepps ps = concat $ map changeps $ ps write_checkpoint_patch :: Patch -> IO () write_checkpoint_patch p = case patch2patchinfo p of Just pinfo -> do stubbornly $ createDirectory "_darcs/checkpoints" gzWritePatch ("_darcs/checkpoints/"++make_filename pinfo) p cpi <- (map fst) `liftM` read_checkpoints "." writeBinFile "_darcs/checkpoints/inventory" $ format_inv $ reverse $ pinfo:cpi Nothing -> bug "bad patch in write_checkpoint_patch" with_tag :: PatchInfo -> (IO ()) -> IO () with_tag pinfo job = do ps <- read_repo "." s <- slurp_recorded "." case get_patches_beyond_tag pinfo ps of [extras] -> withTempDir "checkpoint" $ \_ -> do slurp_write s apply_patches noPut noPut $ map invert_it extras job _ -> bug "with_tag" where noPut _ = return () invert_it (pin, Just p) = (pin, Just $ invert p) invert_it (pin, Nothing) = error $ "Couldn't read patch "++ human_friendly pin \end{code} The \verb!_darcs! directory also contains a directory called ``\verb!prefs!'', which is described in Chapter~\ref{configuring}. \begin{comment} \section{Getting interesting info on change history} One can query the repository for the entire markup history of a file. This provides a data structure which contains a history of \emph{all} the revisions ever made on a given file. \begin{code} get_markedup_file :: PatchInfo -> FilePath -> IO MarkedUpFile get_markedup_file pinfo f = do patches <- liftM (dropWhile (\ (pi',_)-> pi' /= pinfo)) $ read_repo_patches "." return $ snd $ do_mark_all patches (f, empty_markedup_file) do_mark_all :: [(PatchInfo, Maybe Patch)] -> (FilePath, MarkedUpFile) -> (FilePath, MarkedUpFile) do_mark_all ((n,Just p):pps) (f, mk) = do_mark_all pps $ markup_file n p (f, mk) do_mark_all ((_,Nothing):_) (f, _) = (f, [(packString "Error reading a patch!",None)]) do_mark_all [] (f, mk) = (f, mk) \end{code} \begin{code} apply_patches :: (String -> IO ()) -> (String -> IO ()) -> [(PatchInfo, Maybe Patch)] -> IO () apply_patches _ _ [] = return () apply_patches putVerbose putInfo patches = do sl <- mmap_slurp "." aps sl patches return () where aps s ((pinfo, Just p):ps) = do putVerbose $ "Applying patch "++human_friendly pinfo++"\n" case apply_to_slurpy p s of Nothing -> do putInfo "Unapplicable patch:\n" putInfo $ human_friendly pinfo fail "Unapplicable patch!" Just s' -> do putVerbose $ "Patch applies cleanly...\n" s'' <- slurp_write_and_read_dirty s' --slurp_write_dirty s' --s'' <- mmap_slurp "." aps s'' ps aps _ ((pinfo, Nothing):_) = fail $ "Couldn't read patch "++ human_friendly pinfo aps sl [] = return sl \end{code} \end{comment}