make spaces work properly, clean up everything

This commit is contained in:
Mirek Kratochvil 2025-07-17 15:29:52 +02:00
parent 960f316059
commit 44bd3e8c14
2 changed files with 139 additions and 75 deletions

42
Main.hs
View file

@ -244,25 +244,31 @@ zeal Config {..} (Conflict m o y) =
xs -> [Ok $ reverse xs] xs -> [Ok $ reverse xs]
pops (m:ms) (y:ys) pops (m:ms) (y:ys)
| m == y = (m :) <$> pops ms ys | m == y = (m :) <$> pops ms ys
| SpacesMy <- cfgSpaces | cfgSpaceRetain == ResolveMy
, Toks.space m , Toks.space m
, Toks.space y = (m :) <$> pops ms ys , Toks.space y = (m :) <$> pops ms ys
| SpacesYour <- cfgSpaces | cfgSpaceRetain == ResolveYour
, Toks.space m , Toks.space m
, Toks.space y = (y :) <$> pops ms ys , Toks.space y = (y :) <$> pops ms ys
pops ms ys = ((ms, ys), []) pops ms ys = ((ms, ys), [])
zeal _ x = [x] zeal _ x = [x]
resolveSpace Config {..} c@(Conflict m o y) resolveSpace Config {..} c@(Conflict m o y)
| not (all Toks.space $ concat [m, o, y]) | not (all Toks.space $ concat [m, o, y]) = c
|| cfgSpaces `elem` [SpacesNormal, SpacesConflict] = c
| m == o && o == y = Ok o | m == o && o == y = Ok o
| otherwise = | cfgSpaceRetain == ResolveMy = Ok m
case cfgSpaces of | cfgSpaceRetain == ResolveOld = Ok o
SpacesMy -> Ok m | cfgSpaceRetain == ResolveYour = Ok y
SpacesOld -> Ok o | cfgSpaceResolution == SpaceNormal = c
SpacesYour -> Ok y | cmResolveSeparate cfgSpaceConflicts && m == o = Ok y
_ -> error $ "spaces resolution error " ++ show cfgSpaces | cmResolveSeparate cfgSpaceConflicts && o == y = Ok m
| cmResolveOverlaps cfgSpaceConflicts && m == y = Ok m
| SpaceSpecial r <- cfgSpaceResolution =
case r of
ResolveMy -> Ok m
ResolveOld -> Ok o
ResolveYour -> Ok y
ResolveKeep -> c
resolveSpace _ x = x resolveSpace _ x = x
expand :: Int -> [Merged] -> [Merged] expand :: Int -> [Merged] -> [Merged]
@ -280,12 +286,18 @@ expand n = go
go (x:xs) = x : go xs go (x:xs) = x : go xs
resolve cfg@Config {..} c@(Conflict m o y) resolve cfg@Config {..} c@(Conflict m o y)
| cfgSpaces /= SpacesNormal && all Toks.space (concat [m, o, y]) = | cfgSpaceResolution /= SpaceNormal
resolveSpace cfg c , all Toks.space (concat [m, o, y]) = resolveSpace cfg c
| m == o && o == y = Ok o | m == o && o == y = Ok o
| m == o && cfgResolveSeparate = Ok y | cmResolveSeparate cfgConflicts && m == o = Ok y
| o == y && cfgResolveSeparate = Ok m | cmResolveSeparate cfgConflicts && o == y = Ok m
| m == y && cfgResolveOverlaps = Ok m | cmResolveOverlaps cfgConflicts && m == y = Ok m
| otherwise =
case cfgResolution of
ResolveMy -> Ok m
ResolveOld -> Ok o
ResolveYour -> Ok y
ResolveKeep -> c
resolve _ x = x resolve _ x = x
merge cfg@Config {..} ms ys = merge cfg@Config {..} ms ys =

170
Opts.hs
View file

@ -23,66 +23,127 @@ tokenizer =
(long "tok-filter" (long "tok-filter"
<> short 'F' <> short 'F'
<> metavar "FILTER" <> metavar "FILTER"
<> help "external program to separate the text to tokens") <> help "External program to separate the text to tokens")
, flag' , flag'
TokenizeCharCategorySimple TokenizeCharCategorySimple
(long "simple-tokens" (long "simple-tokens"
<> short 'i' <> short 'i'
<> help <> help
"use wider character class to separate the tokens (results in larger tokens and ignores case)") "Use wider character class to separate the tokens (results in larger tokens and ignores case)")
, flag' , flag'
TokenizeCharCategory TokenizeCharCategory
(long "full-tokens" (long "full-tokens"
<> short 'I' <> short 'I'
<> help <> help
"separate characters by all known character classes (default)") "Separate characters by all known character classes (default)")
, pure TokenizeCharCategory , pure TokenizeCharCategory
] ]
data Spaces data ConflictMask = ConflictMask
= SpacesNormal { cmResolveOverlaps :: Bool
| SpacesConflict , cmResolveSeparate :: Bool
| SpacesMy } deriving (Show)
| SpacesOld
| SpacesYour 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 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) deriving (Show, Eq)
spaceMode x spaceMode x
| x `isPrefixOf` "normal" = Right SpacesNormal | x `isPrefixOf` "normal" = Right SpaceNormal
| x `isPrefixOf` "conflict" = Right SpacesConflict | Right y <- resolutionMode x = Right (SpaceSpecial y)
| x `isPrefixOf` "my" = Right SpacesMy
| x `isPrefixOf` "old" = Right SpacesOld
| x `isPrefixOf` "your" = Right SpacesYour
| otherwise = | otherwise =
Left Left
$ "could not parse value `" $ "Could not parse value `"
++ x ++ x
++ "', use one of `normal', `conflict', `my', `old', and `your'" ++ "', use one of `normal', `keep', `my', `old', and `your'"
data Config = Config data Config = Config
{ cfgTokenizer :: Tokenizer { cfgTokenizer :: Tokenizer
, cfgSpaces :: Spaces
, cfgContext :: Int
, cfgZealous :: Bool , cfgZealous :: Bool
, cfgSpaceRetain :: Resolution
, cfgSpaceResolution :: SpaceResolution
, cfgSpaceConflicts :: ConflictMask
, cfgContext :: Int
, cfgResolution :: Resolution
, cfgConflicts :: ConflictMask
, cfgLabelStart :: String , cfgLabelStart :: String
, cfgLabelMyOld :: String , cfgLabelMyOld :: String
, cfgLabelOldYour :: String , cfgLabelOldYour :: String
, cfgLabelEnd :: String , cfgLabelEnd :: String
, cfgResolveSpaces :: Bool
, cfgResolveOverlaps :: Bool
, cfgResolveSeparate :: Bool
} deriving (Show) } deriving (Show)
config = do config = do
cfgTokenizer <- tokenizer cfgTokenizer <- tokenizer
cfgSpaces <- cfgZealous <-
option (eitherReader spaceMode) asum
$ long "spaces" [ flag' False $ long "no-zeal" <> help "avoid zealous mode (default)"
<> short 's' , flag' True
<> metavar "(normal|conflict|my|old|your)" $ long "zeal"
<> short 'z'
<> help <> help
"mode of merging the space-only changes; instead of usual resolution one may choose to always conflict or to default the space from the source files (default: normal)" "Try to zealously minify conflicts, potentially resolving them"
<> value SpacesNormal , 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 --resolve-space-* (default: normal)"
]
cfgSpaceConflicts <- conflictMask "conflict-space" "space-only tokens"
cfgContext <- cfgContext <-
option auto option auto
$ long "expand-context" $ long "expand-context"
@ -91,50 +152,41 @@ config = do
<> value 1 <> value 1
<> showDefault <> showDefault
<> help <> help
"Consider changes that are at most N tokens apart to be a single change. Zero may cause bad resolutions of near conflicting edits." "Consider changes that are at most N tokens apart to be a single change. Zero may cause bad resolutions of near conflicting edits"
cfgZealous <- cfgResolution <-
asum option (eitherReader resolutionMode)
[ flag' False $ long "no-zeal" <> help "avoid zealous mode (default)" $ long "resolve"
, flag' True <> metavar "(keep|my|old|your)"
$ long "zeal" <> value ResolveKeep
<> short 'z'
<> help <> help
"try to zealously minify conflicts, potentially resolving them" "Resolve general conflicts in favor of a given version, or keep the conflicts (default: keep)"
, pure False cfgConflicts <- conflictMask "conflict" "general tokens"
]
color <- color <-
flag False True flag False True
$ long "color" $ long "color"
<> short 'G' <> short 'G'
<> help <> help
"use shorter, gaily colored output markers by default (requires ANSI color support; good for terminals or `less -R')" "Use shorter, gaily colored output markers by default (requires ANSI color support; good for terminals or `less -R')"
labelStart <- labelStart <-
optional . strOption optional . strOption
$ long "label-start" $ long "label-start"
<> metavar "\"<<<<<\"" <> metavar "\"<<<<<\""
<> help "label for beginning of the conflict" <> help "Label for beginning of the conflict"
labelMyOld <- labelMyOld <-
optional . strOption optional . strOption
$ long "label-mo" $ long "label-mo"
<> metavar "\"|||||\"" <> metavar "\"|||||\""
<> help "separator of local edits and original" <> help "Separator of local edits and original"
labelOldYour <- labelOldYour <-
optional . strOption optional . strOption
$ long "label-oy" $ long "label-oy"
<> metavar "\"=====\"" <> metavar "\"=====\""
<> help "separator of original and other people's edits" <> help "Separator of original and other people's edits"
labelEnd <- labelEnd <-
optional . strOption optional . strOption
$ long "label-end" $ long "label-end"
<> metavar "\">>>>>\"" <> metavar "\">>>>>\""
<> help "label for end of the conflict" <> help "Label for end of the conflict"
cfgResolveOverlaps <-
fmap not . switch
$ long "conflict-overlaps" <> help "do not resolve overlapping changes"
cfgResolveSeparate <-
fmap not . switch
$ long "conflict-separate"
<> help "do not resolve separate (non-overlapping) changes"
pure pure
Config Config
{ cfgLabelStart = { cfgLabelStart =
@ -161,10 +213,10 @@ data Command
deriving (Show) deriving (Show)
cmdDiff3 = do cmdDiff3 = do
d3my <- strArgument $ metavar "MYFILE" <> help "version with local edits" d3my <- strArgument $ metavar "MYFILE" <> help "Version with local edits"
d3old <- strArgument $ metavar "OLDFILE" <> help "original file version" d3old <- strArgument $ metavar "OLDFILE" <> help "Original file version"
d3your <- d3your <-
strArgument $ metavar "YOURFILE" <> help "version with other people's edits" strArgument $ metavar "YOURFILE" <> help "Version with other people's edits"
pure CmdDiff3 {..} pure CmdDiff3 {..}
cmdGitMerge = do cmdGitMerge = do
@ -173,12 +225,12 @@ cmdGitMerge = do
[ fmap Just . some [ fmap Just . some
$ strArgument $ strArgument
$ metavar "UNMERGED" $ metavar "UNMERGED"
<> help "unmerged git file (can be specified repeatedly)" <> help "Unmerged git file (can be specified repeatedly)"
, flag' , flag'
Nothing Nothing
(long "unmerged" (long "unmerged"
<> short 'u' <> short 'u'
<> help "process all files marked as unmerged by git") <> help "Process all files marked as unmerged by git")
] ]
gmDoAdd <- gmDoAdd <-
asum asum
@ -186,8 +238,8 @@ cmdGitMerge = do
True True
(long "add" (long "add"
<> short 'a' <> short 'a'
<> help "run `git add' for fully merged files") <> help "Run `git add' for fully merged files")
, flag' False (long "no-add" <> help "prevent running `git add'") , flag' False (long "no-add" <> help "Prevent running `git add'")
, pure False , pure False
] ]
pure CmdGitMerge {..} pure CmdGitMerge {..}
@ -202,7 +254,7 @@ cmd =
$ progDesc "diff3-style merge of two changesets" $ progDesc "diff3-style merge of two changesets"
, command "git" , command "git"
$ info cmdGitMerge $ info cmdGitMerge
$ progDesc "automerge unmerged files in git conflict" $ progDesc "Automerge unmerged files in git conflict"
] ]
parseOpts :: IO (Config, Command) parseOpts :: IO (Config, Command)
@ -215,4 +267,4 @@ parseOpts =
(fullDesc (fullDesc
<> header <> header
"werge -- blanks-friendly mergetool for tiny interdwindled changes" "werge -- blanks-friendly mergetool for tiny interdwindled changes"
<> (footer $ "werge is a free software, use it accordingly.")) <> footer "werge is a free software, use it accordingly.")