converge: phase 2 — SSE relay client for realtime pull on push
- RepoConfig gains rcGiteaRepo :: Maybe Text for relay matching - Options gains optRelayUrl :: Maybe Text (--relay-url CLI, relay_url config) - connectRelay connects to converge-relay SSE endpoint, parses push events, triggers gitPull on matching repos (by gitea_repo + branch) - Exponential backoff reconnection (1s .. 60s) on disconnect/error - Skips pull if repo is mid-operation (merge/rebase/etc.) - Dependencies: http-client, http-client-tls, http-types, aeson, bytestring - SSE parser handles event:/data: fields, blank-line delimiters, CRLF endings - Updated SPEC.md, example config, and help text
This commit is contained in:
@@ -18,3 +18,8 @@
|
|||||||
## Can notify on pull: per-repo notify_on_pull setting sends a desktop notification when new commits are pulled from the remote
|
## Can notify on pull: per-repo notify_on_pull setting sends a desktop notification when new commits are pulled from the remote
|
||||||
|
|
||||||
## --sync / -s flag runs a single sync cycle (commit + pull + push) and exits, useful for wake-from-sleep hooks
|
## --sync / -s flag runs a single sync cycle (commit + pull + push) and exits, useful for wake-from-sleep hooks
|
||||||
|
|
||||||
|
## Can connect to a converge-relay SSE endpoint and automatically pull when push events arrive
|
||||||
|
### Configured via --relay-url CLI flag or relay_url config file entry
|
||||||
|
### Matches repos by gitea_repo field (Gitea repository full_name) and branch
|
||||||
|
### Reconnects automatically with exponential backoff on disconnect or error
|
||||||
|
|||||||
+18
-5
@@ -24,6 +24,8 @@ data CliArgs = CliArgs
|
|||||||
-- ^ Log file path.
|
-- ^ Log file path.
|
||||||
, cliSync :: !Bool
|
, cliSync :: !Bool
|
||||||
-- ^ Run a single sync cycle and exit (useful for wake-from-sleep hooks).
|
-- ^ Run a single sync cycle and exit (useful for wake-from-sleep hooks).
|
||||||
|
, cliRelayUrl :: !(Maybe Text)
|
||||||
|
-- ^ URL of a converge-relay SSE endpoint (overrides config).
|
||||||
}
|
}
|
||||||
|
|
||||||
cliParser :: Parser CliArgs
|
cliParser :: Parser CliArgs
|
||||||
@@ -97,6 +99,13 @@ cliParser =
|
|||||||
<> short 's'
|
<> short 's'
|
||||||
<> help "Run a single sync cycle (commit + pull + push) and exit"
|
<> help "Run a single sync cycle (commit + pull + push) and exit"
|
||||||
)
|
)
|
||||||
|
<*> optional
|
||||||
|
( strOption
|
||||||
|
( long "relay-url"
|
||||||
|
<> metavar "URL"
|
||||||
|
<> help "URL of a converge-relay SSE endpoint for realtime push notifications"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
parserInfo :: ParserInfo CliArgs
|
parserInfo :: ParserInfo CliArgs
|
||||||
parserInfo =
|
parserInfo =
|
||||||
@@ -114,7 +123,7 @@ main = do
|
|||||||
(options, configSource) <- case cliConfig cli of
|
(options, configSource) <- case cliConfig cli of
|
||||||
Just cfgPath -> do
|
Just cfgPath -> do
|
||||||
cfg <- loadConfig cfgPath
|
cfg <- loadConfig cfgPath
|
||||||
let opts = applyOverrides mDebounceOverride (cliLogLevel cli) (cliLogFile cli) (configToOptions cfg)
|
let opts = applyOverrides mDebounceOverride (cliLogLevel cli) (cliLogFile cli) (cliRelayUrl cli) (configToOptions cfg)
|
||||||
pure (opts, Just cfgPath)
|
pure (opts, Just cfgPath)
|
||||||
Nothing -> do
|
Nothing -> do
|
||||||
exists <- configFileExists
|
exists <- configFileExists
|
||||||
@@ -122,7 +131,7 @@ main = do
|
|||||||
then do
|
then do
|
||||||
cfgPath <- defaultConfigPath
|
cfgPath <- defaultConfigPath
|
||||||
cfg <- loadConfig cfgPath
|
cfg <- loadConfig cfgPath
|
||||||
let opts = applyOverrides mDebounceOverride (cliLogLevel cli) (cliLogFile cli) (configToOptions cfg)
|
let opts = applyOverrides mDebounceOverride (cliLogLevel cli) (cliLogFile cli) (cliRelayUrl cli) (configToOptions cfg)
|
||||||
pure (opts, Just cfgPath)
|
pure (opts, Just cfgPath)
|
||||||
else
|
else
|
||||||
pure
|
pure
|
||||||
@@ -130,6 +139,7 @@ main = do
|
|||||||
mDebounceOverride
|
mDebounceOverride
|
||||||
(cliLogLevel cli)
|
(cliLogLevel cli)
|
||||||
(cliLogFile cli)
|
(cliLogFile cli)
|
||||||
|
(cliRelayUrl cli)
|
||||||
defaultOptions
|
defaultOptions
|
||||||
{ optRepos =
|
{ optRepos =
|
||||||
[ defaultRepoConfig
|
[ defaultRepoConfig
|
||||||
@@ -168,13 +178,16 @@ main = do
|
|||||||
mapM_ (\r -> hPutStrLn stderr (" - " <> rcPath r)) repos
|
mapM_ (\r -> hPutStrLn stderr (" - " <> rcPath r)) repos
|
||||||
runConverge options
|
runConverge options
|
||||||
|
|
||||||
-- | Apply CLI overrides (debounce, log level, log file) to Options.
|
-- | Apply CLI overrides (debounce, log level, log file, relay url) to Options.
|
||||||
applyOverrides :: Maybe Int -> Maybe LogLevel -> Maybe FilePath -> Options -> Options
|
applyOverrides :: Maybe Int -> Maybe LogLevel -> Maybe FilePath -> Maybe Text -> Options -> Options
|
||||||
applyOverrides mDebounce mLogLevel mLogFile opts =
|
applyOverrides mDebounce mLogLevel mLogFile mRelayUrl opts =
|
||||||
opts
|
opts
|
||||||
{ optDebounceMs = maybe (optDebounceMs opts) id mDebounce
|
{ optDebounceMs = maybe (optDebounceMs opts) id mDebounce
|
||||||
, optLogLevel = fromMaybe (optLogLevel opts) mLogLevel
|
, optLogLevel = fromMaybe (optLogLevel opts) mLogLevel
|
||||||
, optLogFile = case mLogFile of
|
, optLogFile = case mLogFile of
|
||||||
Just _ -> mLogFile -- CLI --log-file overrides config
|
Just _ -> mLogFile -- CLI --log-file overrides config
|
||||||
Nothing -> optLogFile opts
|
Nothing -> optLogFile opts
|
||||||
|
, optRelayUrl = case mRelayUrl of
|
||||||
|
Just _ -> mRelayUrl -- CLI --relay-url overrides config
|
||||||
|
Nothing -> optRelayUrl opts
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,18 @@
|
|||||||
# Place at ~/.config/converge/config.yaml to use by default,
|
# Place at ~/.config/converge/config.yaml to use by default,
|
||||||
# or pass explicitly with: converge --config converge.yaml
|
# or pass explicitly with: converge --config converge.yaml
|
||||||
|
|
||||||
|
# relay_url: "http://localhost:8080/events" # converge-relay SSE endpoint for realtime push notifications
|
||||||
|
|
||||||
repos:
|
repos:
|
||||||
- path: /home/jbrechtel/org
|
- path: /home/jbrechtel/org
|
||||||
remote: origin
|
remote: origin
|
||||||
branch: main
|
branch: main
|
||||||
|
# gitea_repo: "myorg/org-repo" # Gitea repository full_name for relay matching
|
||||||
# notify_on_pull: true # send a notification when new commits are pulled
|
# notify_on_pull: true # send a notification when new commits are pulled
|
||||||
- path: /work/personal/hawat
|
- path: /work/personal/hawat
|
||||||
# remote defaults to "origin"
|
# remote defaults to "origin"
|
||||||
# branch defaults to "main"
|
# branch defaults to "main"
|
||||||
|
# gitea_repo: "myorg/hawat"
|
||||||
|
|
||||||
# debounce_ms: 5000 # optional, defaults to 5000
|
# debounce_ms: 5000 # optional, defaults to 5000
|
||||||
|
|
||||||
|
|||||||
@@ -29,10 +29,15 @@ ghc-options:
|
|||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
- base >= 4.7 && < 5
|
- base >= 4.7 && < 5
|
||||||
|
- aeson
|
||||||
- async
|
- async
|
||||||
|
- bytestring
|
||||||
- directory
|
- directory
|
||||||
- filepath
|
- filepath
|
||||||
- fsnotify
|
- fsnotify
|
||||||
|
- http-client
|
||||||
|
- http-client-tls
|
||||||
|
- http-types
|
||||||
- optparse-applicative
|
- optparse-applicative
|
||||||
- process
|
- process
|
||||||
- text
|
- text
|
||||||
|
|||||||
+161
-1
@@ -35,20 +35,31 @@ module Converge (
|
|||||||
|
|
||||||
-- * File filtering
|
-- * File filtering
|
||||||
isGitIgnored,
|
isGitIgnored,
|
||||||
|
|
||||||
|
-- * Relay listener (SSE client)
|
||||||
|
connectRelay,
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
|
|
||||||
|
import Control.Applicative ((<|>))
|
||||||
import Control.Concurrent (forkIO, threadDelay)
|
import Control.Concurrent (forkIO, threadDelay)
|
||||||
import Control.Concurrent.Async (mapConcurrently_)
|
import Control.Concurrent.Async (mapConcurrently_)
|
||||||
import Control.Concurrent.MVar (MVar, newEmptyMVar, newMVar, takeMVar, tryPutMVar, tryTakeMVar, withMVar)
|
import Control.Concurrent.MVar (MVar, newEmptyMVar, newMVar, takeMVar, tryPutMVar, tryTakeMVar, withMVar)
|
||||||
import Control.Exception (bracket)
|
import Control.Exception (SomeException, bracket, try)
|
||||||
import Control.Monad (forever, unless, void, when)
|
import Control.Monad (forever, unless, void, when)
|
||||||
|
import Data.Aeson (decode)
|
||||||
|
import qualified Data.ByteString as BS
|
||||||
|
import qualified Data.ByteString.Lazy.Char8 as BL
|
||||||
import Data.Maybe (fromMaybe)
|
import Data.Maybe (fromMaybe)
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
|
import Data.Text.Encoding (decodeUtf8)
|
||||||
import Data.Time.Clock (getCurrentTime)
|
import Data.Time.Clock (getCurrentTime)
|
||||||
import Data.Time.Format (defaultTimeLocale, formatTime)
|
import Data.Time.Format (defaultTimeLocale, formatTime)
|
||||||
import Data.Yaml (FromJSON (..), decodeFileThrow, withObject, withText, (.:), (.:?))
|
import Data.Yaml (FromJSON (..), decodeFileThrow, withObject, withText, (.:), (.:?))
|
||||||
|
import Network.HTTP.Client
|
||||||
|
import Network.HTTP.Client.TLS (tlsManagerSettings)
|
||||||
|
import Network.HTTP.Types.Status (status200)
|
||||||
import System.Directory (XdgDirectory (XdgConfig), doesDirectoryExist, doesFileExist, getCurrentDirectory, getXdgDirectory)
|
import System.Directory (XdgDirectory (XdgConfig), doesDirectoryExist, doesFileExist, getCurrentDirectory, getXdgDirectory)
|
||||||
import System.Exit (ExitCode (..))
|
import System.Exit (ExitCode (..))
|
||||||
import System.FSNotify
|
import System.FSNotify
|
||||||
@@ -66,6 +77,8 @@ data RepoConfig = RepoConfig
|
|||||||
-- ^ Branch to pull.
|
-- ^ Branch to pull.
|
||||||
, rcNotifyOnPull :: !Bool
|
, rcNotifyOnPull :: !Bool
|
||||||
-- ^ Send a notification when new commits are pulled from the remote.
|
-- ^ Send a notification when new commits are pulled from the remote.
|
||||||
|
, rcGiteaRepo :: !(Maybe Text)
|
||||||
|
-- ^ Gitea repository full_name (e.g. "myorg/myrepo") for relay matching.
|
||||||
}
|
}
|
||||||
deriving (Show, Eq)
|
deriving (Show, Eq)
|
||||||
|
|
||||||
@@ -97,6 +110,8 @@ data Options = Options
|
|||||||
-- ^ Minimum log level to emit.
|
-- ^ Minimum log level to emit.
|
||||||
, optLogFile :: !(Maybe FilePath)
|
, optLogFile :: !(Maybe FilePath)
|
||||||
-- ^ Optional log file path (logs to stderr when Nothing).
|
-- ^ Optional log file path (logs to stderr when Nothing).
|
||||||
|
, optRelayUrl :: !(Maybe Text)
|
||||||
|
-- ^ URL of a converge-relay SSE endpoint for realtime push notifications.
|
||||||
}
|
}
|
||||||
deriving (Show, Eq)
|
deriving (Show, Eq)
|
||||||
|
|
||||||
@@ -108,6 +123,7 @@ defaultRepoConfig =
|
|||||||
, rcRemote = "origin"
|
, rcRemote = "origin"
|
||||||
, rcBranch = "main"
|
, rcBranch = "main"
|
||||||
, rcNotifyOnPull = False
|
, rcNotifyOnPull = False
|
||||||
|
, rcGiteaRepo = Nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
-- | Sensible defaults for options.
|
-- | Sensible defaults for options.
|
||||||
@@ -119,6 +135,7 @@ defaultOptions =
|
|||||||
, optNotifyCmd = "notify-send"
|
, optNotifyCmd = "notify-send"
|
||||||
, optLogLevel = Info
|
, optLogLevel = Info
|
||||||
, optLogFile = Nothing
|
, optLogFile = Nothing
|
||||||
|
, optRelayUrl = Nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
{- | Logger that serializes output so concurrent repo watchers
|
{- | Logger that serializes output so concurrent repo watchers
|
||||||
@@ -153,6 +170,7 @@ data Config = Config
|
|||||||
, cfgDebounceMs :: !(Maybe Int)
|
, cfgDebounceMs :: !(Maybe Int)
|
||||||
, cfgLogLevel :: !(Maybe LogLevel)
|
, cfgLogLevel :: !(Maybe LogLevel)
|
||||||
, cfgLogFile :: !(Maybe FilePath)
|
, cfgLogFile :: !(Maybe FilePath)
|
||||||
|
, cfgRelayUrl :: !(Maybe Text)
|
||||||
}
|
}
|
||||||
deriving (Show, Eq)
|
deriving (Show, Eq)
|
||||||
|
|
||||||
@@ -163,6 +181,7 @@ instance FromJSON Config where
|
|||||||
<*> o .:? "debounce_ms"
|
<*> o .:? "debounce_ms"
|
||||||
<*> o .:? "log_level"
|
<*> o .:? "log_level"
|
||||||
<*> o .:? "log_file"
|
<*> o .:? "log_file"
|
||||||
|
<*> o .:? "relay_url"
|
||||||
|
|
||||||
instance FromJSON RepoConfig where
|
instance FromJSON RepoConfig where
|
||||||
parseJSON = withObject "RepoConfig" $ \o ->
|
parseJSON = withObject "RepoConfig" $ \o ->
|
||||||
@@ -171,6 +190,7 @@ instance FromJSON RepoConfig where
|
|||||||
<*> (fromMaybe "origin" <$> o .:? "remote")
|
<*> (fromMaybe "origin" <$> o .:? "remote")
|
||||||
<*> (fromMaybe "main" <$> o .:? "branch")
|
<*> (fromMaybe "main" <$> o .:? "branch")
|
||||||
<*> (fromMaybe False <$> o .:? "notify_on_pull")
|
<*> (fromMaybe False <$> o .:? "notify_on_pull")
|
||||||
|
<*> o .:? "gitea_repo"
|
||||||
|
|
||||||
-- | Load a YAML config file.
|
-- | Load a YAML config file.
|
||||||
loadConfig :: FilePath -> IO Config
|
loadConfig :: FilePath -> IO Config
|
||||||
@@ -184,6 +204,7 @@ configToOptions cfg =
|
|||||||
, optDebounceMs = fromMaybe 5000 (cfgDebounceMs cfg)
|
, optDebounceMs = fromMaybe 5000 (cfgDebounceMs cfg)
|
||||||
, optLogLevel = fromMaybe Info (cfgLogLevel cfg)
|
, optLogLevel = fromMaybe Info (cfgLogLevel cfg)
|
||||||
, optLogFile = cfgLogFile cfg
|
, optLogFile = cfgLogFile cfg
|
||||||
|
, optRelayUrl = cfgRelayUrl cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
-- | The default XDG config file path: @$XDG_CONFIG_HOME/converge/config.yaml@.
|
-- | The default XDG config file path: @$XDG_CONFIG_HOME/converge/config.yaml@.
|
||||||
@@ -239,6 +260,13 @@ logRepo logger repo msg = logRepoAt logger Info repo msg
|
|||||||
runConverge :: Options -> IO ()
|
runConverge :: Options -> IO ()
|
||||||
runConverge opts = do
|
runConverge opts = do
|
||||||
logger <- newLogger (optLogLevel opts) (optLogFile opts)
|
logger <- newLogger (optLogLevel opts) (optLogFile opts)
|
||||||
|
-- Start relay listener if configured (runs in background forever)
|
||||||
|
case optRelayUrl opts of
|
||||||
|
Just url
|
||||||
|
| url /= "" -> do
|
||||||
|
_ <- forkIO $ connectRelay url logger (optRepos opts)
|
||||||
|
pure ()
|
||||||
|
_ -> pure ()
|
||||||
mapConcurrently_ (watchRepo opts logger) (optRepos opts)
|
mapConcurrently_ (watchRepo opts logger) (optRepos opts)
|
||||||
|
|
||||||
{- | Watch a single repository for changes.
|
{- | Watch a single repository for changes.
|
||||||
@@ -518,3 +546,135 @@ runGitIn :: FilePath -> [String] -> IO (ExitCode, Text, Text)
|
|||||||
runGitIn repoPath args = do
|
runGitIn repoPath args = do
|
||||||
(code, out, err) <- readProcessWithExitCode "git" ("-C" : repoPath : args) ""
|
(code, out, err) <- readProcessWithExitCode "git" ("-C" : repoPath : args) ""
|
||||||
pure (code, T.pack out, T.pack err)
|
pure (code, T.pack out, T.pack err)
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
-- Relay listener (SSE client)
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- | Push event received from a converge-relay SSE endpoint.
|
||||||
|
data PushEvent = PushEvent
|
||||||
|
{ peFullName :: !Text
|
||||||
|
-- ^ Gitea repository full_name (e.g. "myorg/myrepo").
|
||||||
|
, peBranch :: !Text
|
||||||
|
-- ^ Branch name.
|
||||||
|
, peRef :: !Text
|
||||||
|
-- ^ Full ref (e.g. "refs/heads/main").
|
||||||
|
}
|
||||||
|
deriving (Show, Eq)
|
||||||
|
|
||||||
|
instance FromJSON PushEvent where
|
||||||
|
parseJSON = withObject "PushEvent" $ \o ->
|
||||||
|
PushEvent
|
||||||
|
<$> o .: "full_name"
|
||||||
|
<*> o .: "branch"
|
||||||
|
<*> o .: "ref"
|
||||||
|
|
||||||
|
{- | Connect to a converge-relay SSE endpoint and trigger pulls on matching
|
||||||
|
repositories when push events arrive. Runs forever, reconnecting with
|
||||||
|
exponential backoff (1s .. 60s) on disconnection or error.
|
||||||
|
|
||||||
|
This is spawned as a background thread by 'runConverge' when an
|
||||||
|
@optRelayUrl@ is configured.
|
||||||
|
-}
|
||||||
|
connectRelay :: Text -> Logger -> [RepoConfig] -> IO ()
|
||||||
|
connectRelay url logger repos = do
|
||||||
|
logMsg logger Info ("relay: connecting to " <> url)
|
||||||
|
req <- parseRequest (T.unpack url)
|
||||||
|
manager <- newManager tlsManagerSettings
|
||||||
|
let loop delay = do
|
||||||
|
let delaySec = min delay (60 :: Int)
|
||||||
|
result <- try $
|
||||||
|
withResponse req manager $ \resp ->
|
||||||
|
if responseStatus resp /= status200
|
||||||
|
then logMsg logger Error ("relay returned " <> T.pack (show (responseStatus resp)))
|
||||||
|
else do
|
||||||
|
logMsg logger Info "relay: connected, waiting for push events"
|
||||||
|
processSSE logger repos (responseBody resp)
|
||||||
|
case result of
|
||||||
|
Left (e :: SomeException) ->
|
||||||
|
logMsg logger Warn ("relay: connection error: " <> T.pack (show e) <> ", reconnecting in " <> T.pack (show delaySec) <> "s")
|
||||||
|
Right () ->
|
||||||
|
logMsg logger Info ("relay: connection closed, reconnecting in " <> T.pack (show delaySec) <> "s")
|
||||||
|
threadDelay (delaySec * 1000000)
|
||||||
|
loop (delaySec * 2)
|
||||||
|
loop 1
|
||||||
|
|
||||||
|
-- | Process an SSE stream from a 'BodyReader', dispatching push events to matching repos.
|
||||||
|
processSSE :: Logger -> [RepoConfig] -> BodyReader -> IO ()
|
||||||
|
processSSE logger repos reader = go BS.empty Nothing []
|
||||||
|
where
|
||||||
|
go buf mEvtType dataLines = do
|
||||||
|
chunk <- brRead reader
|
||||||
|
if BS.null chunk
|
||||||
|
then pure ()
|
||||||
|
else do
|
||||||
|
let (completeLines, leftover) = extractLines (buf <> chunk)
|
||||||
|
let (mEvtType', dataLines', events) = foldLines mEvtType dataLines completeLines
|
||||||
|
mapM_ (handleSSEEvent logger repos) events
|
||||||
|
go leftover mEvtType' dataLines'
|
||||||
|
|
||||||
|
-- | Split a 'BS.ByteString' into complete lines (stripping trailing \r) and leftover partial data.
|
||||||
|
extractLines :: BS.ByteString -> ([BS.ByteString], BS.ByteString)
|
||||||
|
extractLines bs = case BS.breakSubstring "\n" bs of
|
||||||
|
(line, rest)
|
||||||
|
| not (BS.null rest) ->
|
||||||
|
let line' = stripTrailingCr line
|
||||||
|
(lines', leftover) = extractLines (BS.drop 1 rest)
|
||||||
|
in (line' : lines', leftover)
|
||||||
|
_ -> ([], bs)
|
||||||
|
|
||||||
|
-- | Strip trailing \r from a line (for CRLF line endings).
|
||||||
|
stripTrailingCr :: BS.ByteString -> BS.ByteString
|
||||||
|
stripTrailingCr bs
|
||||||
|
| not (BS.null bs) && BS.last bs == cr = BS.init bs
|
||||||
|
| otherwise = bs
|
||||||
|
where
|
||||||
|
cr = 13 -- '\r'
|
||||||
|
|
||||||
|
-- | Fold a list of SSE lines into accumulated state and parsed events.
|
||||||
|
foldLines :: Maybe Text -> [BS.ByteString] -> [BS.ByteString] -> (Maybe Text, [BS.ByteString], [PushEvent])
|
||||||
|
foldLines evtType0 dLines0 sseLines = foldl' step (evtType0, dLines0, []) sseLines
|
||||||
|
where
|
||||||
|
step (evt, dl, events) line
|
||||||
|
| BS.null line =
|
||||||
|
case dl of
|
||||||
|
[] -> (Nothing, [], events)
|
||||||
|
_ -> case parseEvent evt dl of
|
||||||
|
Just evt' -> (Nothing, [], evt' : events)
|
||||||
|
Nothing -> (Nothing, [], events)
|
||||||
|
| Just rest <- pick "event: " line <|> pick "event:" line =
|
||||||
|
(Just (decodeUtf8 (BS.dropWhile (== space) rest)), dl, events)
|
||||||
|
| Just rest <- pick "data: " line <|> pick "data:" line =
|
||||||
|
(evt, BS.dropWhile (== space) rest : dl, events)
|
||||||
|
| otherwise = (evt, dl, events)
|
||||||
|
pick = BS.stripPrefix
|
||||||
|
space = 32 -- ' '
|
||||||
|
|
||||||
|
-- | Parse accumulated SSE data lines into a 'PushEvent' (push events only).
|
||||||
|
parseEvent :: Maybe Text -> [BS.ByteString] -> Maybe PushEvent
|
||||||
|
parseEvent (Just "push") dataLines =
|
||||||
|
decode (BL.fromStrict (BS.intercalate "\n" (reverse dataLines)))
|
||||||
|
parseEvent _ _ = Nothing
|
||||||
|
|
||||||
|
-- | Dispatch a parsed push event to matching repos (by 'rcGiteaRepo' and 'rcBranch').
|
||||||
|
handleSSEEvent :: Logger -> [RepoConfig] -> PushEvent -> IO ()
|
||||||
|
handleSSEEvent logger repos evt = do
|
||||||
|
let matching =
|
||||||
|
filter
|
||||||
|
( \r ->
|
||||||
|
rcGiteaRepo r == Just (peFullName evt)
|
||||||
|
&& rcBranch r == peBranch evt
|
||||||
|
)
|
||||||
|
repos
|
||||||
|
if null matching
|
||||||
|
then logMsg logger Debug ("relay: ignoring event for unconfigured repo: " <> peFullName evt <> " (branch: " <> peBranch evt <> ")")
|
||||||
|
else
|
||||||
|
mapM_
|
||||||
|
( \repo -> do
|
||||||
|
logRepo logger repo "relay: push event received, pulling"
|
||||||
|
inProgress <- isInMiddleOfOperation (rcPath repo)
|
||||||
|
if inProgress
|
||||||
|
then logRepoAt logger Warn repo "relay: skipping pull -- operation in progress"
|
||||||
|
else void $ gitPull logger repo
|
||||||
|
)
|
||||||
|
matching
|
||||||
|
|||||||
Reference in New Issue
Block a user