389 lines
12 KiB
Haskell
389 lines
12 KiB
Haskell
{-# LANGUAGE RecordWildCards #-}
|
|
|
|
module Main where
|
|
|
|
import Control.Exception
|
|
import Control.Monad
|
|
import Data.Bool
|
|
import Data.Foldable
|
|
import Data.Function
|
|
import Data.List
|
|
import Data.Traversable
|
|
import System.Exit
|
|
import System.FilePath
|
|
import System.IO
|
|
import System.IO.Temp
|
|
|
|
import Opts
|
|
import Progs
|
|
import qualified Toks
|
|
import Toks (Tok)
|
|
|
|
import Debug.Trace
|
|
|
|
{-
|
|
- merge algorithms
|
|
-}
|
|
data Op
|
|
= Del
|
|
| Keep
|
|
| Add
|
|
deriving (Show, Eq)
|
|
|
|
pdiff' :: [String] -> [(Op, Tok)]
|
|
pdiff' = map go
|
|
where
|
|
go ('-':s) = (Del, s)
|
|
go (' ':s) = (Keep, s)
|
|
go ('+':s) = (Add, s)
|
|
go _ = error "unexpected contents in diff"
|
|
|
|
pdiff :: FilePath -> IO [(Op, Tok)]
|
|
pdiff path = pdiff' . lines <$> readFile path
|
|
|
|
data Merged
|
|
= Ok [String]
|
|
| Replace [String] [String]
|
|
| Conflict [String] [String] [String]
|
|
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 (Keep, _) = True
|
|
isKeepTok _ = False
|
|
|
|
isDelTok :: (Op, String) -> Bool
|
|
isDelTok (Del, _) = True
|
|
isDelTok _ = False
|
|
|
|
-- TODO: Diff output is not necessarily deterministic; we could make the chunk
|
|
-- sequences more unique by rolling them to front (or back), possibly enabling
|
|
-- more conflict resolution and preventing mismerges.
|
|
--
|
|
-- Example: " a " can be made out of " {+a +}" or "{+ a+} "
|
|
chunks :: [(Op, String)] -> [Merged]
|
|
chunks [] = []
|
|
chunks xs@((Keep, _):_) =
|
|
let (oks, ys) = span isKeepTok xs
|
|
in Ok (map snd oks) : chunks ys
|
|
chunks xs =
|
|
let (reps, ys) = break isKeepTok xs
|
|
in uncurry (Replace `on` map snd) (partition isDelTok reps) : chunks ys
|
|
|
|
align1 :: Eq a => [a] -> [a] -> ([a], [a], [a])
|
|
align1 as [] = ([], as, [])
|
|
align1 [] bs = ([], [], bs)
|
|
align1 (a:as) (b:bs)
|
|
| a == b
|
|
, (xs, as', bs') <- align1 as bs = (a : xs, as', bs')
|
|
align1 _ _ = error "chunks do not align"
|
|
|
|
align :: [Merged] -> [Merged] -> [Merged]
|
|
align m0 y0 = connect $ slice m0 y0
|
|
where
|
|
erase x = Replace x []
|
|
nemap _ [] = []
|
|
nemap f xs = [f xs]
|
|
slice (Ok m:ms) (Ok y:ys) =
|
|
let (ok, m', y') = align1 m y
|
|
in (Ok ok, Ok ok) : slice (nemap Ok m' ++ ms) (nemap Ok y' ++ ys)
|
|
slice (Replace m mr:ms) (Ok y:ys) =
|
|
let (ok, m', y') = align1 m y
|
|
in (Replace ok mr, Ok ok)
|
|
: slice (nemap erase m' ++ ms) (nemap Ok y' ++ ys)
|
|
slice (Ok m:ms) (Replace y yr:ys) =
|
|
let (ok, m', y') = align1 m y
|
|
in (Ok ok, Replace ok yr)
|
|
: slice (nemap Ok m' ++ ms) (nemap erase y' ++ ys)
|
|
slice (Replace m mr:ms) (Replace y yr:ys) =
|
|
let (ok, m', y') = align1 m y
|
|
in (Replace ok mr, Replace ok yr)
|
|
: slice (nemap erase m' ++ ms) (nemap erase y' ++ ys)
|
|
slice [Replace [] mr] [] = [(Replace [] mr, Ok [])]
|
|
slice [] [Replace [] yr] = [(Ok [], Replace [] yr)]
|
|
slice [] [] = []
|
|
slice _ _ = error "unacceptable chunks"
|
|
coFlag (Ok _) = False
|
|
coFlag (Replace _ _) = True
|
|
coFlag _ = error "flagging unacceptable chunks"
|
|
coSig (a, b) = (coFlag a, coFlag b)
|
|
coConn' (a, b) (a', b') = (a && a') || (b && b')
|
|
coConn = coConn' `on` coSig
|
|
coGroup [] = []
|
|
coGroup (x:xs) =
|
|
case coGroup xs of
|
|
(ys@(y:_):yss)
|
|
| coConn x y -> (x : ys) : yss
|
|
xs' -> [x] : xs'
|
|
connect = map confl . coGroup
|
|
toCon (Ok m, Ok _) = Ok m
|
|
toCon (Ok o, Replace _ y) = Conflict o o y
|
|
toCon (Replace o m, Ok _) = Conflict m o o
|
|
toCon (Replace o m, Replace _ y) = Conflict m o y
|
|
toCon _ = error "converting unacceptable chunks"
|
|
confl = foldr coAppend (Ok []) . map toCon
|
|
coAppend (Ok x) (Ok o) = Ok (x ++ o)
|
|
coAppend (Ok _) (Conflict _ _ _) = error "align consistency check fail" -- Conflict (x++m) (x++o) (x++y)
|
|
coAppend (Conflict m o y) (Ok x) = Conflict (m ++ x) (o ++ x) (y ++ x)
|
|
coAppend (Conflict m o y) (Conflict m' o' y') =
|
|
Conflict (m ++ m') (o ++ o') (y ++ y')
|
|
coAppend _ _ = error "appending unacceptable chunks"
|
|
|
|
regroup :: [Merged] -> [Merged]
|
|
regroup [] = []
|
|
regroup (Ok []:xs) = regroup xs
|
|
regroup (x@(Ok a):xs) =
|
|
case regroup xs of
|
|
(Ok b:xs') -> Ok (a ++ b) : xs'
|
|
xs' -> x : xs'
|
|
regroup (x:xs) = x : regroup xs
|
|
|
|
zeal :: Config -> Merged -> [Merged]
|
|
zeal Config {..} (Conflict m0 o0 y0) =
|
|
before' ++ (Conflict (reverse m'') o0 (reverse y'') : after')
|
|
where
|
|
((m', y'), before) = pops m0 y0
|
|
((m'', y''), rafter) = pops (reverse m') (reverse y')
|
|
before' =
|
|
case before of
|
|
[] -> []
|
|
xs -> [Ok xs]
|
|
after' =
|
|
case rafter of
|
|
[] -> []
|
|
xs -> [Ok $ reverse xs]
|
|
pops (m:ms) (y:ys)
|
|
| m == y = (m :) <$> pops ms ys
|
|
| cfgSpaceRetain == ResolveMy
|
|
, Toks.space m
|
|
, Toks.space y = (m :) <$> pops ms ys
|
|
| cfgSpaceRetain == ResolveYour
|
|
, Toks.space m
|
|
, Toks.space y = (y :) <$> pops ms ys
|
|
pops ms ys = ((ms, ys), [])
|
|
zeal _ x = [x]
|
|
|
|
resolveSpace :: Config -> Merged -> Merged
|
|
resolveSpace Config {..} c@(Conflict m o y)
|
|
| not (all Toks.space $ concat [m, o, y]) = c
|
|
| m == o && o == y = Ok o
|
|
| cfgSpaceRetain == ResolveMy = Ok m
|
|
| cfgSpaceRetain == ResolveOld = Ok o
|
|
| cfgSpaceRetain == ResolveYour = Ok y
|
|
| cfgSpaceResolution == SpaceNormal = c
|
|
| cmResolveSeparate cfgSpaceConflicts && m == o = Ok y
|
|
| 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
|
|
|
|
expand :: Int -> [Merged] -> [Merged]
|
|
expand n = go
|
|
where
|
|
go [] = []
|
|
go (x@(Conflict m1 o1 y1):xs) =
|
|
case go xs of
|
|
(Conflict m2 o2 y2:xs')
|
|
| n > 0 -> Conflict (m1 ++ m2) (o1 ++ o2) (y1 ++ y2) : xs'
|
|
(Ok a:Conflict m2 o2 y2:xs')
|
|
| length a < n ->
|
|
Conflict (m1 ++ a ++ m2) (o1 ++ a ++ o2) (y1 ++ a ++ y2) : xs'
|
|
xs' -> x : xs'
|
|
go (x@(Replace o1 n1):xs) =
|
|
case go xs of
|
|
(Replace o2 n2:xs')
|
|
| n > 0 -> Replace (o1 ++ o2) (n1 ++ n2) : xs'
|
|
(Ok a:Replace o2 n2:xs')
|
|
| length a < n -> Replace (o1 ++ a ++ o2) (n1 ++ a ++ n2) : xs'
|
|
xs' -> x : xs'
|
|
go (x:xs) = x : go xs
|
|
|
|
resolve :: Config -> Merged -> Merged
|
|
resolve cfg@Config {..} c@(Conflict m o y)
|
|
| cfgSpaceResolution /= SpaceNormal
|
|
, all Toks.space (concat [m, o, y]) = resolveSpace cfg c
|
|
| m == o && o == y = Ok o
|
|
| cmResolveSeparate cfgConflicts && m == o = Ok y
|
|
| cmResolveSeparate cfgConflicts && o == y = 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
|
|
|
|
merge :: Config -> [(Op, String)] -> [(Op, String)] -> [Merged]
|
|
merge cfg@Config {..} ms ys =
|
|
regroup
|
|
. map (resolve cfg)
|
|
. expand cfgContext
|
|
. regroup
|
|
. map (resolveSpace cfg)
|
|
. bool id (concatMap $ zeal cfg) cfgZealous
|
|
. regroup
|
|
$ align (chunks ms) (chunks ys)
|
|
|
|
diff Config {..} = expand cfgContext . chunks
|
|
|
|
{-
|
|
- front-end
|
|
-}
|
|
format :: Config -> Handle -> [Merged] -> IO Bool
|
|
format Config {..} h = go False
|
|
where
|
|
go c [] = pure c
|
|
go c (Ok x:xs) = do
|
|
hPutStr h (Toks.glue x)
|
|
go c xs
|
|
go _ (Conflict m o y:xs) = do
|
|
hPutStr h
|
|
$ mconcat
|
|
[ cfgLabelStart
|
|
, Toks.glue m
|
|
, cfgLabelMyOld
|
|
, Toks.glue o
|
|
, cfgLabelOldYour
|
|
, Toks.glue y
|
|
, cfgLabelEnd
|
|
]
|
|
go True xs
|
|
go _ (Replace o n:xs) = do
|
|
hPutStr h
|
|
$ mconcat
|
|
[cfgLabelStart, Toks.glue o, cfgLabelDiff, Toks.glue n, cfgLabelEnd]
|
|
go True xs
|
|
|
|
fmtPatch :: Config -> Handle -> Handle -> IO ()
|
|
fmtPatch cfg out h = hGetContents h >>= go . lines
|
|
where
|
|
go all@(l:ls)
|
|
| patchLine l = do
|
|
let (p, ls') = span patchLine all
|
|
format cfg out . chunks $ pdiff' p
|
|
go ls'
|
|
| otherwise = hPutStrLn out l >> go ls
|
|
go [] = pure ()
|
|
patchLine (' ':_) = True
|
|
patchLine ('-':_) = True
|
|
patchLine ('+':_) = True
|
|
patchLine _ = False
|
|
|
|
runCmd :: Command -> Config -> IO ()
|
|
runCmd CmdDiff3 {..} cfg =
|
|
withSystemTempDirectory "werge-diff3" $ \workdir -> do
|
|
let [fMy, fOld, fYour, fdMy, fdYour] =
|
|
map (workdir </>) ["my", "old", "your", "mydiff", "yourdiff"]
|
|
for_ [(d3my, fMy), (d3old, fOld), (d3your, fYour)] $ \(path, tmp) ->
|
|
bracketFile path ReadMode $ \h -> hSplitToFile cfg h tmp
|
|
runDiff fOld fMy fdMy
|
|
runDiff fOld fYour fdYour
|
|
conflicted <-
|
|
merge cfg <$> pdiff fdMy <*> pdiff fdYour >>= format cfg stdout
|
|
if conflicted
|
|
then exitWith (ExitFailure 1)
|
|
else exitSuccess
|
|
runCmd CmdGitMerge {..} cfg = do
|
|
relroot <- gitRepoRelRoot
|
|
unmerged <-
|
|
case gmFiles of
|
|
Nothing -> map (relroot </>) <$> gitUnmerged
|
|
Just fs -> pure fs
|
|
conflicts <-
|
|
for unmerged $ \u ->
|
|
withSystemTempDirectory "werge-git" $ \workdir -> do
|
|
let [fMy, fOld, fYour, fdMy, fdYour] =
|
|
map (workdir </>) ["my", "old", "your", "mydiff", "yourdiff"]
|
|
gitCheckoutMOY cfg u fMy fOld fYour
|
|
runDiff fOld fMy fdMy
|
|
runDiff fOld fYour fdYour
|
|
readFile u >>= writeFile (u ++ ".werge-backup")
|
|
conflict <-
|
|
bracketFile u WriteMode $ \h ->
|
|
merge cfg <$> pdiff fdMy <*> pdiff fdYour >>= format cfg h
|
|
unless conflict $ when gmDoAdd $ gitAdd u
|
|
pure conflict
|
|
if or conflicts
|
|
then exitWith (ExitFailure 1)
|
|
else exitSuccess
|
|
runCmd CmdDiff {..} cfg = do
|
|
withSystemTempDirectory "werge-diff" $ \workdir -> do
|
|
let [fOld, fYour, fDiff] = map (workdir </>) ["old", "your", "diff"]
|
|
for_ [(diffOld, fOld), (diffYour, fYour)] $ \(path, tmp) ->
|
|
bracketFile path ReadMode $ \h -> hSplitToFile cfg h tmp
|
|
conflicted <-
|
|
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
|
|
then exitWith (ExitFailure 1)
|
|
else exitSuccess
|
|
runCmd CmdPatch {..} cfg = do
|
|
withSystemTempDirectory "werge-patch" $ \workdir -> do
|
|
let f = workdir </> "file"
|
|
case patchTarget of
|
|
Just patchMy -> do
|
|
bracketFile patchMy ReadMode $ \h -> hSplitToFile cfg h f
|
|
_ <-
|
|
case patchInput of
|
|
Nothing -> runPatch f stdin
|
|
Just path -> bracketFile path ReadMode $ runPatch f
|
|
conflicted <- pmerge f >>= format cfg stdout -- TODO try to resolve more?
|
|
if conflicted
|
|
then exitWith (ExitFailure 1)
|
|
else exitSuccess
|
|
Nothing -> do
|
|
case patchInput of
|
|
Nothing -> fmtPatch cfg stdout stdin
|
|
Just path -> bracketFile path ReadMode $ fmtPatch cfg stdout
|
|
runCmd CmdBreak cfg = hSplit cfg stdin stdout
|
|
runCmd CmdGlue _ = getContents >>= putStr . Toks.glue . Toks.fromFile
|
|
|
|
main :: IO ()
|
|
main = catch go bad
|
|
where
|
|
go = parseOpts >>= uncurry (flip runCmd)
|
|
bad e = do
|
|
hPutStrLn stderr $ "fatal: " ++ displayException (e :: IOException)
|
|
exitWith (ExitFailure 2)
|