{-# LANGUAGE ApplicativeDo #-} {-# LANGUAGE RecordWildCards #-} module Opts where import Data.Bool import Data.List import Data.Maybe import Data.Version (showVersion) import Options.Applicative import Paths_werge (version) data Tokenizer = TokenizeFilter String | TokenizeCharCategory | TokenizeCharCategorySimple deriving (Show) tokenizer :: Parser Tokenizer tokenizer = asum [ TokenizeFilter <$> strOption (long "tok-filter" <> short 'F' <> metavar "FILTER" <> help "External program to separate the text to tokens") , flag' TokenizeCharCategorySimple (long "simple-tokens" <> short 'i' <> help "Use wider character class to separate the tokens (results in larger tokens and ignores case)") , flag' TokenizeCharCategory (long "full-tokens" <> short 'I' <> help "Separate characters by all known character classes (default)") , pure TokenizeCharCategory ] data ConflictMask = ConflictMask { cmResolveOverlaps :: Bool , cmResolveSeparate :: Bool } deriving (Show) conflictMask :: String -> String -> Parser ConflictMask conflictMask label objs = do cmResolveOverlaps' <- fmap not . switch $ long (label ++ "-overlaps") <> help ("Never resolve overlapping changes in " ++ objs) cmResolveSeparate' <- fmap not . switch $ long (label ++ "-separate") <> help ("Never resolve separate (non-overlapping) changes in " ++ objs) cmAll <- fmap not . switch $ long (label ++ "-all") <> help ("Never resolve any changes in " ++ objs) pure ConflictMask { cmResolveSeparate = cmResolveSeparate' && cmAll , cmResolveOverlaps = cmResolveOverlaps' && cmAll } data Resolution = ResolveKeep | ResolveMy | ResolveOld | ResolveYour deriving (Show, Eq) resolutionMode :: String -> Either String Resolution resolutionMode x | x `isPrefixOf` "keep" = Right ResolveKeep | x `isPrefixOf` "my" = Right ResolveMy | x `isPrefixOf` "old" = Right ResolveOld | x `isPrefixOf` "your" = Right ResolveYour | otherwise = Left $ "Could not parse value `" ++ x ++ "', use one of `keep', `my', `old', and `your'" data SpaceResolution = SpaceNormal | SpaceSpecial Resolution deriving (Show, Eq) spaceMode :: String -> Either String SpaceResolution spaceMode x | x `isPrefixOf` "normal" = Right SpaceNormal | Right y <- resolutionMode x = Right (SpaceSpecial y) | otherwise = Left $ "Could not parse value `" ++ x ++ "', use one of `normal', `keep', `my', `old', and `your'" data Config = Config { cfgTokenizer :: Tokenizer , cfgZealous :: Bool , cfgSpaceRetain :: Resolution , cfgSpaceResolution :: SpaceResolution , cfgSpaceConflicts :: ConflictMask , cfgContext :: Int , cfgResolution :: Resolution , cfgConflicts :: ConflictMask , cfgLabelStart :: String , cfgLabelMyOld :: String , cfgLabelDiff :: String , cfgLabelOldYour :: String , cfgLabelEnd :: String } deriving (Show) config :: Parser Config config = do cfgTokenizer <- tokenizer cfgZealous <- asum [ flag' False $ long "no-zeal" <> help "avoid zealous mode (default)" , flag' True $ long "zeal" <> short 'z' <> help "Try to zealously minify conflicts, potentially resolving them" , pure False ] cfgSpaceRetain <- option (eitherReader resolutionMode) $ long "space" <> short 'S' <> metavar "(keep|my|old|your)" <> help "Retain spacing from a selected version, or keep all space changes for merging (default: keep)" <> value ResolveKeep cfgSpaceResolution <- asum [ flag' (SpaceSpecial ResolveKeep) $ short 's' <> help "Shortcut for `--resolve-space keep' (this separates space-only conflicts, enabling better automated resolution)" , option (eitherReader spaceMode) $ long "resolve-space" <> metavar ("(normal|keep|my|old|your)") <> value SpaceNormal <> help "Resolve conflicts in space-only tokens separately, and either keep unresolved conflicts, or resolve in favor of a given version; `normal' resolves the spaces together with other tokens, ignoring choices in --conflict-space-* (default: normal)" ] cfgSpaceConflicts <- conflictMask "conflict-space" "space-only tokens" cfgContext <- option auto $ long "expand-context" <> short 'C' <> metavar "N" <> value 2 <> showDefault <> help "Consider changes that are at less than N tokens apart to be a single change; 0 turns off conflict expansion, 1 may cause bad resolutions of near conflicting edits" cfgResolution <- option (eitherReader resolutionMode) $ long "resolve" <> metavar "(keep|my|old|your)" <> value ResolveKeep <> help "Resolve general conflicts in favor of a given version, or keep the conflicts (default: keep)" cfgConflicts <- conflictMask "conflict" "general tokens" color <- flag False True $ long "color" <> short 'G' <> help "Use shorter, gaily colored output markers by default (requires ANSI color support; good for terminals or `less -R')" labelStart <- optional . strOption $ long "label-start" <> metavar "\"<<<<<\"" <> help "Label for beginning of the conflict" labelMyOld <- optional . strOption $ long "label-mo" <> metavar "\"|||||\"" <> help "Separator of local edits and original" labelDiff <- optional . strOption $ long "label-diff" <> metavar "\"|||||\"" <> help "Separator for old and new version" labelOldYour <- optional . strOption $ long "label-oy" <> metavar "\"=====\"" <> help "Separator of original and other people's edits" labelEnd <- optional . strOption $ long "label-end" <> metavar "\">>>>>\"" <> help "Label for end of the conflict" pure Config { cfgLabelStart = bool "<<<<<" "\ESC[1;37m<\ESC[0;31m" color `fromMaybe` labelStart , cfgLabelMyOld = bool "|||||" "\ESC[1;37m|\ESC[1;30m" color `fromMaybe` labelMyOld , cfgLabelDiff = bool "|||||" "\ESC[1;37m|\ESC[0;32m" color `fromMaybe` labelDiff , cfgLabelOldYour = bool "=====" "\ESC[1;37m=\ESC[0;32m" color `fromMaybe` labelOldYour , cfgLabelEnd = bool ">>>>>" "\ESC[1;37m>\ESC[0m" color `fromMaybe` labelEnd , .. } data Command = CmdDiff3 { d3my :: FilePath , d3old :: FilePath , d3your :: FilePath } | CmdGitMerge { gmFiles :: Maybe [FilePath] , gmDoAdd :: Bool } | CmdDiff { diffOld :: FilePath , diffYour :: FilePath , diffUnified :: Maybe Int } | CmdPatch { patchTarget :: Maybe FilePath , patchInput :: Maybe FilePath } | CmdBreak | CmdGlue deriving (Show) cmdDiff3 :: Parser Command cmdDiff3 = do d3my <- strArgument $ metavar "MYFILE" <> help "Version with local edits" d3old <- strArgument $ metavar "OLDFILE" <> help "Original file version" d3your <- strArgument $ metavar "YOURFILE" <> help "Version with other people's edits" pure CmdDiff3 {..} cmdGitMerge :: Parser Command cmdGitMerge = do gmFiles <- asum [ fmap Just . some $ strArgument $ metavar "UNMERGED" <> help "Unmerged file tracked by git (can be specified repeatedly)" , flag' Nothing (long "unmerged" <> short 'u' <> help "Process all files marked as unmerged by git") ] gmDoAdd <- asum [ flag' True $ long "add" <> short 'a' <> help "Run `git add' for fully merged files" , flag' False $ long "no-add" <> help "Prevent running `git add'" , pure False ] pure CmdGitMerge {..} cmdDiff :: Parser Command cmdDiff = do diffOld <- strArgument $ metavar "OLDFILE" <> help "Original file version" diffYour <- strArgument $ metavar "YOURFILE" <> help "File version with changes" diffUnified <- asum [ flag' (Just 20) $ long "unified" <> short 'u' <> help "Produce unified-diff-like output for `patch' with default context size (20)" , fmap Just . option auto $ long "unified-size" <> short 'U' <> help "Produce unified diff with this context size" , flag Nothing Nothing $ long "merge" <> short 'm' <> help "Highlight the differences as with `merge' (default)" ] pure CmdDiff {..} cmdPatch :: Parser Command cmdPatch = do patchTarget <- asum [ Just <$> strArgument (metavar "MYFILE" <> help "File to be patched") , flag' Nothing $ long "format" <> short 'f' <> help "Do not patch anything, only format the patch using conflict marks on joined tokens" ] patchInput <- optional . strOption $ long "patch" <> short 'p' <> metavar "PATCH" <> help "File with the patch (default: stdin)" pure CmdPatch {..} -- TODO have some option to output the (partially merged) my/old/your files so -- that folks can continue with external program or so (such as meld) cmd :: Parser Command cmd = hsubparser $ mconcat [ command "merge" $ info cmdDiff3 $ progDesc "diff3-style merge of two changesets" , command "git" $ info cmdGitMerge $ progDesc "Automerge unmerged files in git conflict" , command "diff" $ info cmdDiff $ progDesc "Find differences between two files" , command "patch" $ info cmdPatch $ progDesc "Modify a file using a patch from `diff'" , command "break" $ info (pure CmdBreak) $ progDesc "Break text to tokens" , command "glue" $ info (pure CmdGlue) $ progDesc "Glue tokens back to text" ] parseOpts :: IO (Config, Command) parseOpts = customExecParser (prefs $ helpShowGlobals <> subparserInline) $ info (liftA2 (,) config cmd <**> helper <**> simpleVersioner (showVersion version)) (fullDesc <> header "werge -- blanks-friendly mergetool for tiny interdwindled changes" <> footer "werge is a free software, use it accordingly.")