diff options
Diffstat (limited to 'src/Command')
-rw-r--r-- | src/Command/Checkout.hs | 58 | ||||
-rw-r--r-- | src/Command/JobId.hs | 39 | ||||
-rw-r--r-- | src/Command/Run.hs | 317 |
3 files changed, 355 insertions, 59 deletions
diff --git a/src/Command/Checkout.hs b/src/Command/Checkout.hs new file mode 100644 index 0000000..7cba593 --- /dev/null +++ b/src/Command/Checkout.hs @@ -0,0 +1,58 @@ +module Command.Checkout ( + CheckoutCommand, +) where + +import Data.Maybe +import Data.Text (Text) +import Data.Text qualified as T + +import System.Console.GetOpt + +import Command +import Repo + + +data CheckoutCommand = CheckoutCommand CheckoutOptions (Maybe RepoName) (Maybe Text) + +data CheckoutOptions = CheckoutOptions + { coDestination :: Maybe FilePath + , coSubtree :: Maybe FilePath + } + +instance Command CheckoutCommand where + commandName _ = "checkout" + commandDescription _ = "Checkout (part of) a given repository" + + type CommandArguments CheckoutCommand = [ Text ] + + commandUsage _ = T.pack $ unlines $ + [ "Usage: minici checkout [<repo> [<revision>]] [<option>...]" + ] + + type CommandOptions CheckoutCommand = CheckoutOptions + defaultCommandOptions _ = CheckoutOptions + { coDestination = Nothing + , coSubtree = Nothing + } + + commandOptions _ = + [ Option [] [ "dest" ] + (ReqArg (\val opts -> opts { coDestination = Just val }) "<path>") + "destination path" + , Option [] [ "subtree" ] + (ReqArg (\val opts -> opts { coSubtree = Just val }) "<path>") + "repository subtree to checkout" + ] + + commandInit _ co args = CheckoutCommand co + (RepoName <$> listToMaybe args) + (listToMaybe $ drop 1 args) + commandExec = cmdCheckout + +cmdCheckout :: CheckoutCommand -> CommandExec () +cmdCheckout (CheckoutCommand CheckoutOptions {..} name mbrev) = do + repo <- maybe getDefaultRepo getRepo name + mbCommit <- sequence $ fmap (readCommit repo) mbrev + root <- getCommitTree =<< maybe (createWipCommit repo) return mbCommit + tree <- maybe return (getSubtree mbCommit) coSubtree $ root + checkoutAt tree $ maybe "." id coDestination diff --git a/src/Command/JobId.hs b/src/Command/JobId.hs new file mode 100644 index 0000000..9f531d6 --- /dev/null +++ b/src/Command/JobId.hs @@ -0,0 +1,39 @@ +module Command.JobId ( + JobIdCommand, +) where + +import Control.Monad.IO.Class + +import Data.Text (Text) +import Data.Text qualified as T +import Data.Text.IO qualified as T + +import Command +import Eval +import Job.Types + + +data JobIdCommand = JobIdCommand JobRef + +instance Command JobIdCommand where + commandName _ = "jobid" + commandDescription _ = "Resolve job reference to canonical job ID" + + type CommandArguments JobIdCommand = Text + + commandUsage _ = T.pack $ unlines $ + [ "Usage: minici jobid <job ref>" + ] + + commandInit _ _ = JobIdCommand . JobRef . T.splitOn "." + commandExec = cmdJobId + + +cmdJobId :: JobIdCommand -> CommandExec () +cmdJobId (JobIdCommand ref) = do + config <- getConfig + einput <- getEvalInput + JobId ids <- either (tfail . textEvalError) return =<< + liftIO (runEval (evalJobReference config ref) einput) + + liftIO $ T.putStrLn $ T.intercalate "." $ map textJobIdPart ids diff --git a/src/Command/Run.hs b/src/Command/Run.hs index 73baee0..905204e 100644 --- a/src/Command/Run.hs +++ b/src/Command/Run.hs @@ -4,80 +4,277 @@ module Command.Run ( import Control.Concurrent import Control.Concurrent.STM +import Control.Exception import Control.Monad -import Control.Monad.Reader +import Control.Monad.IO.Class +import Data.Either import Data.List -import Data.Maybe import Data.Text (Text) import Data.Text qualified as T import Data.Text.IO qualified as T -import System.Exit +import System.Console.GetOpt +import System.FilePath.Glob import System.IO -import System.Process import Command import Config +import Eval import Job import Repo +import Terminal -data RunCommand = RunCommand Text + +data RunCommand = RunCommand RunOptions [ Text ] + +data RunOptions = RunOptions + { roRanges :: [ Text ] + , roSinceUpstream :: [ Text ] + , roNewCommitsOn :: [ Text ] + , roNewTags :: [ Pattern ] + } instance Command RunCommand where commandName _ = "run" commandDescription _ = "Execude jobs per minici.yaml for given commits" - type CommandArguments RunCommand = Maybe Text + type CommandArguments RunCommand = [ Text ] commandUsage _ = T.pack $ unlines $ [ "Usage: minici run" , " run jobs for commits on current branch not yet in upstream branch" - , " or: minici run <ref>" - , " run jobs for commits on <ref> not yet in its upstream ref" - , " or: minici run <commit>..<commit>" + , " or: minici run <job>..." + , " run jobs specified on the command line" + , " or: minici run [--range=]<commit>..<commit>" , " run jobs for commits in given range" + , " or: minici run <option>..." + , " run jobs based on given options (see below)" + ] + + type CommandOptions RunCommand = RunOptions + defaultCommandOptions _ = RunOptions + { roRanges = [] + , roSinceUpstream = [] + , roNewCommitsOn = [] + , roNewTags = [] + } + + commandOptions _ = + [ Option [] [ "range" ] + (ReqArg (\val opts -> opts { roRanges = T.pack val : roRanges opts }) "<range>") + "run jobs for commits in given range" + , Option [] [ "since-upstream" ] + (ReqArg (\val opts -> opts { roSinceUpstream = T.pack val : roSinceUpstream opts }) "<ref>") + "run jobs for commits on <ref> not yet in its upstream ref" + , Option [] [ "new-commits-on" ] + (ReqArg (\val opts -> opts { roNewCommitsOn = T.pack val : roNewCommitsOn opts }) "<branch>") + "run jobs for new commits on given branch" + , Option [] [ "new-tags" ] + (ReqArg (\val opts -> opts { roNewTags = compile val : roNewTags opts }) "<pattern>") + "run jobs for new annotated tags matching pattern" ] - commandInit _ _ = RunCommand . fromMaybe "HEAD" + commandInit _ = RunCommand commandExec = cmdRun + +data JobSource = JobSource (TMVar (Maybe ( [ JobSet ], JobSource ))) + +emptyJobSource :: MonadIO m => m JobSource +emptyJobSource = JobSource <$> liftIO (newTMVarIO Nothing) + +oneshotJobSource :: MonadIO m => [ JobSet ] -> m JobSource +oneshotJobSource jobsets = do + next <- emptyJobSource + JobSource <$> liftIO (newTMVarIO (Just ( jobsets, next ))) + +takeJobSource :: JobSource -> STM (Maybe ( [ JobSet ], JobSource )) +takeJobSource (JobSource tmvar) = takeTMVar tmvar + +mergeSources :: [ JobSource ] -> IO JobSource +mergeSources sources = do + let go tmvar [] = do + atomically (putTMVar tmvar Nothing) + go tmvar cur = do + ( jobsets, next ) <- atomically (select cur) + if null next + then do + go tmvar next + else do + nextvar <- newEmptyTMVarIO + atomically $ putTMVar tmvar (Just ( jobsets, JobSource nextvar )) + go nextvar next + + tmvar <- newEmptyTMVarIO + void $ forkIO $ go tmvar sources + return $ JobSource tmvar + + where + select :: [ JobSource ] -> STM ( [ JobSet ], [ JobSource ] ) + select [] = retry + select (x@(JobSource tmvar) : xs) = do + tryTakeTMVar tmvar >>= \case + Nothing -> fmap (x :) <$> select xs + Just Nothing -> return ( [], xs ) + Just (Just ( jobsets, x' )) -> return ( jobsets, x' : xs ) + + +argumentJobSource :: [ JobName ] -> CommandExec JobSource +argumentJobSource [] = emptyJobSource +argumentJobSource names = do + config <- getConfig + einput <- getEvalInput + jobsetJobsEither <- fmap Right $ forM names $ \name -> + case find ((name ==) . jobName) (configJobs config) of + Just job -> return job + Nothing -> tfail $ "job `" <> textJobName name <> "' not found" + jobsetCommit <- sequence . fmap createWipCommit =<< tryGetDefaultRepo + oneshotJobSource [ evalJobSet einput JobSet {..} ] + +rangeSource :: Text -> Text -> CommandExec JobSource +rangeSource base tip = do + repo <- getDefaultRepo + einput <- getEvalInput + commits <- listCommits repo (base <> ".." <> tip) + oneshotJobSource . map (evalJobSet einput) =<< mapM loadJobSetForCommit commits + +watchBranchSource :: Text -> CommandExec JobSource +watchBranchSource branch = do + repo <- getDefaultRepo + einput <- getEvalInput + getCurrentTip <- watchBranch repo branch + let go prev tmvar = do + cur <- atomically $ do + getCurrentTip >>= \case + Just cur -> do + when (cur == prev) retry + return cur + Nothing -> retry + + commits <- listCommits repo (textCommitId (commitId prev) <> ".." <> textCommitId (commitId cur)) + jobsets <- map (evalJobSet einput) <$> mapM loadJobSetForCommit commits + nextvar <- newEmptyTMVarIO + atomically $ putTMVar tmvar $ Just ( jobsets, JobSource nextvar ) + go cur nextvar + + liftIO $ do + tmvar <- newEmptyTMVarIO + atomically getCurrentTip >>= \case + Just commit -> + void $ forkIO $ go commit tmvar + Nothing -> do + T.hPutStrLn stderr $ "Branch `" <> branch <> "' not found" + atomically $ putTMVar tmvar Nothing + return $ JobSource tmvar + +watchTagSource :: Pattern -> CommandExec JobSource +watchTagSource pat = do + chan <- watchTags =<< getDefaultRepo + einput <- getEvalInput + + let go tmvar = do + tag <- atomically $ readTChan chan + if match pat $ T.unpack $ tagTag tag + then do + jobset <- evalJobSet einput <$> loadJobSetForCommit (tagObject tag) + nextvar <- newEmptyTMVarIO + atomically $ putTMVar tmvar $ Just ( [ jobset ], JobSource nextvar ) + go nextvar + else do + go tmvar + + liftIO $ do + tmvar <- newEmptyTMVarIO + void $ forkIO $ go tmvar + return $ JobSource tmvar + cmdRun :: RunCommand -> CommandExec () -cmdRun (RunCommand changeset) = do - ( base, tip ) <- case T.splitOn (T.pack "..") changeset of - base : tip : _ -> return ( T.unpack base, T.unpack tip ) - [ param ] -> liftIO $ do - [ deref ] <- readProcessWithExitCode "git" [ "symbolic-ref", "--quiet", T.unpack param ] "" >>= \case - ( ExitSuccess, out, _ ) -> return $ lines out - ( _, _, _ ) -> return [ T.unpack param ] - [ _, tip ] : _ <- fmap words . lines <$> readProcess "git" [ "show-ref", deref ] "" - [ base ] <- lines <$> readProcess "git" [ "for-each-ref", "--format=%(upstream)", tip ] "" - return ( base, tip ) - [] -> error "splitOn should not return empty list" +cmdRun (RunCommand RunOptions {..} args) = do + CommonOptions {..} <- getCommonOptions + tout <- getTerminalOutput + storageDir <- getStorageDir + + ( rangeOptions, jobOptions ) <- partitionEithers . concat <$> sequence + [ forM roRanges $ \range -> case T.splitOn ".." range of + [ base, tip ] -> return $ Left ( Just base, tip ) + _ -> tfail $ "invalid range: " <> range + , forM roSinceUpstream $ return . Left . ( Nothing, ) + , forM args $ \arg -> case T.splitOn ".." arg of + [ base, tip ] -> return $ Left ( Just base, tip ) + [ _ ] -> do + config <- getConfig + if any ((JobName arg ==) . jobName) (configJobs config) + then return $ Right $ JobName arg + else do + liftIO $ T.hPutStrLn stderr $ "standalone `" <> arg <> "' argument deprecated, use `--since-upstream=" <> arg <> "' instead" + return $ Left ( Nothing, arg ) + _ -> tfail $ "invalid argument: " <> arg + ] + + argumentJobs <- argumentJobSource jobOptions + + let rangeOptions' + | null rangeOptions, null roNewCommitsOn, null roNewTags, null jobOptions = [ ( Nothing, "HEAD" ) ] + | otherwise = rangeOptions + + ranges <- forM rangeOptions' $ \( mbBase, paramTip ) -> do + ( base, tip ) <- case mbBase of + Just base -> return ( base, paramTip ) + Nothing -> do + Just base <- flip findUpstreamRef paramTip =<< getDefaultRepo + return ( base, paramTip ) + rangeSource base tip + + branches <- mapM watchBranchSource roNewCommitsOn + tags <- mapM watchTagSource roNewTags liftIO $ do - Just repo <- openRepo "." - commits <- listCommits repo (base <> ".." <> tip) - jobssets <- mapM loadJobSetForCommit commits - let names = nub $ map jobName $ concatMap jobsetJobs jobssets - - putStr $ replicate (8 + 50) ' ' - forM_ names $ \name -> do - T.putStr $ (" "<>) $ fitToLength 7 $ textJobName name - putStrLn "" - - forM_ jobssets $ \jobset -> do - let commit = jobsetCommit jobset - shortCid = T.pack $ take 7 $ showCommitId $ commitId commit - shortDesc = fitToLength 50 (commitDescription commit) - case jobsetJobsEither jobset of - Right jobs -> do - outs <- runJobs "./.minici" commit jobs - let findJob name = snd <$> find ((name ==) . jobName . fst) outs - displayStatusLine shortCid (" " <> shortDesc) $ map findJob names - Left err -> do - T.putStrLn $ "\ESC[91m" <> shortCid <> "\ESC[0m" <> " " <> shortDesc <> " \ESC[91m" <> T.pack err <> "\ESC[0m" - hFlush stdout + mngr <- newJobManager storageDir optJobs + + source <- mergeSources $ concat [ [ argumentJobs ], ranges, branches, tags ] + headerLine <- newLine tout "" + + threadCount <- newTVarIO (0 :: Int) + let changeCount f = atomically $ do + writeTVar threadCount . f =<< readTVar threadCount + let waitForJobs = atomically $ do + flip when retry . (0 <) =<< readTVar threadCount + + let loop _ Nothing = return () + loop names (Just ( [], next )) = do + loop names =<< atomically (takeJobSource next) + + loop pnames (Just ( jobset : rest, next )) = do + let names = nub $ (pnames ++) $ map jobName $ jobsetJobs jobset + when (names /= pnames) $ do + redrawLine headerLine $ T.concat $ + T.replicate (8 + 50) " " : + map ((" " <>) . fitToLength 7 . textJobName) names + + let commit = jobsetCommit jobset + shortCid = T.pack $ take 7 $ maybe (repeat ' ') (showCommitId . commitId) commit + shortDesc <- fitToLength 50 <$> maybe (return "") getCommitTitle commit + + case jobsetJobsEither jobset of + Right jobs -> do + outs <- runJobs mngr tout commit jobs + let findJob name = snd <$> find ((name ==) . jobName . fst) outs + line <- newLine tout "" + mask $ \restore -> do + changeCount (+ 1) + void $ forkIO $ (>> changeCount (subtract 1)) $ + try @SomeException $ restore $ do + displayStatusLine tout line shortCid (" " <> shortDesc) $ map findJob names + Left err -> do + void $ newLine tout $ + "\ESC[91m" <> shortCid <> "\ESC[0m" <> " " <> shortDesc <> " \ESC[91m" <> T.pack err <> "\ESC[0m" + loop names (Just ( rest, next )) + + handle @SomeException (\_ -> cancelAllJobs mngr) $ do + loop [] =<< atomically (takeJobSource source) + waitForJobs + waitForJobs fitToLength :: Int -> Text -> Text @@ -91,33 +288,35 @@ showStatus blink = \case JobWaiting uses -> "\ESC[94m~" <> fitToLength 6 (T.intercalate "," (map textJobName uses)) <> "\ESC[0m" JobSkipped -> "\ESC[0m-\ESC[0m " JobRunning -> "\ESC[96m" <> (if blink then "*" else "•") <> "\ESC[0m " - JobError _ -> "\ESC[91m!!\ESC[0m " + JobError fnote -> "\ESC[91m" <> fitToLength 7 ("!! [" <> T.pack (show (footnoteNumber fnote)) <> "]") <> "\ESC[0m" JobFailed -> "\ESC[91m✗\ESC[0m " + JobCancelled -> "\ESC[0mC\ESC[0m " JobDone _ -> "\ESC[92m✓\ESC[0m " -displayStatusLine :: Text -> Text -> [ Maybe (TVar (JobStatus JobOutput)) ] -> IO () -displayStatusLine prefix1 prefix2 statuses = do - blinkVar <- newTVarIO False - t <- forkIO $ forever $ do - threadDelay 500000 - atomically $ writeTVar blinkVar . not =<< readTVar blinkVar - go blinkVar "\0" - killThread t + JobDuplicate _ s -> case s of + JobQueued -> "\ESC[94m^\ESC[0m " + JobWaiting _ -> "\ESC[94m^\ESC[0m " + JobSkipped -> "\ESC[0m-\ESC[0m " + JobRunning -> "\ESC[96m" <> (if blink then "*" else "^") <> "\ESC[0m " + _ -> showStatus blink s + +displayStatusLine :: TerminalOutput -> TerminalLine -> Text -> Text -> [ Maybe (TVar (JobStatus JobOutput)) ] -> IO () +displayStatusLine tout line prefix1 prefix2 statuses = do + go "\0" where - go blinkVar prev = do + go prev = do (ss, cur) <- atomically $ do ss <- mapM (sequence . fmap readTVar) statuses - blink <- readTVar blinkVar + blink <- terminalBlinkStatus tout let cur = T.concat $ map (maybe " " ((" " <>) . showStatus blink)) ss when (cur == prev) retry return (ss, cur) - when (not $ T.null prev) $ putStr "\r" + let prefix1' = if any (maybe False jobStatusFailed) ss then "\ESC[91m" <> prefix1 <> "\ESC[0m" else prefix1 - T.putStr $ prefix1' <> prefix2 <> cur - hFlush stdout + redrawLine line $ prefix1' <> prefix2 <> cur if all (maybe True jobStatusFinished) ss - then T.putStrLn "" - else go blinkVar cur + then return () + else go cur |