werge/Main.hs

282 lines
8.4 KiB
Haskell

{-# LANGUAGE RecordWildCards #-}
module Main where
import Control.Exception
import Control.Monad
import Data.Bool
import Data.Char
import Data.Foldable
import Data.List
import Data.Traversable
import Opts
import System.Exit
import System.FilePath
import System.IO
import System.IO.Temp
import System.Process
import qualified Toks
import Debug.Trace
-- TODO: the diff w
rundiff f1 f2 out = do
st <-
withFile out WriteMode $ \oh ->
withCreateProcess
(proc
"diff" -- TODO: from WERGE_DIFF env
[ "--text"
, "--new-line-format=+%L"
, "--old-line-format=-%L"
, "--unchanged-line-format= %L"
, f1
, f2
])
{std_in = NoStream, std_out = UseHandle oh, std_err = Inherit} $ \_ _ _ ->
waitForProcess
when (st == ExitFailure 2) $ error "diff failed"
unless (st `elem` [ExitSuccess, ExitFailure 1])
$ error "diff failed for unknown reason (is GNU diffutils installed?)"
gitRepoRelRoot = do
(path, st) <-
withCreateProcess
(proc "git" ["rev-parse", "--show-cdup"])
{std_in = NoStream, std_out = CreatePipe, std_err = Inherit} $ \_ (Just oh) _ p ->
(,) <$> hGetContents' oh <*> waitForProcess p
unless (st == ExitSuccess) $ error "git failed"
let [p] = lines path
pure p
gitUnmerged = do
(paths, st) <-
withCreateProcess
(proc "git" ["status", "--porcelain=v1"])
{std_in = NoStream, std_out = CreatePipe, std_err = Inherit} $ \_ (Just oh) _ p ->
(,)
<$> (map (drop 3) . filter ("UU " `isPrefixOf`) . lines
<$> hGetContents' oh)
<*> waitForProcess p
unless (st == ExitSuccess) $ error "git failed"
pure paths
gitCheckoutMOY u my old your = do
(paths, st) <-
withCreateProcess
(proc "git" ["ls-files", "--unmerged", "--", u])
{std_in = NoStream, std_out = CreatePipe, std_err = Inherit} $ \_ (Just oh) _ p ->
(,)
<$> (sortOn snd
. map ((\[a, b] -> (a, b)) . take 2 . drop 1 . words)
. lines
<$> hGetContents' oh)
<*> waitForProcess p
unless (st == ExitSuccess) $ error "git failed"
let co (hash, _) path = do
st <-
withCreateProcess
(proc "git" ["cat-file", "blob", hash])
{std_in = NoStream, std_out = CreatePipe, std_err = Inherit} $ \_ (Just ho) _ p -> do
hGetContents ho >>= writeFile path . Toks.split -- TODO cfg
waitForProcess p
unless (st == ExitSuccess) . error
$ "failed checking out " ++ u ++ " from blob " ++ hash
case paths of
[(_, "1"), (_, "2"), (_, "3")] ->
zipWithM co paths [old, my, your] >> pure ()
_ -> error $ "bad data from ls-files for unmerged " ++ u
gitAdd path = do
traceM $ "adding " ++ path
st <- rawSystem "git" ["add", "--", path]
unless (st == ExitSuccess) $ error "git-add failed"
data Op
= Del
| Keep
| Add
deriving (Show, Eq)
pdiff path = map go . lines <$> readFile path
where
go [] = error "empty line from diff"
go ('-':s) = (Del, s)
go (' ':s) = (Keep, s)
go ('+':s) = (Add, s)
data Merged
= Ok [String]
| Conflict [String] [String] [String]
deriving (Show)
align :: [(Op, String)] -> [(Op, String)] -> [Merged]
align [] [] = []
align ((Keep, m):ms) ((Keep, y):ys)
| m == y = Ok [m] : align ms ys
align ((Del, m):ms) ((Del, y):ys)
| m == y = Conflict [] [m] [] : align ms ys
align ((Del, m):ms) ((Keep, y):ys)
| m == y = Conflict [] [m] [m] : align ms ys
align ((Keep, m):ms) ((Del, y):ys)
| m == y = Conflict [m] [m] [] : align ms ys
align ((Add, m):ms) ys = Conflict [m] [] [] : align ms ys
align ms ((Add, y):ys) = Conflict [] [] [y] : align ms ys
align _ _ = error "diffs do not align"
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 (Conflict [] [] []:xs) = regroup xs
regroup (x@(Conflict m1 o1 y1):xs) =
case regroup xs of
(Conflict m2 o2 y2:xs') -> Conflict (m1 ++ m2) (o1 ++ o2) (y1 ++ y2) : xs'
xs' -> x : xs'
regroup (x:xs) = x : regroup xs
expand :: Int -> [Merged] -> [Merged]
expand n = go
where
go [] = []
go (x@(Conflict m1 o1 y1):xs) =
case go xs of
(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:xs) = x : go xs
zeal (Conflict m o y) =
before' ++ (Conflict (reverse m'') o (reverse y'') : after')
where
((m', y'), before) = pops m y
((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
pops ms ys = ((ms, ys), [])
zeal x = [x]
resolve cfg@Config {..} c@(Conflict m o y)
| all Toks.space (concat [m, o, y]) && cfgSpaces /= SpacesNormal =
resolveSpace cfg c
| m == o && o == y = Ok o
| m == o && cfgResolveSeparate = Ok y
| o == y && cfgResolveSeparate = Ok m
| m == y && cfgResolveOverlaps = Ok m
resolve _ x = x
-- TODO: there might be a bit of interplay between the spaces handling and
-- separate/overlapped conflict resolution -- e.g., what if someone wants to
-- merge overlapping edits in text but separate edits in spaces? At this point
-- that might be ignorable.
--
-- Also, conflicts that are made out of an ignorable space change and a
-- mergeable non-space change now cause conflicts because the spaces are no
-- longer truly separable/alignable here. Ideally some part of the space
-- merging should be done at alignment (e.g., fake all spaces to cause them to
-- align well). Also it might be necessary to group space-tokens together
-- (newline and indent are now 2 space tokens, which won't ever merge with a
-- single space)
resolveSpace Config {..} c@(Conflict m o y)
| m == o && o == y = Ok o
| otherwise =
case cfgSpaces of
SpacesConflict -> c
SpacesMy -> Ok m
SpacesOld -> Ok o
SpacesYour -> Ok y
_ -> error "spaces resolution failed"
resolveSpaces _ x = x
merge cfg@Config {..} ms ys =
regroup
. map (resolve cfg)
. traceShowId
. regroup
. bool id (concatMap zeal) cfgZealous
. expand cfgContext
. regroup
$ align ms ys
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.glueToks x)
go c xs
go c (Conflict m o y:xs) = do
hPutStr h
$ mconcat
[ cfgLabelStart
, Toks.glueToks m
, cfgLabelMyOld
, Toks.glueToks o
, cfgLabelOldYour
, Toks.glueToks y
, cfgLabelEnd
]
go True xs
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) ->
readFile path >>= writeFile tmp . Toks.split -- TODO cfg
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 u fMy fOld fYour
rundiff fOld fMy fdMy
rundiff fOld fYour fdYour
readFile u >>= writeFile (u ++ ".werge-backup")
conflict <-
withFile u WriteMode $ \h ->
merge cfg <$> pdiff fdMy <*> pdiff fdYour >>= format cfg h
traceShowM conflict
traceShowM gmDoAdd
unless conflict $ when gmDoAdd $ gitAdd u
pure conflict
if or conflicts
then exitWith (ExitFailure 1)
else exitSuccess
main :: IO ()
main = catch go bad
where
go = do
(cfg, cmd) <- parseOpts
runCmd cmd cfg
bad e = do
hPutStrLn stderr $ "fatal: " ++ displayException (e :: IOException)
exitWith (ExitFailure 2)