Support multiple repositories per execution

- Add RepoConfig type for per-repo path/remote/branch
- Options now holds [RepoConfig] instead of single repo
- YAML config file support (--config) for multi-repo setup
- --repo still works for quick single-repo usage
- Concurrent watchers via async (one thread per repo)
- --debounce flag overrides config file value
This commit is contained in:
2026-05-11 09:48:26 -04:00
parent bf3329e5ce
commit eb77e7c0e6
5 changed files with 233 additions and 100 deletions
+67 -47
View File
@@ -1,43 +1,49 @@
module Main (main) where
import Converge
import Control.Monad (unless)
import Control.Monad (when)
import Data.Maybe (fromMaybe, isJust)
import Options.Applicative
import System.Exit (ExitCode (..))
import System.IO (hPutStrLn, stderr)
data CliOptions = CliOptions
{ cliRepoPath :: !FilePath
, cliDebounce :: !Int
data CliArgs = CliArgs
{ cliConfig :: !(Maybe FilePath)
-- ^ Optional path to a YAML config file.
, cliRepo :: !(Maybe FilePath)
-- ^ Optional single repo path (requires --config to be absent).
, cliRemote :: !Text
-- ^ Remote name (used only in single-repo mode).
, cliBranch :: !Text
-- ^ Branch name (used only in single-repo mode).
, cliDebounce :: !(Maybe Int)
-- ^ Debounce override for both modes.
}
cliParser :: Parser CliOptions
cliParser :: Parser CliArgs
cliParser =
CliOptions
<$> strOption
( long "repo"
<> short 'r'
<> metavar "PATH"
<> value "."
<> showDefault
<> help "Path to the git repository to watch (default: cwd)"
CliArgs
<$> optional
( strOption
( long "config"
<> short 'c'
<> metavar "FILE"
<> help "Path to YAML config file listing repositories"
)
)
<*> option auto
( long "debounce"
<> short 'd'
<> metavar "MS"
<> value 5000
<> showDefault
<> help "Debounce window in milliseconds"
<*> optional
( strOption
( long "repo"
<> short 'r'
<> metavar "PATH"
<> help "Path to a single git repository to watch (default: cwd)"
)
)
<*> strOption
( long "remote"
<> metavar "NAME"
<> value "origin"
<> showDefault
<> help "Remote name to pull from"
<> help "Remote name to pull from (single-repo mode)"
)
<*> strOption
( long "branch"
@@ -45,36 +51,50 @@ cliParser =
<> metavar "NAME"
<> value "main"
<> showDefault
<> help "Branch to pull"
<> help "Branch to pull (single-repo mode)"
)
<*> optional
( option auto
( long "debounce"
<> short 'd'
<> metavar "MS"
<> help "Debounce window in milliseconds (overrides config file)"
)
)
parserInfo :: ParserInfo CliArgs
parserInfo =
info (cliParser <**> helper)
( fullDesc
<> progDesc "Auto-sync git repos by watching, committing, and pulling"
<> header "converge - keep git repos in sync"
)
main :: IO ()
main = do
cli <- execParser opts
let options =
cli <- execParser parserInfo
let mDebounceOverride = cliDebounce cli
options <- case cliConfig cli of
Just cfgPath -> do
cfg <- loadConfig cfgPath
let opts = configToOptions cfg
pure $ case mDebounceOverride of
Just ms -> opts {optDebounceMs = ms}
Nothing -> opts
Nothing ->
pure $
defaultOptions
{ optRepoPath = cliRepoPath cli
, optDebounceMs = cliDebounce cli
, optRemote = cliRemote cli
, optBranch = cliBranch cli
{ optRepos =
[ defaultRepoConfig
{ rcPath = fromMaybe "." (cliRepo cli)
, rcRemote = cliRemote cli
, rcBranch = cliBranch cli
}
]
, optDebounceMs = fromMaybe 5000 mDebounceOverride
}
hPutStrLn stderr $
"Converge watching " <> optRepoPath options
"Converge watching " <> show (length (optRepos options)) <> " repo(s)"
<> " (debounce: " <> show (optDebounceMs options) <> "ms)"
withWatcher options $ \_event -> do
(commitCode, _, commitErr) <- gitCommitAll (optRepoPath options)
unless (commitCode == ExitSuccess) $
hPutStrLn stderr ("git commit failed: " <> show commitErr)
(pullCode, _, pullErr) <- gitPull options
unless (pullCode == ExitSuccess) $
hPutStrLn stderr ("git pull failed: " <> show pullErr)
conflicted <- hasConflicts (optRepoPath options)
when conflicted $
notifyConflict options
where
opts =
info (cliParser <**> helper)
( fullDesc
<> progDesc "Auto-sync a git repo by watching, committing, and pulling"
<> header "converge - keep a git repo in sync"
)
mapM_ (\r -> hPutStrLn stderr (" - " <> rcPath r)) (optRepos options)
runConverge options