make diff+patch work together, document

This commit is contained in:
Mirek Kratochvil 2025-07-18 15:21:08 +02:00
parent 56cf7c69a9
commit cb5257b285
5 changed files with 218 additions and 54 deletions

86
Main.hs
View file

@ -19,6 +19,8 @@ import Progs
import qualified Toks import qualified Toks
import Toks (Tok) import Toks (Tok)
import Debug.Trace
{- {-
- merge algorithms - merge algorithms
-} -}
@ -42,6 +44,36 @@ data Merged
| Conflict [String] [String] [String] | Conflict [String] [String] [String]
deriving (Show) deriving (Show)
pmerge :: FilePath -> IO [Merged]
pmerge path = go . lines <$> readFile path
where
go [] = []
go xs@(x:_)
| Toks.tok x = goOk xs
| otherwise = goC0 xs
eat = span Toks.tok
goOk xs =
let (a, xs') = eat xs
in Ok a : go xs'
goC0 ("<<<<<<<":xs) =
let (m, xs') = eat xs
in goC1 m xs'
goC0 (x:_) = error $ "unexpected token: " ++ x
goC0 [] = error "unexpected end"
goC1 m ("|||||||":xs) =
let (o, xs') = eat xs
in goC2 m o xs'
goC1 _ (x:_) = error $ "unexpected token: " ++ x
goC1 _ [] = error "unexpected end"
goC2 m o ("=======":xs) =
let (y, xs') = eat xs
in goC3 m o y xs'
goC2 _ _ (x:_) = error $ "unexpected token: " ++ x
goC2 _ _ [] = error "unexpected end"
goC3 m o y (">>>>>>>":xs) = Conflict m o y : go xs
goC3 _ _ _ (x:_) = error $ "unexpected token: " ++ x
goC3 _ _ _ [] = error "unexpected end"
isKeepTok :: (Op, String) -> Bool isKeepTok :: (Op, String) -> Bool
isKeepTok (Keep, _) = True isKeepTok (Keep, _) = True
isKeepTok _ = False isKeepTok _ = False
@ -182,19 +214,19 @@ expand n = go
go [] = [] go [] = []
go (x@(Conflict m1 o1 y1):xs) = go (x@(Conflict m1 o1 y1):xs) =
case go xs of case go xs of
(Conflict m2 o2 y2:xs') | n > 0 -> (Conflict m2 o2 y2:xs')
Conflict (m1 ++ m2) (o1 ++ o2) (y1 ++ y2) : xs' | n > 0 -> Conflict (m1 ++ m2) (o1 ++ o2) (y1 ++ y2) : xs'
(Ok a:Conflict m2 o2 y2:xs') (Ok a:Conflict m2 o2 y2:xs')
| length a < n -> | length a < n ->
Conflict (m1 ++ a ++ m2) (o1 ++ a ++ o2) (y1 ++ a ++ y2) : xs' Conflict (m1 ++ a ++ m2) (o1 ++ a ++ o2) (y1 ++ a ++ y2) : xs'
xs' -> x : xs' xs' -> x : xs'
go (x@(Replace o1 n1):xs) = case go xs of go (x@(Replace o1 n1):xs) =
(Replace o2 n2:xs') | n > 0 -> case go xs of
Replace (o1++o2) (n1++n2): xs' (Replace o2 n2:xs')
(Ok a:Replace o2 n2:xs') | n > 0 -> Replace (o1 ++ o2) (n1 ++ n2) : xs'
| length a < n -> (Ok a:Replace o2 n2:xs')
Replace (o1++a++o2) (n1++a++n2): xs' | length a < n -> Replace (o1 ++ a ++ o2) (n1 ++ a ++ n2) : xs'
xs' -> x : xs' xs' -> x : xs'
go (x:xs) = x : go xs go (x:xs) = x : go xs
resolve :: Config -> Merged -> Merged resolve :: Config -> Merged -> Merged
@ -224,7 +256,7 @@ merge cfg@Config {..} ms ys =
. regroup . regroup
$ align (chunks ms) (chunks ys) $ align (chunks ms) (chunks ys)
diff Config{..} = expand cfgContext . chunks diff Config {..} = expand cfgContext . chunks
{- {-
- front-end - front-end
@ -261,8 +293,8 @@ runCmd CmdDiff3 {..} cfg =
map (workdir </>) ["my", "old", "your", "mydiff", "yourdiff"] map (workdir </>) ["my", "old", "your", "mydiff", "yourdiff"]
for_ [(d3my, fMy), (d3old, fOld), (d3your, fYour)] $ \(path, tmp) -> for_ [(d3my, fMy), (d3old, fOld), (d3your, fYour)] $ \(path, tmp) ->
bracketFile path ReadMode $ \h -> hSplitToFile cfg h tmp bracketFile path ReadMode $ \h -> hSplitToFile cfg h tmp
rundiff fOld fMy fdMy runDiff fOld fMy fdMy
rundiff fOld fYour fdYour runDiff fOld fYour fdYour
conflicted <- conflicted <-
merge cfg <$> pdiff fdMy <*> pdiff fdYour >>= format cfg stdout merge cfg <$> pdiff fdMy <*> pdiff fdYour >>= format cfg stdout
if conflicted if conflicted
@ -280,8 +312,8 @@ runCmd CmdGitMerge {..} cfg = do
let [fMy, fOld, fYour, fdMy, fdYour] = let [fMy, fOld, fYour, fdMy, fdYour] =
map (workdir </>) ["my", "old", "your", "mydiff", "yourdiff"] map (workdir </>) ["my", "old", "your", "mydiff", "yourdiff"]
gitCheckoutMOY cfg u fMy fOld fYour gitCheckoutMOY cfg u fMy fOld fYour
rundiff fOld fMy fdMy runDiff fOld fMy fdMy
rundiff fOld fYour fdYour runDiff fOld fYour fdYour
readFile u >>= writeFile (u ++ ".werge-backup") readFile u >>= writeFile (u ++ ".werge-backup")
conflict <- conflict <-
bracketFile u WriteMode $ \h -> bracketFile u WriteMode $ \h ->
@ -293,14 +325,32 @@ runCmd CmdGitMerge {..} cfg = do
else exitSuccess else exitSuccess
runCmd CmdDiff {..} cfg = do runCmd CmdDiff {..} cfg = do
withSystemTempDirectory "werge-diff" $ \workdir -> do withSystemTempDirectory "werge-diff" $ \workdir -> do
let [fOld, fNew, fDiff] = map (workdir </>) ["old", "new", "diff"] let [fOld, fYour, fDiff] = map (workdir </>) ["old", "your", "diff"]
for_ [(diffOld, fOld), (diffNew, fNew)] $ \(path, tmp) -> for_ [(diffOld, fOld), (diffYour, fYour)] $ \(path, tmp) ->
bracketFile path ReadMode $ \h -> hSplitToFile cfg h tmp bracketFile path ReadMode $ \h -> hSplitToFile cfg h tmp
rundiff fOld fNew fDiff conflicted <-
conflicted <- pdiff fDiff >>= format cfg stdout . diff cfg case diffUnified of
Just u -> do
c <- runDiffRaw u fOld fYour fDiff
readFile fDiff >>= putStr . unlines . drop 2 . lines
pure c
Nothing -> do
runDiff fOld fYour fDiff
pdiff fDiff >>= format cfg stdout . diff cfg
if conflicted if conflicted
then exitWith (ExitFailure 1) then exitWith (ExitFailure 1)
else exitSuccess else exitSuccess
runCmd CmdPatch {..} cfg = do
withSystemTempDirectory "werge-patch" $ \workdir -> do
let f = workdir </> "file"
bracketFile patchMy ReadMode $ \h -> hSplitToFile cfg h f
_ <- runPatch f stdin
conflicted <- pmerge f >>= format cfg stdout -- TODO try to resolve more?
if conflicted
then exitWith (ExitFailure 1)
else exitSuccess
runCmd CmdBreak cfg = hSplit cfg stdin stdout
runCmd CmdGlue _ = getContents >>= putStr . Toks.glue . Toks.fromFile
main :: IO () main :: IO ()
main = catch go bad main = catch go bad

60
Opts.hs
View file

@ -224,9 +224,15 @@ data Command
, gmDoAdd :: Bool , gmDoAdd :: Bool
} }
| CmdDiff | CmdDiff
{ diffOld :: FilePath { diffOld :: FilePath
, diffNew :: FilePath , diffYour :: FilePath
} , diffUnified :: Maybe Int
}
| CmdPatch
{ patchMy :: FilePath
}
| CmdBreak
| CmdGlue
deriving (Show) deriving (Show)
cmdDiff3 :: Parser Command cmdDiff3 :: Parser Command
@ -254,12 +260,11 @@ cmdGitMerge = do
] ]
gmDoAdd <- gmDoAdd <-
asum asum
[ flag' [ flag' 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 {..}
@ -267,9 +272,31 @@ cmdGitMerge = do
cmdDiff :: Parser Command cmdDiff :: Parser Command
cmdDiff = do cmdDiff = do
diffOld <- strArgument $ metavar "OLDFILE" <> help "Original file version" diffOld <- strArgument $ metavar "OLDFILE" <> help "Original file version"
diffNew <- strArgument $ metavar "NEWFILE" <> help "File version with changes" 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 {..} pure CmdDiff {..}
cmdPatch :: Parser Command
cmdPatch = do
patchMy <- strArgument $ metavar "MYFILE" <> help "File to be modified"
pure CmdPatch {..}
-- TODO have some option to output the (partially merged) my/old/your files so -- 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) -- that folks can continue with external program or so (such as meld)
cmd :: Parser Command cmd :: Parser Command
@ -284,12 +311,21 @@ cmd =
$ progDesc "Automerge unmerged files in git conflict" $ progDesc "Automerge unmerged files in git conflict"
, command "diff" , command "diff"
$ info cmdDiff $ info cmdDiff
$ progDesc "Highlight differences between two files" $ progDesc "Find differences between two files"
, command "patch"
$ info cmdPatch
$ progDesc "Apply a patch from `diff' to file"
, 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 :: IO (Config, Command)
parseOpts = parseOpts =
customExecParser (prefs helpShowGlobals) customExecParser (prefs $ helpShowGlobals <> subparserInline)
$ info $ info
(liftA2 (,) config cmd (liftA2 (,) config cmd
<**> helper <**> helper

View file

@ -21,8 +21,11 @@ bracketFile path mode = bracket (openFile path mode) hClose
diffProg :: IO String diffProg :: IO String
diffProg = fromMaybe "diff" <$> lookupEnv "WERGE_DIFF" diffProg = fromMaybe "diff" <$> lookupEnv "WERGE_DIFF"
rundiff :: FilePath -> FilePath -> FilePath -> IO () patchProg :: IO String
rundiff f1 f2 out = do patchProg = fromMaybe "patch" <$> lookupEnv "WERGE_PATCH"
runDiff :: FilePath -> FilePath -> FilePath -> IO ()
runDiff f1 f2 out = do
diff <- diffProg diff <- diffProg
st <- st <-
bracketFile out WriteMode $ \oh -> bracketFile out WriteMode $ \oh ->
@ -41,6 +44,27 @@ rundiff f1 f2 out = do
unless (st `elem` [ExitSuccess, ExitFailure 1]) unless (st `elem` [ExitSuccess, ExitFailure 1])
$ error "diff failed for unknown reason (is GNU diffutils installed?)" $ error "diff failed for unknown reason (is GNU diffutils installed?)"
runDiffRaw :: Int -> FilePath -> FilePath -> FilePath -> IO Bool
runDiffRaw u f1 f2 out = do
diff <- diffProg
st <-
bracketFile out WriteMode $ \oh ->
withCreateProcess
(proc diff ["--text", "--unified=" ++ show u, f1, f2])
{std_in = NoStream, std_out = UseHandle oh} $ \_ _ _ -> waitForProcess
unless (st `elem` [ExitSuccess, ExitFailure 1]) $ error "diff failed"
pure (st /= ExitSuccess) -- report if diff thinks that the files differed
runPatch :: FilePath -> Handle -> IO Bool
runPatch f hi = do
patch <- patchProg
st <-
withCreateProcess
(proc patch ["--silent", "--batch", "--merge=diff3", f])
{std_in = UseHandle hi} $ \_ _ _ -> waitForProcess
unless (st `elem` [ExitSuccess, ExitFailure 1]) $ error "patch failed"
pure (st /= ExitSuccess) -- report if patch thinks that stuff has failed
{- {-
- interface to git - interface to git
-} -}
@ -115,17 +139,19 @@ gitAdd path = do
- TODO this might probably enforce joinSpaces? - TODO this might probably enforce joinSpaces?
- or have joinSpaces as configurable? (probably best, default true) - or have joinSpaces as configurable? (probably best, default true)
-} -}
hSplitToFile :: Config -> Handle -> FilePath -> IO () hSplit :: Config -> Handle -> Handle -> IO ()
hSplitToFile cfg h path = hSplit cfg hi ho =
case cfgTokenizer cfg of case cfgTokenizer cfg of
TokenizeCharCategory -> internal Toks.splitCategory TokenizeCharCategory -> internal Toks.splitCategory
TokenizeCharCategorySimple -> internal Toks.splitSimple TokenizeCharCategorySimple -> internal Toks.splitSimple
TokenizeFilter fltr -> do TokenizeFilter fltr -> do
st <- st <-
bracketFile path WriteMode $ \ho -> withCreateProcess
withCreateProcess (shell fltr) {std_in = UseHandle ho, std_out = UseHandle ho} $ \_ _ _ ->
(shell fltr) {std_in = UseHandle h, std_out = UseHandle ho} $ \_ _ _ -> waitForProcess
waitForProcess
unless (st == ExitSuccess) $ error "tokenize filter failed" unless (st == ExitSuccess) $ error "tokenize filter failed"
where where
internal s = hGetContents h >>= writeFile path . Toks.toFile . s internal s = hGetContents hi >>= hPutStr ho . Toks.toFile . s
hSplitToFile :: Config -> Handle -> FilePath -> IO ()
hSplitToFile cfg hi path = bracketFile path WriteMode $ hSplit cfg hi

View file

@ -144,16 +144,16 @@ automatically to `filename.werge-backup`.
``` ```
werge -- blanks-friendly mergetool for tiny interdwindled changes werge -- blanks-friendly mergetool for tiny interdwindled changes
Usage: werge [(-F|--tok-filter FILTER) | (-i|--simple-tokens) | Usage: werge [(-F|--tok-filter FILTER) | (-i|--simple-tokens) |
(-I|--full-tokens)] [--no-zeal | (-z|--zeal)] (-I|--full-tokens)] [--no-zeal | (-z|--zeal)]
[-S|--space (keep|my|old|your)] [-S|--space (keep|my|old|your)]
[-s | --resolve-space (normal|keep|my|old|your)] [-s | --resolve-space (normal|keep|my|old|your)]
[--conflict-space-overlaps] [--conflict-space-separate] [--conflict-space-overlaps] [--conflict-space-separate]
[--conflict-space-all] [-C|--expand-context N] [--conflict-space-all] [-C|--expand-context N]
[--resolve (keep|my|old|your)] [--conflict-overlaps] [--resolve (keep|my|old|your)] [--conflict-overlaps]
[--conflict-separate] [--conflict-all] [-G|--color] [--conflict-separate] [--conflict-all] [-G|--color]
[--label-start "<<<<<"] [--label-mo "|||||"] [--label-oy "====="] [--label-start "<<<<<"] [--label-mo "|||||"] [--label-diff "|||||"]
[--label-end ">>>>>"] COMMAND [--label-oy "====="] [--label-end ">>>>>"] COMMAND
Available options: Available options:
-F,--tok-filter FILTER External program to separate the text to tokens -F,--tok-filter FILTER External program to separate the text to tokens
@ -183,9 +183,10 @@ Available options:
Never resolve separate (non-overlapping) changes in Never resolve separate (non-overlapping) changes in
space-only tokens space-only tokens
--conflict-space-all Never resolve any changes in space-only tokens --conflict-space-all Never resolve any changes in space-only tokens
-C,--expand-context N Consider changes that are at most N tokens apart to -C,--expand-context N Consider changes that are at less than N tokens apart
be a single change. Zero may cause bad resolutions of to be a single change; 0 turns off conflict
near conflicting edits (default: 1) expansion, 1 may cause bad resolutions of near
conflicting edits (default: 2)
--resolve (keep|my|old|your) --resolve (keep|my|old|your)
Resolve general conflicts in favor of a given Resolve general conflicts in favor of a given
version, or keep the conflicts (default: keep) version, or keep the conflicts (default: keep)
@ -198,6 +199,7 @@ Available options:
`less -R') `less -R')
--label-start "<<<<<" Label for beginning of the conflict --label-start "<<<<<" Label for beginning of the conflict
--label-mo "|||||" Separator of local edits and original --label-mo "|||||" Separator of local edits and original
--label-diff "|||||" Separator for old and new version
--label-oy "=====" Separator of original and other people's edits --label-oy "=====" Separator of original and other people's edits
--label-end ">>>>>" Label for end of the conflict --label-end ">>>>>" Label for end of the conflict
-h,--help Show this help text -h,--help Show this help text
@ -206,6 +208,10 @@ Available options:
Available commands: Available commands:
merge diff3-style merge of two changesets merge diff3-style merge of two changesets
git Automerge unmerged files in git conflict git Automerge unmerged files in git conflict
diff Find differences between two files
patch Apply a patch from `diff' to file
break Break text to tokens
glue Glue tokens back to text
werge is a free software, use it accordingly. werge is a free software, use it accordingly.
``` ```
@ -237,3 +243,45 @@ Available options:
--no-add Prevent running `git add' --no-add Prevent running `git add'
-h,--help Show this help text -h,--help Show this help text
``` ```
#### Finding differences
```
Usage: werge diff OLDFILE YOURFILE
[(-u|--unified) | (-U|--unified-size ARG) | (-m|--merge)]
Find differences between two files
Available options:
OLDFILE Original file version
YOURFILE File version with changes
-u,--unified Produce unified-diff-like output for `patch' with
default context size (20)
-U,--unified-size ARG Produce unified diff with this context size
-m,--merge Highlight the differences as with `merge' (default)
-h,--help Show this help text
```
#### Patching files in place
```
Usage: werge patch MYFILE
Apply a patch from `diff' to file
Available options:
MYFILE File to be modified
-h,--help Show this help text
```
#### Converting between files and tokens
```
Usage: werge break
Break text to tokens
```
```
Usage: werge glue
Glue tokens back to text
```

View file

@ -18,15 +18,19 @@ unescape ('\\':'n':xs) = '\n' : unescape xs
unescape ('\\':_) = error "bad escape on input" unescape ('\\':_) = error "bad escape on input"
unescape (x:xs) = x : unescape xs unescape (x:xs) = x : unescape xs
tok ('.':_) = True
tok ('/':_) = True
tok _ = False
markSpace :: String -> Tok markSpace :: String -> Tok
markSpace [] = error "empty token" markSpace [] = error "empty token"
markSpace s@(c:_) markSpace s@(c:_)
| isSpace c = '.' : s | isSpace c = '.' : s
| otherwise = '|' : s | otherwise = '/' : s
unmarkSpace :: Tok -> String unmarkSpace :: Tok -> String
unmarkSpace ('.':s) = s unmarkSpace ('.':s) = s
unmarkSpace ('|':s) = s unmarkSpace ('/':s) = s
unmarkSpace _ = error "bad space marking on input" unmarkSpace _ = error "bad space marking on input"
space :: Tok -> Bool space :: Tok -> Bool