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