module Expression ( StaticRef', WatchedRef(..), WatchedRef', RevisionExpression'(..), RevisionExpression, DeclaredRevisionExpression, RangeExpression'(..), RangeExpression, DeclaredRangeExpression, CommitRange(..), evaluateDeclaredRevision, evaluateDeclaredRange, evaluateRevision, evaluateRange, getRangeCommits, getAddedRangeCommits, isRangeExpressionDynamic, isRevisionExpressionDynamic, parseRangeExpression, ) where import Control.Concurrent.STM import Control.Monad.IO.Class import Data.Bifunctor import Data.Char import Data.Kind import Data.List.NonEmpty qualified as NE import Data.Maybe import Data.Proxy import Data.Text (Text) import Data.Text qualified as T import Data.Void import Text.Megaparsec import Text.Megaparsec.Char import System.IO.Error import Job.Types import Output import Repo type family StaticRef' d :: Type where StaticRef' Declared = Text StaticRef' Evaluated = Commit data WatchedRef = BranchRef (STM (Maybe Commit)) type family WatchedRef' d :: Type where WatchedRef' Declared = Text WatchedRef' Evaluated = WatchedRef data RevisionExpression' d = StaticRef (StaticRef' d) | WatchedRef (WatchedRef' d) | ModifiedRevision Text (RevisionExpression' d) type RevisionExpression = RevisionExpression' Evaluated type DeclaredRevisionExpression = RevisionExpression' Declared watchedRevision :: DeclaredRevisionExpression -> DeclaredRevisionExpression watchedRevision = \case StaticRef r -> WatchedRef r WatchedRef r -> WatchedRef r ModifiedRevision m r -> ModifiedRevision m (watchedRevision r) data RangeExpression' d = SingleRevisionExpression (RevisionExpression' d) | RangeExpression (RevisionExpression' d) {- tip -} (RevisionExpression' d) {- except -} type RangeExpression = RangeExpression' Evaluated type DeclaredRangeExpression = RangeExpression' Declared watchedRange :: DeclaredRangeExpression -> DeclaredRangeExpression watchedRange = \case SingleRevisionExpression rev -> SingleRevisionExpression (watchedRevision rev) RangeExpression r1 r2 -> RangeExpression (watchedRevision r1) (watchedRevision r2) data CommitRef = CommitRef Text {- suffix -} CommitId deriving (Eq) data CommitRange = EmptyCommitRange | CommitRange CommitRef {- tip -} CommitRef {- except -} | CommitSet [ CommitRef ] {- latest first -} deriving (Eq) textCommitRef :: CommitRef -> Text textCommitRef (CommitRef suffix cid) = textCommitId cid <> suffix evaluateDeclaredRevision :: (MonadIO m, MonadFail m, MonadOutput m) => Repo -> DeclaredRevisionExpression -> m RevisionExpression evaluateDeclaredRevision repo = \case StaticRef ref -> StaticRef <$> readCommit repo ref WatchedRef ref -> do watched <- watchBranch repo ref output <- getOutput outputEvent output $ TestMessage $ "watch-branch-started " <> ref return $ WatchedRef $ BranchRef watched ModifiedRevision suffix rev -> ModifiedRevision suffix <$> evaluateDeclaredRevision repo rev evaluateDeclaredRange :: (MonadIO m, MonadFail m, MonadOutput m) => Repo -> DeclaredRangeExpression -> m RangeExpression evaluateDeclaredRange repo = \case SingleRevisionExpression a -> SingleRevisionExpression <$> evaluateDeclaredRevision repo a RangeExpression a b -> RangeExpression <$> evaluateDeclaredRevision repo a <*> evaluateDeclaredRevision repo b evaluateRevision :: RevisionExpression -> STM (Maybe CommitRef) evaluateRevision = \case StaticRef commit -> return $ Just $ CommitRef "" $ commitId commit WatchedRef (BranchRef getter) -> fmap (CommitRef "" . commitId) <$> getter ModifiedRevision suffix rev -> fmap (\(CommitRef suffix' ref) -> CommitRef (suffix' <> suffix) ref) <$> evaluateRevision rev evaluateRange :: RangeExpression -> STM CommitRange evaluateRange = \case SingleRevisionExpression revExpr -> do rev <- evaluateRevision revExpr return $ fromMaybe EmptyCommitRange $ CommitSet . (: []) <$> rev RangeExpression tipExpr exceptExpr -> do tip <- evaluateRevision tipExpr except <- evaluateRevision exceptExpr return $ fromMaybe EmptyCommitRange $ CommitRange <$> tip <*> except getCommitIdFromRef :: MonadIO m => Repo -> CommitRef -> m (Maybe CommitId) getCommitIdFromRef repo = liftIO . fmap (fmap commitId) . tryReadCommit repo . textCommitRef getRangeCommits :: MonadIO m => Repo -> CommitRange -> m [ Commit ] getRangeCommits repo = \case EmptyCommitRange -> do return [] CommitRange tip except -> do tipId <- getCommitIdFromRef repo tip excId <- getCommitIdFromRef repo except listCommitsFrom repo (catMaybes [ tipId ]) (catMaybes [ excId ]) CommitSet refs -> do return . catMaybes . map (either (const Nothing) Just) =<< mapM (liftIO . tryIOError . readCommitId repo) . catMaybes =<< mapM (getCommitIdFromRef repo) refs getAddedRangeCommits :: MonadIO m => Repo -> CommitRange -> CommitRange -> m [ Commit ] getAddedRangeCommits _ _ EmptyCommitRange = return [] getAddedRangeCommits repo EmptyCommitRange r = getRangeCommits repo r getAddedRangeCommits repo (CommitRange ar br) (CommitRange ar' br') = do a <- getCommitIdFromRef repo ar b <- getCommitIdFromRef repo br a' <- getCommitIdFromRef repo ar' b' <- getCommitIdFromRef repo br' a'b <- fmap (map commitId) $ mergeBase repo $ catMaybes [ a', b ] (++) <$> listCommitsFrom repo a'b (catMaybes [ b' ]) -- added by moving B to B' <*> listCommitsFrom repo (catMaybes [ a' ]) (catMaybes [ a, b, b' ]) -- added by moving A to A' getAddedRangeCommits repo prev cur = do prevList <- getRangeCommits repo prev curList <- getRangeCommits repo cur return $ filter (not . (`elem` prevList)) curList isRangeExpressionDynamic :: RangeExpression -> Bool isRangeExpressionDynamic = \case SingleRevisionExpression x -> isRevisionExpressionDynamic x RangeExpression x y -> isRevisionExpressionDynamic x || isRevisionExpressionDynamic y isRevisionExpressionDynamic :: RevisionExpression -> Bool isRevisionExpressionDynamic = \case StaticRef _ -> False WatchedRef _ -> True ModifiedRevision _ e -> isRevisionExpressionDynamic e type ExprParser = Parsec Void Text parseRangeExpression :: Text -> Either String DeclaredRangeExpression parseRangeExpression = bimap errorBundlePretty id . runParser parser "" where parser :: ExprParser DeclaredRangeExpression parser = do range <- parseRange eof return range parseRange :: ExprParser DeclaredRangeExpression parseRange = do range <- parseRevision choice [ do SingleRevisionExpression rev1 <- return range _ <- string ".." SingleRevisionExpression rev2 <- parseRevision return $ RangeExpression rev2 rev1 , do return range ] parseRevision :: ExprParser DeclaredRangeExpression parseRevision = do tok <- fmap T.concat $ some $ choice [ takeWhile1P Nothing (\x -> isAlphaNum x || x `elem` [ '_', '-', '/' ]) , try $ string "." <* notFollowedBy (char '.') ] range <- choice [ do _ <- char '(' arg <- parseRange _ <- char ')' case tok of "watch" -> return $ watchedRange arg _ -> unexpected $ Tokens $ NE.fromList $ chunkToTokens @Text Proxy tok , do return $ SingleRevisionExpression $ StaticRef tok ] choice [ do SingleRevisionExpression rev <- return range suffix <- takeWhile1P Nothing (\x -> isNumber x || x `elem` [ '~', '^', '@', '{', '}' ]) return $ SingleRevisionExpression $ ModifiedRevision suffix rev , do return range ]