module Subversion.Dump
( RevDate
, Revision(..)
, OpKind(..)
, OpAction(..)
, Operation(..)
, FieldMap
, Entry(..)
, readSvnDump
, readSvnDumpRaw
) where
import Control.Applicative hiding (many, (<|>))
import Control.Monad
import qualified Data.ByteString.Lazy as B
import qualified Data.List as L
import Data.Maybe
import Data.Text.Lazy hiding (map, count)
import Data.Text.Lazy.Encoding as E
import System.FilePath
import Text.Parsec
import Text.Parsec.ByteString.Lazy as PB
import Prelude hiding (getContents)
default (Data.Text.Lazy.Text)
type RevDate = Text
data Revision = Revision { revNumber :: Int
, revDate :: RevDate
, revAuthor :: Maybe Text
, revComment :: Maybe Text
, revOperations :: [Operation] }
deriving Show
data OpKind = File | Directory deriving (Show, Enum, Eq)
data OpAction = Add | Change | Replace | Delete deriving (Show, Enum, Eq)
data Operation = Operation { opKind :: OpKind
, opAction :: OpAction
, opPathname :: FilePath
, opContents :: B.ByteString
, opContentLength :: Int
, opChecksumMD5 :: Maybe String
, opChecksumSHA1 :: Maybe String
, opCopyFromRev :: Maybe Int
, opCopyFromPath :: Maybe FilePath }
deriving Show
readSvnDump :: B.ByteString -> IO (Either ParseError [Revision])
readSvnDump io = do
result <- readSvnDumpRaw io
return $ map processRevs <$> (L.groupBy sameRev <$> result)
where sameRev _ y = isNothing $
L.lookup "Revision-number" (entryTags y)
getField f n x = L.lookup n (f x)
getField' f n x = fromMaybe "" (getField f n x)
tagM = getField entryTags
propM = getField entryProps
tag = getField' entryTags
prop = getField' entryProps
processRevs [] = error "Unexpected"
processRevs (rev:ops) =
Revision {
revNumber = read $ tag "Revision-number" rev
, revDate = parseDate $ prop "svn:date" rev
, revAuthor = propM "svn:author" rev
, revComment = propM "svn:log" rev
, revOperations = map processOp ops }
processOp op =
Operation {
opKind = getOpKind $ tag "Node-kind" op
, opAction = getOpAction $ tag "Node-action" op
, opPathname = tag "Node-path" op
, opContents = entryBody op
, opContentLength = read $ tag "Text-content-length" op
, opCopyFromRev = read <$>
tagM "Node-copyfrom-rev" op
, opCopyFromPath = tagM "Node-copyfrom-path" op
, opChecksumMD5 = tagM "Text-content-md5" op
, opChecksumSHA1 = tagM "Text-content-sha1" op }
getOpKind kind = case kind of
"file" -> File
"dir" -> Directory
_ -> error "Unexpected"
getOpAction kind = case kind of
"add" -> Add
"delete" -> Delete
"change" -> Change
"replace" -> Replace
_ -> error "Unexpected"
type FieldMap a = [(String, a)]
data Entry = Entry { entryTags :: FieldMap String
, entryProps :: FieldMap Text
, entryBody :: B.ByteString }
deriving Show
readSvnDumpRaw :: B.ByteString -> IO (Either ParseError [Entry])
readSvnDumpRaw dump = return $ parse parseSvnDump "" dump
parseTag :: PB.Parser (String, String)
parseTag = (,) <$> fieldKey <* char ':' <* space
<*> fieldValue <* newline
where
fieldKey = (:) <$> letter <*> many fieldChar
fieldChar = letter <|> digit <|> oneOf "-_"
fieldValue = many1 (noneOf "\n")
parseIndicator :: PB.Parser (Char, Integer)
parseIndicator = (,) <$> oneOf "KV" <* space
<*> (read <$> many1 digit <* newline)
readTextRange :: Integer -> PB.Parser B.ByteString
readTextRange len = do
input <- getInput
let value = B.take (fromIntegral len) input
setInput $ B.drop (fromIntegral len) input
return value
parseSpecValue :: Char -> PB.Parser Text
parseSpecValue expected = do
(kind, len) <- parseIndicator
when (kind /= expected) $ unexpected "Unexpected spec value char"
value <- readTextRange len
_ <- newline
return $ E.decodeUtf8 value
parseProperty :: PB.Parser (String, Text)
parseProperty = (,) <$> (unpack <$> parseSpecValue 'K')
<*> parseSpecValue 'V'
parseEntry :: PB.Parser Entry
parseEntry = do
fields <- many1 parseTag <* newline
props <- case L.lookup "Prop-content-length" fields of
Nothing -> return []
Just _ -> many parseProperty <* string "PROPS-END\n"
body <- case L.lookup "Text-content-length" fields of
Nothing -> return B.empty
Just len -> readTextRange (read len)
_ <- many newline <?> "entry-terminating newline"
return Entry { entryTags = fields
, entryProps = props
, entryBody = body }
parseHeader :: PB.Parser ()
parseHeader = do
_ <- string "SVN-fs-dump-format-version: 2\n\n"
<?> "Dump file starts without a recognizable tag"
_ <- string "UUID: " <* many1 (hexDigit <|> char '-')
<* newline <* newline
return ()
parseSvnDump :: PB.Parser [Entry]
parseSvnDump = parseHeader >> many parseEntry
parseDate :: Text -> RevDate
parseDate = id