packages feed

ReviewBoard-0.2.2: example/rbpatch.hs

#!/usr/bin/env runhaskell
-----------------------------------------------------------------------------
-- |
-- Module      :  rbpatch.hs
--
-- Maintainer  :  adam.smyczek@gmail.com
-- Stability   :  experimental
-- Portability :  portable
--
-- A command line tool to patch local repository from a review request diff
--
-- Sample usage:
-- > rbpatch -r <review request id>
--
-- Same as for @mkrr@ most of the required/optional command line parameters
-- may be pre-defined in @~/.rbpatchrc@ config file. The supported parameters
-- are:
--
-- * url            - ReviewBoard server URL
--
-- * user           - User name
-- 
-- * password       - Password
--
-- * localbasedir   - Path to the repository root
--
-- Run @rbpatch --help@ for details.
--
-----------------------------------------------------------------------------
module Main (main) where

import System.Environment
import System.Console.GetOpt
import System.Directory
import Data.List
import Data.Maybe
import Data.Char
import ReviewBoard.Api
import qualified ReviewBoard.Response as R
import Control.Monad.Error
import System.Process
import System.IO
import System.Exit

-- | Main
--
main :: IO ()
main = do
    -- Parse and validate command line options
    (opts, _) <- getArgs >>= parseOpts
    validateRequired opts

    -- Create the patch action
    let action = patchAction (fromJust . optRRId $ opts) 
                             (optBasedir opts)
                             (optPrintOnly opts) 

    -- ... and run it
    execRBAction (fromJust . optUrl $ opts) 
                 (fromJust . optUser $ opts) 
                 (fromJust . optPassword $ opts) 
                 action

-- | The patch action
--
patchAction :: String -> Maybe String -> Bool -> RBAction ()
patchAction id basedir po = do
    rsp <- httpGet ("/r/" ++ id ++ "/diff/raw/") []
    case rsp of
        RBok r  -> liftIO . (patch po basedir) . R.body $ r
        RBerr e -> throwError e
    where
        patch True  _         = putStr
        patch False Nothing   = doPatch ["-p0"]
        patch False (Just bd) = doPatch ["-d " ++ bd, "-p0"]

-- | Execute patch command
--
doPatch :: [String] -> String -> IO ()
doPatch args p = do
    pf <- findPatch
    (inp, out, err, pid) <- runInteractiveProcess pf args Nothing Nothing
    hPutStr inp p
    hClose inp
    result <- hGetContents out
    errors <- hGetContents err
    when (result == result) $ return ()
    when (errors == errors) $ return ()
    hClose out
    hClose err
    waitForProcess pid
    when (not . null $ result) $ putStr (result ++ "\nDone.\n")
    when (not . null $ errors) $ putStr errors
    return () 
   
-- ---------------------------------------------------------------------------
-- Option handling

-- | Parse options
--
parseOpts :: [String] -> IO (Options, [String])
parseOpts argv = do
    dos <- defaultOpts
    case getOpt Permute options argv of
        (o, n, []) ->  foldM (flip id) dos o >>= \os -> return (os, n)
        (_, _, es) -> ioError (userError (concat es ++ usageInfo header options))

-- | rbpatch help header
--
header = "Usage: rbpatch [OPTION...]"

-- | Option set
--
data Options = Options
    { optUrl        :: Maybe String
    , optUser       :: Maybe String
    , optPassword   :: Maybe String
    , optRRId       :: Maybe String
    , optBasedir    :: Maybe String
    , optPrintOnly  :: Bool
    } deriving Show

-- | Option descriptor list
--
options :: [OptDescr (Options -> IO Options)]
options =
   [ Option ['u'] []       (ReqArg (\ u opts -> return opts { optUrl = Just u }) "URL") "URL to ReviewBoard server"
   , Option ['U'] []       (ReqArg (\ u opts -> return opts { optUser = Just u }) "USER") "USER name"
   , Option ['P'] []       (ReqArg (\ p opts -> return opts { optPassword = Just p }) "PASSWORD") "User PASSWORD"
   , Option ['b'] []       (ReqArg (\ b opts -> return opts { optBasedir = Just b }) "BASEDIR") "Local base dir"
   , Option ['r'] []       (ReqArg (\ r opts -> return opts { optRRId = Just  r }) "REVIEW_REQUEST_ID") "Review request id"
   , Option ['p'] []       (NoArg  (\   opts -> return opts { optPrintOnly = True })) "Print only, do not apply the patch"
   , Option ['h'] ["help"] (NoArg  (\   opts -> putStr (usageInfo header options) >> exitWith ExitSuccess)) "Print this help"
   ]

-- | Load default options from configuration file
--
defaultOpts :: IO Options
defaultOpts = do
    hd <- getHomeDirectory
    cf <- readFile $ hd ++ "/.rbpatchrc"
    return $ foldl parseOpt initOpts $ lines cf
    where
        parseOpt os l | isPrefixOf "url"            l = os { optUrl        = Just $ parseValue l }
                      | isPrefixOf "user"           l = os { optUser       = Just $ parseValue l }
                      | isPrefixOf "password"       l = os { optPassword   = Just $ parseValue l }
                      | isPrefixOf "localbasedir"   l = os { optBasedir    = Just $ parseValue l }
                      | otherwise                     = os
        parseValue :: String -> String
        parseValue = takeWhile (not . isSeparator) . dropWhile isSeparator . drop 1 . dropWhile (/= '=')

-- | Initial option set
--
initOpts :: Options
initOpts = Options
    { optUrl        = Nothing
    , optUser       = Nothing
    , optPassword   = Nothing
    , optRRId       = Nothing
    , optBasedir    = Nothing
    , optPrintOnly  = False }

-- | Validate required arguments
--
validateRequired :: Options -> IO ()
validateRequired opts = do
    -- TODO: this is imperative, improve this
    when (isNothing . optUrl $ opts)        $ error $ "URL required!" ++ help
    when (isNothing . optUser $ opts)       $ error $ "USER required!" ++ help
    when (isNothing . optPassword $ opts)   $ error $ "PASSWORD required!" ++ help
    when (isNothing . optRRId $ opts)       $ error $ "REVIEW_REQUEST_ID required!" ++ help
    where
        help = " Try rbpatch --help"

-- ---------------------------------------------------------------------------
-- Patch command utils

-- | Common patch locations
--
patchCmds = 
    [ "/usr/bin/patch"
    , "/usr/local/bin/patch"
    , "/opt/bin/sendmail"
    , "/opt/local/bin/sendmail"
    ] :: [String]


-- | Find patch command and throw error if patch 
-- is not installed in one of patchCmds locations
-- 
findPatch :: IO String
findPatch = do
    pf <- foldM fExist "" patchCmds
    if null pf then error "No patch file found!"
               else return pf
    where
        fExist r f | (not . null) r = return r
                   | otherwise      = do
                        e <- doesFileExist f 
                        return $ if e then f else ""