1-- Copyright (C) 2005 David Roundy 2-- 3-- This file is licensed under the GPL, version two or later. 4 5{- | The format file. 6 7The purpose of the format file is to check compatibility between 8repositories in different formats and to allow the addition of new features 9without risking corruption by old darcs versions that do not yet know about 10these features. 11 12This allows a limited form of forward compatibility between darcs versions. 13Old versions of darcs that are unaware of features added in later versions 14will fail with a decent error message instead of crashing or misbehaving or 15even corrupting new repos. 16 17The format file lives at _darcs/format and must only contain printable ASCII 18characters and must not contain the characters @<@ and @>@. 19 20(We currently do not strip whitespace from the lines, but may want to do so 21in the future.) 22 23The file consists of format properties. A format property can contain any 24allowed ASCII character except the vertical bar (@|@) and newlines. Empty 25lines are ignored and multiple properties on the same line are separated 26with a @|@. 27 28If multiple properties appear on the same line (separated by vertical bars), 29then this indicates alternative format properties. These have a generic 30meaning: 31 32 * If we know *any* of these properties, then we can read the repo. 33 34 * If we know *all* of them, we can also write the repo. 35 36The above rules are necessary conditions, not sufficient ones. It is allowed 37to further restrict read and/or write access for specific commands, but care 38should be taken to not unnecessarily break forward compatibility. It is not 39recommended, but sometimes necessary, to impose ad-hoc restrictions on the 40format, see 'transferProblem' and 'readProblem' for examples. 41 42The no-working-dir property is an example for how to use alternative 43properties. An old darcs version that does not know this format can perform 44most read-only operations correctly even if there is no working tree; 45however, whatsnew will report that the whole tree was removed, so the 46solution is not perfect. 47 48When you add a new property as an alternative to an existing one, you should 49make sure that the old format remains to be updated in parallel to the new 50one, so that reading the repo with old darcs versions behaves correctly. If 51this cannot be guaranteed, it is better to add the new format on a separate 52line. 53 54It is not advisable for commands to modify an existing format file. However, 55sometimes compatibility requirements may leave us no other choice. In this 56case make sure to write the format file only after having checked that the 57existing repo format allows modification of the repo, and that you have 58taken the repo lock. 59 60-} 61 62{-# LANGUAGE OverloadedStrings #-} 63module Darcs.Repository.Format 64 ( RepoFormat(..) 65 , RepoProperty(..) 66 , identifyRepoFormat 67 , tryIdentifyRepoFormat 68 , createRepoFormat 69 , writeRepoFormat 70 , writeProblem 71 , readProblem 72 , transferProblem 73 , formatHas 74 , addToFormat 75 , removeFromFormat 76 ) where 77 78import Darcs.Prelude 79 80import Control.Monad ( mplus, (<=<) ) 81import qualified Data.ByteString.Char8 as BC ( split, pack, unpack, elem ) 82import qualified Data.ByteString as B ( ByteString, null, empty, stripPrefix ) 83import Data.List ( partition, intercalate, (\\) ) 84import Data.Maybe ( mapMaybe ) 85import Data.String ( IsString ) 86import System.FilePath.Posix( (</>) ) 87 88import Darcs.Util.External 89 ( fetchFilePS 90 , Cachable( Cachable ) 91 ) 92import Darcs.Util.Lock ( writeBinFile ) 93import qualified Darcs.Repository.Flags as F 94 ( WithWorkingDir (..), PatchFormat (..) ) 95import Darcs.Repository.Paths ( formatPath, oldInventoryPath ) 96import Darcs.Util.SignalHandler ( catchNonSignal ) 97import Darcs.Util.Exception ( catchall, prettyException ) 98 99import Darcs.Util.ByteString ( linesPS ) 100import Darcs.Util.Progress ( beginTedious, endTedious, finishedOneIO ) 101 102data RepoProperty = Darcs1 103 | Darcs2 104 | Darcs3 105 | HashedInventory 106 | NoWorkingDir 107 | RebaseInProgress 108 | RebaseInProgress_2_16 109 | UnknownFormat B.ByteString 110 deriving ( Eq ) 111 112-- | Define string constants in one place, for reuse in show/parse functions. 113darcs1Format, darcs2Format, darcs3Format, 114 hashedInventoryFormat, noWorkingDirFormat, 115 rebaseInProgressFormat, rebaseInProgress_2_16, 116 newStyleRebaseInProgress :: IsString s => s 117 118darcs1Format = "darcs-1.0" 119darcs2Format = "darcs-2" 120darcs3Format = "darcs-3" 121hashedInventoryFormat = "hashed" 122noWorkingDirFormat = "no-working-dir" 123rebaseInProgressFormat = "rebase-in-progress" 124rebaseInProgress_2_16 = "rebase-in-progress-2-16" 125-- compatibility alias, may want to remove this at some point in the future 126newStyleRebaseInProgress = "new-style-rebase-in-progress" 127 128instance Show RepoProperty where 129 show Darcs1 = darcs1Format 130 show Darcs2 = darcs2Format 131 show Darcs3 = darcs3Format 132 show HashedInventory = hashedInventoryFormat 133 show NoWorkingDir = noWorkingDirFormat 134 show RebaseInProgress = rebaseInProgressFormat 135 show RebaseInProgress_2_16 = rebaseInProgress_2_16 136 show (UnknownFormat f) = BC.unpack f 137 138readRepoProperty :: B.ByteString -> RepoProperty 139readRepoProperty input 140 | input == darcs1Format = Darcs1 141 | input == darcs2Format = Darcs2 142 | input == darcs3Format = Darcs3 143 | input == hashedInventoryFormat = HashedInventory 144 | input == noWorkingDirFormat = NoWorkingDir 145 | input == rebaseInProgressFormat = RebaseInProgress 146 | input == newStyleRebaseInProgress = RebaseInProgress_2_16 147 | input == rebaseInProgress_2_16 = RebaseInProgress_2_16 148 | otherwise = UnknownFormat input 149 150-- | Representation of the format of a repository. Each 151-- sublist corresponds to a line in the format file. 152newtype RepoFormat = RF [[RepoProperty]] 153 154-- | Is a given property contained within a given format? 155formatHas :: RepoProperty -> RepoFormat -> Bool 156formatHas f (RF rps) = f `elem` concat rps 157 158-- | Add a single property to an existing format. 159addToFormat :: RepoProperty -> RepoFormat -> RepoFormat 160addToFormat f (RF rps) = RF (rps ++ [[f]]) 161 162-- | Remove a single property from an existing format. 163removeFromFormat :: RepoProperty -> RepoFormat -> RepoFormat 164removeFromFormat f (RF rps) = RF (rps \\ [[f]]) 165 166instance Show RepoFormat where 167 show (RF rf) = unlines $ map (intercalate "|" . map show) rf 168 169-- | Identify the format of the repository at the 170-- given location (directory, URL, or SSH path). 171-- Fails if we weren't able to identify the format. 172identifyRepoFormat :: String -> IO RepoFormat 173identifyRepoFormat = either fail return <=< tryIdentifyRepoFormat 174 175-- | Identify the format of the repository at the 176-- given location (directory, URL, or SSH path). 177-- Return @'Left' reason@ if it fails, where @reason@ explains why 178-- we weren't able to identify the format. Note that we do no verification of 179-- the format, which is handled by 'readProblem' or 'writeProblem' on the 180-- resulting 'RepoFormat'. 181tryIdentifyRepoFormat :: String -> IO (Either String RepoFormat) 182tryIdentifyRepoFormat repo = do 183 let k = "Identifying repository " ++ repo 184 beginTedious k 185 finishedOneIO k "format" 186 formatInfo <- (fetchFilePS (repo </> formatPath) Cachable) 187 `catchall` (return B.empty) 188 -- We use a workaround for servers that don't return a 404 on nonexistent 189 -- files (we trivially check for something that looks like a HTML/XML tag). 190 format <- 191 if B.null formatInfo || BC.elem '<' formatInfo then do 192 finishedOneIO k "inventory" 193 missingInvErr <- checkFile (repo </> oldInventoryPath) 194 case missingInvErr of 195 Nothing -> return . Right $ RF [[Darcs1]] 196 Just e -> return . Left $ makeErrorMsg e 197 else return . Right $ readFormat formatInfo 198 endTedious k 199 return format 200 where 201 readFormat = 202 RF . map (map (readRepoProperty . fixupUnknownFormat)) . splitFormat 203 204 -- silently fixup unknown format entries broken by previous darcs versions 205 fixupUnknownFormat s = 206 case B.stripPrefix "Unknown format: " s of 207 Nothing -> s 208 Just s' -> fixupUnknownFormat s' -- repeat until not found anymore 209 210 -- split into lines, then split each non-empty line on '|' 211 splitFormat = map (BC.split '|') . filter (not . B.null) . linesPS 212 213 checkFile path = (fetchFilePS path Cachable >> return Nothing) 214 `catchNonSignal` 215 (return . Just . prettyException) 216 217 makeErrorMsg e = "Not a repository: " ++ repo ++ " (" ++ e ++ ")" 218 219-- | Write the repo format to the given file. 220writeRepoFormat :: RepoFormat -> FilePath -> IO () 221writeRepoFormat rf loc = writeBinFile loc $ BC.pack $ show rf 222-- note: this assumes show returns ascii 223 224-- | Create a repo format. The first argument specifies the patch 225-- format; the second says whether the repo has a working tree. 226createRepoFormat :: F.PatchFormat -> F.WithWorkingDir -> RepoFormat 227createRepoFormat fmt wwd = RF $ (HashedInventory : flags2wd wwd) : flags2format fmt 228 where 229 flags2format F.PatchFormat1 = [] 230 flags2format F.PatchFormat2 = [[Darcs2]] 231 flags2format F.PatchFormat3 = [[Darcs3]] 232 flags2wd F.NoWorkingDir = [NoWorkingDir] 233 flags2wd F.WithWorkingDir = [] 234 235-- | @'writeProblem' source@ returns 'Just' an error message if we cannot write 236-- to a repo in format @source@, or 'Nothing' if there's no such problem. 237writeProblem :: RepoFormat -> Maybe String 238writeProblem target = readProblem target `mplus` findProblems target wp 239 where 240 wp [] = error "impossible case" 241 wp x = case partition isKnown x of 242 (_, []) -> Nothing 243 (_, unknowns) -> Just . unwords $ 244 "Can't write repository: unknown formats:" : map show unknowns 245 246-- | @'transferProblem' source target@ returns 'Just' an error message if we 247-- cannot transfer patches from a repo in format @source@ to a repo in format 248-- @target@, or 'Nothing' if there are no such problem. 249transferProblem :: RepoFormat -> RepoFormat -> Maybe String 250transferProblem source target 251 | formatHas Darcs3 source /= formatHas Darcs3 target = 252 Just "Cannot mix darcs-3 repositories with older formats" 253 | formatHas Darcs2 source /= formatHas Darcs2 target = 254 Just "Cannot mix darcs-2 repositories with older formats" 255 | formatHas RebaseInProgress source = 256 Just "Cannot transfer patches from a repository \ 257 \where an old-style rebase is in progress" 258 | otherwise = readProblem source `mplus` writeProblem target 259 260-- | @'readProblem' source@ returns 'Just' an error message if we cannot read 261-- from a repo in format @source@, or 'Nothing' if there's no such problem. 262readProblem :: RepoFormat -> Maybe String 263readProblem source 264 | formatHas Darcs1 source && formatHas Darcs2 source = 265 Just "Invalid repository format: format 2 is incompatible with format 1" 266 | formatHas RebaseInProgress source && formatHas RebaseInProgress_2_16 source = 267 Just "Invalid repository format: \ 268 \cannot have both old-style and new-style rebase in progress" 269readProblem source = findProblems source rp 270 where 271 rp x | any isKnown x = Nothing 272 rp [] = error "impossible case" 273 rp x = Just . unwords $ "Can't read repository: unknown formats:" : map show x 274 275-- |'findProblems' applies a function that maps format-entries to an optional 276-- error message, to each repoformat entry. Returning any errors. 277findProblems :: RepoFormat -> ([RepoProperty] -> Maybe String) -> Maybe String 278findProblems (RF ks) formatHasProblem = case mapMaybe formatHasProblem ks of 279 [] -> Nothing 280 xs -> Just $ unlines xs 281 282-- | Does this version of darcs know how to handle this property? 283isKnown :: RepoProperty -> Bool 284isKnown p = p `elem` knownProperties 285 where 286 knownProperties :: [RepoProperty] 287 knownProperties = [ Darcs1 288 , Darcs2 289 , Darcs3 290 , HashedInventory 291 , NoWorkingDir 292 , RebaseInProgress 293 , RebaseInProgress_2_16 294 ] 295