module Darcs.Patch.Match
( helpOnMatchers
, matchFirstPatchset
, matchSecondPatchset
, splitSecondFL
, matchAPatch
, rollbackToPatchSetMatch
, firstMatch
, secondMatch
, haveNonrangeMatch
, PatchSetMatch(..)
, patchSetMatch
, checkMatchSyntax
, hasIndexRange
, getMatchingTag
, matchAPatchset
, MatchFlag(..)
, matchingHead
, Matchable
, MatchableRP
) where
import Darcs.Prelude
import Text.ParserCombinators.Parsec
( parse
, CharParser
, (<?>)
, (<|>)
, noneOf
, option
, eof
, many
, try
, between
, spaces
, char
, oneOf
, string
, choice
)
import Text.ParserCombinators.Parsec.Expr
( OperatorTable
, Assoc( AssocLeft )
, Operator ( Infix, Prefix )
, buildExpressionParser
)
import Text.Regex ( mkRegex, matchRegex )
import Control.Exception ( Exception, throw )
import Data.Maybe ( isJust )
import System.IO.Unsafe ( unsafePerformIO )
import Data.List ( isPrefixOf, intercalate )
import Data.Char ( toLower )
import Data.Typeable ( Typeable )
import Darcs.Util.Path ( AbsolutePath )
import Darcs.Patch
( IsRepoType
, hunkMatches
, listTouchedFiles
)
import Darcs.Patch.Info ( justName, justAuthor, justLog, makePatchname,
piDate, piTag )
import qualified Data.ByteString.Char8 as BC
import Darcs.Patch.PatchInfoAnd ( PatchInfoAnd, info, conscientiously )
import Darcs.Patch.Set
( Origin
, PatchSet(..)
, SealedPatchSet
, Tagged(..)
, patchSetDrop
)
import Darcs.Patch.Apply ( Apply(..) )
import Darcs.Patch.Depends ( splitOnTag, contextPatches )
import Darcs.Patch.Commute ( Commute(..) )
import Darcs.Patch.Ident ( Ident(..), PatchId )
import Darcs.Patch.Info ( PatchInfo )
import Darcs.Patch.Inspect ( PatchInspect )
import Darcs.Patch.Witnesses.Ordered
( RL(..), FL(..), (:>)(..), reverseRL, mapRL, (+<+) )
import Darcs.Patch.Witnesses.Sealed
( Sealed2(..), seal, seal2, unseal2, unseal )
import Darcs.Util.Printer ( text, ($$) )
import Darcs.Patch.ApplyMonad ( ApplyMonad(..) )
import Darcs.Util.DateMatcher ( parseDateMatcher )
import Darcs.Util.Path ( anchorPath )
import Darcs.Util.Tree ( Tree )
type Matchable p =
( Apply p
, PatchInspect p
, Ident p
, PatchId p ~ PatchInfo
)
type MatchableRP p =
( Apply p
, Commute p
, PatchInspect p
)
data MatchFun = MatchFun (forall p. Matchable p => Sealed2 p -> Bool)
data Matcher = MATCH String MatchFun
instance Show Matcher where
show (MATCH s _) = '"':s ++ "\""
data MatchFlag
= OnePattern String
| SeveralPattern String
| AfterPattern String
| UpToPattern String
| OnePatch String
| SeveralPatch String
| AfterPatch String
| UpToPatch String
| OneHash String
| AfterHash String
| UpToHash String
| OneTag String
| AfterTag String
| UpToTag String
| LastN Int
| OneIndex Int
| IndexRange Int Int
| Context AbsolutePath
deriving (Show)
makeMatcher :: String -> MatchFun -> Matcher
makeMatcher = MATCH
applyMatcher :: Matchable p => Matcher -> p wX wY -> Bool
applyMatcher (MATCH _ (MatchFun m)) = m . seal2
parseMatch :: String -> Either String Matcher
parseMatch pattern =
case parse matchParser "match" pattern of
Left err -> Left $ "Invalid --match pattern '"++ pattern ++
"'.\n"++ unlines (map (" "++) $ lines $ show err)
Right m -> Right (makeMatcher pattern m)
matchPattern :: String -> Matcher
matchPattern pattern =
case parseMatch pattern of
Left err -> error err
Right m -> m
matchParser :: CharParser st MatchFun
matchParser = submatcher <?> helpfulErrorMsg
where
submatcher = do
m <- option matchAnyPatch submatch
eof
return m
helpfulErrorMsg = "valid expressions over: "
++ intercalate ", " (map (\(name, _, _, _, _) -> name) ps)
++ "\nfor more help, see `darcs help patterns`."
ps = primitiveMatchers
matchAnyPatch = MatchFun (const True)
submatch :: CharParser st MatchFun
submatch = buildExpressionParser table match
table :: OperatorTable Char st MatchFun
table = [ [prefix "not" negate_match,
prefix "!" negate_match ]
, [binary "||" or_match,
binary "or" or_match,
binary "&&" and_match,
binary "and" and_match ]
]
where binary name fun = Infix (tryNameAndUseFun name fun) AssocLeft
prefix name fun = Prefix $ tryNameAndUseFun name fun
tryNameAndUseFun name fun = do _ <- trystring name
spaces
return fun
negate_match (MatchFun m) = MatchFun $ \p -> not (m p)
or_match (MatchFun m1) (MatchFun m2) = MatchFun $ \p -> m1 p || m2 p
and_match (MatchFun m1) (MatchFun m2) = MatchFun $ \p -> m1 p && m2 p
trystring :: String -> CharParser st String
trystring s = try $ string s
match :: CharParser st MatchFun
match = between spaces spaces (parens submatch <|> choice matchers_)
where
matchers_ = map createMatchHelper primitiveMatchers
createMatchHelper :: (String, String, String, [String], String -> MatchFun)
-> CharParser st MatchFun
createMatchHelper (key,_,_,_,matcher) =
do _ <- trystring key
spaces
q <- quoted
return $ matcher q
helpOnMatchers :: [String]
helpOnMatchers =
["Selecting Patches:",
"",
"The --patches option yields patches with names matching an *extended*",
"regular expression. See regex(7) for details. The --matches option",
"yields patches that match a logical (Boolean) expression: one or more",
"primitive expressions combined by grouping (parentheses) and the",
"complement (not), conjunction (and) and disjunction (or) operators.",
"The C notation for logic operators (!, && and ||) can also be used.",
"",
" --patches=regex is a synonym for --matches='name regex'",
" --hash=HASH is a synonym for --matches='hash HASH'",
" --from-patch and --to-patch are synonyms for",
" --from-match='name... and --to-match='name...",
" --from-patch and --to-match can be unproblematically combined:",
" `darcs log --from-patch='html.*docu' --to-match='date 20040212'`",
"",
"The following primitive Boolean expressions are supported:"
,""]
++ keywords
++ ["", "Here are some examples:", ""]
++ examples
where ps = primitiveMatchers
keywords = [showKeyword (unwords [k,a]) d | (k,a,d,_,_) <- ps]
examples = [showExample k e | (k,_,_,es,_) <- ps, e <- es]
showKeyword keyword description =
" " ++ keyword ++ " - " ++ description ++ "."
showExample keyword example =
" darcs log --match "
++ "'" ++ keyword ++ " " ++ example ++ "'"
primitiveMatchers :: [(String, String, String, [String], String -> MatchFun)]
primitiveMatchers =
[ ("exact", "STRING", "check literal STRING is equal to patch name"
, ["\"Resolve issue17: use dynamic memory allocation.\""]
, exactmatch )
, ("name", "REGEX", "match REGEX against patch name"
, ["issue17", "\"^[Rr]esolve issue17\\>\""]
, namematch )
, ("author", "REGEX", "match REGEX against patch author"
, ["\"David Roundy\"", "droundy", "droundy@darcs.net"]
, authormatch )
, ("hunk", "REGEX", "match REGEX against contents of a hunk patch"
, ["\"foo = 2\"", "\"^instance .* Foo where$\""]
, hunkmatch )
, ("comment", "REGEX", "match REGEX against the full log message"
, ["\"prevent deadlocks\""]
, logmatch )
, ("hash", "HASH", "match HASH against (a prefix of) the hash of a patch"
, ["c719567e92c3b0ab9eddd5290b705712b8b918ef","c7195"]
, hashmatch )
, ("date", "DATE", "match DATE against the patch date"
, ["\"2006-04-02 22:41\"", "\"tea time yesterday\""]
, datematch )
, ("touch", "REGEX", "match file paths for a patch"
, ["src/foo.c", "src/", "\"src/*.(c|h)\""]
, touchmatch ) ]
parens :: CharParser st MatchFun
-> CharParser st MatchFun
parens = between (string "(") (string ")")
quoted :: CharParser st String
quoted = between (char '"') (char '"')
(many $ do { _ <- char '\\'
; try (oneOf "\\\"") <|> return '\\'
}
<|> noneOf "\"")
<|> between spaces spaces (many $ noneOf " ()")
<?> "string"
datematch, hashmatch, authormatch, exactmatch, namematch, logmatch,
hunkmatch, touchmatch :: String -> MatchFun
namematch r =
MatchFun $ \(Sealed2 hp) ->
isJust $ matchRegex (mkRegex r) $ justName (ident hp)
exactmatch r = MatchFun $ \(Sealed2 hp) -> r == justName (ident hp)
authormatch a =
MatchFun $ \(Sealed2 hp) ->
isJust $ matchRegex (mkRegex a) $ justAuthor (ident hp)
logmatch l =
MatchFun $ \(Sealed2 hp) ->
isJust $ matchRegex (mkRegex l) $ justLog (ident hp)
hunkmatch r =
MatchFun $ \(Sealed2 hp) ->
let regexMatcher = isJust . matchRegex (mkRegex r) . BC.unpack
in hunkMatches regexMatcher hp
hashmatch h =
MatchFun $ \(Sealed2 hp) ->
let rh = show $ makePatchname (ident hp)
lh = map toLower h
in (lh `isPrefixOf` rh) || (lh == rh ++ ".gz")
datematch d =
MatchFun $ \(Sealed2 hp) ->
let dm = unsafePerformIO $ parseDateMatcher d
in dm $ piDate (ident hp)
touchmatch r =
MatchFun $ \(Sealed2 hp) ->
let files = listTouchedFiles hp
in any (isJust . matchRegex (mkRegex r)) (map (anchorPath ".") files)
haveNonrangeMatch :: [MatchFlag] -> Bool
haveNonrangeMatch fs = isJust (nonrangeMatcher fs)
data PatchSetMatch
= IndexMatch Int
| PatchMatch Matcher
| TagMatch Matcher
| ContextMatch AbsolutePath
patchSetMatch :: [MatchFlag] -> Maybe PatchSetMatch
patchSetMatch [] = Nothing
patchSetMatch (OneTag t:_) = strictJust $ TagMatch $ tagmatch t
patchSetMatch (OnePattern m:_) = strictJust $ PatchMatch $ matchPattern m
patchSetMatch (OnePatch p:_) = strictJust $ PatchMatch $ patchmatch p
patchSetMatch (OneHash h:_) = strictJust $ PatchMatch $ hashmatch' h
patchSetMatch (OneIndex n:_) = strictJust $ IndexMatch n
patchSetMatch (Context p:_) = strictJust $ ContextMatch p
patchSetMatch (_:fs) = patchSetMatch fs
firstMatch :: [MatchFlag] -> Bool
firstMatch fs = isJust (hasLastn fs)
|| isJust (firstMatcher fs)
|| isJust (hasIndexRange fs)
secondMatch :: [MatchFlag] -> Bool
secondMatch fs =
isJust (secondMatcher fs) ||
isJust (hasIndexRange fs)
checkMatchSyntax :: [MatchFlag] -> IO ()
checkMatchSyntax opts =
case getMatchPattern opts of
Nothing -> return ()
Just p ->
either
fail
(const $ return ())
(parseMatch p)
getMatchPattern :: [MatchFlag] -> Maybe String
getMatchPattern [] = Nothing
getMatchPattern (OnePattern m:_) = Just m
getMatchPattern (SeveralPattern m:_) = Just m
getMatchPattern (AfterPattern m:_) = Just m
getMatchPattern (UpToPattern m:_) = Just m
getMatchPattern (_:fs) = getMatchPattern fs
tagmatch :: String -> Matcher
tagmatch r = makeMatcher ("tag-name "++r) (MatchFun tm)
where
tm (Sealed2 p) =
case piTag (ident p) of
Just t -> isJust (matchRegex (mkRegex r) t)
Nothing -> False
patchmatch :: String -> Matcher
patchmatch r = makeMatcher ("patch-name "++r) (namematch r)
hashmatch' :: String -> Matcher
hashmatch' r = makeMatcher ("hash "++r) (hashmatch r)
strictJust :: a -> Maybe a
strictJust x = Just $! x
nonrangeMatcher :: [MatchFlag] -> Maybe Matcher
nonrangeMatcher [] = Nothing
nonrangeMatcher (OnePattern m:_) = strictJust $ matchPattern m
nonrangeMatcher (OneTag t:_) = strictJust $ tagmatch t
nonrangeMatcher (OnePatch p:_) = strictJust $ patchmatch p
nonrangeMatcher (OneHash h:_) = strictJust $ hashmatch' h
nonrangeMatcher (SeveralPattern m:_) = strictJust $ matchPattern m
nonrangeMatcher (SeveralPatch p:_) = strictJust $ patchmatch p
nonrangeMatcher (_:fs) = nonrangeMatcher fs
firstMatcher :: [MatchFlag] -> Maybe Matcher
firstMatcher [] = Nothing
firstMatcher (OnePattern m:_) = strictJust $ matchPattern m
firstMatcher (AfterPattern m:_) = strictJust $ matchPattern m
firstMatcher (AfterTag t:_) = strictJust $ tagmatch t
firstMatcher (OnePatch p:_) = strictJust $ patchmatch p
firstMatcher (AfterPatch p:_) = strictJust $ patchmatch p
firstMatcher (OneHash h:_) = strictJust $ hashmatch' h
firstMatcher (AfterHash h:_) = strictJust $ hashmatch' h
firstMatcher (_:fs) = firstMatcher fs
firstMatcherIsTag :: [MatchFlag] -> Bool
firstMatcherIsTag [] = False
firstMatcherIsTag (AfterTag _:_) = True
firstMatcherIsTag (_:fs) = firstMatcherIsTag fs
secondMatcher :: [MatchFlag] -> Maybe Matcher
secondMatcher [] = Nothing
secondMatcher (OnePattern m:_) = strictJust $ matchPattern m
secondMatcher (UpToPattern m:_) = strictJust $ matchPattern m
secondMatcher (OnePatch p:_) = strictJust $ patchmatch p
secondMatcher (UpToPatch p:_) = strictJust $ patchmatch p
secondMatcher (OneHash h:_) = strictJust $ hashmatch' h
secondMatcher (UpToHash h:_) = strictJust $ hashmatch' h
secondMatcher (UpToTag t:_) = strictJust $ tagmatch t
secondMatcher (_:fs) = secondMatcher fs
secondMatcherIsTag :: [MatchFlag] -> Bool
secondMatcherIsTag [] = False
secondMatcherIsTag (UpToTag _:_) = True
secondMatcherIsTag (_:fs) = secondMatcherIsTag fs
matchAPatch :: Matchable p => [MatchFlag] -> p wX wY -> Bool
matchAPatch fs p =
case nonrangeMatcher fs of
Nothing -> True
Just m -> applyMatcher m p
hasLastn :: [MatchFlag] -> Maybe Int
hasLastn [] = Nothing
hasLastn (LastN (-1):_) = error "--last requires a positive integer argument."
hasLastn (LastN n:_) = Just n
hasLastn (_:fs) = hasLastn fs
hasIndexRange :: [MatchFlag] -> Maybe (Int,Int)
hasIndexRange [] = Nothing
hasIndexRange (IndexRange x y:_) = Just (x,y)
hasIndexRange (_:fs) = hasIndexRange fs
matchFirstPatchset :: MatchableRP p
=> [MatchFlag] -> PatchSet rt p wStart wX
-> Maybe (SealedPatchSet rt p wStart)
matchFirstPatchset fs patchset
| Just n <- hasLastn fs = Just $ patchSetDrop n patchset
| Just (_, b) <- hasIndexRange fs = Just $ patchSetDrop b patchset
| Just m <- firstMatcher fs =
Just $ unseal (patchSetDrop 1) $
if firstMatcherIsTag fs
then getMatchingTag m patchset
else matchAPatchset m patchset
| otherwise = Nothing
matchSecondPatchset :: MatchableRP p
=> [MatchFlag] -> PatchSet rt p wStart wX
-> Maybe (SealedPatchSet rt p wStart)
matchSecondPatchset fs ps
| Just (a, _) <- hasIndexRange fs = Just $ patchSetDrop (a - 1) ps
| Just m <- secondMatcher fs =
Just $
if secondMatcherIsTag fs
then getMatchingTag m ps
else matchAPatchset m ps
| otherwise = Nothing
splitSecondFL :: Matchable p
=> (forall wA wB . q wA wB -> Sealed2 p)
-> [MatchFlag]
-> FL q wX wY
-> (FL q :> FL q) wX wY
splitSecondFL extract fs ps =
case hasIndexRange fs of
Just _ ->
error "index matches not supported by splitSecondPatchesFL"
Nothing ->
case secondMatcher fs of
Nothing -> error "Couldn't splitSecondPatches"
Just m -> splitMatchFL extract m ps
splitMatchFL
:: Matchable p
=> (forall wA wB. q wA wB -> Sealed2 p)
-> Matcher
-> FL q wX wY
-> (FL q :> FL q) wX wY
splitMatchFL _extract m NilFL = error $ "Couldn't find a patch matching " ++ show m
splitMatchFL extract m (p :>: ps)
| unseal2 (applyMatcher m) . extract $ p = (p :>: NilFL) :> ps
| otherwise = case splitMatchFL extract m ps of
before :> after -> (p :>: before) :> after
data MatchFailure = MatchFailure String
deriving Typeable
instance Exception MatchFailure
instance Show MatchFailure where
show (MatchFailure m) =
"Couldn't find a patch matching " ++ m
matchAPatchset
:: MatchableRP p
=> Matcher
-> PatchSet rt p wStart wX
-> SealedPatchSet rt p wStart
matchAPatchset m (PatchSet NilRL NilRL) =
throw $ MatchFailure $ show m
matchAPatchset m (PatchSet (ts :<: Tagged t _ ps) NilRL) =
matchAPatchset m (PatchSet ts (ps :<: t))
matchAPatchset m (PatchSet ts (ps :<: p))
| applyMatcher m p = seal (PatchSet ts (ps :<: p))
| otherwise = matchAPatchset m (PatchSet ts ps)
splitOnMatchingTag :: MatchableRP p
=> Matcher
-> PatchSet rt p wStart wX
-> PatchSet rt p wStart wX
splitOnMatchingTag _ s@(PatchSet NilRL NilRL) = s
splitOnMatchingTag m s@(PatchSet (ts :<: Tagged t _ ps) NilRL)
| applyMatcher m t = s
| otherwise = splitOnMatchingTag m (PatchSet ts (ps:<:t))
splitOnMatchingTag m (PatchSet ts (ps:<:p))
| applyMatcher m p =
case splitOnTag (info p) (PatchSet ts (ps:<:p)) of
Just x -> x
Nothing -> error "splitOnTag failed"
| otherwise =
case splitOnMatchingTag m (PatchSet ts ps) of
PatchSet ts' ps' -> PatchSet ts' (ps' :<: p)
getMatchingTag :: MatchableRP p
=> Matcher
-> PatchSet rt p wStart wX
-> SealedPatchSet rt p wStart
getMatchingTag m ps =
case splitOnMatchingTag m ps of
PatchSet NilRL _ -> throw $ userError $ "Couldn't find a tag matching " ++ show m
PatchSet ps' _ -> seal $ PatchSet ps' NilRL
rollbackToPatchSetMatch :: ( ApplyMonad (ApplyState p) m
, IsRepoType rt, MatchableRP p, ApplyState p ~ Tree
)
=> PatchSetMatch
-> PatchSet rt p Origin wX
-> m ()
rollbackToPatchSetMatch psm repo =
case psm of
IndexMatch n -> applyNInv (n-1) repo
TagMatch m ->
case splitOnMatchingTag m repo of
PatchSet NilRL _ -> throw $ MatchFailure $ show m
PatchSet _ extras -> unapply extras
PatchMatch m -> applyInvToMatcher m repo
ContextMatch _ -> error "rollbackToPatchSetMatch: unexpected context match"
applyInvToMatcher :: (IsRepoType rt, MatchableRP p, ApplyMonad (ApplyState p) m)
=> Matcher
-> PatchSet rt p Origin wX
-> m ()
applyInvToMatcher m (PatchSet NilRL NilRL) =
throw $ MatchFailure $ show m
applyInvToMatcher m (PatchSet (ts :<: Tagged t _ ps) NilRL) =
applyInvToMatcher m (PatchSet ts (ps :<: t))
applyInvToMatcher m (PatchSet xs (ps :<: p))
| applyMatcher m p = return ()
| otherwise = applyInvp p >> applyInvToMatcher m (PatchSet xs ps)
applyNInv :: (IsRepoType rt, MatchableRP p, ApplyMonad (ApplyState p) m)
=> Int -> PatchSet rt p Origin wX -> m ()
applyNInv n _ | n <= 0 = return ()
applyNInv _ (PatchSet NilRL NilRL) = throw $ userError "Index out of range"
applyNInv n (PatchSet (ts :<: Tagged t _ ps) NilRL) =
applyNInv n (PatchSet ts (ps :<: t))
applyNInv n (PatchSet xs (ps :<: p)) =
applyInvp p >> applyNInv (n - 1) (PatchSet xs ps)
applyInvp :: (Apply p, ApplyMonad (ApplyState p) m)
=> PatchInfoAnd rt p wX wY -> m ()
applyInvp = unapply . fromHopefully
where fromHopefully = conscientiously $ \e ->
text "Sorry, patch not available:"
$$ e
$$ text ""
$$ text "If you think what you're trying to do is ok then"
$$ text "report this as a bug on the darcs-user list."
matchingHead :: forall rt p wR. MatchableRP p
=> [MatchFlag] -> PatchSet rt p Origin wR
-> (PatchSet rt p :> FL (PatchInfoAnd rt p)) Origin wR
matchingHead matchFlags set =
case mh set of
(start :> patches) -> start :> reverseRL patches
where
mh :: forall wX . PatchSet rt p Origin wX
-> (PatchSet rt p :> RL (PatchInfoAnd rt p)) Origin wX
mh s@(PatchSet _ x)
| or (mapRL (matchAPatch matchFlags) x) = contextPatches s
mh (PatchSet (ts :<: Tagged t _ ps) x) =
case mh (PatchSet ts (ps :<: t)) of
(start :> patches) -> start :> patches +<+ x
mh ps = ps :> NilRL