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:
@@ -1,30 +1,54 @@
|
|||||||
# Converge
|
# Converge
|
||||||
|
|
||||||
Auto-sync a git repository by watching for file changes, automatically committing them, and pulling from a remote.
|
Auto-sync one or more git repositories by watching for file changes, automatically committing them, and pulling from a remote.
|
||||||
|
|
||||||
Converge notifies the user via libnotify / `notify-send` whenever a merge conflict occurs during automatic pulling.
|
Converge notifies the user via libnotify / `notify-send` whenever a merge conflict occurs during automatic pulling.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
### Single repository
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Watch the current directory (defaults)
|
# Watch the current directory (defaults)
|
||||||
./scripts/run
|
./scripts/run
|
||||||
|
|
||||||
# Watch a specific repo
|
# Watch a specific repo
|
||||||
./scripts/run -- --repo /path/to/repo --branch master
|
./scripts/run -- --repo /path/to/repo --branch master --debounce 10000
|
||||||
|
```
|
||||||
|
|
||||||
# Custom debounce (10 seconds)
|
### Multiple repositories (config file)
|
||||||
./scripts/run -- --debounce 10000
|
|
||||||
|
```bash
|
||||||
|
# Create a config file
|
||||||
|
cp converge.example.yaml converge.yaml
|
||||||
|
# Edit to list your repos
|
||||||
|
vim converge.yaml
|
||||||
|
|
||||||
|
# Run with config
|
||||||
|
./scripts/run -- --config converge.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
### Options
|
### Options
|
||||||
|
|
||||||
| Option | Default | Description |
|
| Option | Default | Description |
|
||||||
|--------|---------|-------------|
|
|--------|---------|-------------|
|
||||||
| `-r`, `--repo PATH` | `.` (cwd) | Path to the git repository |
|
| `-c`, `--config FILE` | — | Path to YAML config file listing repositories |
|
||||||
| `-d`, `--debounce MS` | `5000` | Debounce window in milliseconds |
|
| `-r`, `--repo PATH` | `.` (cwd) | Path to a single git repository |
|
||||||
| `--remote NAME` | `origin` | Remote name to pull from |
|
| `-d`, `--debounce MS` | `5000` | Debounce window in milliseconds (overrides config) |
|
||||||
| `-b`, `--branch NAME` | `main` | Branch to pull |
|
| `--remote NAME` | `origin` | Remote name (single-repo mode) |
|
||||||
|
| `-b`, `--branch NAME` | `main` | Branch to pull (single-repo mode) |
|
||||||
|
|
||||||
|
### Config file format
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
repos:
|
||||||
|
- path: /absolute/or/relative/path
|
||||||
|
remote: origin # optional, defaults to "origin"
|
||||||
|
branch: main # optional, defaults to "main"
|
||||||
|
- path: /another/repo
|
||||||
|
|
||||||
|
debounce_ms: 5000 # optional, defaults to 5000
|
||||||
|
```
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
@@ -48,5 +72,7 @@ All development tools run inside Docker via the `hs` wrapper script.
|
|||||||
|
|
||||||
- **Language**: Haskell (GHC 9.10.3 via lts-24.38)
|
- **Language**: Haskell (GHC 9.10.3 via lts-24.38)
|
||||||
- **File watching**: [fsnotify](https://hackage.haskell.org/package/fsnotify)
|
- **File watching**: [fsnotify](https://hackage.haskell.org/package/fsnotify)
|
||||||
|
- **Concurrency**: one watcher thread per repository via `async`
|
||||||
- **Notifications**: shell out to `notify-send` (libnotify)
|
- **Notifications**: shell out to `notify-send` (libnotify)
|
||||||
|
- **Configuration**: YAML config file for multi-repo, CLI flags for single-repo
|
||||||
- **Daemonization**: TBD (likely systemd service)
|
- **Daemonization**: TBD (likely systemd service)
|
||||||
|
|||||||
+67
-47
@@ -1,43 +1,49 @@
|
|||||||
module Main (main) where
|
module Main (main) where
|
||||||
|
|
||||||
import Converge
|
import Converge
|
||||||
import Control.Monad (unless)
|
import Control.Monad (when)
|
||||||
|
import Data.Maybe (fromMaybe, isJust)
|
||||||
import Options.Applicative
|
import Options.Applicative
|
||||||
import System.Exit (ExitCode (..))
|
|
||||||
import System.IO (hPutStrLn, stderr)
|
import System.IO (hPutStrLn, stderr)
|
||||||
|
|
||||||
data CliOptions = CliOptions
|
data CliArgs = CliArgs
|
||||||
{ cliRepoPath :: !FilePath
|
{ cliConfig :: !(Maybe FilePath)
|
||||||
, cliDebounce :: !Int
|
-- ^ Optional path to a YAML config file.
|
||||||
|
, cliRepo :: !(Maybe FilePath)
|
||||||
|
-- ^ Optional single repo path (requires --config to be absent).
|
||||||
, cliRemote :: !Text
|
, cliRemote :: !Text
|
||||||
|
-- ^ Remote name (used only in single-repo mode).
|
||||||
, cliBranch :: !Text
|
, 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 =
|
cliParser =
|
||||||
CliOptions
|
CliArgs
|
||||||
<$> strOption
|
<$> optional
|
||||||
( long "repo"
|
( strOption
|
||||||
<> short 'r'
|
( long "config"
|
||||||
<> metavar "PATH"
|
<> short 'c'
|
||||||
<> value "."
|
<> metavar "FILE"
|
||||||
<> showDefault
|
<> help "Path to YAML config file listing repositories"
|
||||||
<> help "Path to the git repository to watch (default: cwd)"
|
)
|
||||||
)
|
)
|
||||||
<*> option auto
|
<*> optional
|
||||||
( long "debounce"
|
( strOption
|
||||||
<> short 'd'
|
( long "repo"
|
||||||
<> metavar "MS"
|
<> short 'r'
|
||||||
<> value 5000
|
<> metavar "PATH"
|
||||||
<> showDefault
|
<> help "Path to a single git repository to watch (default: cwd)"
|
||||||
<> help "Debounce window in milliseconds"
|
)
|
||||||
)
|
)
|
||||||
<*> strOption
|
<*> strOption
|
||||||
( long "remote"
|
( long "remote"
|
||||||
<> metavar "NAME"
|
<> metavar "NAME"
|
||||||
<> value "origin"
|
<> value "origin"
|
||||||
<> showDefault
|
<> showDefault
|
||||||
<> help "Remote name to pull from"
|
<> help "Remote name to pull from (single-repo mode)"
|
||||||
)
|
)
|
||||||
<*> strOption
|
<*> strOption
|
||||||
( long "branch"
|
( long "branch"
|
||||||
@@ -45,36 +51,50 @@ cliParser =
|
|||||||
<> metavar "NAME"
|
<> metavar "NAME"
|
||||||
<> value "main"
|
<> value "main"
|
||||||
<> showDefault
|
<> 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 :: IO ()
|
||||||
main = do
|
main = do
|
||||||
cli <- execParser opts
|
cli <- execParser parserInfo
|
||||||
let options =
|
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
|
defaultOptions
|
||||||
{ optRepoPath = cliRepoPath cli
|
{ optRepos =
|
||||||
, optDebounceMs = cliDebounce cli
|
[ defaultRepoConfig
|
||||||
, optRemote = cliRemote cli
|
{ rcPath = fromMaybe "." (cliRepo cli)
|
||||||
, optBranch = cliBranch cli
|
, rcRemote = cliRemote cli
|
||||||
|
, rcBranch = cliBranch cli
|
||||||
|
}
|
||||||
|
]
|
||||||
|
, optDebounceMs = fromMaybe 5000 mDebounceOverride
|
||||||
}
|
}
|
||||||
hPutStrLn stderr $
|
hPutStrLn stderr $
|
||||||
"Converge watching " <> optRepoPath options
|
"Converge watching " <> show (length (optRepos options)) <> " repo(s)"
|
||||||
<> " (debounce: " <> show (optDebounceMs options) <> "ms)"
|
<> " (debounce: " <> show (optDebounceMs options) <> "ms)"
|
||||||
withWatcher options $ \_event -> do
|
mapM_ (\r -> hPutStrLn stderr (" - " <> rcPath r)) (optRepos options)
|
||||||
(commitCode, _, commitErr) <- gitCommitAll (optRepoPath options)
|
runConverge 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"
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Example converge configuration file.
|
||||||
|
# Usage: converge --config converge.yaml
|
||||||
|
|
||||||
|
repos:
|
||||||
|
- path: /home/jbrechtel/org
|
||||||
|
remote: origin
|
||||||
|
branch: main
|
||||||
|
- path: /work/personal/hawat
|
||||||
|
# remote defaults to "origin"
|
||||||
|
# branch defaults to "main"
|
||||||
|
|
||||||
|
# debounce_ms: 5000 # optional, defaults to 5000
|
||||||
@@ -29,6 +29,7 @@ ghc-options:
|
|||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
- base >= 4.7 && < 5
|
- base >= 4.7 && < 5
|
||||||
|
- async
|
||||||
- directory
|
- directory
|
||||||
- filepath
|
- filepath
|
||||||
- fsnotify
|
- fsnotify
|
||||||
@@ -37,6 +38,7 @@ dependencies:
|
|||||||
- text
|
- text
|
||||||
- time
|
- time
|
||||||
- unix
|
- unix
|
||||||
|
- yaml
|
||||||
|
|
||||||
library:
|
library:
|
||||||
source-dirs: src
|
source-dirs: src
|
||||||
|
|||||||
+118
-45
@@ -1,10 +1,17 @@
|
|||||||
module Converge
|
module Converge
|
||||||
( -- * Core types
|
( -- * Core types
|
||||||
Options (..),
|
Options (..),
|
||||||
|
RepoConfig (..),
|
||||||
defaultOptions,
|
defaultOptions,
|
||||||
|
defaultRepoConfig,
|
||||||
|
|
||||||
-- * Watching
|
-- * Config file
|
||||||
withWatcher,
|
Config (..),
|
||||||
|
loadConfig,
|
||||||
|
configToOptions,
|
||||||
|
|
||||||
|
-- * Running
|
||||||
|
runConverge,
|
||||||
|
|
||||||
-- * Git operations
|
-- * Git operations
|
||||||
gitCommitAll,
|
gitCommitAll,
|
||||||
@@ -17,65 +24,135 @@ module Converge
|
|||||||
where
|
where
|
||||||
|
|
||||||
import Control.Concurrent (threadDelay)
|
import Control.Concurrent (threadDelay)
|
||||||
|
import Control.Concurrent.Async (mapConcurrently_)
|
||||||
import Control.Exception (bracket)
|
import Control.Exception (bracket)
|
||||||
import Control.Monad (forever, unless, when)
|
import Control.Monad (forever, unless, when)
|
||||||
|
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.Yaml (FromJSON (..), decodeFileThrow, withObject, (.:), (.:?))
|
||||||
|
import GHC.Generics (Generic)
|
||||||
import System.Directory (getCurrentDirectory)
|
import System.Directory (getCurrentDirectory)
|
||||||
import System.Exit (ExitCode (..))
|
import System.Exit (ExitCode (..))
|
||||||
import System.FilePath ((</>))
|
import System.FilePath (isAbsolute, (</>))
|
||||||
import System.FSNotify
|
import System.FSNotify
|
||||||
import System.IO (hPutStrLn, stderr)
|
import System.IO (hPutStrLn, stderr)
|
||||||
import System.Process (readProcessWithExitCode, spawnProcess, waitForProcess)
|
import System.Process (readProcessWithExitCode, spawnProcess)
|
||||||
|
|
||||||
-- | Command-line / configuration options for converge.
|
-- | Configuration for a single repository to watch.
|
||||||
data Options = Options
|
data RepoConfig = RepoConfig
|
||||||
{ optRepoPath :: !FilePath
|
{ rcPath :: !FilePath
|
||||||
-- ^ Path to the git repository to watch. Defaults to cwd.
|
-- ^ Path to the git repository.
|
||||||
, optDebounceMs :: !Int
|
, rcRemote :: !Text
|
||||||
-- ^ Debounce window in milliseconds. Default: 5000.
|
-- ^ Remote name to pull from.
|
||||||
, optRemote :: !Text
|
, rcBranch :: !Text
|
||||||
-- ^ Remote name to pull from. Default: @"origin"@.
|
-- ^ Branch to pull.
|
||||||
, optBranch :: !Text
|
|
||||||
-- ^ Branch to pull. Default: @"main"@.
|
|
||||||
, optNotifyCmd :: !Text
|
|
||||||
-- ^ Command used for desktop notifications. Default: @"notify-send"@.
|
|
||||||
}
|
}
|
||||||
deriving (Show, Eq)
|
deriving (Show, Eq)
|
||||||
|
|
||||||
-- | Sensible defaults.
|
-- | Top-level options for converge.
|
||||||
|
data Options = Options
|
||||||
|
{ optRepos :: ![RepoConfig]
|
||||||
|
-- ^ Repositories to watch.
|
||||||
|
, optDebounceMs :: !Int
|
||||||
|
-- ^ Debounce window in milliseconds.
|
||||||
|
, optNotifyCmd :: !Text
|
||||||
|
-- ^ Command used for desktop notifications.
|
||||||
|
}
|
||||||
|
deriving (Show, Eq)
|
||||||
|
|
||||||
|
-- | Sensible defaults for a single repo (cwd, origin, main).
|
||||||
|
defaultRepoConfig :: RepoConfig
|
||||||
|
defaultRepoConfig =
|
||||||
|
RepoConfig
|
||||||
|
{ rcPath = "."
|
||||||
|
, rcRemote = "origin"
|
||||||
|
, rcBranch = "main"
|
||||||
|
}
|
||||||
|
|
||||||
|
-- | Sensible defaults for options.
|
||||||
defaultOptions :: Options
|
defaultOptions :: Options
|
||||||
defaultOptions =
|
defaultOptions =
|
||||||
Options
|
Options
|
||||||
{ optRepoPath = "."
|
{ optRepos = [defaultRepoConfig]
|
||||||
, optDebounceMs = 5000
|
, optDebounceMs = 5000
|
||||||
, optRemote = "origin"
|
|
||||||
, optBranch = "main"
|
|
||||||
, optNotifyCmd = "notify-send"
|
, optNotifyCmd = "notify-send"
|
||||||
}
|
}
|
||||||
|
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
-- File watcher
|
-- Config file (YAML)
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
-- | Watch the repository for file changes, debounced.
|
-- | Shape of the optional YAML config file.
|
||||||
-- Calls the action after a quiet period of 'optDebounceMs' ms.
|
data Config = Config
|
||||||
withWatcher :: Options -> (Event -> IO ()) -> IO ()
|
{ cfgRepos :: ![RepoConfig]
|
||||||
withWatcher opts onQuietPeriod = do
|
, cfgDebounceMs :: !(Maybe Int)
|
||||||
repoPath <- if optRepoPath opts == "."
|
}
|
||||||
then getCurrentDirectory
|
deriving (Show, Eq, Generic)
|
||||||
else pure (optRepoPath opts)
|
|
||||||
|
instance FromJSON Config where
|
||||||
|
parseJSON = withObject "Config" $ \o ->
|
||||||
|
Config
|
||||||
|
<$> o .: "repos"
|
||||||
|
<*> o .:? "debounce_ms"
|
||||||
|
|
||||||
|
instance FromJSON RepoConfig where
|
||||||
|
parseJSON = withObject "RepoConfig" $ \o ->
|
||||||
|
RepoConfig
|
||||||
|
<$> o .: "path"
|
||||||
|
<*> o .:? "remote" .!= "origin"
|
||||||
|
<*> o .:? "branch" .!= "main"
|
||||||
|
|
||||||
|
-- | Load a YAML config file.
|
||||||
|
loadConfig :: FilePath -> IO Config
|
||||||
|
loadConfig = decodeFileThrow
|
||||||
|
|
||||||
|
-- | Convert a Config to Options (filling in defaults).
|
||||||
|
configToOptions :: Config -> Options
|
||||||
|
configToOptions cfg =
|
||||||
|
defaultOptions
|
||||||
|
{ optRepos = cfgRepos cfg
|
||||||
|
, optDebounceMs = fromMaybe 5000 (cfgDebounceMs cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
-- Running
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- | Watch all configured repositories concurrently.
|
||||||
|
runConverge :: Options -> IO ()
|
||||||
|
runConverge opts =
|
||||||
|
mapConcurrently_ (watchRepo opts) (optRepos opts)
|
||||||
|
|
||||||
|
watchRepo :: Options -> RepoConfig -> IO ()
|
||||||
|
watchRepo opts repo = do
|
||||||
|
absPath <- resolvePath (rcPath repo)
|
||||||
let debounceUs = optDebounceMs opts * 1000
|
let debounceUs = optDebounceMs opts * 1000
|
||||||
bracket
|
bracket
|
||||||
(startManagerConf defaultConfig {confDebounce = NoDebounce})
|
(startManagerConf defaultConfig {confDebounce = NoDebounce})
|
||||||
stopManager
|
stopManager
|
||||||
$ \mgr -> do
|
$ \mgr -> do
|
||||||
_ <- watchDir mgr repoPath (const True) $ \_event -> do
|
_ <- watchDir mgr absPath (const True) $ \_event -> do
|
||||||
-- Debounce: wait for quiet period, then fire
|
|
||||||
threadDelay debounceUs
|
threadDelay debounceUs
|
||||||
onQuietPeriod _event
|
syncRepo opts repo
|
||||||
-- Never finish
|
|
||||||
forever $ threadDelay maxBound
|
forever $ threadDelay maxBound
|
||||||
|
where
|
||||||
|
resolvePath p
|
||||||
|
| isAbsolute p = pure p
|
||||||
|
| otherwise = (</> p) <$> getCurrentDirectory
|
||||||
|
|
||||||
|
-- | Run a full sync cycle on a single repo: commit + pull + conflict check.
|
||||||
|
syncRepo :: Options -> RepoConfig -> IO ()
|
||||||
|
syncRepo opts repo = do
|
||||||
|
(commitCode, _, commitErr) <- gitCommitAll (rcPath repo)
|
||||||
|
unless (commitCode == ExitSuccess) $
|
||||||
|
hPutStrLn stderr ("[" <> rcPath repo <> "] git commit failed: " <> show commitErr)
|
||||||
|
(pullCode, _, pullErr) <- gitPull repo
|
||||||
|
unless (pullCode == ExitSuccess) $
|
||||||
|
hPutStrLn stderr ("[" <> rcPath repo <> "] git pull failed: " <> show pullErr)
|
||||||
|
conflicted <- hasConflicts (rcPath repo)
|
||||||
|
when conflicted $
|
||||||
|
notifyConflict opts repo
|
||||||
|
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
-- Git operations
|
-- Git operations
|
||||||
@@ -84,37 +161,33 @@ withWatcher opts onQuietPeriod = do
|
|||||||
-- | Stage all changes and commit with an auto-generated message.
|
-- | Stage all changes and commit with an auto-generated message.
|
||||||
gitCommitAll :: FilePath -> IO (ExitCode, Text, Text)
|
gitCommitAll :: FilePath -> IO (ExitCode, Text, Text)
|
||||||
gitCommitAll repoPath = do
|
gitCommitAll repoPath = do
|
||||||
-- Stage everything
|
|
||||||
(_, _, stageErr) <- runGitIn repoPath ["add", "-A"]
|
(_, _, stageErr) <- runGitIn repoPath ["add", "-A"]
|
||||||
unless (T.null stageErr) $
|
unless (T.null stageErr) $
|
||||||
hPutStrLn stderr ("git add stderr: " <> T.unpack stageErr)
|
hPutStrLn stderr ("git add stderr: " <> T.unpack stageErr)
|
||||||
-- Commit
|
runGitIn repoPath ["commit", "-m", "converge: auto-commit"]
|
||||||
let msg = "converge: auto-commit"
|
|
||||||
runGitIn repoPath ["commit", "-m", T.unpack msg]
|
|
||||||
|
|
||||||
-- | Pull from the configured remote and branch.
|
-- | Pull from the configured remote and branch.
|
||||||
gitPull :: Options -> IO (ExitCode, Text, Text)
|
gitPull :: RepoConfig -> IO (ExitCode, Text, Text)
|
||||||
gitPull opts =
|
gitPull repo =
|
||||||
runGitIn (optRepoPath opts)
|
runGitIn (rcPath repo)
|
||||||
["pull", "--no-edit", T.unpack (optRemote opts), T.unpack (optBranch opts)]
|
["pull", "--no-edit", T.unpack (rcRemote repo), T.unpack (rcBranch repo)]
|
||||||
|
|
||||||
-- | Check if the repository currently has merge conflicts.
|
-- | Check if the repository currently has merge conflicts.
|
||||||
hasConflicts :: FilePath -> IO Bool
|
hasConflicts :: FilePath -> IO Bool
|
||||||
hasConflicts repoPath = do
|
hasConflicts repoPath = do
|
||||||
let conflictMarkers = [<<"<<<<<<<", "=======", ">>>>>>>"]
|
|
||||||
(_, diffOut, _) <- runGitIn repoPath ["diff", "--check"]
|
(_, diffOut, _) <- runGitIn repoPath ["diff", "--check"]
|
||||||
pure $ any (`T.isInfixOf` diffOut) conflictMarkers
|
pure $ any (`T.isInfixOf` diffOut) ["<<<<<<<", "=======", ">>>>>>>"]
|
||||||
|
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
-- Notifications
|
-- Notifications
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
-- | Send a desktop notification about a merge conflict.
|
-- | Send a desktop notification about a merge conflict.
|
||||||
notifyConflict :: Options -> IO ()
|
notifyConflict :: Options -> RepoConfig -> IO ()
|
||||||
notifyConflict opts = do
|
notifyConflict opts repo = do
|
||||||
let cmd = T.unpack (optNotifyCmd opts)
|
let cmd = T.unpack (optNotifyCmd opts)
|
||||||
title = "Converge: Merge Conflict"
|
title = "Converge: Merge Conflict"
|
||||||
body = "A merge conflict occurred in " <> optRepoPath opts
|
body = "A merge conflict occurred in " <> T.pack (rcPath repo)
|
||||||
<> ". Manual resolution required."
|
<> ". Manual resolution required."
|
||||||
_ <- spawnProcess cmd ["--urgency=critical", title, T.unpack body]
|
_ <- spawnProcess cmd ["--urgency=critical", title, T.unpack body]
|
||||||
pure ()
|
pure ()
|
||||||
|
|||||||
Reference in New Issue
Block a user