{-# OPTIONS -O2 -Wall #-}
{-# LANGUAGE FlexibleContexts, RecordWildCards #-}
import Control.Applicative
import Control.Monad.State
import Control.Monad.Writer
import Data.Algorithm.Diff (Diff, getDiff)
import Data.List
import PPDiff (ppDiff, ColorEnable(..))
import System.Directory (renameFile, removeFile)
import System.Environment (getProgName, getArgs, getEnv)
import System.FilePath
import System.Posix.IO (stdOutput)
import System.Posix.Terminal (queryTerminal)
import System.Process
data Side = A | B
deriving (Eq, Ord, Show)
type LineNo = Int
data Conflict = Conflict
{ _lineNo :: LineNo
, _markerA :: String -- <<<<<<<....
, _markerBase :: String -- |||||||....
, _markerB :: String -- =======....
, _markerEnd :: String -- >>>>>>>....
, _linesA :: [String]
, _linesBase :: [String]
, _linesB :: [String]
} deriving (Show)
prettyConflict :: Conflict -> String
prettyConflict (Conflict _ markerA markerBase markerB markerEnd linesA linesBase linesB) =
unlines $ concat
[ markerA : linesA
, markerBase : linesBase
, markerB : linesB
, [markerEnd]
]
resolveConflict :: Conflict -> Maybe String
resolveConflict Conflict{..}
| _linesA == _linesBase = Just $ unlines _linesB
| _linesB == _linesBase = Just $ unlines _linesA
| _linesA == _linesB = Just $ unlines _linesA
| otherwise = Nothing
breakUpToMarker :: MonadState [(LineNo, String)] m => Char -> m [(LineNo, String)]
breakUpToMarker c = state (break ((replicate 7 c `isPrefixOf`) . snd))
readHead :: MonadState [a] m => m (Maybe a)
readHead = state f
where
f [] = (Nothing, [])
f (l:ls) = (Just l, ls)
readUpToMarker :: MonadState [(LineNo, String)] m => Char -> m ([(LineNo, String)], Maybe (LineNo, String))
readUpToMarker c =
do
ls <- breakUpToMarker c
mHead <- readHead
return (ls, mHead)
parseConflict :: MonadState [(LineNo, String)] m => LineNo -> String -> m Conflict
parseConflict lineNo markerA = do
(linesA , Just (_, markerBase)) <- readUpToMarker '|'
(linesBase, Just (_, markerB)) <- readUpToMarker '='
(linesB , Just (_, markerEnd)) <- readUpToMarker '>'
return Conflict
{ _lineNo = lineNo
, _markerA = markerA
, _markerBase = markerBase
, _markerB = markerB
, _markerEnd = markerEnd
, _linesA = map snd linesA
, _linesB = map snd linesB
, _linesBase = map snd linesBase
}
parseConflicts :: String -> [Either String Conflict]
parseConflicts input =
snd $ runWriter $ evalStateT loop (zip [1..] (lines input))
where
loop =
do
(ls, mMarkerA) <- readUpToMarker '<'
tell $ map (Left . snd) ls
case mMarkerA of
Nothing -> return ()
Just (lineNo, markerA) ->
do
tell . return . Right =<< parseConflict lineNo markerA
loop
type SideDiff = (Side, LineNo, [Diff String])
data NewContent = NewContent
{ _resolvedSuccessfully :: Int
, _failedToResolve :: Int
, _newContent :: String
, _diffs :: [SideDiff]
}
getConflictDiffs :: Conflict -> [SideDiff]
getConflictDiffs Conflict{..} =
[ (A, _lineNo, getDiff _linesBase _linesA) | not (null _linesA) ] ++
[ (B, _lineNo, getDiff _linesBase _linesB) | not (null _linesB) ]
resolveContent :: [Either String Conflict] -> NewContent
resolveContent = asResult . mconcat . map go
where
asResult (Sum successes, Sum failures, newContent, diffs) = NewContent
{ _resolvedSuccessfully = successes
, _failedToResolve = failures
, _newContent = newContent
, _diffs = diffs
}
go (Left line) = (Sum 0, Sum 0, unlines [line], [])
go (Right conflict) =
case resolveConflict conflict of
Nothing ->
( Sum 0, Sum 1, prettyConflict conflict
, getConflictDiffs conflict
)
Just trivialLines -> (Sum 1, Sum 0, trivialLines, [])
gitAdd :: FilePath -> IO ()
gitAdd fileName =
callProcess "git" ["add", "--", fileName]
data Options = Options
{ shouldUseEditor :: Bool
, shouldDumpDiffs :: Bool
, shouldUseColor :: Maybe ColorEnable
}
instance Monoid Options where
mempty = Options False False Nothing
Options a0 b0 c0 `mappend` Options a1 b1 c1 =
Options
(combineBool a0 a1 "-e")
(combineBool b0 b1 "-d")
(combineMaybe c0 c1 "-c or -C")
where
err flag = error $ "Multiple " ++ flag ++ " flags used"
combineMaybe (Just _) (Just _) flag = err flag
combineMaybe Nothing Nothing _ = Nothing
combineMaybe (Just x) Nothing _ = Just x
combineMaybe Nothing (Just y) _ = Just y
combineBool True True flag = err flag
combineBool x y _ = x || y
openEditor :: Options -> FilePath -> IO ()
openEditor opts path
| shouldUseEditor opts =
do
editor <- getEnv "EDITOR"
callProcess editor [path]
| otherwise = return ()
dumpDiffs :: ColorEnable -> Options -> FilePath -> [SideDiff] -> IO ()
dumpDiffs colorEnable opts filePath diffs
| shouldDumpDiffs opts = mapM_ dumpDiff diffs
| otherwise = return ()
where
dumpDiff (side, lineNo, diff) =
do
putStrLn $ filePath ++ ":" ++ show lineNo ++ ":Diff" ++ show side
putStr $ unlines $ map (ppDiff colorEnable) diff
dumpAndOpenEditor :: ColorEnable -> Options -> FilePath -> [SideDiff] -> IO ()
dumpAndOpenEditor colorEnable opts path diffs =
do
dumpDiffs colorEnable opts path diffs
openEditor opts path
resolve :: ColorEnable -> Options -> FilePath -> IO ()
resolve colorEnable opts fileName =
do
content <- parseConflicts <$> readFile fileName
case resolveContent content of
NewContent successes failures newContent diffs
| successes == 0 &&
failures == 0 -> do
putStrLn $ fileName ++ ": No conflicts, git-adding"
gitAdd fileName
| successes == 0 -> do
putStrLn $ concat
[ fileName, ": Failed to resolve any of the "
, show failures, " conflicts" ]
dumpAndOpenEditor colorEnable opts fileName diffs
| otherwise ->
do
putStrLn $ concat
[ fileName, ": Successfully resolved ", show successes
, " conflicts (failed to resolve " ++ show failures ++ " conflicts)"
, if failures == 0 then ", git adding" else ""
]
let bkup = fileName <.> "bk"
renameFile fileName bkup
writeFile fileName newContent
removeFile bkup
if failures == 0
then gitAdd fileName
else dumpAndOpenEditor colorEnable opts fileName diffs
stripNewline :: String -> String
stripNewline x
| "\n" `isSuffixOf` x = init x
| otherwise = x
getOpts :: [String] -> IO Options
getOpts = fmap mconcat . mapM parseArg
where
parseArg "-e" = return mempty { shouldUseEditor = True }
parseArg "-d" = return mempty { shouldDumpDiffs = True }
parseArg "-c" = return mempty { shouldUseColor = Just EnableColor }
parseArg "-C" = return mempty { shouldUseColor = Just DisableColor }
parseArg _ =
do prog <- getProgName
fail $ unlines
[ "Usage: " ++ prog ++ " [-e] [-d] [-c]"
, ""
, "-e Execute $EDITOR for each conflicted file that remains conflicted"
, "-d Dump the left/right diffs from base in each conflict remaining"
, "-c Enable color"
, "-C Disable color"
]
shouldUseColorByTerminal :: IO ColorEnable
shouldUseColorByTerminal =
do istty <- queryTerminal stdOutput
return $ if istty then EnableColor else DisableColor
main :: IO ()
main =
do opts <- getOpts =<< getArgs
colorEnable <-
case shouldUseColor opts of
Nothing -> shouldUseColorByTerminal
Just colorEnable -> return colorEnable
let stdin = ""
statusPorcelain <- readProcess "git" ["status", "--porcelain"] stdin
let rootRelativeFileNames =
map ((!! 1) . words) $ filter ("UU" `isPrefixOf`) $ lines statusPorcelain
rootDir <- stripNewline <$> readProcess "git" ["rev-parse", "--show-toplevel"] stdin
mapM_ (resolve colorEnable opts .
(rootDir </>)) rootRelativeFileNames