282 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			Haskell
		
	
	
	
	
	
			
		
		
	
	
			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)
 |