cr-1.2: Cr.hs
{-# LANGUAGE DeriveDataTypeable, OverloadedStrings, ExtendedDefaultRules, ScopedTypeVariables #-}
{-# OPTIONS_GHC -fno-warn-type-defaults #-}
module Main where
import Prelude hiding ( catch )
import Control.Monad.IO.Class ( MonadIO(..) )
import Data.String ( fromString )
import Data.Text.Lazy as LT
import Shelly
import System.Console.CmdArgs
import System.Directory ( doesDirectoryExist )
import System.Exit ( ExitCode, exitFailure )
import System.Environment ( getEnvironment )
import System.IO ( hFlush, stdout )
import System.Process ( StdStream(..)
, CreateProcess(..), CmdSpec(..), createProcess
, waitForProcess )
import System.Posix.Directory ( getWorkingDirectory, changeWorkingDirectory )
import Text.Printf ( printf )
default (LT.Text)
-- FIXME Make this an abstract type.
type Commit = String
data Modes = Init { initCommit :: Commit }
| Review
| Todo
deriving ( Show, Data, Typeable )
-- FIXME Move all the config related things to a separate module.
data Config = V1 Commit
deriving ( Show, Read )
io :: MonadIO m => IO a -> m a
io = liftIO
io_ :: MonadIO m => IO a -> m ()
io_ act = liftIO act >> return ()
crModes :: [Modes]
crModes =
[ Init { initCommit = def &= typ "COMMIT" &= argPos 0 &= opt ("HEAD" :: String) }
&= help "initialize cr for this repo"
, Review &= help "review new changes"
, Todo &= help "what's next"
]
&= program "cr"
&= summary "cr v1.2 - Simplified code review"
readConfig :: IO Config
readConfig = do
(read <$> readFile ".cr")
`catchany` \_ -> do
putStrLn "cr not initialised (try running `cr init`)"
exitFailure
-- FIXME Move this to a separate module.
interactiveProcess :: String -> [String] -> Sh ExitCode
interactiveProcess exe arguments = do
environment <- liftIO getEnvironment
(_, _, _, pHandle) <- io $
createProcess $ CreateProcess
{ cmdspec = RawCommand exe arguments
, cwd = Nothing
, env = Just environment
, std_in = Inherit
, std_out = Inherit
, std_err = Inherit
, close_fds = False
, create_group = False
}
io $ waitForProcess pHandle
promptYN :: MonadIO m => String -> m () -> m ()
promptYN prompt act = do
io_ $ printf "%s (Y/n) " prompt
io_ $ hFlush stdout
c <- io getLine
case c of
"" -> act
('y' : _) -> act
_ -> return ()
latestCommit :: Sh Commit
latestCommit = do
unpack <$> strip <$> run "git" ["rev-list", "--max-count", "1", "HEAD"]
shortenCommit :: Commit -> Commit
shortenCommit = Prelude.take 7
cdToRepoRoot :: Sh ()
cdToRepoRoot = do
gitHere <- io $ doesDirectoryExist ".git"
when (not gitHere) $ do
wd <- io $ getWorkingDirectory
if wd == "/"
then io $ do
putStrLn "this doesn't look like a git repo"
exitFailure
else do
io $ changeWorkingDirectory ".."
cd ".."
cdToRepoRoot
-- FIXME Proper error handling: user should not see git errors.
-- FIXME Mercurial support.
-- FIXME Add support for reviewing files independently.
-- FIXME Add support for `cr todo`.
main :: IO ()
main = shellyNoDir $ silently $ do
cdToRepoRoot
opts <- liftIO $ cmdArgs $ modes crModes
case opts of
Init commit -> do
commit' <- if commit == "HEAD" then latestCommit else return commit
io_ $ printf "Initialising cr with commit %s\n" (shortenCommit commit')
-- FIXME Guard against overwriting an existing config.
io_ $ writeFile ".cr" (show (V1 commit'))
Review -> do
(V1 commit) <- io readConfig
commit' <- latestCommit
if commit' == commit
then do
io_ $ putStrLn "No changes to review"
exit 0
else do
io_ $ printf "Reviewing changes since %s:\n" (shortenCommit commit)
print_stdout True $ run_ "git" ["diff", "--stat", fromString commit, "HEAD"]
promptYN "Continue?" $ do
_ <- interactiveProcess "git" ["diff", fromString commit, "HEAD"]
promptYN (printf "Mark as %s reviewed?" (shortenCommit commit')) $ do
io_ $ writeFile ".cr" (show (V1 commit'))
Todo -> do
print_stdout True (run_ "grep" [ "-RE", "FIXME|TODO|XXX"
, "--exclude-dir", ".git"
, "--binary-files", "without-match" ])
`catchany_sh` (\_ -> return ())